← Back to Writeups
HTBN/AReversing

Data Needs Splitting

XESXOR8/23/20266 min read
#reversing#htb#n/a

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
PortServiceVersionNotes
<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

  1. Decimal_stream_backtracking
  2. Dns_chunk_reassembly
  3. Java_bytecode_reversing
  4. 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

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

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:

  1. Query the TXT records.
  2. For each record, split it into index = chunk[:2] and data = chunk[2:].
  3. Sort by index.
  4. Concatenate all data pieces.
  5. 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
  • 2194307438957234483
  • 148527584754938272
  • 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:

Enter the flag:
Correct!
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR