Skip to main content

Starting the box


Link to the box: https://app.hackthebox.com/machines/gavel

Port Scan

We start off the box by running a port scan on the provided IP.
Attacker Linux
Output of Rustscan
Terminal Output
Output of Nmap:
Terminal Output
A few key notes:

Edit the Hosts file

As always, we edit the /etc/hosts file to add the hostname:
Attacker Linux
/etc/hosts
Nano Interface

Initial Foothold


Enumerating Port 80: Web Server

Subdomain Enumeration

We first run a quick vhost brute-force to look for potential subdomains.
GoBuster (vhost):
We will brute-force the sub-domains using GoBuster’s vhost mode.
Attacker Linux
  • There are only error responses, no valid subdomains are listed.

Directing Busting

We will first run a quick directory enumeration with Feroxbuster.
Feroxbuster:
Attacker Linux
Key output:
Terminal Output
  • Filtered all static images & files from /assets folder.
DirSearch:
Running another tool just to be sure we are not missing anything important:
Attacker Linux
  • Aha. We found .git that are missing from the default wordlist of Feroxbuster.
  • Before we check the .git directory, let’s first manually enumerate the website.

Enumerating the webpage

http://gavel.htb This is an auction site with user registration & login functionality.

Manual Enumeration

register.php:
http://gavel.htb/register.php We will register a user with the following credentials:
login.php:
http://gavel.htb/login.php We now login with the newly registered credentials, and we are redirected back to http://gavel.htb/index.php.
index.php:
http://gavel.htb/index.php: A quick check on the cookie gavel_session reveals that the HttpOnly flag is on, meaning that we will not be able grab the token from attacks like XSS. We now have access to the following new pages:
  • /inventory.php, and
  • /bidding.php
bidding.php:
http://gavel.htb/bidding.php We can submit bids to the bidding items. A bid is submitted via a POST request to the /includes/bid_handler.php. A success or error message in JSON will be returned. Once the bidding time has passed, the page will refresh, and the user has the highest bid will have that item in the inventory.php page. For each bidding item, there is also a specific bidding rule. For example, some item may require Bid at least 10% more than the current price, whereas some may ask for Bids must be in multiples of 5. Your account balance must cover the bid amount, etc. So far we have understood the basic functionalities of the application. Let’s move on to the exposed .git directory to look for potential source code files.

Enumerating .git directory

We can use this handy tool, git-dumper, to easily dump the whole git repository from a website.
Attacker Linux

Git Commit Review

We can run the git log command to check the commit history:
Attacker Linux
To check the difference between the first and last commit, we can run the git diff command:
Attacker Linux
  • Only the default.yaml has been changed.
  • There are no deleted / removed credentials.

Source Code Review

We can now enumerate the source code of the website.
Attacker Linux
.git/config:
Attacker Linux
  • A username: sado.
includes/config.php:
Attacker Linux
A few key notes:
  • Database is running locally on the web server.
  • The Database name, user and password are all gavel.
  • From db.php, we can know that it is running on MySQL.

Identifying vulnerable functions

Locating function vulnerable to RCE

includes/bid_handler.php:
After diving deep into the code, we notice the following function uses runkit:
PHP snippet
  • With some research, we note that runkit is a PHP extension that allow users to create user-defined functions & classes.
In particular, the runkit_function_add function allows users to create a new function that will run eval() in the background on the provided code.
PHP: runkit7_function_add - Manual
PHP: create_function - Manual
  • create_function β€” Create a function dynamically by evaluating a string of code
  • This function internally performs an eval() and as such has the same security issues as eval(). It also has bad performance and memory usage characteristics, because the created functions are global and can not be freed.
This function takes the following arguments:
PHP snippet
  • The first argument takes the function name.
  • The second argument takes the list of arguments for this function.
  • The third argument takes the PHP codes to run.
Comparing to our code snippet:
PHP snippet
  • We can see that the function is taking the $rule variable as the codes to be executed.
  • The $rule value is set by $rule = $auction['rule'];
The $auction value is set by fetching data from the database:
PHP snippet
rules/default.yaml:
The example rules can be seen in rules/default.yaml. The rule values are written in PHP codes.
rules/default.yaml
admin.php:
From the codes, we learn that the rules can be updated from the admin panel. Therefore, the goal here is to escalate ourselves into application admin (auctioneer role) first.
PHP snippet

