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
Terminal Output
Terminal Output
- Port 22: Running SSH.
- Port 80: Web server redirect to http://gavel.htb
Edit the Hosts file
As always, we edit the/etc/hosts file to add the hostname:
Attacker Linux
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
Terminal Output
- Filtered all static images & files from
/assetsfolder.
DirSearch:
Running another tool just to be sure we are not missing anything important:Attacker Linux
- Aha. We found
.gitthat are missing from the default wordlist of Feroxbuster. - Before we check the
.gitdirectory, 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 cookiegavel_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 thegit log command to check the commit history:
Attacker Linux
git diff command:
Attacker Linux
- Only the
default.yamlhas 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
- 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 usesrunkit:
PHP snippet
- With some research, we note that
runkitis a PHP extension that allow users to create user-defined functions & classes.
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
- runkit7_function_add — Add a new function, similar to create_function()
PHP: create_function - ManualThis 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.
PHP snippet
- We can see that the function is taking the
$rulevariable as the codes to be executed. - The
$rulevalue is set by$rule = $auction['rule'];
$auction value is set by fetching data from the database:
PHP snippet
rules/default.yaml:
The example rules can be seen inrules/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 inputuser_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
$sortItemvalue from user input is sanitized by replacing the backticks, the$userIdwas missed out for validation.
$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:- 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. - 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
?) (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 thePDO::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
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
$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_idparameter, we can trick PDO into parsing the malicious payload into the$colposition, and construct a functional SQL query.
?) 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.?--.
$userId will be placed into the first ? in the query:
SQL Query
$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.
$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
'xas 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.
'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 yis required for a sub-query to be performed within the main query.
') 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
$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.
HTTP Request
user_idis where our malicious query lies.sortis what would become$colin the query.
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 (hereschema_name), for aliasing the resulting table as`'x`.
HTTP Response
- There is a custom database
gavel.
HTTP Request
- For some reason, the
WHEREclause did not work.
HTTP Request
users table is likely valuable.
HTTP Response
WHERE clauses for column name is not working, therefore we have to list all columns from all tables:
HTTP Request
users table:
HTTP Response
HTTP Request
HTTP Response
- The admin username is just
auctioneer, which is the same name as the role.
CONCAT function in MySQL to return two columns in one string.
HTTP Request
HTTP Response
Cracking Password Hashes with Hashcat
Starting from the configuration password hash, let’s check if it is crackable.Password Hash
Attacker Windows
Attacker Windows
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 theadmin.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
wget request to see if we can use the system function to execute system commands:
HTTP Request
HTTP Request
- May require a few clicks on the button.
HTTP Response
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
rulebox in the admin panel.
Attacker Linux
Victim Linux
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!
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
LinPEAS Output
- 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
gaveldirectory in the/optdirectory. - There is an
invoice.txtin 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
/usr/local/bin/gavel-util:
Thegavel-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
Victim Linux
invoice.txt:
Theinvoice.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 thegavel 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 runningstrings to see what is in the binary.
Victim Linux
- 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 thephp.iniconfiguration file, the binary is likely running a PHP sandbox for PHP code execution.
Victim Linux
/opt/gavel/.config/php/php.ini:
This is thephp.ini config file being referenced in the binary. As we can see, this is a heavily sandboxed PHP environment.
Victim Linux
- The
open_basediris set as/opt/gavel, meaning that all file reads & writes are restricted to the/opt/gaveldirectory only. - The
disable_functionscontains almost all command execution and file read & write. However, thefile_put_contentsfunction is not disabled. There may be potential file write within the/opt/gaveldirectory. See more at PHP: file_put_contents - Manual. - The
allow_url_fopenandallow_url_includeare both off, disallowing all file inclusion possibilities.
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 runstrings 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
- From the error messages (
send xxx failedandNo 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/gaveldbinary (that is running by root).
netstat or ss command, we can confirm that the socket is listening:
Victim Linux
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
/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
submit command directly with the sample.yaml file we previously found in the /opt/gavel directory. However, the following error is returned:
Victim Linux
item, instead of the 6 required keys.
Victim Linux
sample.yaml file is root-owned and non-writeable, we will simply create a copy in our home directory.
Victim Linux
nano to modify the file.
Victim Linux
- In the case where text editors are not available, we can always use commands like
catto 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.
nano, we will remove the item key, and all trailing spaces in front of the 6 keys.
/tmp/sample.yaml
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
- The last (4th) argument of the
__sandbox_eval()function is very likely taking the PHP code from therulevalue from the supplied YAML file. - The response of the function is stored in
$res. - The value of
$resis then passed through two checks. - The first check looks for a Boolean response from the output of the function. The error
SANDBOX_RETURN_ERRORis returned if the output is not a Boolean value. - The second check validates if the value of
$resisTrue. If it is returningTrue, the server should respond withILLEGAL_RULE. - However, if we look closer at the codes, we can see that the
ifconditionals only returnecho, with no additional actions that may stop the codes from running. - Moreover, the
__sandbox_eval()is run BEFORE theifconditionals, 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
- While most functions are disabled, the
file_put_contentsfunction is still available, which allows root-privilege file write. - File writes are restricted to the
/opt/gaveldirectory only.
php.ini file clean, essentially removing all restrictions.
Some extra readings during research:
- The
php.inifile is loaded every time when run on CLI - PHP: The configuration file - Manual. Therefore no restart of PHP service is required in this scenario. - The
php.inifile is not required for PHP to run - what happens if php.ini is missing? - Stack Overflow. Therefore a bare-bone (empty)php.iniwill still work.
Overwriting the php.ini file
We can construct the malicious YAML file using nano.
Victim Linux
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
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
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
rce.yaml
gaveld server.
Victim Linux
Attacker Linux
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 thePDO::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:
- The
php.inifile is loaded every time when run on CLI - PHP: The configuration file - Manual. Therefore restarting PHP service is not required for the modification of thephp.inifile to be updated. - The
php.inifile is not required for PHP to run - what happens if php.ini is missing? - Stack Overflow. Therefore a bare-bone (empty)php.iniwill still work.

