HTB: Uplink Artifact Challenge
Uplink Artifact - HackTheBox Challenge Writeup
Challenge Information
| Property | Value |
|---|---|
| Name | Uplink Artifact |
| Category | Misc / Forensics |
| Difficulty | Very Easy |
| Author | d3vn0mi |
Description
During an analysis of a compromised satellite uplink, a suspicious dataset was recovered. Intelligence indicates it may encode physical access credentials hidden within the spatial structure of Volnaya’s covert data infrastructure.
The challenge hands over a CSV file describing a 3D point cloud (x, y, z, label), framed as telemetry recovered from a compromised satellite uplink. The task is to figure out what’s actually encoded in the “spatial structure” of the data.
Solution
The handed-out CSV turned out to be a 0-byte stub — the real artifact was a 97 KB CSV bundled inside a password-protected zip (hackthebox) in the output directory.
The dataset contained points split across 4 labels. Profiling each label’s coordinate distribution revealed that three of them (0, 2, 3) were just random floating-point noise scattered in space — a red herring. Label 1, however, stood out immediately:
- All 322 of its points sat on integer (x, y) coordinates
- Those coordinates fit neatly inside a 25×25 grid
- The
zvalue was clamped tight to ~0.5 for every point (i.e., flat/planar — the third dimension carried no information)
A 25×25 grid of “on/off” pixels is a strong signal for a QR code (Version 2 QR codes are exactly 25×25 modules). Rendering the label-1 points as a black/white bitmap confirmed it — the image showed the three classic corner finder patterns of a QR code. Adding a quiet-zone border and feeding the bitmap to cv2.QRCodeDetector decoded it cleanly, revealing the flag.
Key Steps
1. Extract the real artifact from the password-protected zip:
mkdir -p /out/solve_.../extract && cd /out/solve_.../extractunzip -o -P hackthebox /out/<challenge_id>.zip2. Profile the point cloud by label to find the anomaly:
import csvfrom collections import Counter
rows = list(csv.reader(open('uplink_spatial_auth.csv')))[1:]labels = Counter(r[3] for r in rows)print('label counts:', labels)
# Check which label(s) sit on an integer coordinate gridpts = set()for r in rows: if r[3] == '1': pts.add((int(float(r[0])), int(float(r[1]))))
max_x = max(p[0] for p in pts)max_y = max(p[1] for p in pts)print(f"label 1: {len(pts)} points, bounding box {max_x+1}x{max_y+1}")# -> 322 points, 25x25 grid, z pinned near 0.5 for all of them3. Render the label-1 points as a bitmap (QR bit-grid):
import csv, numpy as np, cv2
rows = list(csv.reader(open('uplink_spatial_auth.csv')))[1:]pts = [(int(float(r[0])), int(float(r[1]))) for r in rows if r[3] == '1']
grid = np.zeros((25, 25), dtype=np.uint8)for x, y in pts: grid[y, x] = 255 # "on" module -> white pixel
# Scale up and add a quiet zone border (required for QR detection)img = cv2.copyMakeBorder(grid, 4, 4, 4, 4, cv2.BORDER_CONSTANT, value=0)img = cv2.resize(img, (img.shape[1] * 10, img.shape[0] * 10), interpolation=cv2.INTER_NEAREST)cv2.imwrite('qr.png', img)4. Decode the QR code:
import cv2
img = cv2.imread('qr.png', cv2.IMREAD_GRAYSCALE)detector = cv2.QRCodeDetector()data, points, _ = detector.detectAndDecode(img)print(data) # -> HTB{REDACTED}Tools Used
unzip— extract the password-protected challenge artifact- Python
csv— parse the raw point-cloud data collections.Counter— profile point distribution per labelnumpy— build the pixel grid from point coordinatesopencv-python-headless(cv2) — render, border/scale, and decode the QR bitmap viaQRCodeDetector
Key Learnings
- Always verify handed artifacts before trusting them. The provided CSV was a 0-byte stub; the real data lived in a password-protected zip in the output directory. A quick
ls -la/ size check saves time later. - Profile structured data by label/class before diving into content. Splitting the point cloud by its
labelcolumn immediately isolated the signal (label 1) from the noise (labels 0, 2, 3) — no need to eyeball all 4 classes’ raw values first. - Integer-grid coordinates in a spatial dataset are a strong steganography tell. Random noise coordinates are floats; a cluster of points snapped to a clean integer lattice (especially one matching a known encoding’s dimensions, like 25×25 for QR v2) is a signal that the positions themselves are the payload, not the values.
- Collapse unused dimensions. With
zclamped to a near-constant value across all label-1 points, the real information was entirely in the (x, y) plane — a hint to project down to 2D and render as an image. - QR codes need a quiet zone. Even a perfectly correct 25×25 module grid can fail to decode without a blank border around it —
cv2.copyMakeBorderbefore detection avoids a false negative.
Flag: HTB{REDACTED}