{"id":430,"date":"2025-11-11T05:10:00","date_gmt":"2025-11-11T05:10:00","guid":{"rendered":"https:\/\/hackingwithj.com\/?p=430"},"modified":"2025-11-10T13:10:04","modified_gmt":"2025-11-10T13:10:04","slug":"ceh-compete-ssrfurnace","status":"publish","type":"post","link":"https:\/\/hackingwithj.com\/?p=430","title":{"rendered":"CEH Compete"},"content":{"rendered":"\n<p>Because I followed the full CEH route I was eligible for a monthly challenge. These are fun, but the timebox forces you to move fast \u2014 you need good skills and quick decision making to grab all flags. Today\u2019s box hinted at SSRF in the title. Fresh off a TryHackMe SSRF room, I felt confident I could at least find something useful.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Target &amp; first impressions<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Single IP target.<\/li>\n\n\n\n<li>Quick <code>nmap<\/code> showed only port <strong>80\/tcp<\/strong> open.<\/li>\n\n\n\n<li>Visiting the site reveals a simple login and register form<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"442\" src=\"https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-85-1024x442.png\" alt=\"\" class=\"wp-image-431\" style=\"width:880px;height:auto\" srcset=\"https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-85-1024x442.png 1024w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-85-300x130.png 300w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-85-768x332.png 768w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-85.png 1512w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>I created an account to get a feel for the app. That produced nothing immediately useful, but while viewing the page source I noticed the legal disclaimer referenced a file hosted on a different server \u2014 a prime SSRF candidate.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"246\" src=\"https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-86-1024x246.png\" alt=\"\" class=\"wp-image-432\" srcset=\"https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-86-1024x246.png 1024w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-86-300x72.png 300w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-86-768x185.png 768w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-86.png 1502w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Enumeration \u2014 probing the SSRF vector<\/h2>\n\n\n\n<p>I tried a <code>file:\/\/<\/code> style request to see if the server would fetch local files for me. I proxied the request through Burp and sent it to Repeater to experiment.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"342\" src=\"https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-87-1024x342.png\" alt=\"\" class=\"wp-image-433\" srcset=\"https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-87-1024x342.png 1024w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-87-300x100.png 300w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-87-768x256.png 768w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-87.png 1498w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>The returned content included PHP source snippets. From those snippets I saw a connection to a database file in <code>DB\/DbOperation.php<\/code>. Following that reference led me to <code>DbConnect.php<\/code>, and inside that file there was another host listed along with database credentials.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"305\" src=\"https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-92-1024x305.png\" alt=\"\" class=\"wp-image-437\" srcset=\"https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-92-1024x305.png 1024w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-92-300x89.png 300w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-92-768x229.png 768w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-92.png 1502w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>From my attacker machine I could ping that new host. Using the discovered credentials I connected to MySQL from Kali:<\/p>\n\n\n\n<pre class=\"wp-block-code has-vivid-purple-color has-text-color has-link-color wp-elements-237af8c82b8abbdd2076b3fb455ce8f4\"><code>mysql -h 10.10.2.222 -P 3306 -u ssrfdbuser -p --ssl=off<\/code><\/pre>\n\n\n\n<p>Notes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>-P<\/code> to specify port (confirmed via nmap).<\/li>\n\n\n\n<li><code>-p<\/code> to input password interactively.<\/li>\n\n\n\n<li><code>--ssl=off<\/code> because the client threw an SSL error otherwise.<\/li>\n<\/ul>\n\n\n\n<p>After succesfull connection we can query data from the <code>users<\/code> table:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"948\" height=\"362\" src=\"https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-93.png\" alt=\"\" class=\"wp-image-438\" srcset=\"https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-93.png 948w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-93-300x115.png 300w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-93-768x293.png 768w\" sizes=\"auto, (max-width: 948px) 100vw, 948px\" \/><\/figure>\n\n\n\n<p>Armed with the admin login, I went back to the web app to see what else I could do.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Exploitation \u2014 upload service and SSRF pivots<\/h2>\n\n\n\n<p>There was an image upload feature ostensibly for the admin. Upload attempts returned a message that the \u201cupload service is offline\u201d and pointed to a specific internal IP for status checks. Directly hitting that IP from my Kali was blocked, but I could load the admin page source via the SSRF and inspect the logic.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"371\" src=\"https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-90-1024x371.png\" alt=\"\" class=\"wp-image-440\" srcset=\"https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-90-1024x371.png 1024w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-90-300x109.png 300w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-90-768x278.png 768w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-90.png 1189w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>From the source I learned there\u2019s an API with endpoints like <code>\/start<\/code>. Querying the <code>\/start<\/code> endpoint returned the status <code>inactive<\/code> \u2014 I needed a way to start that service. After some trial and error and fuzzing the API, I found an API command that let me change the state and trigger behaviour the app expected.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"286\" src=\"https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-91-1024x286.png\" alt=\"\" class=\"wp-image-441\" srcset=\"https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-91-1024x286.png 1024w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-91-300x84.png 300w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-91-768x214.png 768w, https:\/\/hackingwithj.com\/wp-content\/uploads\/2025\/11\/image-91.png 1221w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Timebox &amp; results<\/h2>\n\n\n\n<p>And then: time. The one-hour box timer expired before I could fully exploit the service and complete the remaining flags. Frustrating, but I still captured <strong>4 out of 7<\/strong> flags \u2014 not bad given the clock.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Lessons learned &amp; takeaways<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>SSRF is powerful<\/strong>: It let me read server-side files and discover internal hosts and secrets. Always check external references in page source and anywhere the app fetches third-party resources.<\/li>\n\n\n\n<li><strong>Follow the breadcrumbs<\/strong>: Small hints in source (file paths, includes) led to real credentials and a pivot. Don\u2019t skip inspecting referenced files.<\/li>\n\n\n\n<li><strong>Fuzz smartly<\/strong>: When an endpoint returns <code>inactive<\/code> or similar, try parameter fuzzing and common control commands \u2014 often the admin endpoints are crude and can be triggered by a simple path or parameter you wouldn\u2019t try first.<\/li>\n\n\n\n<li><strong>No public writeups \u2014 bigger win<\/strong>: Unlike a lot of TryHackMe rooms, there were <strong>no writeups or walkthroughs online<\/strong> for this particular challenge. That meant I couldn\u2019t rely on someone else\u2019s path or shortcuts \u2014 everything I found was purely my own work. That made pushing through the SSRF \u2192 DB creds \u2192 admin pivot feel a lot more satisfying and validated my methodology.<\/li>\n\n\n\n<li><strong>Timeboxing sucks but teaches<\/strong>: The pressure forces prioritization \u2014 enumeration + quick pivots wins over deep manual exploration early on. In this run, extra time (one more hour) would very likely have let me finish the machine.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">What I would do next (if I had another hour)<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Use the discovered admin credentials to fully control the upload service or swap the uploaded image for a webshell (if file validation could be bypassed).<\/li>\n\n\n\n<li>Explore the API further for authenticated endpoints exposed only to internal networks.<\/li>\n\n\n\n<li>Try to get a persistent foothold on the internal host discovered via DB creds.<\/li>\n\n\n\n<li>Document everything cleanly \u2014 since there are no public writeups, I\u2019d prepare a detailed walkthrough (notes, commands, and sanitized screenshots) so others can learn from the path I found without spoiling the challenge for new players. That\u2019d also make a nice addition to my blog and help consolidate what I learned.<\/li>\n<\/ol>\n\n\n\n<h1 class=\"wp-block-heading\">Final notes<\/h1>\n\n\n\n<p>I left with mixed feelings \u2014 annoyed by the time limit, but happy about the methodology that paid off: SSRF \u2192 internal file read \u2192 DB creds \u2192 admin account \u2192 API fuzzing. If you\u2019re practicing SSRF: exercise reading local config files and chasing any hard-coded hostnames or includes. Those are often the fastest path to escalation.<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Because I followed the full CEH route I was eligible for a monthly challenge. These are fun, but the timebox forces you to move fast \u2014 you need good skills and quick decision making to grab all flags. Today\u2019s box hinted at SSRF in the title. Fresh off a TryHackMe SSRF room, I felt confident [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"site-container-style":"default","site-container-layout":"default","site-sidebar-layout":"default","disable-article-header":"default","disable-site-header":"default","disable-site-footer":"default","disable-content-area-spacing":"default","footnotes":""},"categories":[8,35],"tags":[],"class_list":["post-430","post","type-post","status-publish","format-standard","hentry","category-ctf","category-training"],"_links":{"self":[{"href":"https:\/\/hackingwithj.com\/index.php?rest_route=\/wp\/v2\/posts\/430","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/hackingwithj.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/hackingwithj.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/hackingwithj.com\/index.php?rest_route=\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/hackingwithj.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=430"}],"version-history":[{"count":6,"href":"https:\/\/hackingwithj.com\/index.php?rest_route=\/wp\/v2\/posts\/430\/revisions"}],"predecessor-version":[{"id":445,"href":"https:\/\/hackingwithj.com\/index.php?rest_route=\/wp\/v2\/posts\/430\/revisions\/445"}],"wp:attachment":[{"href":"https:\/\/hackingwithj.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=430"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/hackingwithj.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=430"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/hackingwithj.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=430"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}