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
bash read-only • copy allowednmap -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
bash read-only • copy allowed<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
bash read-only • copy allowedsudo -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.
bash read-only • copy allowed<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:
text read-only • copy allowedDawgCTF{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:
bash read-only • copy allowednslookup -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:
text read-only • copy allowedMETA-INF/MANIFEST.MF Loader.class Main.class assets/file.dat
Useful inspection commands:
bash read-only • copy allowedunzip -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 constants2194307438957234483148527584754938272
- the rotation uses
(i % 4) * 16, so the masks repeat every four characters - it computes:
text read-only • copy allowedtransformed = 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:
text read-only • copy allowed145511939249997195145441944550467175145531942549987228145401943650017203145451934650207244145651934650127169
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:
python read-only • copy allowed#!/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:
text read-only • copy allowedDawgCTF{REDACTED}
Verification
The recovered string can be checked directly against the rebuilt JAR:
bash read-only • copy allowedprintf 'DawgCTF{REDACTED}\n' | java -jar challenge_blob.bin
Observed output:
</details>text read-only • copy allowedEnter the flag: Correct!
signed by XESXOR