VPN 어플라이언스 탐색: 한 연구원의 여정

Akamai Wave Blue

에 의해 작성

Ben Barnea

February 11, 2025

Akamai Wave Blue

에 의해 작성

Ben Barnea

벤 바니아는 Akamai의 보안 연구원으로, Windows, Linux, 사물 인터넷, 모바일 등 다양한 아키텍처에서 저수준 보안 리서치와 취약점 리서치 수행에 관한 관심과 경험을 보유하고 있습니다. 그는 복잡한 메커니즘의 작동 원리, 특히 복잡한 메커니즘의 실패 방식을 배우는 것을 좋아합니다.

VPN은 기업의 네트워크로 들어가는 관문이기 때문에, VPN 어플라이언스의 취약점은 기업에 큰 영향을 미칩니다.
VPN은 기업의 네트워크로 들어가는 관문이기 때문에, VPN 어플라이언스의 취약점은 기업에 큰 영향을 미칩니다.
  • Akamai 연구원 벤 바르네아(Ben Barnea)는 Fortinet의 FortiOS에서 여러 취약점을 발견했습니다.
  • 인증되지 않은 공격자가 DoS와 RCE로 이어질 수 있는 취약점 유발 가능
  • DoS 취약점은 악용하기 쉽고, 취약점이 악용되면 Fortigate 어플라이언스가 작동하지 않게 됩니다.
  • Akamai는 RCE 취약점이 악용하기 어렵다고 가정합니다.
  • 이 취약점은 Fortinet에 책임 있게 공개되었으며 CVE-2024-46666CVE-2024-46668로 지정되었습니다.
  • Fortinet은 2025년 1월 14일에 바르네아가 발견한 취약점을 해결했으며, 최신 버전의 FortiOS를 사용하는 디바이스는 이 취약점으로부터 보호됩니다.

서론

지난 몇 년 동안 VPN 솔루션은 공격자들에 의해 악용된 많은 중대한 취약점으로 인해 고통받았습니다. 이러한 취약점 중 일부는 악용하기가 매우 쉽고 인터넷에 노출된 VPN 어플라이언스에 대한 RCE와 같은 파괴적인 영향을 미칩니다. 네트워크에 침입한 공격자는 측면으로 이동해 민감한 데이터, 지식 재산권, 기타 고가치 자산에 접근할 수 있습니다.

Akamai 연구원 오리 데이비드(Ori David)는 VPN 취약점의 초기 악용 외에도 악용 후 기술을 문서화하고 감염된 VPN 서버를 통해 공격자가 네트워크의 다른 중요한 자산을 쉽게 제어할 수 있음을 보여주었습니다.

안타깝게도, VPN 어플라이언스를 조사하려는 보안 연구원들은 펌웨어를 쉽게 구할 수 없고 벤더사가 배포한 암호화 메커니즘으로 펌웨어가 보호되기 때문에 리서치를 시작하는 데 어려움을 겪습니다. 그러나 VPN 어플라이언스가 악용의 주요 표적이라는 점을 감안하면 그러한 보호 기능을 극복하는 것은 공격자에게 확실히 가치가 있습니다.

이번 블로그 게시물에서는 Fortinet의 VPN 솔루션을 조사하는 리서치 과정을 살펴보겠습니다. 펌웨어를 구하고, 암호를 해독하고, 디버거를 설정하고, 마지막으로 취약점을 찾는 과정을 따라가 볼 것입니다.

이번 게시물에서 소개하는 리서치 중 일부는 새로운 것이 아닙니다. Optistream, Bishop Fox, Assetnote, Lexfo 등이 Fortinet의 FortiOS에 대해 훌륭한 리서치를 수행했습니다.  Fortinet가 암호화 및 복호화 방법을 자주 변경해 디바이스 분석이 더 어렵기 때문에, Akamai는 최신 버전의 FortiOS를 사용해 초기 리서치를 업데이트했습니다.

펌웨어 이미지 얻기

전통적으로 VPN은 별도의 물리적 어플라이언스로 판매되었기 때문에 이를 구매하고 펌웨어를 추출하는 것이 복잡할 수 있었습니다. 그러나 요즘에는 가상머신(VM)에 배포할 수 있는 가상 어플라이언스로서의 VPN 어플라이언스를 찾는 것이 훨씬 더 일반적입니다.

다행히도, Fortinet은 웹사이트에서 등록 후 다운로드 가능한 체험판 VM을 제공합니다(그림 1). 이 VM은 CPU가 1개만 허용되며 RAM은 2GB로 제한되는 제약이 있습니다.

Fortinet은 웹사이트에서 등록 후 다운로드 가능한 체험판 VM을 제공합니다(그림 1). 그림 1: 다운로드 가능한 체험판 VM

디버깅 환경 만들기

제공된 VM에는 (1) 커널 이미지를 포함한 부트 이미지(flatkc라고 함)와 (2) 대부분의 중요한 파일이 들어 있는 암호화된 파일 시스템인 rootfs라는 2가지 관심 포인트가 있습니다. 파일 시스템 내부에서 일단 해독이 되면 /bin/ 디렉토리 안에서 init라는 바이너리를 발견할 수 있습니다.