Locating function vulnerable to SQLi

inventory.php:
After some more deep diving, we notice the user input user_id in the sorting function in inventory.php is not validated and sanitized at all.
PHP snippet
  • While prepared statement is in-use, and the $sortItem value from user input is sanitized by replacing the backticks, the $userId was missed out for validation.
Also, we can see that the $results (column data) are simply all fetched and returned directly in the $name variable, which is visible to the user in the item cards.
PHP snippet
Breaking out the Prepared Statement in PHP:
Normally, prepared statements prevent SQL injections by separating query and data for the database. Conceptually, it splits the query into 2 phases:
  1. Prepare - Defining Query Structure. The database will first receive a β€œprepared statement”, which is a pre-defined SQL query with the required data being replaced by placeholders (?). The database will then parse & ready to execute this query.
  2. Execute - Supplying Data. User-supplied values are then sent to the database as literal strings. The database will not re-parse the received strings as SQL.
PHP snippet
However, placeholders (?) (or bound parameters) in prepared statements can only parameterize values, not database identifiers, such as column names, table names, etc., since they will be interpreted as literal text. To allow user-supplied database identifiers in prepared statement, we will have to concatenate the identifier to the statement, similar to the implementation in the above codes. However, to do this safely, either a whitelist approach (validate against a list of allowed identifiers), or heavy sanitization has to be performed
PHP snippet

Exploiting SQLi in PHP PDO Prepared Statement

Identifying the SQLi vulnerability

After some research, from the article Novel SQL Injection Technique in PDO Prepared Statements, we learn that PDO emulates all prepared statements in MySQL by default, unless if the PDO::ATTR_EMULATE_PREPARES attribute is explicitly set as false. PDO will run its own SQL parser to escape strings and build the prepared statement, and it then sends the parsed full SQL statement to the database. This opens an attack vector towards SQLi - if we can trick the PDO parser into mis-parsing our input, we can potentially smuggle in our query. First, we can check if the attribute is enabled by checking db.php. Since PDO::ATTR_EMULATE_PREPARES is not explicitly called, we can assume that it is set to true by default.
PHP snippet
In inventory.php, we can see that there is only little sanitization on the $col user input, and there is no sanitization in $userId. Only the backticks are removed from $sortItem.
PHP snippet
Based on the techniques shared in the above article, we can identify the following vulnerable injection point. We can see that $col is being directly concatenated into the query.
PHP snippet
  • If we can smuggle in a bound parameter (?), and supply our malicious payload via the non-validated $user_id parameter, we can trick PDO into parsing the malicious payload into the $col position, and construct a functional SQL query.
According to the article, we can sneak in the bound parameter (?) in $col simply by having it followed by a null byte (%00 or \0). This breaks the parsing logic for the second backtick, and it will ignore the first backtick with the SKIP_ONE(PDO_PARSER_TEXT) parsing logic for special characters. This thus makes the PDO parser sees the ? as a legit bound parameter, since the backticks are ignored. We can see the PDO parsing logic here: php-src/ext/pdo_mysql/mysql_sql_parser.re at master Β· php/php-src.
PHP snippet
  • The article also mentioned that null byte is not even necessary for older versions of PHP to pull off this attack.
  • I personally find that the null byte (%00) does not matter at all here, as long as there is a comment operator following the ?, i.e. ?--.
Now, anything in $userId will be placed into the first ? in the query:
SQL Query
Say if $userId = x:
SQL Query
  • The parser should return a syntax error on the first line, since the second backtick is corrupted / disabled, causing the backtick not closing properly.
We can close it by adding a backtick after our $userId value - x`;--, with the semicolon & comment to close off the remaining part of the query.
SQL Query
  • Now, this is a correctly parsed statement, and MySQL will interpret 'x as a column name.
  • Again from my attempts, I notice that the comment operator (--) after the semicolon does not matter at all (the one in $userId). However, the one after ? (in $col) must be present to make the payload work.
Since we can make MySQL interpret 'x as a column name, now we just need to create a subquery and name the resulting table as 'x. Something like this:
SQL Query
  • The alias AS y is required for a sub-query to be performed within the main query.
However, directly injecting this payload is not possible, since the single quote (') will be escaped by PDO by adding a backslash (\'). The data part is parsed into a string that is wrapped between 2 single quotes before constructing the full SQL query. Therefore, sending over 'x, will result in '\'x' in PDO’s parsing logic. The trick here is to simply make our resulting column name into \'x by adding a backslash before the ?, which makes the resulting payload looks like this:
Payload
The PDO parser will take in the $userId value x` ... ;--, and place it into ?, which makes the resulting column name as \'x.
SQL Query
  • This is the final working SQL injection payload.
