HTB Cyber Apocalypse 2025 Writeup - All Web

Table of Contents
This year saw a significant improvement to PwnSecâs performance. We moved from 77th last year to achieving 15th this year.
Below is a short account of each of the web challenges I solved. This is rather summarized compared to my usual comprehensive walkthroughs due to time constraints on my side. Apologies for that.
Web: Trial By Fire - Very Easyâ
A Jinja2 SSTI that is triggered in the /battle-report
based on your warriorâs name:
HTB{Fl4m3_P34ks_Tr14l_Burn5_Br1ght_e9f8c148ac7f49bb3d2d00093473056d}
Web: Whispers of The Moonbeam - Very Easyâ
Simple command injection in the gossip
command, some initial attempts:
We notice gossip
displays an ls
-like output. Command injection:
gossip ; cat flag.txt
HTB{Sh4d0w_3x3cut10n_1n_Th3_M00nb34m_T4v3rn_8bafcd34a8f4593225ff0b8ee006ac88}
Web: Cyber Attack - Easyâ
Solved by my teammates @zAbuQasem and @0xNEF.
Header Injection to SSRF to access the protected /attack-ip
endpoint.
We then abuse IPv6 parsing vulnerability in Pythonâs ipaddress
library to introduce a command injection in the /cgi-bin/attack-ip
endpoint via Scope ID.
Scope IDs append extra information to an IPv6 address using a %
suffix such as ::1%eth1
for example.
We can use that to deliver a PHP reverse shell:
And⌠we get a shell back.
HTB{h4ndl1n6_m4l4k4r5_f0rc35}
Web: Eldoria Realms - Mediumâ
Ruby class pollution to gRPC command injection via gopher://
SSRF. I used these two references:
- https://blog.doyensec.com/2024/10/02/class-pollution-ruby.html
- https://bkubiak.github.io/grpc-raw-requests/
We can connect to the gRPC service locally using grpcurl
or grpcui
. I used grpcui
:
It looks like this:
I set up Wireshark and filtered for messages on tcp/50051
, as per the gRPC referenceâs tip:
If you donât see packets in HTTP2 protocol, click âAnalyzeâ -> âDecode AsâŚâ. Then, add TCP port X with HTTP2 protocol, where X is port of gRPC server (e.g. 8083).
- I grabbed every HTTP/2 payload, by right click, âCopy TCP payloadâ to make up the complete packet we want to send.
- We use the Ruby class pollution to poison
realm_url
and inject our payload. - We use
gopher://
, a legacy protocol supported bycurl
because it allows us to send raw TCP packets over the wire.
You can see me here doing some debugging with sleep
statements. It seemed like the HTTP/2 packet was getting corrupted:
We should have PRI
not 50RI
in the output above.
I tried prepending the gopher://
payload with an extra character and it worked. My script:
HTB{p0llut3_4nd_h1t_pr0toc0lz_w_4_sw1tch_b3c81179fbcfe17e57f49e24d64f1240}
Web: Eldoria Panel - Mediumâ
There was a mutation XSS in DOMPurify 3.1.2 as shown here by mizu.re
:
I used this to successfully steal the adminâs API key. However, as I noticed later, the X-API-Key
logic was incorrectly implemented and served no purpose. We could access admin functionality directly and without it.
In one of the admin routes, file_get_contents
is used against user-controlled input, however file_exists
is used to validate the input beforehand. Since php://
and http://
wrappers do not support stat()
functions, we can not use these wrappers. A URL scheme that supports stat()
is ftp://
. This means we can deliver our payload via FTP.
We canât use ngrok
or single port forwarding techniques because my intended way of hosting the FTP server involved twisted
which uses FTP passive mode and subsequentially requires two ports (control port at 21 and data port agreed upon during file transmission). Thatâs why we must use an external server (or enforce FTP in active mode - not sure if that can be enforced server-side).
On a throwaway VPS, I hosted twistd
FTP, placing a web shell at login.php
:
Modify the template path to an anonymous FTP share:
Visiting /?1=cat /flag.txt
, we get our flag.
HTB{p41n_c4us3d_by_th3_usu4l_5u5p3ct_d16c96bc407976fddf4b45d3ec032cf6}
Web: Aurors Archive - Hardâ
After analyzing the OAuth server provided, I found an unintended that completely ignores the OAuth flow. There was an XSS via SSTI in the submissions and bids endpoint:
The author probably thought that dump
(Convert data to a JSON string) will prevent bypass? Not sure.
Anyways, we can escape by submitting a bid with the following payload, using single quotes:
The XSS reflected in /my-submissions
, which made it difficult to deliver to admin. I played around with different CSRF techniques to get the admin to poison his own page before visiting it, but all attempts ultimately failed due to SameSite=Lax
.
I ended up using the bids
endpoint which was publicly accessible and provided intuitive delivery.
However, since there is a length check on bids
:
We can easily bypass that by sending our payload inside a JSON array, allowing us to send an arbitrarily large payload.
We can send a payload like this, allowing us to perform a CSRF and exfiltrate the internal /tables
admin endpoint:
Our XSS works.
We notice an SQL injection in the /table
endpoint. We can grab the users
table and the SQL version using:
And boom:
I searched for techniques for a postgres RCE using SELECT statements, before my teammate Logan0x found this great walkthrough:
Essentially, I spun up ngrok
and used the exact same postgres module demonstrated in the article. I used the ngrok
endpointâs IP in there too:
To compile it for the target environment, I found the postgres development package name for Alpine, then compiled it within our local Docker environment:
This allows us to create a small, shared executable that will still run correctly in the target environment. I copied the .so
from my local Docker container.
The demonstrated technique makes use of the postgres lo_*
function family which deal with âlarge objectsâ and contains means to read and write binary data as well as export them to the file system.
We use this to overwrite the postgres configuration with one that loads our generated malicious shared object .so
file:
We then run a series of SELECT-only SQL commands to upload our configuration, our malicious .so
and finally call pg_reload_conf()
.
The following code provides a summary of the exploit. First, we call xss()
to exfiltrate the adminâs password.
We then acquire an admin cookie and use it for the SQL injection step. This is done for convenience to perform the SQL injection directly instead of through the bot via XSS.
On the next SQL query, we get a hit:
HTB{l00k_0ut_f0r_0auth_155u35}
Note: I later confirmed with an HTB admin and he said:
Blockchain: Eldorion - Very Easyâ
This was my first time ever working with smart contracts or blockchain challenges. ChatGPT did ease the transition and I realize how similar it is to traditional âWeb 2.0â tasks.
Itâs just the same code review and logic flaws, indeed with a sweet twist, but it is still the same.
This challenge had Eldorion
, who had his health reset at the start of every block. I deployed a smart contract that performed multiple attacks in a single transaction, essentially defeating Eldorion
.
Receiving our connection information:
Our exploit smart contract:
Putting it all together:
We shouldâve defeated Eldorion, letâs check:
HTB{w0w_tr1pl3_hit_c0mbo_ggs_y0u_defe4ted_Eld0r10n}
Blockchain: HeliosDEX - Easyâ
HeliosDEX was a bit more subtle. It involved a currency exchange. Again, ChatGPT identified the vulnerability for me:
This first line in swapForHLS()
is suspicious:
The OpenZeppelin Math library defines Math.Rounding
which accepts:
Math.mulDiv
is defined as follows:
By using Math.Rounding(3)
, we set it to expand âaway from zeroâ, this can lead to catastrophic impact on a currency exchange :)
Essentially, we can abuse the rounding error from Math.round(3)
to exchange ETH for HLS and refund back at a profit margin.
We deploy this smart contract to exploit it (Thanks ChatGPT):
We can then connect:
Setup our environment:
We use a simple bash loop to deploy our contract, grab the âDeployed toâ address and execute it with a 0.1 ETH amount to abuse the rounding error:
We can see our profits coming while our DHS store remaining untouched:
At 20 ETH, we satisfy the win condition and grab our flag:
HTB{0n_Heli0s_tr4d3s_a_d3cim4l_f4d3s_and_f0rtun3s_ar3_m4d3}