대부분의 VM의 바이너리는 이 하나의 바이너리로 정적으로 컴파일되었습니다. /bin/init에는 SSLVPND와 관리 웹 서버라는 흥미로운 바이너리가 2개 있습니다. 이 게시물의 후반부에 이 바이너리에 대해 다시 설명하겠습니다.

Akamai는 Fortinet에서 제공하는 제한된 CLI가 아닌 완전한 셸을 갖춘 환경을 만들고 싶었습니다. 또한, 바이너리를 쉽게 디버깅할 수 있는 gdb 바이너리를 갖고 싶었습니다.

이러한 환경을 만들기 위해 다음을 수행합니다.

  1. GZIP으로 압축된 CPIO(파일 아카이브 형식)의 압축 풀기
  2. Bishop Fox의 스크립트를 사용해 rootfs 해독
  3. bin.tar.xz 아카이브의 압축 풀기
  4. /bin/init 무결성 검사 패치
  5. vmlinux-to-elf를 사용해 flatkc를 ELF로 변환
  6. 런타임에 패치해 추가 무결성 검사를 비활성화할 수 있도록 IDA에서 fgt_verify_initrd의 주소 탐색
  7. 정적으로 컴파일된 busybox와 gdb를 /bin/ 안에 넣기
  8. 텔넷 서버를 생성하는 스텁을 컴파일하고 이 스텁으로 /bin/smartctl 덮어쓰기
  9. /bin/ 폴더를 tar로 압축
  10. 루트파일을 다시 압축하고 암호화
  11. 암호화된 rootfs의 끝에 패딩 추가
  12. (VMDK를 마운트하는) 헬퍼 Ubuntu VM을 사용해 VMDK의 rootfs를 교체

그림 2에 설명된 단계에서는 디버깅 기능을 갖춘 편집된 VM을 만듭니다. rootfs의 무결성 검사가 실패하기 때문에 커널의 실행이 중단됩니다. 따라서 커널(flatkc)을 패치한 다음 부트로더 무결성 검증 코드도 패치하거나 커널 무결성 검사의 동적 패치에 의존해야 합니다. Akamai는 후자의 접근 방식을 선택하기로 했습니다.

그림 2에 설명된 단계에서는 디버깅 기능을 갖춘 편집된 VM을 만듭니다. 그림 2: 리서치 환경을 위한 FortiGate 패치

VMware의 VM 디버깅 기능을 사용하려고 VMDK 파일을 편집했습니다. 이 기능을 사용하면 머신이 실행될 때 사용할 수 있는 GDB 디버거가 설정됩니다. 안타깝게도, 이 기능 구축 과정에서 Hyper-V가 활성화된 머신에서 실행할 때 문제가 발생했습니다. 브레이크포인트가 발생하자마자 머신이 충돌했습니다. 이는 Hyper-V 머신에서 실행할 때 커널 디버깅 기능이 불완전하게 구축되었기 때문인 것으로 보입니다.

QEMU를 사용해 실행 중인 VM을 생성하려고 시도

VMware를 사용해 커널 디버깅을 실행하는 데 여러 번 실패한 후, QEMU를 사용해 실행 중인 VM을 생성하기로 했습니다. qcow2와 VMDK 파일 포맷 간에 변환해야 하는 점을 제외하고는 유사한 단계를 사용해 수정된 VM을 정적으로 생성했습니다.

QEMU를 사용할 때 커널을 디버깅하려면 qemu-system에 -s 플래그를 제공할 수 있습니다. 마지막으로 VM을 실행하고 GDB를 연결한 다음, 무결성 검사를 덮어쓰기 위해 브레이크포인트를 추가하면 CLI가 나타납니다. (그림 3).

마지막으로 VM을 실행하고 GDB를 연결한 다음, 무결성 검사를 덮어쓰기 위해 브레이크포인트를 추가하면 CLI가 나타납니다. (그림 3). 그림 3: CLI를 사용하여 작동하는 VM

네트워크 설정을 구성하고 DHCP 서버로부터 유효한 IP를 받은 후, 현재 디렉토리 내용, Linux ID 명령을 출력하고 busybox 텔넷 세션을 여는 수정된 smartctl 바이너리를 실행할 수 있습니다. (그림 4).

네트워크 설정을 구성하고 DHCP 서버로부터 유효한 IP를 받은 후, 현재 디렉토리 내용, Linux ID 명령을 출력하고 busybox 텔넷 세션을 여는 수정된 smartctl 바이너리를 실행할 수 있습니다. (그림 4). 그림 4: 백도어 활성화

그리고 마지막으로, 그림 5는 새로 생성된 텔넷 서버에 연결되었음을 보여줍니다.

그림 5는 새로 생성된 텔넷 서버에 연결했음을 보여줍니다. 그림 5: 쉘에 연결하기

이제 끝났을까요? 아닙니다. 

그림 6에서 볼 수 있듯이 유효한 라이선스가 없어서 관리자 패널과 상호 작용할 수 없습니다.

그림 6에서 볼 수 있듯이 유효한 라이선스가 없어서 관리자 패널과 상호 작용할 수 없습니다. 그림 6: 라이선스 오류

우회하지 마시오

처음에는 CPU 1개와 RAM 2048MB의 제한을 초과했기 때문에 라이선스가 유효하지 않다고 생각했습니다. 2가지 선택지가 있었습니다. 이 제한을 수용하고 속도가 매우 느린 VM을 사용하거나, 제한을 우회하는 것입니다.

몇 번의 리버싱을 거친 후, 데몬에서 주기적으로 호출되는 upd_vm_check_license 함수를 발견했습니다(그림 7). 이 함수는 머신의 RAM과 CPU 수가 제한을 초과하지 않는지 확인합니다.

몇 번의 리버싱을 거친 후, 데몬에서 주기적으로 호출되는 upd_vm_check_license 함수를 발견했습니다(그림 7). 그림 7: VM의 제약 조건을 담당하는 디컴파일된 코드

num_max_CPUs() max_allowed_RAM()의 반환값을 동적으로 수정해 제한을 우회한 후, 이제 강력한 제약 없는 VM을 갖게 되었고 머신을 부팅할 때 오류가 감소했지만 여전히 유효하지 않은 라이선스 오류가 발생했습니다.

라이선스 유효성 검사 기능을 리버싱하느라 너무 많은 시간을 허비하고, 이 문제를 야기한 인생 선택에 대해 생각한 끝에, 마침내 라이선스가 SMBIOS UUID를 사용해 구축된 머신의 일련 번호를 사용하고 있다는 사실을 발견했습니다. QEMU에 일련 번호를 제공하지 않았기 때문에 NULL을 사용했고, 이것이 생성된 일련 번호가 ‘FGVMEV0000000000’인 이유입니다. 다음 플래그를 사용해 SMBIOS UUID를 QEMU에 제공한 후,

  -smbios type=1,manufacturer=t1manufacturer,product=t1product,version=t1version,serial=t1serial,
  uuid=25359cc8-5fe7-4d50-ab82-9fd15ecaf221,sku=t1sku,family=t1family

마침내 머신을 부팅하고 유효한 라이선스를 받았습니다. 

새 버전. 새 암호화. 그 이유는?

이 시점에서 디버깅 환경을 구축했습니다. 관리 웹 서버를 조사하기 시작했고 이 게시물의 뒷부분에서 설명할 몇 가지 취약점을 발견했습니다. 이러한 취약점을 발견하는 과정에서, Fortinet이 FortiOS 버전 7.4.4를 출시했다는 사실을 알게 되었습니다.

새로운 릴리스에 취약점이 여전히 존재하는지 확인하고 싶었지만, 업데이트된 VM의 rootfs를 해독하는 데 실패한 후 해독하기 훨씬 더 어려운 완전히 다른 암호화를 발견했습니다. 이번에는 새로운 rootfs를 해독하는 데 노력을 기울이기로 했지만, 디버깅 환경을 만드는 데는 노력을 기울이지 않기로 했습니다. 이 시점에서 주요 목표는 취약점이 여전히 존재하는지 확인하는 것이었기 때문입니다.

먼저, 이전 버전(7.4.4 이전)에서 rootfs를 해독하는 방법을 설명하겠습니다.

1. 커널이 rootfs 시스템의 무결성을 확인하고, 유효한 경우 다음 단계로 진행합니다.

2. 커널이 fgt_verifier_key_iv를 호출합니다. 이 함수는 다음과 같이 키와 IV를 계산합니다.

a. 키: 글로벌 데이터의 sha256()

b. IV: 동일한 글로벌 데이터의 다른 부분의 sha256(). 그런 다음, 결과를 16바이트로 자릅니다

3. 위의 키와 IV를 사용해 Chacha20을 사용해 rootfs를 해독합니다.

이제 새로운 알고리즘을 살펴보겠습니다(그림 8).

1. 해독 코드는 이전 알고리즘에서와 같이 글로벌 데이터 버퍼의 sha256()에 의해 키와 IV를 계산합니다

2. ChaCha는 키와 IV를 사용해 메모리 블록을 해독합니다. 이 메모리 블록은 RSA 프라이빗 키를 나타내는 ASN1입니다

3. RSA 프라이빗 키에서 d, n을 가져와 알려진 공식 M = Cd mod N을 사용해 암호화된 펌웨어의 마지막 256바이트에 있는 데이터 블록을 해독합니다

4. 데이터 블록에서 다음을 가져옵니다.

  • 논스+카운터인 16바이트 

  • 32키 바이트  

5. 키와 논스+카운터를 사용해 CTR 모드에서 AES 암호 해독을 수행합니다. 이 코드는 사용자 정의 CTR 추가 기능을 사용합니다

6. 추가 기능은 (논스+카운터)의 니블을 XOR 연산한 결과입니다

 

새로운 알고리즘(그림 8): 그림 8: 펌웨어 해독 흐름도

이 다단계 해독 알고리즘의 복잡성을 더하기 위해 새로운 flatkc는 더 이상 기호를 가지지 않아 펌웨어를 자동으로 복호화하는 툴을 작성하는 것이 더 어려워졌습니다. 예를 들어, ChaCha 해독에 사용되는 글로벌 데이터와 암호화된 RSA 프라이빗 키를 찾는 것이 더 어려워졌습니다.

위에서 설명한 모든 단계를 수행한 후, rootfs를 볼 수 있습니다(그림 9).

위에서 설명한 모든 단계를 수행한 후, rootfs를 볼 수 있습니다(그림 9). 그림 9: rootfs의 tar

이번에는 수정된 환경을 만들지 않았습니다. 이렇게 하려면 위에서 설명한 대로 성공적으로 해독해야 하는 rootfs 아카이브를 만들어야 합니다. 또 다른 옵션은 브레이크포인트를 동적으로 설정하고 해독된 rootfs으로 메모리를 덮어쓰는 것입니다.

관리자 웹 서버 리버싱

드디어 관리자 웹 서버를 되돌릴 수 있게 되었습니다. 이 서버는 Apache를 기반으로 하고 있으며 일반적으로 인터넷에 접속할 수 없어야 합니다(인터넷에 접속할 수 있는 sslvpn 인터페이스와는 대조적입니다).

httpd config를 열면 URL을 핸들러로 지정하는 몇 가지 위치 지시문을 확인할 수 있습니다(그림 10).

httpd config를 열면 URL을 핸들러로 지정하는 몇 가지 위치 지시문을 확인할 수 있습니다(그림 10). 그림 10: httpd config 파일의 스니펫

그런 다음, 바이너리에서 핸들러 문자열 중 하나를 검색해 핸들러 테이블을 찾을 수 있습니다(그림 11).

그런 다음, 바이너리에서 핸들러 문자열 중 하나를 검색해 핸들러 테이블을 찾을 수 있습니다(그림 11). 그림 11: IDA에 표시된 핸들러 목록

Akamai는 인증되지 않은 취약점을 찾는 데 관심이 있었기 때문에 /api/v2/authentication URL을 통해 접속할 수 있는 api_authentication-handler에 집중하기로 했습니다.

리버싱 작업을 시작하기 전에 작업을 쉽게 하기 위해 IDA에 몇 가지 Apache 구조체와 연결 구조체를 만드는 것이 좋습니다(그림 12, 13).

  struct __attribute__((aligned(8))) _request_rec
{
    apr_pool_t *pool;
    conn_rec *connection;
    void *server;
    _request_rec *next;
    _request_rec *prev;
    _request_rec *main;
    char *the_request;
    int assbackwards;
    int proxyreq;
    int header_only;
    int proto_num;
    char *protocol;
    const char *hostname;
    unsigned __int64 request_time;
    const char *status_line;
    int status;
    enum http_methods method_number;
    const char *method;
    unsigned __int64 allowed;
    void *allowed_xmethods;
    void *allowed_methods;
    unsigned __int64 sent_bodyct;
    unsigned __int64 bytes_sent;
    unsigned __int64 mtime;
    const char *range;
    unsigned __int64 clength;
    int chunked;
    int read_body;
    int read_chunked;
    unsigned int expecting_100;
    void *kept_body;
    void *body_table;
    unsigned __int64 remaining;
    unsigned __int64 read_length;
    void *headers_in;
    void *headers_out;
    void *err_headers_out;
    void *subprocess_env;
    void *notes;
    const char *content_type;
    const char *handler;
    const char *content_encoding;
    void *content_languages;
    char *vlist_validator;
    char *user;
    char *ap_auth_type;
    char *unparsed_uri;
    char *uri;
    char *filename;
    char *canonical_filename;
    char *path_info;
    char *args;
    int used_path_info;
    int eos_sent;
    void *per_dir_config;
    void *request_config;
    void *log;
    const char *log_id;
    void *htaccess;
    void *output_filters;
    void *input_filters;
    void *proto_output_filters;
    void *proto_input_filters;
    int no_cache;
    int no_local_copy;
    void *invoke_mtx;
    apr_uri_t parsed_uri;
    apr_finfo_t finfo;
    void *useragent_addr;
    char *useragent_ip;
    void *trailers_in;
    void *trailers_out;
    char *useragent_host;
    int double_reverse;
    unsigned __int64 bnotes;
};

그림 12: Apache 구조체

  struct __attribute__((aligned(8))) conn_rec
{
    apr_pool_t *pool;
    void *base_server;
    void *vhost_lookup_data;
    apr_sockaddr_t *local_addr;
    sockaddr *client_addr;
    char *client_ip;
    char *remote_host;
    char *remote_logname;
    char *local_ip;
    char *local_host;
    __int64 id;
    void *conn_config;
    void *notes;
    void *input_filters;
    void *output_filters;
    void *sbh;
    void *bucket_alloc;
    void *cs;
    int data_in_input_filters;
    int data_in_output_filters;
    unsigned __int32 clogging_input_filters : 1;
    __int32 double_reverse : 2;
    unsigned int aborted;
    ap_conn_keepalive_e keepalive;
    int keepalives;
    void *log;
    const char *log_id;
    conn_rec *master;
    int outgoing;
};

그림 13: 연결 구조체

인증 핸들러를 리버싱할 때 먼저 api_login_handler라는 POST 방식 핸들러를 리버싱했습니다. 이 함수는 api_login_parse_param을 호출해 요청에서 로그인 매개변수를 가져옵니다. 이 함수는 콘텐츠 종류 헤더에 따라 POST 데이터를 구문 분석하려고 시도합니다.

  1. ‘multipart/form-data’로 설정된 경우, 요청에 HTML 양식이 있습니다.

  2. 그렇지 않은 경우, 일반 POST 데이터를 읽습니다.

두 번째 옵션은 매우 간단하기 때문에 Akamai는 대부분 첫 번째 부분에 집중했습니다. 디컴파일된 코드를 훑어보면서 Akamai는 libapreq 라이브러리를 가리키는 디버그 문자열을 빠르게 발견했습니다(그림 14).

디컴파일된 코드를 훑어보면서 Akamai는 libapreq 라이브러리를 가리키는 디버그 문자열을 빠르게 발견했습니다(그림 14). 그림 14: 코드가 libapreq를 사용함을 나타내는 문자열

libapreq는 오픈 소스 Apache 라이브러리이기 때문에 Akamai는 소스 코드 대신 디컴파일된 코드에서 취약점을 찾을 이유가 (거의) 없었습니다. 따라서, Akamai가 가장 먼저 해야 할 일은 라이브러리 버전을 찾는 것이었습니다. 몇 번의 시도 끝에 Akamai는 바이너리에 존재하지만 한 커밋 후에 제거되는 문자열과 특정 커밋을 찾아서 버전을 좁힐 수 있었습니다(그림 15).

가장 먼저 해야 할 일은 라이브러리 버전을 찾는 것이었습니다. 몇 번의 시도 끝에 Akamai는 바이너리에 존재하지만 한 커밋 후에 제거되는 문자열과 특정 커밋을 찾아서 버전을 좁힐 수 있었습니다(그림 15). 그림 15: 디컴파일된 바이너리 코드와 문자열을 제거한 소스 코드 커밋의 비교

놀라운 사실은 바이너리에 존재하는 라이브러리가 2000년 3월에 작성된 가장 오래된 버전이라는 것입니다(그림 16).

놀라운 사실은 바이너리에 존재하는 라이브러리가 2000년 3월에 작성된 가장 오래된 버전이라는 것입니다(그림 16). 그림 16: libapreq의 좁혀진 버전

취약점

Fortinet은 최적화 목적으로 아주 사소한 변경을 제외하고는 거의 25년 전과 똑같은 방식으로 이 모듈을 사용합니다. 처음 이것을 보았을 때, 2000년 당시의 코드가 취약점이 없을 리가 없다고 생각했습니다. 그리고 Akamai가 옳았습니다!

취약점을 살펴보기 전에 이 라이브러리의 목적과 용도를 설명해 보겠습니다. Apreq는 클라이언트 요청 데이터를 처리하는 데 사용되는 Apache 라이브러리입니다. 사용자의 데이터를 수신하는 일반적인 방법 중 하나는 HTML 양식입니다. 채워진 양식 데이터는 다양한 인코딩 방법을 사용해 서버로 전달될 수 있지만 일반적인 방법은 application/x-www-form-urlencodedmultipart/form-data입니다.

multipart/form-data가 사용될 때, 클라이언트(보통 브라우저)는 양식 데이터의 다른 필드들 사이의 경계로 임의의 텍스트를 선택합니다. 경계는 HTTP 헤더를 통해 지정됩니다. 경계는 양식 데이터의 끝을 표시하는 데도 사용됩니다(그림 17).

  POST /foo HTTP/1.1
  Content-Length: 68137
  Content-Type: multipart/form-data; boundary=ExampleBoundaryString

  --ExampleBoundaryString
  Content-Disposition: form-data; name="description"

  Description input value
  --ExampleBoundaryString
  Content-Disposition: form-data; name="myFile"; filename="foo.txt"
  Content-Type: text/plain

  [content of the file foo.txt chosen by the user]
  --ExampleBoundaryString--

그림 17: HTTP 경계 양식의 예(출처)

이제, Akamai가 발견한 취약점들 중 일부를 살펴보겠습니다. 여기에는 NULL 바이트의 OOB(Out-Of-Bound), 와일드 카피, 디바이스 DoS, 웹 서버 DoS, OOB 읽기 등이 포함됩니다.

NULL 바이트의 OOB 쓰기

multipart_buffer_read가 내부 버퍼를 채우고 경계를 찾은 후, 현재 위치와 발견된 경계 사이의 문자열을 반환합니다. 버그는 경계가 내부 버퍼의 시작 부분에 있지 않은 경우, 행 끝(‘\r\n’)으로 추정되는 마지막 두 문자를 제거한 후 문자열을 반환한다는 것입니다. 코드는 반환된 문자열의 길이가 2보다 크다고 잘못 가정합니다.