We can validate this by sending over the payload:
HTTP Request
  • user_id is where our malicious query lies.
  • sort is what would become $col in the query.
The response:
HTTP Response

Exploiting SQLi for Credentials

Let’s query what databases are present:
HTTP Request
  • Note that the AS `'x` should be placed after the column (here schema_name), for aliasing the resulting table as `'x`.
The response:
HTTP Response
  • There is a custom database gavel.
Let’s list the tables from gavel:
HTTP Request
  • For some reason, the WHERE clause did not work.
Listing all tables worked:
HTTP Request
Some of the outputs. The users table is likely valuable.
HTTP Response
Similarly, the WHERE clauses for column name is not working, therefore we have to list all columns from all tables:
HTTP Request
Some of the outputs that are likely from the users table:
HTTP Response
We can then try to extract the credentials using the following payload:
HTTP Request
The response:
HTTP Response
  • The admin username is just auctioneer, which is the same name as the role.
Checking the password. We can use the CONCAT function in MySQL to return two columns in one string.
HTTP Request
The response:
HTTP Response

Cracking Password Hashes with Hashcat

Starting from the configuration password hash, let’s check if it is crackable.
Password Hash
We’ll use hashcat’s auto-detect mode to determine its hash type. We’ll start from mode 3200.
Attacker Windows
Using mode 3200 on the hash, and it is successfully cracked.
Attacker Windows
We can now login to the application with the following credentials:
Credentials
  • Since the password is very weak, it is also possible that we can simply brute-force our way into the application.

Exploiting RCE vulnerability in PHP Runkit

Command Injection via Admin Panel

http://gavel.htb/admin.php With the newly obtained credentials, we can now access the admin.php panel, which we have previously identified to be the injection point for the RCE vulnerability in includes/bid_handler.php. The auction rules & messages can be updated via the following POST request:
HTTP Request
We will first send a simple wget request to see if we can use the system function to execute system commands:
HTTP Request
Then, on http://gavel.htb/bidding.php, we will place a bid on the corresponding auction item to trigger the custom function created by runkit:
HTTP Request
  • May require a few clicks on the button.
We received the following responses from the server:
HTTP Response
At the same time, we received a call back on our controlled web server, indicating that RCE with the system() function is possible.
Attacker Linux

Obtaining a Reverse Shell

After a couple attempts, the following reverse shell payload worked:
PHP snippet
  • This can be placed directly inside the rule box in the admin panel.
On our Penelope reverse shell listener, we successfully established a shell session:
Attacker Linux
After poking around for a while, we notice that there is only one user directory.
Victim Linux
Since this account has the same username (auctioneer) with the one that we obtained the password, we simply try to spray the password and see if we can login:
Victim Linux
  • Success!
Getting the user flag:
Victim Linux

Privilege Escalation


Post-ex Enumeration

Checking Sudo Exploitability

Always a quick run on sudo before everything. Let’s move on since we cannot use sudo.
Victim Linux

Running LinPEAS

As usual, let’s first run LinPEAS for some quick information gathering.
Victim Linux
Some interesting output from LinPEAS:
LinPEAS Output
A few key notes:
  • A few custom scripts & binaries are running as root.
  • No interesting internal ports are opened except Port 33060.
  • There is a user-owned / added executable: /usr/local/bin/gavel-util.
  • There is a gavel directory in the /opt directory.
  • There is an invoice.txt in the / (system root) directory.
Checking Port 33060:
We start off by trying to login to MySQL on port 33060. However, the following error message returned.
Victim Linux
While we can probably try to port-forward the service and enumerate it using an updated client on our machine, it is too much work. Let’s move on and come back to this later if everything else did not work.
/usr/local/bin/gavel-util:
The gavel-util feels way more important since it’s named after the box (gavel). A quick ownership check shows that our user is in the group listed in the binary, although it has the same permission with other users (read & execute).
Victim Linux
A quick run of the application returned the following menu. While it is worth investigating, let’s also check out other interesting files and come back later.
Victim Linux
invoice.txt:
The invoice.txt looks like an output of some application, likely related to the auction application. Maybe it is generated from gavel-util?
Victim Linux

Enumerating /opt directory

Enumerating files in /opt/gavel

Let’s enumerate the gavel folder in opt:
Victim Linux
  • There are quite a few root-owned files, some readable, some not. Let’s enumerate them one by one.
/opt/gavel/sample.yaml:
Starting with the YAML file, we can see that this is a template for an auction item, with rules that are written in PHP codes. It looks very similar to the ones that we exploited for RCE on the web application.
Victim Linux
/opt/gavel/gaveld:
This is a root-owned binary. We can try running strings to see what is in the binary.
Victim Linux
A few key notes:
  • It is likely reading YAML files from /opt/gavel/submission/.
  • There may be a log file in /var/log/gavel_prod.log.
  • It is likely creating the invoice.txt.
  • It is likely binding to the socket on /var/run/gaveld.sock.
  • It is likely using a PHP config file in /opt/gavel/.config/php/php.ini.
  • There is a __sandbox_eval() function written in PHP. Combining this with the php.ini configuration file, the binary is likely running a PHP sandbox for PHP code execution.
We can try to run the binary by making a copy of it. However, an error message is returned. This is a good sign, since that probably means the binary is already running, likely by the root user as we have enumerated.
Victim Linux
/opt/gavel/.config/php/php.ini:
This is the php.ini config file being referenced in the binary. As we can see, this is a heavily sandboxed PHP environment.
Victim Linux
A few key notes:
  • The open_basedir is set as /opt/gavel, meaning that all file reads & writes are restricted to the /opt/gavel directory only.
  • The disable_functions contains almost all command execution and file read & write. However, the file_put_contents function is not disabled. There may be potential file write within the /opt/gavel directory. See more at PHP: file_put_contents - Manual.
  • The allow_url_fopen and allow_url_include are both off, disallowing all file inclusion possibilities.
Let’s move on to the gavel-util we found, since there is not much we can do for now.

Enumerating the gavel-util binary

Inspecting the gavel-util binary

Again, we can run strings to inspect the binary. Since this is not an SUID binary, and we are not running it in sudo, the binary itself should not have any elevated privileges that can be abused. However, a deeper look into the string outputs shows that this binary may just be a client that sends data to the server.
Victim Linux
A few key notes:
  • From the error messages (send xxx failed and No response from server, etc.), we can induce that this binary is only running as a client, and it is sending data to a locally hosted server (that may be running as root).
  • The server is likely listening on a socket on /var/run/gaveld.sock, which shares the same name as the /opt/gavel/gaveld binary (that is running by root).
By running the netstat or ss command, we can confirm that the socket is listening:
Victim Linux
Therefore, the attack plan is clear. By supplying a malicious YAML file to the gavel-util binary, the binary will relay the input to the root-running server, opening possibilities for root-level code execution.

Running the gavel-util binary

Running the binary returns the help menu. We can see that there are three commands that we can run.
Victim Linux
Running the invoice command:
Running the invoice command returns an error. It is also not taking any additional arguments. From the strings dumped from the gaveld binary, we can assume that this is likely trying to read the log from /var/log/gavel_prod.log.
Victim Linux
However, there is no such file in the /var/log folder:
Victim Linux
Running the stat command:
The stat command does not take any inputs and only lists the auction dashboard. Nothing of interest so far.
Victim Linux
Running the submit command:
The submit command has the highest attacking value, since it is taking a user-supplied YAML file, and potentially parsing it in a root context. Running the command alone returned an error asking for a file.
Victim Linux
We can try to run the submit command directly with the sample.yaml file we previously found in the /opt/gavel directory. However, the following error is returned:
Victim Linux
It means that the binary cannot find the keys from the YAML file. Re-checking the file shows that the top level key here is item, instead of the 6 required keys.
Victim Linux
Since the original sample.yaml file is root-owned and non-writeable, we will simply create a copy in our home directory.
Victim Linux
We can then run text editors like nano to modify the file.
Victim Linux
  • In the case where text editors are not available, we can always use commands like cat to input everything within the command line, or we can edit the file offline in our attacker server, then upload the edited file to the victim machine.
