El otro día me encontré con un servicio que verificaba la firma de las solicitudes en el lado del servidor. Se trataba de un pequeño casino online que, para cada solicitud, verificaba un valor enviado por el usuario desde el navegador. Independientemente de lo que estuvieras haciendo en el casino: hacer una apuesta o un depósito, un parámetro adicional en cada solicitud era el valor "sign", que consistía en un conjunto de caracteres aparentemente aleatorios. Era imposible enviar una solicitud sin él: el sitio devolvía un error y me impedía enviar mis propias solicitudes personalizadas.
De no haber sido por este valor, habría abandonado el sitio en ese momento y no habría vuelto a pensar en él. Pero, contra todo pronóstico, no fue la sensación de obtener ganancias rápidas lo que me entusiasmó, sino más bien el interés por la investigación y el desafío que me ofrecía el casino con su sistema infalible.
Independientemente del propósito que los desarrolladores tenían en mente cuando agregaron este parámetro, me parece que fue una pérdida de tiempo. Después de todo, la firma en sí se genera en el lado del cliente y cualquier acción del lado del cliente puede estar sujeta a ingeniería inversa.
En este artículo, hablaré sobre cómo logré:
Este artículo le enseñará cómo ahorrar su valioso tiempo y rechazar soluciones inútiles si es un desarrollador interesado en realizar proyectos seguros. Y si es un pentester, después de leer este artículo, podrá aprender algunas lecciones útiles sobre depuración, así como sobre cómo programar sus propias extensiones para la navaja suiza de la seguridad. En resumen, todos están en el lado positivo.
Vayamos finalmente al grano.
Entonces, el servicio es un casino en línea con un conjunto de juegos clásicos:
La interacción con el servidor funciona completamente en base a solicitudes HTTP. Independientemente del juego que elijas, cada solicitud POST al servidor debe estar firmada; de lo contrario, el servidor generará un error. La firma de solicitudes en cada uno de estos juegos funciona según el mismo principio: solo tomaré un juego para investigar, de modo que no tenga que hacer el mismo trabajo dos veces.
Y voy a coger un juego llamado Dragon Dungeon.
La esencia de este juego es elegir las puertas del castillo en orden, en el papel de un caballero. Detrás de cada puerta se esconde un tesoro o un dragón. Si el jugador se encuentra con un dragón detrás de una puerta, el juego se detiene y pierde dinero. Si se encuentra el tesoro, el importe de la apuesta inicial aumenta y el juego continúa hasta que el jugador se lleve las ganancias, pierda o supere todos los niveles.
Antes de comenzar el juego, el jugador debe especificar el monto de la apuesta y el número de dragones.
Ingreso el número 10 como suma, dejo un dragón y miro la solicitud que se enviará. Esto se puede hacer desde las herramientas para desarrolladores en cualquier navegador, en Chromium la pestaña Red es la encargada de esto.
Aquí también puedes ver que la solicitud se envió al punto final /srv/api/v1/dungeon
.
La pestaña Carga útil muestra el cuerpo de la solicitud en formato JSON
Los dos primeros parámetros son obvios: los elegí desde la interfaz de usuario; el último, como puedes adivinar, es timestamp
o el tiempo que ha transcurrido desde el 1 de enero de 1970, con la precisión típica de Javascript de milisegundos.
Eso deja un parámetro sin resolver: la firma en sí. Para entender cómo se forma, voy a la pestaña Fuentes: este lugar contiene todos los recursos del servicio que ha cargado el navegador, incluido Javascript, que es responsable de toda la lógica de la parte cliente del sitio.
No es tan fácil entender este código, está minimizado. Puedes intentar desofuscarlo por completo, pero es un proceso largo y tedioso que llevará mucho tiempo (teniendo en cuenta la cantidad de código fuente). No estoy listo para hacerlo.
La segunda opción, más sencilla, es simplemente buscar la parte necesaria del código mediante una palabra clave y utilizar el depurador. Eso es lo que haré yo, porque no necesito saber cómo funciona todo el sitio, sólo necesito saber cómo se genera la firma.
Entonces, para encontrar la parte del código que se encarga de generar el código, se puede abrir una búsqueda a través de todas las fuentes utilizando la combinación de teclas CTRL+SHIFT+F
y buscar la asignación de un valor a la clave sign
que se envía en la solicitud.
Afortunadamente sólo hay un partido, lo que significa que estoy en el camino correcto.
Si haces clic en una coincidencia, puedes acceder a la sección de código donde se genera la propia firma. El código está ofuscado como antes, por lo que sigue siendo difícil de leer.
Frente a la línea de código puse un punto de interrupción, actualicé la página y realicé una nueva oferta en "dragones"; ahora el script ha dejado de funcionar exactamente en el momento de la formación de la firma y puedes ver el estado de algunas variables.
La función llamada consta de una sola letra, las variables también, pero no hay problema. Puedes ir a la consola y visualizar los valores de cada una de ellas. La situación empieza a aclararse.
El primer valor que obtengo es el valor de la variable H
, que es una función. Puedes hacer clic en ella desde la consola y moverte al lugar donde está declarada en el código, a continuación se muestra el listado.
Este es un fragmento de código bastante grande en el que vi una pista: SHA256. Se trata de un algoritmo hash. También puedes ver que se pasan dos parámetros a la función, lo que indica que puede que no se trate solo de SHA256, sino de HMAC SHA256 con un secreto.
Probablemente las variables que se pasan aquí (también se envían a la consola):
10;1;6693a87bbd94061678473bfb;1732817300080;gRdVWfmU-YR_RCuSkWFLCUTly_GZfDx3KEM8
- directamente el valor al que se aplica la operación HMAC SHA256.31754cff-be0f-446f-9067-4cd827ba8707
es una constante estática que actúa como un secreto
Para asegurarme de esto, llamo a la función y obtengo la firma asumida.
Ahora voy al sitio que cuenta HMAC SHA256 y le paso valores.
Y comparándolo con el que me enviaron en la solicitud cuando hice la oferta.
El resultado es idéntico, lo que significa que mis conjeturas eran correctas: realmente utiliza HMAC SHA256 con un secreto estático, al que se le pasa una cadena especialmente formada con la tasa, el número de dragones y algunos otros parámetros, sobre los que les hablaré más adelante en el transcurso del artículo.
El algoritmo es bastante simple y directo, pero aún no es suficiente: si el objetivo de un proyecto de trabajo fuera encontrar vulnerabilidades, necesitaría aprender a enviar mis propias consultas mediante Burp Suite.
Y esto definitivamente necesita automatización, que es de lo que voy a hablar ahora.
He descubierto el algoritmo de generación de firmas. Ahora es el momento de aprender a generarlo automáticamente para abstraer todo lo innecesario al enviar solicitudes.
Puedes enviar solicitudes usando ZAP, Caido, Burp Suite y otras herramientas de pentest. Este artículo se centrará en Burp Suite, ya que considero que es la más fácil de usar y casi perfecta. La Community Edition se puede descargar de forma gratuita desde el sitio oficial y es suficiente para todos los experimentos.
Burp Suite no sabe generar HMAC SHA256 de forma predeterminada, por lo que para ello puede utilizar extensiones que complementen la funcionalidad de Burp Suite.
Las extensiones son creadas tanto por miembros de la comunidad como por los propios desarrolladores y se distribuyen a través de la tienda BApp gratuita integrada, Github u otros repositorios de código fuente.
Hay dos caminos que puedes tomar:
Cada uno de estos caminos tiene sus pros y sus contras, te los mostraré ambos.
El método con una extensión ya preparada es el más sencillo. Se trata de descargarla de la BApp Store y utilizar sus funciones para generar un valor para el parámetro sign
.
La extensión que utilicé se llama Hackvertor . Te permite utilizar una sintaxis similar a XML para poder codificar/decodificar, cifrar/descifrar y convertir en hash de forma dinámica diversos datos.
Para poder instalarlo Burp requiere:
Vaya a la pestaña Extensiones
Escribe Hackvertor en la búsqueda
Seleccione la extensión encontrada en la lista
Haga clic en Instalar
Una vez instalada, aparecerá en Burp una pestaña con el mismo nombre. Puedes acceder a ella y evaluar las capacidades de la extensión y la cantidad de etiquetas disponibles, cada una de las cuales se puede combinar con las demás.
Para dar un ejemplo, puedes cifrar algo con AES simétrico usando la etiqueta <@aes_encrypt('supersecret12356','AES/ECB/PKCS5PADDING')>MySuperSecretText<@/aes_encrypt>
.
El secreto y el algoritmo se encuentran entre paréntesis y, entre las etiquetas, el texto que se va a cifrar. Se pueden utilizar todas las etiquetas en Repeater, Intruder y otras herramientas integradas de Burp Suite.
Con la ayuda de la extensión Hackvertor puedes describir cómo se debe generar una firma a nivel de etiqueta. Lo haré con el ejemplo de una solicitud real.
Entonces, hago una apuesta en Dragon Dungeon, intercepto la misma solicitud que intercepté al principio de este artículo con Intercept Proxy y la estreso en Repeater para poder editarla y volver a enviarla.
Ahora, en lugar del valor ae04afe621864f569022347f1d1adcaa3f11bebec2116d49c4539ae1d2c825fc
, necesitamos sustituir el algoritmo para generar HMAC SHA256 utilizando las etiquetas proporcionadas por Hackvertor.
Формула генерации у меня получилась следующая <@hmac_sha256('31754cff-be0f-446f-9067-4cd827ba8707')>10;1;6693a87bbd94061678473bfb;<@timestamp/>000;MDWpmNV9-j8tKbk-evbVLtwMsMjKwQy5YEs4<@/hmac_sha256>
.
Considere todos los parámetros:
10
- importe de la apuesta1
- número de dragones6693a87bbd94061678473bfb
: ID de usuario único de la base de datos MongoDB. Lo vi mientras analizaba la firma desde el navegador, pero no escribí sobre ello en ese momento. Pude encontrarlo buscando en el contenido de las consultas en Burp Suite. Proviene de la consulta del punto final /srv/api/v1/profile/me
.
<@timestamp/>000
- generación de marca de tiempo, los últimos tres ceros refinan el tiempo a milisegundosMDWpmNV9-j8tKbk-evbVLtwMsMjKwQy5YEs4
: token CSRF, que se devuelve desde el punto final /srv/api/v1/csrf
y se sustituye en cada solicitud, en el encabezado X-Xsrf-Token
.<@hmac_sha256('31754cff-be0f-446f-9067-4cd827ba8707')>
y <@/hmac_sha256>
: etiquetas de apertura y cierre para generar HMAC SHA256 a partir del valor sustituido con el secreto como una constante 31754cff-be0f-446f-9067-4cd827ba8707
.
Importante tener en cuenta: los parámetros deben estar conectados entre sí a través de ;
en estricto orden, - de lo contrario la firma se generará incorrectamente - como en esta captura de pantalla donde he intercambiado la tasa y la cantidad de dragones
Ahí es donde reside toda la magia.
Ahora hago una consulta correcta, donde especifico los parámetros en el orden correcto, y obtengo información de que todo fue exitoso y el juego comenzó; esto significa que Hackvertor generó una firma en lugar de una fórmula, la sustituyó en la consulta y todo funciona.
Sin embargo, este método tiene una desventaja importante: no se puede prescindir por completo del trabajo manual. Cada vez que se cambia la tasa o la cantidad de dragones en JSON, hay que cambiarlos en la propia firma para que coincidan.
Además, si envía una nueva solicitud desde la pestaña Proxy a Intruder o Repeater, debe volver a escribir la fórmula, lo que resulta muy, muy inconveniente cuando necesita muchas pestañas para diferentes casos de prueba.
Esta fórmula también fallará en otras consultas donde se utilicen otros parámetros.
Así que decidí escribir mi propia extensión para superar estas desventajas.
Puedes escribir extensiones para Burp Suite en Java y Python. Yo usaré el segundo lenguaje de programación porque es más simple y visual. Pero debes prepararte de antemano: primero debes descargar Jython Standalone desde el sitio web oficial y luego la ruta al archivo descargado en la configuración de Burp Suite.
Después de eso, necesitas crear un archivo con el código fuente y la extensión *.py
.
Ya tengo un tocho que define la lógica básica, aquí está su contenido:
Todo es intuitivamente sencillo y directo:
getActionName
: este método devuelve el nombre de la acción que realizará la extensión. La extensión en sí misma agrega una regla de manejo de sesiones que se puede aplicar de manera flexible a cualquiera de las solicitudes, pero hablaremos más sobre eso más adelante. Es importante saber que este nombre puede ser diferente del nombre de la extensión y que se podrá seleccionar desde la interfaz.performAction
: aquí se explicará la lógica de la regla en sí, que se aplicará a las solicitudes seleccionadas.
Ambos métodos se declaran de acuerdo con la interfaz ISessionHandlingAction .
Ahora, la interfaz IBurpExtender declara el único método necesario, registerExtenderCallbacks
, que se ejecuta inmediatamente después de cargar la extensión y es necesario para que funcione.
Aquí es donde se realiza la configuración básica:
callbacks.setExtensionName(EXTENSION_NAME)
: registra la extensión actual como una acción para manejar sesionessys.stdout = callbacks.getStdout()
- redirige la salida estándar (stdout) a la ventana de salida de Burp Suite (el panel “Extensiones”)self.stderr = PrintWriter(callbacks.getStdout(), True)
: crea una secuencia para generar erroresself.stdout.println(EXTENSION_NAME)
: imprime el nombre de la extensión en Burp Suiteself.callbacks = callbacks
: guarda el objeto de callbacks como un atributo propio. Esto es necesario para el uso posterior de la API de Burp Suite en otras partes del código de extensión.self.helpers = callbacks.getHelpers()
- también obtiene métodos útiles que serán necesarios a medida que se ejecuta la extensión
Una vez realizados los preparativos preliminares, ya está. Ahora puedes cargar la extensión y asegurarte de que funciona. Para ello, ve a la pestaña Extensiones y haz clic en Agregar.
En la ventana que aparece, especifique
Y haga clic en Siguiente.
Si el archivo de código fuente se ha formateado correctamente, no deberían producirse errores y la pestaña Salida mostrará el nombre de la extensión. Esto significa que todo funciona correctamente.
La extensión se carga y funciona, pero lo único que se cargó fue un contenedor sin lógica alguna. Ahora necesito el código directamente para firmar la solicitud. Ya lo escribí y se muestra en la captura de pantalla a continuación.
La forma en que funciona toda la extensión es que antes de que la solicitud se envíe al servidor, será modificada por mi extensión.
Primero tomo la solicitud que interceptó la extensión y obtengo la tasa y la cantidad de dragones de su cuerpo.
json_body = json.loads(message_body) amount_currency = json_body["amountCurrency"] dragons = json_body["dragons"]
A continuación, leo la marca de tiempo actual y obtengo el token CSRF del encabezado correspondiente.
currentTime = str(time.time()).split('.')[0]+'100' xcsrf_token = None for header in headers: if header.startswith("X-Xsrf-Token"): xcsrf_token = header.split(":")[1].strip()
A continuación, la solicitud en sí se firma mediante HMAC SHA256.
hmac_sign = hmac_sha256(key, message=";".join([str(amount_currency), str(dragons), user_id, currentTime, xcsrf_token]))
La función en sí y las constantes que denotan el secreto y el ID del usuario fueron declaradas previamente en la parte superior.
def hmac_sha256(key, message): return hmac.new( key.encode("utf-8"), message.encode("utf-8"), hashlib.sha256 ).hexdigest() key = "434528cb-662f-484d-bda9-1f080b861392" user_id = "zex2q6cyc4ba3gvkyex5f80m"
Luego, los valores se escriben en el cuerpo de la solicitud y se convierten a JSON.
json_body["sign"] = hmac_sign json_body["t"] = currentTime message_body = json.dumps(json_body)
El paso final es generar una solicitud firmada y modificada y enviarla a
httpRequest = self.helpers.buildHttpMessage(get_final_headers, message_body) baseRequestResponse.setRequest(httpRequest)
Eso es todo, el código fuente está escrito. Ahora puedes volver a cargar la extensión en Burp Suite (debe hacerse después de cada modificación del script) y asegurarte de que todo funciona.
Pero primero debes agregar una nueva regla para procesar las solicitudes. Para ello, ve a Configuración, a la sección Sesiones. Aquí encontrarás todas las diferentes reglas que se activan al enviar solicitudes.
Haga clic en Agregar para agregar una extensión que se active en determinados tipos de solicitudes.
En la ventana que aparece dejo todo como está y selecciono Agregar en acciones de Regla
Aparecerá una lista desplegable. En ella, seleccione Invocar una extensión Burp.
Y especificarle la extensión que se llamará al enviar solicitudes. Tengo una y es Burp Extension.
Luego de seleccionar la extensión, hago clic en Aceptar. Y voy a la pestaña Ámbito, donde especifico:
Ámbito de aplicación de las herramientas: Repetidor (la extensión debería activarse cuando envío solicitudes manualmente a través del Repetidor)
Ámbito de URL: incluye todas las URL (para que funcione en todas las solicitudes que envío).
Debería funcionar como en la captura de pantalla a continuación.
Después de hacer clic en Aceptar, la regla de extensión apareció en la lista general.
¡Por fin puedes probar todo en acción! Ahora puedes cambiar alguna consulta y ver cómo se actualiza dinámicamente la firma. Y aunque la consulta fallará, será porque elegí una tasa negativa, no porque haya algo mal con la firma (simplemente no quiero gastar dinero 😀). La extensión en sí funciona y la firma se genera correctamente.
Todo está muy bien, pero hay tres problemas:
Para resolver esto, necesitamos agregar dos solicitudes adicionales, que se pueden realizar mediante la biblioteca incorporada de Burp Suite, en lugar de las de terceros, en lugar de requests
.
Para ello, he incluido una lógica estándar para que las consultas sean más cómodas. A través de los métodos estándar de Burp, la interacción con las consultas se realiza en pleintext.
def makeRequest(self, method="GET", path="/", headers=None, body=None): first_line = method + " " + path + " HTTP/1.1" headers[0] = first_line if body is None: body = "{}" http_message = self.helpers.buildHttpMessage(headers, body) return self.callbacks.makeHttpRequest(self.request_host, self.request_port, True, http_message)
Y agregué dos funciones que extraen los datos que necesito, el token CSRF y el UserID.
def get_csrf_token(self, headers): response = self.makeRequest("GET", "/srv/api/v1/csrf", headers) message = self.helpers.analyzeRequest(response) raw_headers = str(message.getHeaders()) match = re.search(r'XSRF-TOKEN=([a-zA-Z0-9_-]+)', raw_headers) return match.group(1) def get_user_id(self, headers): raw_response = self.makeRequest("POST", "/srv/api/v1/profile/me", headers) response = self.helpers.bytesToString(raw_response) match = re.search(r'"_id":"([a-f0-9]{24})"', response) return match.group(1)
Y actualizando el token en sí mismo en los encabezados enviados
def update_csrf(self, headers, token): for i, header in enumerate(headers): if header.startswith("X-Xsrf-Token:"): headers[i] = "X-Xsrf-Token: " + token return headers
La función de firma se ve así. Aquí es importante tener en cuenta que tomo todos los parámetros personalizados que se envían en la solicitud, agrego el user_id
estándar, currentTime
y csrf_token
al final de ellos y los firmo todos juntos usando ;
como separador.
def sign_body(self, json_body, user_id, currentTime, csrf_token): values = [] for key, value in json_body.items(): if key == "sign": break values.append(str(value)) values.extend([str(user_id), str(currentTime), str(csrf_token)]) return hmac_sha256(hmac_secret, message=";".join(values))
El flu principal se redujo a unas pocas líneas:
OrderedDict
, que genera el diccionario en una secuencia rígida, ya que es importante conservarlo al firmar. csrf_token = self.get_csrf_token(headers) final_headers = self.update_csrf(final_headers, csrf_token) user_id = self.get_user_id(headers) currentTime = str(time.time()).split('.')[0]+'100' json_body = json.loads(message_body, object_pairs_hook=OrderedDict) sign = self.sign_body(json_body, user_id, currentTime, csrf_token) json_body["sign"] = sign json_body["t"] = currentTime message_body = json.dumps(json_body) httpRequest = self.helpers.buildHttpMessage(final_headers, message_body) baseRequestResponse.setRequest(httpRequest)
Una captura de pantalla, solo para estar seguro
Ahora, si vas a otro juego donde los parámetros personalizados ya son 3 en lugar de 2 y envías una solicitud, podrás ver que se enviará correctamente. Esto significa que mi extensión ahora es universal y funciona para todas las solicitudes.
Ejemplo de envío de una solicitud de reposición de cuenta
Las extensiones son una parte integral de Burp Suite. A menudo, los servicios implementan funciones personalizadas que nadie más que usted escribirá de antemano. Por eso es importante no solo descargar extensiones ya preparadas, sino también escribir las suyas propias, que es lo que intenté enseñarle en este artículo.
Eso es todo por ahora, mejora y no te enfermes.
Enlace al código fuente de la extensión: *clic* .