그림 18에서 retval은 반환된 문자열이고 start는 1인 길이입니다. 이 경우, blen도 start와 같습니다. 그런 다음, -1의 값으로 2만큼 감소됩니다. 따라서 버퍼 앞에 1바이트의 NULL을 쓸 수 있습니다.

그림 18에서 retval은 반환된 문자열이고 start는 1인 길이입니다. 이 경우, blen도 start와 같습니다. 그런 다음, -1의 값으로 2만큼 감소됩니다. 따라서 버퍼 앞에 1바이트의 NULL을 쓸 수 있습니다. 그림 18: 버퍼 앞에 NULL을 OOB 쓰기로 만드는 취약점

악용 시도

비록 1바이트 오버플로우(비트 오버플로우도 포함)만으로도 코드 실행에 충분할 수 있지만 이 취약점이 실제로 악용될 가능성은 낮다고 생각합니다. 첫째, NULL 바이트만 쓸 수 있고, 버퍼 앞에 1바이트만 쓸 수 있습니다. 버퍼는 힙에 할당되므로 두 가지 옵션이 있습니다.

1. 버퍼는 힙 노드에 할당되는 첫 번째 버퍼입니다. 이 경우, 할당 전에 힙 노드 메타데이터가 있습니다(그림 19).

버퍼는 힙 노드에 할당되는 첫 번째 버퍼입니다. 이 경우, 할당 전에 힙 노드 메타데이터가 있습니다(그림 19). 그림 19: Apache 메모리 힙 노드를 나타내는 구조체

endp 포인터의 한 바이트를 덮어씁니다. 엔디안 때문에 포인터의 가장 높은 바이트를 덮어쓰기 때문에 포인터의 값에 영향을 미치지 않습니다. VM이 x64이기 때문에 이 바이트는 항상 0입니다.

2. 이전에 할당이 있었다면 데이터의 한 바이트를 덮어쓸 수 있습니다. 안타깝게도, 이전 예에서와 마찬가지로 대부분의 경우, 구조체의 끝에 포인터, 패딩 또는 이미 NULL로 종료된 C 문자열이 있습니다.

흥미로운 오브젝트인 multipart_buffer C 구조체를 발견했습니다(그림 20).

흥미로운 오브젝트인 multipart_buffer C 구조체를 발견했습니다(그림 20). 그림 20: multipart_buffer C 구조체

여기서 Akamai는 구조체의 마지막 변수인 buffer_len을 이전 취약점을 통해 음수로 만들 수 있고, 이후 이 취약점을 통해 음수에서 큰 양의 값으로 변경할 수 있다고 생각했습니다(숫자를 음수로 표시하는 MSB 바이트를 덮어쓰기함으로써).

이 접근 방식은 흥미로운 것처럼 보였지만 다음 2가지 문제가 있습니다.

1. 구조체는 form parser를 만들 때 한 번만 생성됩니다. 즉, 이 오브젝트를 쉽게 스프레이할 수 없습니다.

2. 이전 취약점을 이용하면 filling 함수에서 읽은 길이를 제한할 수 있습니다. 즉, filling 함수가 끝나면 전체 요청의 POST 데이터를 읽은 후 self->length가 0이 됩니다. 다음에 코드가 multipart_buffer_read를 호출할 때, 고급 버퍼에서 경계를 찾지 못할 것입니다(시작 전에 엔드 포인터가 나타나도록 했기 때문입니다). 그리고 self->length가 0이기 때문에 잘못된 포맷의 업로드 경고를 종료할 것입니다.

Akamai는 이를 경쟁 조건 시도를 통해 악용하려고 생각했지만 Apache MPM(Multiprocessing Mode)를 살펴보면 그림 21과 같은 것을 볼 수 있습니다.

Akamai는 이를 경쟁 조건 시도를 통해 악용하려고 생각했지만 Apache MPM(Multiprocessing Mode)를 살펴보면 그림 21과 같은 것을 볼 수 있습니다. 그림 21: Apache MPM 모드 확인

이것은 Apache가 각각 요청을 처리하는 여러 프로세스를 포크한다는 것을 의미합니다. 또한 이러한 프로세스가 멀티스레드가 아니라는 것도 알 수 있습니다. 즉, 경쟁 조건 방식으로 이를 악용할 수 없습니다.

와일드 카피

동일한 함수인 multipart_buffer_read에서 코드가 경계(시작이 -1인 경우)를 찾지 못하면 내부 버퍼의 일부 (바이트 - 경계 길이)만 반환합니다. 여기서 오류는 바이트가 상수 값 5120으로 설정되어 있는데 경계 길이는 훨씬 더 길 수 있습니다(헤더 길이의 한계까지).

따라서 경계가 첫 번째 청크에 있지 않고 경계 길이가 5120보다 큰 필드를 보내면 blen이 음수가 될 수 있습니다. 이로 인해 코드가 self->buffer를 버퍼 앞에 설정하고 self->buffer_len을 더 큰 값으로 설정하게 됩니다(그림 22).

