pwneglyph logo

HackTheBox Challenges

Web challenge writeups from HackTheBox — a broad mix of server- and client-side bugs: nginx cache poisoning, Next.js SSRF + Jinja2 SSTI, Go zip-slip session forgery, PHP POP chains and php-cgi argument injection, H2 SQL→RCE, Mongoose prototype pollution, Tornado object-walk gadget, and a TensorFlow Lambda-layer RCE.

0 categories 16 notes
16 total

A weak Django-style regex (.*^profile) lets /profile.gif hit the authenticated profile JSON. nginx caches any *.gif response for 3 min regardless of auth, so make the admin bot fetch /profile.gif, then read the cached admin api_key from the same URL with no token.

web web-server nginx cache-poisoning web-cache-deception reverse-proxy +5

No admin row is seeded, so registering username 'admin' grants isAdmin. The PDF export pipeline stores HTML through sanitize-html (strips <iframe>) but node-html-markdown later decodes HTML entities, so a double-encoded &lt;iframe src=file:///flag.txt&gt; is reborn as a live tag and markdown-pdf (remarkable html:true) renders it, reading local files into the PDF.

web web-server nodejs express broken-access-control registration-logic +10

Sessions are JSON files at /tmp/sessions/<user>/<id> loaded by username→id from Redis. A tar/zip symlink entry (sessions -> /tmp/sessions/<user>) turns extraction into write-what-where, planting a role:admin session file. Because login sets the Redis username→sessionID mapping BEFORE checking the password, a deliberately-failed login binds your forged file without overwriting it — then manual cookies hit /admin as admin.

web web-server golang fiber zip-slip tar-slip +9

A Next.js redirect() Server Action trusts the Host header (CVE-2024-34351), so a forged Host + attacker redirect server turns the app into an SSRF proxy onto the internal AV service on localhost:3000. That Flask app feeds ?directory= into render_template_string with a brutal blacklist (no _ . [ ] {{ }} x \\); rebuild __globals__→__builtins__→__import__('os').popen via {% %} blocks, request|attr(), and GET params smuggling the banned strings.

web web-server web-client nextjs server-actions ssrf +10

The /guardian route only exists on the random guardian.<SECRET_ALLEY> vhost. Leak the domain by sending an HTTP/1.0 request with no Host header so nginx's default_server reflects alley.<SECRET_ALLEY> via the header-echo /think endpoint. Then call /guardian?quote=http://localhost:1337/think — guardian fetches that URL with the flag in a Key request header, and /think echoes all headers, exposing the flag.

web web-server nginx virtual-host vhost-enumeration host-header +6

A text-adventure game validates commands client-side, but the full command tree (including a 'secret' branch) is served by /api/options. Read the secret command from the API and type it into the game to print the flag.

web web-client api client-side-logic information-disclosure hidden-endpoint +2

A Symfony page builds a Twig template from ?location= (createTemplate), giving SSTI inside a disable_functions/open_basedir-hardened PHP. Twig's |sort accepts a 2-arg PHP callable comparator, so |sort('file_put_contents') writes arbitrary files: plant a .cgi /readflag script and a .htaccess enabling ExecCGI (AllowOverride All), chmod it via |sort('chmod') (octal!), then request the CGI to run outside the PHP sandbox.

web web-server php symfony twig ssti +10

The checker rejects strings shorter than 1000 and compares string[i] vs string[len-i-1]. Send palindrome as an object {'NaN':'','0':''} so obj.length is undefined (undefined<1000 is false, passing the length gate), Array(undefined).keys() yields one i=0, and obj[0] === obj[NaN] === '' (both strings) — every check passes and the flag prints, all under nginx's 75-byte body cap.

web web-server nodejs hono type-confusion json-type-confusion +5

/api/team?id= validates with /^[0-9]+$/m (multiline → a newline-separated digit line passes) and blocks '/' and '..' via String.includes — but Next.js turns repeated ?id= into an ARRAY, whose .includes() checks elements, not substrings. The read path is sliced to 100 chars, so naive ../ collapses; use the even-length /proc/thread-self/root symlink to land exactly on /flag.txt within the byte budget.

web web-server nextjs path-traversal lfi regex-bypass +7

The /note search concatenates the name into a native query (filtering only '$' and 'concat'). It's an H2 database, so beyond UNION SELECT you can stack 'CREATE ALIAS ... AS '<java>'' to define a Java UDF wrapping Runtime.exec, then UNION SELECT that function to run commands and read the randomly-named flag.

web web-server java spring-boot sqli union-based-sqli +5

order.php runs unserialize(base64_decode($_POST['data'])) on attacker input. Chain four magic methods — Pizza::__destruct echoes $size->what → Spaghetti::__get invokes ($sauce)() → IceCream::__invoke foreach over $flavors → ArrayHelpers (extends ArrayIterator)::current() runs call_user_func($callback,$value). Set callback=passthru to execute commands, output reflected via Apache stdout.

web web-server php php-deserialization object-injection pop-chain +8

The screenshot feature fetches user URLs; gopher://127.0.0.1.nip.io:6379 bypasses the localhost check and speaks RAW Redis. RPUSH a hand-crafted Laravel job envelope onto laravel_database_queues:default whose rmFile job runs system(\"rm \".$uuid) — inject through $uuid for command execution when the queue:work worker picks it up.

web web-server php laravel ssrf gopher +7

/update passes req.body straight into findByIdAndUpdate, so a {$rename:{content:'__proto__._peername'}} update pollutes Object.prototype (CVE-2023-3696). Set Object.prototype._peername='x' and address='127.0.0.1' so net.Socket.remoteAddress getter returns 127.0.0.1 for the /flag route's localhost check — reading the flag with no real localhost request.

web web-server nodejs express mongodb mongoose +7

The 'spookify' font tool renders user input through a Mako template (flask-mako), so ${7*7} → 49. Mako exposes Python directly, so ${open('../../flag.txt').read()} reads the flag — no import gymnastics needed.

web web-server python flask mako flask-mako +4

report_tornado sends a Selenium bot to an attacker page. PNA blocks direct fetches to 127.0.0.1, so window.open the app and postMessage a tornado whose status field is rendered with innerHTML (XSS in the loopback origin). That XSS POSTs /update_tornado, whose recursive update_tornados() does getattr-walking — {__class__:{__init__:{__globals__:{USERS:[...]}}}} rewrites the module-global USERS so you can log in and GET /stats for the flag.

web web-client web-server python tornado xss +10

The complaint 'prediction' field is rendered via innerHTML (img onerror XSS) to a Selenium admin who logs in. CSRF is just an X-SPACE-NO-CSRF:1 header, so the XSS can hit authenticated endpoints. Upload a Keras .h5 whose Lambda layer embeds Python; test_model() calls keras.models.load_model() which executes the Lambda on load — os.system copies /app/flag.txt into the web-served models dir, then exfiltrate.

web web-client web-server python flask xss +10