目錄遍歷(Path Traversal)攻擊

目錄遍歷(Path Traversal)攻擊是透過相對路徑的方式來跨目錄的方式,藉機取得Server上原本是不公開的檔案。用比喻的方式來說的話,就大概是類似:

你授權請助理小姐去書桌第一格抽屜(目錄)拿開會要用的報告(公開的檔案),但助理小姐知道第二格抽屜(目錄)有500萬現金,雖然你沒授權他可以開,在你也沒有上鎖的情況下,助理小姐就自己打開第二格抽屜(目錄)拿走了500萬(非公開的檔案)。

上面這個範例,大概就是目錄遍歷(Path Traversal)攻擊的方式。


絕對路徑 & 相對路徑

前面有提到,目錄遍歷(Path Traversal)攻擊是透過相對路徑的方式來跨目錄取得Server上原本是不公開的檔案。那我們就有必要先來了解一下什麼是絕對路徑根相對路徑。假設我有一個應用程式的路徑為D:\Homer\Project\DesignAuditRise\DesignAuditRise.Web\wwwroot\

絕對路徑
_hostEnv.WebRootPath
//D:\Homer\Project\DesignAuditRise\DesignAuditRise.Web\wwwroot\
相對路徑
Path.GetFullPath(Path.Combine(_hostEnv.WebRootPath, "/"))
//回到根目錄:D:\

Path.GetFullPath(Path.Combine(_hostEnv.WebRootPath, "./"))
//回到當前目錄:D:\Homer\Project\DesignAuditRise\DesignAuditRise.Web\wwwroot\

Path.GetFullPath(Path.Combine(_hostEnv.WebRootPath, "../"))
//回到上層目錄:D:\Homer\Project\DesignAuditRise\DesignAuditRise.Web\

Path.GetFullPath(Path.Combine(_hostEnv.WebRootPath, "../"))
//回到上上層目錄:D:\Homer\Project\DesignAuditRise\

Path.GetFullPath(Path.Combine(_hostEnv.WebRootPath, "./assets"))
//到下層目錄:D:\Homer\Project\DesignAuditRise\DesignAuditRise.Web\wwwroot\assets

從上面可以看到,我們不必透過完整的路徑。透過./,  ../,  ../../,  ./xxx就可以到達我們所要到達的目錄位置。


目錄遍歷(Path Traversal)

看完了相對路徑的用法之後,我們現在就來做個情境來模擬目錄遍歷攻擊。情境大概是這樣:我們有一個WebAPI,接收前端給我們的參數{fileName},到後端之後再把指定目錄跟{fileName}Path.Combine起來。取得完整檔案路徑後,再把結果傳回前端,給使用者下載。

前端程式(Angular)
  GetFile()
  {
    let fileName = `TemplateExcel.xlsx`;
    //let fileName = `../../Content/Template/TemplateExcel.xlsx`; 
    //let fileName = `..%2F..%2FContent%2FTemplateFile%2FTemplateExcel.xlsx`; 
    //let fileName = `..%2F..%2Fappsettings.Production.json`; 
    //let fileName = `~%2FTemplateExcel.xlsx`;

    this._rdaService.GetFile(fileName).subscribe({
      next: (res)=>
      {
        this._rdaService.downloadFile(
          `${fileName}`, 
          res, 
          'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
          );

        this._salService.showSwal("", "Excel下載完成.", "success");
      },
      error: (err: HttpErrorResponse)=>{
        this._salService.showSwal("", `Excel下載失敗.`, "error");
      }
    });
  }

  GetFile(fileName: string): Observable<any>
  {
    const url = `${environment.apiBaseUrl}/{Web API的ControllerName}/GetFile/${fileName}`;
    const options:any = this.generatePostOptions();
    options.responseType = 'arraybuffer';

    return this.httpClient.get<any>(url, options)
  }

  public downloadFile(name: string, data: any, type: string)
  {     
    const blob = new Blob([data], { type: type });
    const url = window.URL.createObjectURL(blob);
    var link = document.createElement('a');
    link.href = url;
    link.download = name;
    link.click();
    link.remove();
  }

 

後端程式
        [HttpGet]
        [Route("{fileName}")]
        public async Task<IActionResult> GetFile(string fileName)
        {
            
            var baseDirect = $"{_hostEnv.ContentRootPath}//Content//TemplateFile";
            var decodeFileName = HttpUtility.UrlDecode(fileName);

            var filePath = Path.Combine(baseDirect, decodeFileName);

            var fullPath = Path.GetFullPath(filePath);

            var mimeType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
            byte[] fileBytes = System.IO.File.ReadAllBytes(filePath);
            return File(fileBytes, mimeType);
        }

 

我們看一下Excel的完整檔案路徑為:
D:\Homer\Project\DesignAuditRise\DesignAuditRise.Web\Content\TemplateFile\TemplateExcel.xlsx。
知道了相對路徑的用法後,我們就可以用它來玩一些有趣的東西。