이로 인해 코드가 self->buffer를 버퍼 앞에 설정하고 self->buffer_len을 더 큰 값으로 설정하게 됩니다(그림 22). 그림 22: 정수 언더플로우로 인한 취약점

악용 시도

이 취약점과 이전 취약점에는 차이가 있습니다. 이번에는 시작이 음수(경계가 발견되지 않음)이기 때문에 NULL 바이트를 쓰는 코드로 이동하지 않습니다.

blen은 multipart_buffer_read 함수의 매개변수이므로 이 함수를 호출하고 blen을 출력으로 받는 multipart_buffer_read_body 함수를 살펴봅시다.

그림 23은 blen이 두 번 사용된다는 것을 보여줍니다.

1. 첫 번째 버퍼가 생성된 경우, multipart_buffer_read에서 받은 문자열이 blen을 사용해 복제됩니다. 이 경우, Apache는 OOM(Out-Of-Memory) 오류를 발생시키고 코드가 중단됩니다. 이 기능은 DoS 공격을 시작하는 데 사용될 수 있습니다.

2. 두 번째 청크인 경우, 이전 청크와 현재 청크가 my_join 함수를 사용해 결합됩니다. 함수는 음수를 사용해 memcpy를 호출해 와일드 카피를 발생시킵니다.

그림 23은 blen이 두 번 사용된다는 것을 보여줍니다 그림 23: multipart_buffer_read_body의 두 가지 다른 흐름

(이 코드에서 또 다른 버그를 발견한 분들도 있을 것입니다. 즉 old_len이 old_len + blen 대신 blen으로 업데이트되어 사용자 데이터가 잘리는 버그입니다.)

이러한 와일드 카피를 악용하는 것은 가능하더라도 매우 어렵습니다. 첫째, 와일드 카피를 ‘중지’할 수 있는 옵션이 없습니다. 즉, 대용량 memcpy입니다. 둘째, 멀티스레딩이 없기 때문에 다른 스레드에서 동시에 사용될 오브젝트를 덮어쓸 수 없습니다. 따라서 가능한 유일한 옵션은 시그널 핸들러가 안전하게 종료되지 않을 경우 이를 악용하는 것이라고 가정합니다.

디바이스 DoS

이 취약점은 실제로 라이브러리 자체에 있는 것이 아니라 라이브러리를 사용하는 Fortinet의 코드에 있습니다.

사용자가 POST 데이터 안에 Content-Disposition 헤더로 지정된 양식을 통해 파일을 업로드하면(그림 17 참조) /tmp/ 폴더 안에 파일 이름이 /tmp/uploadXXXXXX인 새 파일이 생성됩니다. 여기서 X는 임의의 문자입니다.

각 파일 업로드에 대해 적절한 구조체가 생성되어 업로드된 파일의 연결된 목록에 삽입됩니다. 연결된 목록의 첫 번째 노드에 있는 파일만 삭제되면 구문 분석의 마지막에 버그가 발생합니다. 이로 인해 공격자가 /tmp/ 폴더를 채워서 공격을 시작할 수 있습니다. /tmp/는 tmpfs 파일 시스템이므로 데이터가 RAM에 저장됩니다. 이로 인해 시스템 전체 OOM이 발생해 디바이스가 멈추게 됩니다.

디바이스를 다시 시작해야만 정상적으로 사용할 수 있지만 이 방법도 보장할 수 없습니다. Akamai가 시도한 방법 중 하나는 디바이스에 일종의 네트워크 브릭을 일으키는 것이었습니다. 디바이스를 다시 시작한 후에도 네트워크 기능이 제대로 작동하지 않아 디바이스를 사용하거나 연결할 수 없었습니다.

악용 시도

이 취약점은 악용하기가 매우 쉽습니다. 여러 개의 파일이 포함된 양식을 반복적으로 요청하는 것만으로도 충분합니다. 잠시 후, 디바이스가 멈추게 됩니다(그림 24).

여러 개의 파일이 포함된 양식을 반복적으로 요청하는 것만으로도 충분합니다. 잠시 후, 디바이스가 멈추게 됩니다(그림 24). 그림 24: 삭제되지 않은 파일로 VPN 어플라이언스의 RAM을 채움으로써 결국 메모리 부족으로 DoS를 달성함

웹 서버 DoS

이것은 multipart_buffer_headers 함수에서 발견되는 사소한 취약점입니다. 이 함수는 내부 버퍼를 채우는 multipart_buffer_fill을 호출한 다음, 내부 버퍼에서 끝나는 이중 행을 찾습니다.

버그는 multipart_buffer_fill을 호출한 후 내부 버퍼가 유효한지 확인하지 않는다는 것입니다. multipart_buffer_fill이 입력을 기다리는 동안 클라이언트가 연결을 끊으면 버퍼를 NULL로 설정하고 이로 인해 NULL 역참조가 발생합니다(그림 25).

multipart_buffer_fill이 입력을 기다리는 동안 클라이언트가 연결을 끊으면 버퍼를 NULL로 설정하고 이로 인해 NULL 역참조가 발생합니다(그림 25). 그림 25: 유효한지 확인하지 않고 내부 버퍼에 접속

