
Exploit para desbordamiento de búfer basado en pila encontrado en el binario conn-indicator del router TP-Link Archer AX50
El router TP-Link Archer AX50 es vulnerable a un desbordamiento de búfer basado en pila en su versión de firmware 1.0.14 Build 20240108 rel.42655(4555), lo que conduce a la ejecución remota de código tanto en la LAN como en la WAN. Esta vulnerabilidad tiene la misma causa raíz que la CVE-2020-10881, encontrada por el equipo Flashback y detallada ampliamente en su serie de videos al respecto (consulte las referencias). Sin embargo, el proceso de explotación difiere un poco, por lo que se tuvo que escribir un nuevo exploit.

Esta vulnerabilidad ocurre en el binario conn-indicator, que se encarga de comprobar si el router está conectado a Internet enviando periódicamente consultas DNS y escuchando sus respuestas en un puerto UDP aleatorio entre 32000 y 61000.
La función que recibe y procesa primero estos paquetes de respuesta DNS es TPDns_RecvAndResolve(), ubicada en 0x00405e3c. Al recibir un paquete con recvfrom, se almacena en buf, que tiene un tamaño de 2960 bytes. Luego, comprueba que el código de retorno sea correcto (RCODE == 0) y verifica el número de preguntas y respuestas del paquete (QDCOUNT y ANCOUNT). Para procesar las respuestas, llama a process_resolved_IP() y le pasa un puntero a buf, answer_ptr, que es un puntero a donde se encuentran las respuestas dentro del paquete en buf, el número de respuestas (ANCOUNT) y otros indicadores:
undefined4 * TPDns_RecvAndResolve(int socket,void *param_2,int param_3){
[...]
byte buf [2960];
[...]
while( true ) {
recv_bytes = recvfrom(socket,buf + total_recv_bytes,0xb90 - total_recv_bytes,0,&sStack_50,
local_38);
piVar2 = __errno_location();
if (recv_bytes == 0) goto RECV_ERROR;
if (recv_bytes < 0) break;
total_recv_bytes = total_recv_bytes + recv_bytes;
if (2959 < total_recv_bytes) goto PROCESS_DNS_RESP;
}
[...]
/* Check that ANCOUNT is not 0 (answer contains at least one domain) */
puVar5 = (undefined4 *)0x0;
if (buf._6_2_ != 0) {
local_40 = 0;
puVar5 = process_resolved_IP(buf,answer_ptr,(uint)buf._6_2_,&local_3c,&local_40);
answer_ptr = answer_ptr + local_40;
}
[...]
La función process_resolved_IP(), ubicada en 0x00405818, recorre cada respuesta y llama a DNS_answer_parser() por cada una de ellas. Le pasa como argumentos el mismo puntero a buf, answer, que es un puntero a la respuesta actual que se está analizando dentro del paquete original en buf, y un puntero a current_answer, que es un búfer de tamaño 256:
undefined4 * process_resolved_IP(byte *buf,byte *answer_ptr,uint ANCOUNT,undefined4 *param_4,int *param_5){
[...]
byte current_answer [256];
ushort answer_flags [5];
i = 0;
puVar9 = (undefined4 *)0x0;
puVar7 = (undefined4 *)0x0;
answer = answer_ptr;
do {
/* Check if all answers have been parsed already */
if (i == ANCOUNT) {
if (param_4 != (undefined4 *)0x0) {
*param_4 = puVar7;
}
if (param_5 != (int *)0x0) {
*param_5 = (int)answer - (int)answer_ptr;
}
return puVar9;
}
bytes_processed = DNS_answer_parser(buf,answer,current_answer,1);
memcpy(answer_flags,answer + bytes_processed,10);
uVar1 = answer_flags._4_4_;
bytes_processed = bytes_processed + 10;
uVar6 = (uint)answer_flags[4];
uVar8 = (uint)answer_flags[0];
if (uVar8 == 2) {
LAB_00405924:
DNS_answer_parser(buf,answer + bytes_processed,abStack_240,1);
}
else if (uVar8 < 3) {
pbVar2 = answer + bytes_processed;
if (uVar8 == 1) {
sprintf((char *)abStack_240,"%u.%u.%u.%u",(uint)*pbVar2,(uint)pbVar2[1],(uint)pbVar2[2],
(uint)pbVar2[3]);
}
}
else {
if (uVar8 == 5) goto LAB_00405924;
if (uVar8 == 0x1c) {
inet_ntop(10,answer,(char *)abStack_240,0xff);
}
}
answer = answer + bytes_processed + uVar6;
[...]
i = i + 1;
} while( true );
}
La función DNS_answer_parser(), ubicada en 0x004054e0, analiza cada respuesta individualmente, la cual consiste en un nombre de dominio representado como <len><domain><len><domain>.... Por ejemplo, example.com se representaría como 7example3com. La función itera sobre cada par de <len><domain> que constituyen el nombre de dominio en la respuesta y comprueba que <len> sea menor que 63 (domain_name & 0xc0 != 0). Luego, llama a memcpy y copia <len> bytes de answer, correspondientes a <domain>, dentro de current_answer. Repite el proceso para el siguiente par de <len><domain> en el nombre de dominio.
int DNS_answer_parser(byte *buf,byte *answer,byte *current_answer,int flag){
int iVar1;
uint __n;
int iVar2;
uint uVar3;
ushort flag_and_offset;
byte domain_name;
iVar2 = 0;
do {
domain_name = *answer;
__n = (uint)domain_name;
iVar1 = 1;
if (__n == 0) {
*current_answer = 0;
LAB_004055b0:
return iVar2 + iVar1;
}
/* Check if compression mode is used */
if ((domain_name & 0xc0) != 0) {
flag_and_offset = CONCAT11(domain_name,answer[1]);
DNS_answer_parser(buf,buf + (flag_and_offset & 0x3fff),current_answer,flag);
iVar1 = 2;
goto LAB_004055b0;
}
uVar3 = __n + 1;
if (flag == 0) {
*current_answer = '.';
memcpy(current_answer + 1,answer + 1,__n);
__n = uVar3;
}
else {
memcpy(current_answer,answer + 1,__n);
}
answer = answer + uVar3;
current_answer = current_answer + __n;
iVar2 = iVar2 + uVar3;
flag = 0;
} while( true );
}
Dado que current_answer solo tiene 256 bytes de longitud, es posible que un atacante envíe un paquete con una respuesta que contenga un nombre de dominio lo suficientemente grande como para desbordar el búfer.
El proceso de explotación es muy similar al explicado por el equipo Flashback para la CVE-2020-10881 en su serie de videos al respecto (ver referencias), con algunas diferencias clave:
move, ya que ahora el registro que se mueve a $a2 como argumento count para memcpy() es $s1 en lugar de $s0.i en process_resolved_IP() se carga en $s5 en lugar de $s8. Luego, al compararse con $v1, $v1 se toma de $sp+616, que está muy lejos del búfer desbordado, por lo que la pila tuvo que corromperse más allá de la respuesta y el comando./tmp, por lo que todas las escrituras y lecturas deben realizarse allí. Esto hace que sea imposible que la reverse shell tenga menos de 62 caracteres, así que para desplegarla en el objetivo, debe servirse desde un servidor web y enviarse al objetivo un comando para descargarla y ejecutarla.Teniendo esto en cuenta, explicaré todo el proceso de todos modos.
El binario conn-indicator tiene NX habilitado y ASLR en todo excepto en sí mismo: