{"id":2011,"date":"2026-05-14T13:59:09","date_gmt":"2026-05-14T17:59:09","guid":{"rendered":"https:\/\/www.peteonsoftware.com\/?p=2011"},"modified":"2026-05-14T13:59:09","modified_gmt":"2026-05-14T17:59:09","slug":"hack-the-box-walkthrough-opensecret","status":"publish","type":"post","link":"https:\/\/www.peteonsoftware.com\/index.php\/2026\/05\/14\/hack-the-box-walkthrough-opensecret\/","title":{"rendered":"Hack the Box Walkthrough: OpenSecret"},"content":{"rendered":"<p><img decoding=\"async\" src=\"https:\/\/www.peteonsoftware.com\/images\/2026\/opensecret_icon.jpg\" alt=\"OpenSecret Icon, courtesy of Jippity\" title=\"OpenSecret Icon, courtesy of Jippity\" style=\"float:left; margin: .5rem;\">Today, we&#8217;re going to tackle a Hack the Box Challenge called <a href=\"https:\/\/app.hackthebox.com\/challenges\/OpenSecret?tab=play_challenge\">OpenSecret<\/a>.  Unlike the last few of these I&#8217;ve done, this is more of an offensive security challenge.  Our challenge scenario is<\/p>\n<p><em>A simple help desk portal where users can submit support tickets. The application uses JWT tokens for session management, but something seems off about how they&#8217;re implemented. Can you find the security flaw?<\/em><\/p>\n<p><br style=\"clear:both;\"><\/p>\n<p><strong>Task 1: Submit challenge Flag<\/strong><\/p>\n<p>After starting the challenge, you&#8217;ll be given a public IP to hit on a specific port, so no VPN access is required.  For me, that IP:Port is <em>154.57.164.83:30250<\/em>.  Given the nature of the challenge (and that it is under the Category of &#8220;Web&#8221;), I know it will be a web application.  Regardless, I did an nmap scan on just that port at that IP so I could know a little bit about it and it seems that this is a Node.js\/Express application.<\/p>\n<pre>\r\n$ nmap -sCV -vv -p 30250 154.57.164.83                 \r\nStarting Nmap 7.99 ( https:\/\/nmap.org ) at 2026-05-14 13:19 -0400\r\nNSE: Loaded 158 scripts for scanning.\r\nNSE: Script Pre-scanning.\r\nNSE: Starting runlevel 1 (of 3) scan.\r\nInitiating NSE at 13:19\r\nCompleted NSE at 13:19, 0.00s elapsed\r\nNSE: Starting runlevel 2 (of 3) scan.\r\nInitiating NSE at 13:19\r\nCompleted NSE at 13:19, 0.00s elapsed\r\nNSE: Starting runlevel 3 (of 3) scan.\r\nInitiating NSE at 13:19\r\nCompleted NSE at 13:19, 0.00s elapsed\r\nInitiating Ping Scan at 13:19\r\nScanning 154.57.164.83 [4 ports]\r\nCompleted Ping Scan at 13:19, 0.02s elapsed (1 total hosts)\r\nInitiating Parallel DNS resolution of 1 host. at 13:19\r\nCompleted Parallel DNS resolution of 1 host. at 13:19, 0.49s elapsed\r\nInitiating SYN Stealth Scan at 13:19\r\nScanning 154-57-164-83.static.isp.htb.systems (154.57.164.83) [1 port]\r\nDiscovered open port 30250\/tcp on 154.57.164.83\r\nDiscovered open port 30250\/tcp on 154.57.164.83\r\nCompleted SYN Stealth Scan at 13:19, 0.24s elapsed (1 total ports)\r\nInitiating Service scan at 13:19\r\nScanning 1 service on 154-57-164-83.static.isp.htb.systems (154.57.164.83)\r\nCompleted Service scan at 13:19, 11.45s elapsed (1 service on 1 host)\r\nNSE: Script scanning 154.57.164.83.\r\nNSE: Starting runlevel 1 (of 3) scan.\r\nInitiating NSE at 13:19\r\nCompleted NSE at 13:19, 5.13s elapsed\r\nNSE: Starting runlevel 2 (of 3) scan.\r\nInitiating NSE at 13:19\r\nCompleted NSE at 13:19, 0.75s elapsed\r\nNSE: Starting runlevel 3 (of 3) scan.\r\nInitiating NSE at 13:19\r\nCompleted NSE at 13:19, 0.00s elapsed\r\nNmap scan report for 154-57-164-83.static.isp.htb.systems (154.57.164.83)\r\nHost is up, received reset ttl 128 (0.028s latency).\r\nScanned at 2026-05-14 13:19:09 EDT for 17s\r\n\r\nPORT      STATE SERVICE REASON          VERSION\r\n30250\/tcp open  http    syn-ack ttl 128 Node.js (Express middleware)\r\n| http-methods: \r\n|_  Supported Methods: GET HEAD POST OPTIONS\r\n|_http-title: OpenSecret Helpdesk - Support Portal\r\n\r\nNSE: Script Post-scanning.\r\nNSE: Starting runlevel 1 (of 3) scan.\r\nInitiating NSE at 13:19\r\nCompleted NSE at 13:19, 0.00s elapsed\r\nNSE: Starting runlevel 2 (of 3) scan.\r\nInitiating NSE at 13:19\r\nCompleted NSE at 13:19, 0.00s elapsed\r\nNSE: Starting runlevel 3 (of 3) scan.\r\nInitiating NSE at 13:19\r\nCompleted NSE at 13:19, 0.00s elapsed\r\nRead data files from: \/usr\/share\/nmap\r\nService detection performed. Please report any incorrect results at https:\/\/nmap.org\/submit\/ .\r\nNmap done: 1 IP address (1 host up) scanned in 18.39 seconds\r\n           Raw packets sent: 6 (240B) | Rcvd: 3 (128B)\r\n<\/pre>\n<p>Navigating to the site, we see this<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.peteonsoftware.com\/images\/2026\/opensecret_homepage.jpg\" alt=\"OpenSecret Helpdesk Homepage\" title=\"OpenSecret Helpdesk Homepage\"><\/p>\n<p>Given the description, I checked to see if there are any JWTs in storage already in the browser.  I checked Cache Storage, Cookies, Indexed DB, Local Storage, and Session Storage, but nothing is there yet.  Okay, I don&#8217;t see any other links, so I submitted the form with a name, email, and some pretend issue description words.  Nothing fancy, no XSS attempts, etc.  When I do, it tells me that no session token is provided.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.peteonsoftware.com\/images\/2026\/opensecret_notokenprovided.jpg\" alt=\"OpenSecret No session token provided\" title=\"OpenSecret No session token provided\"><\/p>\n<p>Looking at the network call there, I just see this payload and these headers.  No Cookies sent, and I don&#8217;t see an Auth Header.<\/p>\n<pre>{\"name\":\"Bob Smith\",\"description\":\"Blah blah blah.  All you ever do is say blah like things.\"}<\/pre>\n<p><img decoding=\"async\" src=\"https:\/\/www.peteonsoftware.com\/images\/2026\/opensecret_requestheaders.jpg\" alt=\"OpenSecret Request Headers\" title=\"OpenSecret Request Headers\"><\/p>\n<p>Here is the response.<\/p>\n<pre>{\"message\":\"No session token provided\"}<\/pre>\n<p>So I need to see how this is being packaged up, so I take a look at the HTML source and &#8230; well, I guess the challenge is over.  There is nothing really interesting in the HTML source until you get to the script tag at the end.<\/p>\n<pre>\r\n &lt;script&gt;\r\n    \/\/ JWT Secret Key\r\n    const SECRET_KEY = \"HTB{0p3n_s3cr3ts_ar3_n0t_s3cr3ts}\";\r\n\r\n    \/\/ Helper function to convert string to Base64URL\r\n    function base64url(str) {\r\n        return btoa(str)\r\n            .replace(\/\\+\/g, \"-\")\r\n            .replace(\/\\\/\/g, \"_\")\r\n            .replace(\/=\/g, \"\");\r\n    }\r\n\r\n    \/\/ Generate a JWT session token for the user\r\n    async function generateJWT() {\r\n        \/\/ Check if user already has a token\r\n        const existingToken = document.cookie\r\n            .split(\"; \")\r\n            .find((row) =&gt; row.startsWith(\"session_token=\"));\r\n\r\n        if (existingToken) {\r\n            console.log(\"Session token already exists\");\r\n            return;\r\n        }\r\n\r\n        \/\/ Create a random guest username\r\n        const username = \"guest_\" + Math.floor(Math.random() * 10000);\r\n\r\n        \/\/ JWT Header\r\n        const header = { alg: \"HS256\", typ: \"JWT\" };\r\n\r\n        \/\/ JWT Payload\r\n        const payload = { username: username };\r\n\r\n        \/\/ Encode header and payload\r\n        const encodedHeader = base64url(JSON.stringify(header));\r\n        const encodedPayload = base64url(JSON.stringify(payload));\r\n        const data = encodedHeader + \".\" + encodedPayload;\r\n\r\n        \/\/ Sign with SECRET_KEY using HMAC-SHA256\r\n        const key = await crypto.subtle.importKey(\r\n            \"raw\",\r\n            new TextEncoder().encode(SECRET_KEY),\r\n            { name: \"HMAC\", hash: \"SHA-256\" },\r\n            false,\r\n            [\"sign\"]\r\n        );\r\n\r\n        const signature = await crypto.subtle.sign(\r\n            \"HMAC\",\r\n            key,\r\n            new TextEncoder().encode(data)\r\n        );\r\n\r\n        \/\/ Encode signature\r\n        const encodedSignature = base64url(\r\n            String.fromCharCode(...new Uint8Array(signature))\r\n        );\r\n\r\n        \/\/ Complete JWT token\r\n        const token = data + \".\" + encodedSignature;\r\n\r\n        \/\/ Store token in cookie\r\n        document.cookie = `session_token=${token}; path=\/; max-age=86400`;\r\n\r\n        console.log(\"Generated session for:\", username);\r\n    }\r\n\r\n    \/\/ Generate JWT token on page load\r\n    generateJWT();\r\n\r\n    \/\/ Handle ticket submission\r\n    document\r\n        .getElementById(\"submit-btn\")\r\n        .addEventListener(\"click\", async (event) =&gt; {\r\n            event.preventDefault();\r\n\r\n            const name = document.getElementById(\"ticket-name\").value;\r\n            const description =\r\n                document.getElementById(\"ticket-desc\").value;\r\n\r\n            const response = await fetch(\"\/submit-ticket\", {\r\n                method: \"POST\",\r\n                headers: {\r\n                    \"Content-Type\": \"application\/json\",\r\n                },\r\n                body: JSON.stringify({ name, description }),\r\n            });\r\n\r\n            const result = await response.json();\r\n            document.getElementById(\"message-display\").textContent =\r\n                result.message || \"Ticket submitted successfully!\";\r\n        });\r\n&lt;\/script&gt;\r\n<\/pre>\n<p><em><strong>Task 1 Answer: HTB{0p3n_s3cr3ts_ar3_n0t_s3cr3ts}<\/strong><\/em><\/p>\n<p>That&#8217;s all there was to it.  We don&#8217;t actually have to use that code and that &#8220;key&#8221; to impersonate anyone, this was just an easy example of the dangers of storing secrets openly (OHHHH, the room name makes so much sense now \ud83d\ude09 )<\/p>\n<p>If you have any questions, let me know!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Today, we&#8217;re going to tackle a Hack the Box Challenge called OpenSecret. Unlike the last few of these I&#8217;ve done, this is more of an offensive security challenge. Our challenge scenario is A simple help &hellip;<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[153],"tags":[141,142,149],"class_list":["post-2011","post","type-post","status-publish","format-standard","hentry","category-capture-the-flag","tag-information-security","tag-infosec","tag-offensive-security"],"_links":{"self":[{"href":"https:\/\/www.peteonsoftware.com\/index.php\/wp-json\/wp\/v2\/posts\/2011","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.peteonsoftware.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.peteonsoftware.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.peteonsoftware.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.peteonsoftware.com\/index.php\/wp-json\/wp\/v2\/comments?post=2011"}],"version-history":[{"count":0,"href":"https:\/\/www.peteonsoftware.com\/index.php\/wp-json\/wp\/v2\/posts\/2011\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.peteonsoftware.com\/index.php\/wp-json\/wp\/v2\/media?parent=2011"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.peteonsoftware.com\/index.php\/wp-json\/wp\/v2\/categories?post=2011"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.peteonsoftware.com\/index.php\/wp-json\/wp\/v2\/tags?post=2011"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}