Data Needs Splitting
Data Needs Splitting
Platform: Metactf | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-10 | Status: Solved Techniques: decimal_stream_backtracking, dns_chunk_reassembly, java_bytecode_reversing, xor_mask_reversal
Summary
Task: a MetaCTF domain hides its real payload in numbered DNS TXT records instead of a normal host response. Solution: rebuild the base64 JAR from TXT chunks, reverse the dynamically loaded Java validator, and invert the rotating XOR transformation to recover the only valid input string.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
metactf| ID:20260410_metactf_data_needs_splitting - Tags: base64, xor, jar, dns_txt, java_bytecode, dynamic_class_loading
- Indicators: DNS resolution returns numbered TXT chunks instead of a normal A record, concatenated TXT payload decodes into a PK/JAR file, a resource file is loaded as a Java class at runtime, validation compares a concatenated decimal string produced from rotating 16-bit XOR masks, the recovered flag prefix does not match the advertised event name
- Source:
20260410_metactf_data_needs_splitting.md
Foothold
Vulnerability / Misconfiguration
- Decimal_stream_backtracking
- Dns_chunk_reassembly
- Java_bytecode_reversing
- Xor_mask_reversal
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- decimal_stream_backtracking
- dns_chunk_reassembly
- java_bytecode_reversing
- xor_mask_reversal
- Tags: base64, xor, jar, dns_txt, java_bytecode, dynamic_class_loading
Original Writeup
<details><summary>Click to expand original content</summary>Description
Target domain: data-needs-splitting.umbccd.net
English summary: the challenge hides its actual artifact inside DNS TXT records. Reassembling those records yields a Java archive whose validator must be reversed to recover the correct flag string.
Challenge Overview
The main trick is that the domain does not behave like a normal service endpoint. Instead of returning a useful A record, it stores the payload across many numbered TXT records. Those chunks reconstruct a JAR file, and the JAR hides its real validator logic in assets/file.dat, which is dynamically loaded as a Java class.
One important oddity is the final flag format: the challenge was solved under MetaCTF, but the valid flag is clearly DawgCTF-formatted:
DawgCTF{REDACTED}
That mismatch should not be ignored during solving, because it is a strong hint that the backend content was reused or shared even though the event metadata says MetaCTF.
Reconnaissance
Initial DNS resolution did not produce a useful normal host record. Querying TXT records immediately revealed multiple long strings prefixed with two-digit indices such as 00, 01, 02, and so on.
Useful enumeration commands:
nslookup -type=TXT data-needs-splitting.umbccd.net curl -s -H "accept: application/dns-json" "https://dns.google/resolve?name=data-needs-splitting.umbccd.net&type=TXT"
This strongly suggests chunked data exfiltration or staged storage over DNS. The numbered prefixes indicate the intended recovery process: strip the index, sort by index, and concatenate the remaining base64 data.
DNS Chunk Reconstruction
Each TXT answer starts with a two-digit sequence number followed by a base64 fragment. After removing the index bytes, sorting numerically, and concatenating the rest, the result decodes cleanly into a ZIP/JAR file.
Recovery workflow:
- Query the TXT records.
- For each record, split it into
index = chunk[:2]anddata = chunk[2:]. - Sort by
index. - Concatenate all
datapieces. - Base64-decode the result into
challenge_blob.bin.
The reconstructed file is actually a Java archive even though it has a .bin extension. Listing it shows:
META-INF/MANIFEST.MF Loader.class Main.class assets/file.dat
Useful inspection commands:
unzip -l challenge_blob.bin javap -classpath challenge_blob.bin -c -p Main javap -classpath challenge_blob.bin -c -p Loader unzip -p challenge_blob.bin assets/file.dat > file.dat cp file.dat Validator.class javap -c -p Validator
Java Reversing Steps
Main loads /assets/file.dat through Loader and invokes validate(). Although the file is named like data, it is actually a compiled Java class named Validator.
Disassembly of Validator.validate() shows the whole check:
- it prompts with
Enter the flag: - it reads one line of input
- for each character at position
i, it extracts two rotating 16-bit masks from the long constants 2194307438957234483148527584754938272- the rotation uses
(i % 4) * 16, so the masks repeat every four characters - it computes:
transformed = ord(ch) ^ mask1 ^ mask2
- it appends each transformed value as a decimal string to a
StringBuilder - it compares the final concatenated decimal string against the hardcoded target:
145511939249997195145441944550467175145531942549987228145401943650017203145451934650207244145651934650127169
The critical detail is that the program does not store separators between numbers. So reversing requires both undoing the XOR and segmenting the big decimal string correctly.
Recovering the Valid Flag String
Because the masks are fully known and repeat every four positions, each character can be tested independently once we choose how many decimal digits belong to that position. A small backtracking script over printable characters recovers the unique string whose transformed values concatenate to the target decimal blob.
Full working solve script:
#!/usr/bin/env python3
import json
import base64
import zipfile
from functools import lru_cache
from pathlib import Path
DOMAIN = "data-needs-splitting.umbccd.net"
DNS_JSON = Path("dns_txt.json")
JAR_PATH = Path("challenge_blob.bin")
TARGET = "145511939249997195145441944550467175145531942549987228145401943650017203145451934650207244145651934650127169"
CONST_A = 2194307438957234483
CONST_B = 148527584754938272
def rebuild_jar_from_dns() -> None:
data = json.loads(DNS_JSON.read_text())
answers = data["Answer"]
txt_chunks = []
for answer in answers:
if answer.get("type") != 16:
continue
chunk = answer["data"]
txt_chunks.append((int(chunk[:2]), chunk[2:]))
blob_b64 = "".join(piece for _, piece in sorted(txt_chunks))
JAR_PATH.write_bytes(base64.b64decode(blob_b64))
def mask_at(index: int) -> int:
shift = (index % 4) * 16
m1 = (CONST_A >> shift) & 0xFFFF
m2 = (CONST_B >> shift) & 0xFFFF
return m1 ^ m2
@lru_cache(maxsize=None)
def recover(pos: int, idx: int):
if pos == len(TARGET):
return [""]
out = []
mask = mask_at(idx)
for ch in range(32, 127):
encoded = str(ch ^ mask)
if TARGET.startswith(encoded, pos):
for rest in recover(pos + len(encoded), idx + 1):
out.append(chr(ch) + rest)
return out
def main() -> None:
rebuild_jar_from_dns()
with zipfile.ZipFile(JAR_PATH) as zf:
print("JAR entries:")
for name in zf.namelist():
print(f" - {name}")
candidates = recover(0, 0)
if len(candidates) != 1:
raise SystemExit(f"Expected one solution, got {len(candidates)}: {candidates}")
flag = candidates[0]
print(f"Recovered flag: {flag}")
if __name__ == "__main__":
main()
Running the logic yields exactly one valid candidate:
DawgCTF{REDACTED}
Verification
The recovered string can be checked directly against the rebuilt JAR:
printf 'DawgCTF{REDACTED}\n' | java -jar challenge_blob.bin
Observed output:
</details>Enter the flag: Correct!
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR