97 lines
3.7 KiB
Python
97 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Solve script for DNS 0x20 exfiltration challenge.
|
||
|
||
The webshell exfiltrates data via DNS by:
|
||
1. base64-encoding the content (with + and / replaced by -)
|
||
2. prepending a 3-char MD5 checksum
|
||
3. performing a DNS_A lookup for: {base64}.{checksum}.{attacker_domain}
|
||
|
||
DNS 0x20 encoding preserves the case of query labels in the response,
|
||
meaning the mixed-case payload in the DNS log IS the original base64.
|
||
|
||
However, intermediate resolvers may have corrupted some letter cases,
|
||
so we reconstruct the correct capitalisation by:
|
||
- splitting the payload into groups of 4 base64 characters
|
||
- trying all case variants per group
|
||
- keeping only variants that decode to printable ASCII
|
||
- finding the combination whose MD5[:3] matches the checksum in the log
|
||
"""
|
||
|
||
import base64
|
||
from hashlib import md5
|
||
from itertools import product
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Paste the data label and checksum from the 0x20-encoded DNS log entry here
|
||
# ---------------------------------------------------------------------------
|
||
PAYLOAD = "Su5tE1QwdDRSBHLfBJn2zXjFAgFWcdNUzWQTAxjSLtfLodhjMtUzfq"
|
||
CHECKSUM = "190" # first 3 hex chars of md5(flag)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def reverse_strtr(s: str) -> str:
|
||
"""The PHP replaces + and / with -. Reverse where possible (no-op if no dashes)."""
|
||
# If there are no dashes the base64 had neither + nor /, nothing to do.
|
||
# If there ARE dashes we'd need to try both — handled by the case-brute-force below.
|
||
return s
|
||
|
||
|
||
def case_variants(group: str):
|
||
"""Yield every capitalisation variant of a base64 group that decodes to printable ASCII."""
|
||
letter_positions = [(i, c) for i, c in enumerate(group) if c.isalpha()]
|
||
for bits in product([True, False], repeat=len(letter_positions)):
|
||
candidate = list(group)
|
||
for (idx, _), upper in zip(letter_positions, bits):
|
||
candidate[idx] = candidate[idx].upper() if upper else candidate[idx].lower()
|
||
s = "".join(candidate)
|
||
padding = "=" * ((4 - len(s) % 4) % 4)
|
||
try:
|
||
decoded = base64.b64decode(s + padding)
|
||
if all(32 <= b < 127 for b in decoded): # printable ASCII only
|
||
yield s, decoded
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def solve(payload: str, checksum: str) -> str:
|
||
payload = reverse_strtr(payload)
|
||
|
||
# Split into groups of 4 (standard base64 block size)
|
||
groups = [payload[i:i+4] for i in range(0, len(payload), 4)]
|
||
|
||
# Collect valid byte candidates for each group
|
||
group_candidates = []
|
||
for i, g in enumerate(groups):
|
||
cands = list(case_variants(g))
|
||
if not cands:
|
||
raise ValueError(f"No printable-ASCII candidate found for group {i} ({g!r})")
|
||
group_candidates.append([dec for _, dec in cands])
|
||
print(f" group[{i:02d}] {g!r:6s} → {[dec for _, dec in cands]}")
|
||
|
||
print(f"\nSearching {' × '.join(str(len(c)) for c in group_candidates)} combinations "
|
||
f"for MD5[:3] == {checksum!r} …\n")
|
||
|
||
# Enumerate combinations and check MD5 checksum
|
||
for combo in product(*group_candidates):
|
||
candidate_bytes = b"".join(combo)
|
||
if md5(candidate_bytes).hexdigest()[:3] == checksum:
|
||
return candidate_bytes.decode()
|
||
|
||
raise ValueError("No combination matched the checksum — check PAYLOAD / CHECKSUM values.")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
print("=" * 60)
|
||
print("DNS 0x20 Exfiltration — Solve Script")
|
||
print("=" * 60)
|
||
print(f"Payload : {PAYLOAD}")
|
||
print(f"Checksum : {CHECKSUM}\n")
|
||
print("Candidate bytes per base64 group (printable ASCII filter):")
|
||
|
||
flag = solve(PAYLOAD, CHECKSUM)
|
||
|
||
print("=" * 60)
|
||
print(f"FLAG: {flag}")
|
||
print("=" * 60)
|