我們先來測試正常情況,fileName參數傳入TemplateExcel.xlsx。看來是沒有問題的,TemplateExcel.xlsx也成功地下載完成。

 

現在假設我們知道Excel檔的完整路徑,然後參數用相對路徑的方式來試試看。fileName參數傳入../../Content/TemplateFile/TemplateExcel.xlsx。結果卻是回應404找不到網頁。

讓我們來看看為什麼會找不到網頁的詳細原因。原因是因為我們將相對路徑(../../)直接加在URL上並送出Request。導致我們的URL在Client端時就已經被解析了,變成了下圖中紅色框框的URL。當Request送到Server端時,因為找不到正確的路由,所以回應404
若是要避免客戶端解析我們傳送的目錄,便需要對 / 斜線符號進行Url Encode。把 / 變成%2F(Url Encode請參考:HTML URL Encoding Reference)

接著我們再用經過Encode的網址,進行API呼叫測試。可以看到Reqest透過了正確的路由找到了我們的GetFile這支API,而且參數也成功取得了。

透過HttpUtility.UrlDecode把我們的fileName參數進行Decode。可以看到經過Decode後,就是我們熟悉的目錄格式了。

Url Encode:..%2F..%2FContent%2FTemplateFile%2FTemplateExcel.xlsx

Url Decode:../../Content/TemplateFile/TemplateExcel.xlsx

接著透過Path.Combine組合出完整的相對路徑。

再透過Path.GetFullPath轉換成絕對路徑。可以看到最後的路徑跟Excel檔原本的位置是一樣的。API也成功回傳檔案了。

 

那這樣我們參數傳TemplateExcel.xlsx跟傳相對路徑../../Content/TemplateFile/TemplateExcel.xlsx的差別在哪裡呢?

  • TemplateExcel.xlsx

很單純的就是baseDirect D:\Homer\Project\DesignAuditRise\DesignAuditRise.Web\Content\TemplateFile加上TemplateExcel.xlsx組成一個絕對路徑

  • ../../Content/TemplateFile/TemplateExcel.xlsx
之前我們有說到../是往上一層目錄,所以當baseDirect D:\Homer\Project\DesignAuditRise\DesignAuditRise.Web\Content\TemplateFile遇到../../時會往上兩層目錄到達D:\Homer\Project\DesignAuditRise\DesignAuditRise.Web,然後再加上Content/TemplateFile/TemplateExcel.xlsx,就又能找到原本的TemplateExcel.xlsx了。這樣回去又回來,透過../在目錄間穿梭就是所謂的目錄遍歷了

目錄遍歷(Path Traversal)攻擊範例

知道了如何用相對路徑的方式來目錄遍歷後,就可以用這方式進入到原本沒有開放的目錄,取得不該被下載的檔案。現在假設我們知道有一個AppSettings檔的完整路徑在D:\Homer\Project\DesignAuditRise\DesignAuditRise.Web\appsettings.Production.json,然後傳入相對路徑參數..%2F..%2Fappsettings.Production.json來看看會發生什麼可怕的事情!!

哇靠!居然就這樣取得到appsettings.Production.json的絕對路徑了。

用瀏覽器call API看看,居然也成功把appsettings.Production.json檔案給下載下來了。

你各位寫C#的也知道appsettings裡面會放一些連線字串等等機密的東西。就這樣簡單的被下載走了。

目錄遍歷(Path Traversal)攻擊,就是這麼簡單。


如何避免目錄遍歷(Path Traversal)

這邊提供幾種方法

  • 替資料夾目錄加上權限限制
  • 檢查Url.Decode後的參數是否包含敏感字元:"/", @"\", "$", "..", "?"
  • 檢查檔案路徑格式:譬如檔案路徑結尾的副檔名只能為.xlsx

另外也可以檢查Path.GetFullPath後的結果是否包含baseDirectory。以我們上面的例子來看
原本的baseDirectory為:D:\Homer\Project\DesignAuditRise\DesignAuditRise.Web\Content\TemplateFile\
經過目錄遍歷串改後的完整路徑變成:D:\Homer\Project\DesignAuditRise\DesignAuditRise.Web\appsettings.Production.json

            var baseDirect = $"{_hostEnv.ContentRootPath}//Content//TemplateFile";
            var decodeFileName = System.Web.HttpUtility.UrlDecode(fileName);

            var filePath = Path.Combine(baseDirect, decodeFileName);

            var fullPath = Path.GetFullPath(filePath);
            if (!fullPath.StartsWith(baseDirect))
            {
                throw new Exception("err directory path.");
            }

很明顯的最後fullPath的結果並不包含原本的baseDirectory,表示有可能被竄改過了baseDirect,這時就能拋出錯誤,來避免掉攻擊。


總而言之,目錄遍歷(Path Traversal)是個簡單又實用的攻擊?

 

Ref:
1.[ASP.NET] Server.MapPath() 實體路徑 虛擬路徑
2.網站安全🔒 目錄遍歷 Path Traversal 攻擊手法
3.絕對路徑、相對路徑的練習(C++)
4..NET Path Traversal Guide: Examples and Prevention