악용 시도

이 취약점은 악용하기도 매우 쉽습니다. 공격자는 연결을 생성하고 크래시 요청을 보내는 여러 스레드를 만들 수 있습니다. Apache pre-fork MPM은 성능이 좋지 않기 때문에 서버는 여러 크래시를 처리할 수 없고 공격이 진행되는 동안 다른 클라이언트에 서비스를 제공할 수 없습니다.

OOB 읽기

라이브러리는 정기적으로 채워지는 내부 버퍼를 사용합니다. 내부 버퍼가 채워지면 다음과 같이 읽을 바이트 수를 계산합니다.

1. 계산(bytes_requested - current_internal_buffer_len)

2. 경계 길이 + 2 추가('\r\n\r\n'의 경우)

3. 이 결과와 POST 데이터에서 읽을 수 있는 바이트 사이의 최소값 계산

취약점은 multipart_buffer_read에 있습니다. 내부 버퍼의 시작 부분에 경계를 발견했는데, 그것이 양식의 끝을 표시하는 경계가 아니라면 내부 버퍼 포인터를 경계 너머로 이동시키고 행 끝을 위해 2바이트를 추가합니다.

여기서 오류는 공격자가 내부 버퍼를 경계 크기와 정확히 일치하게 만들 수 있다는 점입니다. 따라서 코드가 내부 버퍼의 끝을 2바이트 지나치게 진행하게 됩니다(그림 26). 이것은 boundary_length + bytes_requested보다 적은 바이트를 제공함으로써 내부 버퍼 계산을 ‘제한’할 수 있기 때문에 가능합니다.

여기서 오류는 공격자가 내부 버퍼를 경계 크기와 정확히 일치하게 만들 수 있다는 점입니다. 따라서 코드가 내부 버퍼의 끝을 2바이트 지나치게 진행하게 됩니다(그림 26). 그림 26: 내부 버퍼의 끝을 넘어가기

악용 시도

그림 26에서 볼 수 있듯이, 코드는 NULL을 반환합니다. 그러나 이전 버그에서 보았듯이 multipart_buffer_headers의 코드는 반환 값이 아니라 내부 버퍼에 직접 접속합니다. 이로 인해 코드는 버퍼 뒤의 ‘\r\n’을 찾게 되고 그것을 헤더로 반환합니다.

로그인 핸들러에서 apreq를 사용하면 코드가 폼 필드를 읽지만 클라이언트로 다시 보내지는 것이 아니므로 이 경우 OOB 취약점을 정보 유출로 사용할 수 없습니다. 라이브러리는 웹 서버에서도 여러 번 사용되지만 인증된 사용자만 사용할 수 있는 것으로 보입니다. 따라서 권한이 낮은 사용자 인증정보를 가진 공격자가 메모리에서 높은 권한을 가진 사용자 인증정보를 읽음으로써 권한 상승 취약점으로 사용할 수 있습니다.

SSLVPND

SSLVPND는 Fortinet의 SSL-VPN 구성요소를 처리하는 데몬으로, 인터넷에 접속할 수 있습니다. apreq 라이브러리도 SSLVPND에서 사용됩니다. 여기에서 사용되는 라이브러리는 약간 수정된 동일한 구버전의 apreq를 기반으로 합니다. 디바이스 DoS 취약점을 제외한 위에서 설명한 모든 취약점은 SSLVPND에도 존재합니다.

불행히도 Akamai는 SSLVPND에서 apreq 라이브러리를 트리거할 수 없었습니다. 따라서 이러한 취약점이 인증되지 않은 사용자에게 접근 가능한지, 또는 이러한 취약점이 SSLVPND 맥락에서 악용될 수 있는지 확인할 수 없습니다.

요약

공격자들은 인터넷에 접근할 수 있는 VPN을 표적으로 삼는 경우가 많습니다. 이번 블로그 게시물에서는 VPN 어플라이언스를 조사하는 접근 방식의 한 가지 예를 살펴보았습니다. 중요한 취약점은 발견되지 않았지만 발견되지 않은 취약점이 더 있을 것으로 추정됩니다.

VPN은 기업의 네트워크로 들어가는 관문이기 때문에, VPN 어플라이언스의 취약점은 기업에 큰 영향을 미칩니다. 이 블로그 게시물이 다른 보안 연구자들에게도 VPN 취약점을 찾는 데 도움이 되기를 바랍니다.



Akamai Wave Blue

에 의해 작성

Ben Barnea

February 11, 2025

Akamai Wave Blue

에 의해 작성

Ben Barnea

벤 바니아는 Akamai의 보안 연구원으로, Windows, Linux, 사물 인터넷, 모바일 등 다양한 아키텍처에서 저수준 보안 리서치와 취약점 리서치 수행에 관한 관심과 경험을 보유하고 있습니다. 그는 복잡한 메커니즘의 작동 원리, 특히 복잡한 메커니즘의 실패 방식을 배우는 것을 좋아합니다.