Inside nano, we will remove the item key, and all trailing spaces in front of the 6 keys.
/tmp/sample.yaml
Now we can try resubmitting using the fixed YAML file. We can see that the server happily takes the file.
Victim Linux

Exploiting Command Injection for Privilege Escalation

Inspecting the gaveld binary (again)

Since we now know that the /opt/gavel/gaveld binary is running as root and is a server that takes requests from users, we can revisit the strings output to look for potentially vulnerable behaviours. As mentioned, the __sandbox_eval() function is written in PHP and will execute PHP codes. From our initial foothold path, we know that the application is directly executing the PHP codes from rule in the custom ruleCheck function created by runkit. Here, the __sandbox_eval() function looks almost identical.
PHP snippet from gaveld
A few key notes from reviewing the function:
  • The last (4th) argument of the __sandbox_eval() function is very likely taking the PHP code from the rule value from the supplied YAML file.
  • The response of the function is stored in $res.
  • The value of $res is then passed through two checks.
  • The first check looks for a Boolean response from the output of the function. The error SANDBOX_RETURN_ERROR is returned if the output is not a Boolean value.
  • The second check validates if the value of $res is True. If it is returning True, the server should respond with ILLEGAL_RULE.
  • However, if we look closer at the codes, we can see that the if conditionals only return echo, with no additional actions that may stop the codes from running.
  • Moreover, the __sandbox_eval() is run BEFORE the if conditionals, which means that there is actually no validations or checks.

Inspecting the php.ini file (again)

The __sandbox_eval() function name has hinted that the function is running in a heavily restricted environment. As we previously enumerated, the php.ini is likely the main configuration file that sets the restrictions.
Victim Linux
We previously noted that:
  • While most functions are disabled, the file_put_contents function is still available, which allows root-privilege file write.
  • File writes are restricted to the /opt/gavel directory only.
Combining the above two observations, we can exploit this restricted file-write vulnerability by using it to wipe the php.ini file clean, essentially removing all restrictions. Some extra readings during research:

Overwriting the php.ini file

We can construct the malicious YAML file using nano.
Victim Linux
Since file_put_contents overwrites the file with the provided input by default, we can simply supply an empty string ('') to overwrite the php.ini file, removing all restrictions.
put_file_contents.yaml
We then submit the malicious YAML file to the gaveld server. While the SANDBOX_RETURN_ERROR error is returned, we know from previous code reviews that the PHP codes were being executed regardless.
Victim Linux
We can confirm this by checking the file content of php.ini. As we can see, there is no output when we attempt to cat the file. Using ls also shows that the file size is 0, confirming that our PHP code execution was successful.
Victim Linux

Obtaining a Root Shell

Now that all the restrictions are gone, we can send over a malicious YAML file that contains a reverse shell payload.
Victim Linux
We will use the same payload we used for initial foothold.
rce.yaml
Lastly, we submit the malicious YAML file to the gaveld server.
Victim Linux
On our Penelope reverse shell listener, we successfully received a shell from the machine.
Attacker Linux
Grabbing the root flag.
Victim Linux

Key Learnings


1. Bypassing Prepared Statements in Query Emulation

For PHP applications, if SQL query emulation is on, it is possible to bypass prepared statements by manipulating the PDO parser logic. Unless if the PDO::ATTR_EMULATE_PREPARES is explicitly set as false, PDO emulates all prepared statements in MySQL by default.
  • PDO will run its own SQL parser to escape strings and build the prepared statement, and it then sends the parsed full SQL statement to the database.
  • Therefore, it is possible to abuse the parsing logic of PDO to sneak in SQL queries into the prepared statement.
  • See more from: Novel SQL Injection Technique in PDO Prepared Statements.

2. Working with the php.ini file

A few facts about the php.ini file:

Tags


Initial Access

#Web_Exploit #Git #Source_Code_Review #SQLi #PHP #Prepared_Statements #Command_Injection #Password_Spraying

Privilege Escalation

#Sensitive_Files #File_Write #PHP #Command_Injection
Last modified on May 24, 2026