GO語言Beego框架之WEB安全小系統(5)跨目錄上傳檔案漏洞
跨目錄上傳檔案漏洞
攻擊原理
絕對路徑名或者相對路徑名中可能會包含檔案連結(例如:軟連結、硬連結、快捷方式、影子檔案、別名等),或者包含特殊字元(例如:.與..),這使得驗證檔案路徑變得困難;同時還有很多作業系統和檔案系統相關的命名約定,也增加了驗證檔案路徑的困難。攻擊影響
若不對檔案路徑進行驗證,攻擊者便可以在任意目錄上傳任意檔案,或者利用目錄遍歷、等價路徑等方式,讀取/修改系統重要資料檔案,對系統進行攻擊。防範措施
當檔案路徑來自非信任域時,在檔案操作之前必須對檔案路徑進行驗證,而對檔案路徑標準化使得驗證檔案路徑簡單起來。
新增程式碼
views部分
在views
File
,命名為FileController.tpl
,新增如下程式碼(即在body
標籤裡新增兩個表單,各放一個input
表示要上傳的檔案):
<div class="postform">
<p> 檔案上傳 </p>
<form enctype="multipart/form-data" action="http://127.0.0.1:8080/problems/FileUpload" method="post">
<input type="file" name="uploadname" />
<input type="submit">
</form>
<br><br><br><br>
<p> 檔案上傳防範 </p>
<form enctype="multipart/form-data" action="http://127.0.0.1:8080/problems/SafeFileUpload" method="post">
<input type="file" name="uploadname" />
<input type="submit" >
</form>
</div>
controllers部分
在controllers
資料夾裡新建一個go
檔案,命名為FileController.go
,新增如下程式碼(老慣例,仍然是聲明瞭兩個對比的控制器,並分別重寫了Get
和Post
函式):
package controllers
import (
"log"
"fmt"
"github.com/astaxie/beego"
"path/filepath"
"regexp"
)
// 檔案上傳問題
type FileController struct {
beego.Controller
}
func (c *FileController) Get() {
c.TplName = "FileController.tpl"
}
// 上傳檔案的post請求處理
func (c *FileController) Post() {
c.TplName = "FileController.tpl"
if c.Ctx.Request.MultipartForm.File["uploadname"] == nil {
fmt.Println("哈哈,我很健壯")
return
}
// 獲取控制器資料流裡的檔案
f, h, err := c.GetFile("uploadname")
if err != nil {
log.Fatal("getfile err ", err)
} else {
// 儲存位置在 static/upload, 沒有資料夾要先建立,不然檔案儲存失敗
// 不限制檔案型別,但是存在跨目錄上傳漏洞 ../
fmt.Println("uploadname", "static/upload/" + h.Filename)
c.SaveToFile("uploadname", "static/upload/" + h.Filename)
}
defer f.Close()
}
// 檔案上傳問題防範
type SafeFileController struct {
beego.Controller
}
func (c *SafeFileController) Get() {
c.TplName = "FileController.tpl"
}
/**
* 驗證檔案路徑是否在安全目錄pattern下
*/
func validate(path string, pattern string) bool {
relpath, err := filepath.Abs(path) /** 【修改】對檔案路徑進行標準化 **/
if err != nil {
fmt.Println("It's error when converted to an absolute path.")
return false
}
fmt.Println(relpath)
reg := regexp.MustCompile(pattern)
/**【修改】對標準化後的路徑進行正則匹配,確保在安全目錄下 **/
return reg.MatchString(relpath)
}
// ../a.php,abc\a.php,
func (c *SafeFileController) Post() {
c.TplName = "FileController.tpl"
if c.Ctx.Request.MultipartForm.File["uploadname"] == nil {
fmt.Println("哈哈,我很健壯")
return
}
// 獲取控制器資料流裡的檔案,不限制檔案型別
f, h, err := c.GetFile("uploadname")
if err != nil {
log.Fatal("getfile err ", err)
} else {
// 儲存位置在 static/upload, 沒有資料夾要先建立,不然檔案儲存失敗
pathSrc := "static/upload/" + h.Filename
// 正則匹配,\錶轉義
pattern := `\\static\\upload\\`
// 驗證檔案是否在安全路徑下
if !validate(pathSrc, pattern) {
fmt.Println("file not in security directory.")
return
}
c.SaveToFile("uploadname", pathSrc)
}
defer f.Close()
}
routers部分
對 routers/router.go
檔案新增如下程式碼(即為上述兩個控制器註冊路由):
// 檔案上傳問題
beego.Router("/problems/FileUpload", &controllers.FileController{})
beego.Router("/problems/SafeFileUpload", &controllers.SafeFileController{})
這樣,無論url
是訪問/problems/FileUpload
還是/problems/SafeFileUpload
,兩種Get
請求都能正確渲染FileController.tpl
這個頁面,然後當從表單傳送Post
請求時,一個表單會發送至FileController
的Post
函式響應並處理,而另一個表單會發送至SafeFileController
的Post
函式響應並處理。
進行實驗
在瀏覽器中輸入http://127.0.0.1:8080/problems/FileUpload
:
正常情況
在“檔案上傳”的表單裡選擇任意檔案並提交上傳:
後臺顯示如下:
上傳成功。
跨目錄上傳
使用burpsuite
軟體監聽抓包
在“檔案上傳”的表單裡選擇任意檔案並提交上傳:
burpsuite
軟體抓到的包如下:
將filename="1.png"
修改為filename="../1.png"
後,點選Forward
按鈕,讓修改後的報文送達伺服器。
後臺顯示如下:
可以看到,上傳的檔案“1.png
”出現在了與upload
資料夾同級的目錄中(即static
目錄下)。
跨目錄上傳防範
在“檔案上傳防範”的表單裡選擇任意檔案並提交上傳:
burpsuite
軟體抓到的包如下:
先右鍵(或者CTRL+R
)將其新增到Repeater
在Repeater
這,將filename="2.png"
修改為filename="../2.png"
後,點選GO
按鈕,獲得相應報文。
後臺顯示如下:
後臺輸出了檔案上傳目錄的絕對路徑,然後輸出檔案並不在安全目錄下,上傳失敗。
在Repeater
這,將filename="../2.png"
修改為filename="abc/2.png"
後,點選GO
按鈕,獲得相應報文。
後臺顯示如下:
後臺輸出了檔案上傳目錄的絕對路徑,但是並沒有輸出檔案並不在安全目錄下,然而還是上傳失敗。
原因分析
表單的本意設計是可以選擇一個本機內的檔案,將其上傳至伺服器的\static\upload
目錄下。
然而在FileController
的Post
函式中,直接取了表單上傳的檔名作為引數,傳入c.SaveToFile
函式中
c.SaveToFile("uploadname", "static/upload/" + h.Filename)
所以當檔名被burpsuite
中間人修改為../1.png
後,上傳的目錄隨之變成了static/upload/../1.png
。
“../
”對系統來說表示上級目錄,因此“static/upload/../1.png
”中的“upload
”與“../
”是抵消的,最終實際上傳目錄為“static/1.png
”。
於是便產生了跨目錄上傳漏洞,通過這個漏洞,攻擊者可以將檔案上傳到任意目錄(可以通過新增多個“../
”來找到根目錄)
推薦防範措施:先把目錄傳入filepath.Abs()
函式,刪除所有符號連結,獲得絕對路徑,然後再對標準化後的路徑進行正則匹配,比較是否在安全目錄下。
/**
* 驗證檔案路徑是否在安全目錄pattern下
*/
func validate(path string, pattern string) bool {
relpath, err := filepath.Abs(path) /** 【修改】對檔案路徑進行標準化 **/
if err != nil {
fmt.Println("It's error when converted to an absolute path.")
return false
}
fmt.Println(relpath)
reg := regexp.MustCompile(pattern)
/**【修改】對標準化後的路徑進行正則匹配,確保在安全目錄下 **/
return reg.MatchString(relpath)
}
這裡就是將標準化後的路徑與\\static\\upload\\
進行正則匹配,如果標準化後的路徑缺乏\\static\\upload\\
這一子串,說明肯定不是在安全路徑下(即存在../
跨目錄的問題)。
而在實驗當中,還存在將檔名改為abc/2.png
使得最後的檔案路徑變成\static\upload\abc\2.png
的做法,此時是符合正則匹配的。但是仍然上傳失敗了。還記得c.SaveToFile
這個函式嘛,當發現要儲存的檔案的路徑中存在未建立的資料夾是沒法儲存成功的,所有資料夾必須由我們事先建立好,因而將上傳的檔名改為abc/2.png
是沒辦法開一個子資料夾的。
健壯性
若使用者不選擇任何檔案,直接點選檔案提交,此時後臺程式碼c.GetFile("uploadname")
獲取key值為"uploadname"
的檔案失敗,直接報錯,整個伺服器斷開連線。
解決方案即在獲取檔案前進行一次非空判斷:
if c.Ctx.Request.MultipartForm.File["uploadname"] == nil {
fmt.Println("哈哈,我很健壯")
return
}
問:怎麼發現表單Post
過來的檔案就儲存在c.Ctx.Request.MultipartForm.File
當中呢?
答:通過檢視c.GetFile()
的原始碼發現,返回值為c.Ctx.Request.FormFile(key)
於是再檢視FormFile()
的原始碼:
紅框部分,r
指的是Request
,即c.Ctx.Request
。
而r.MultipartForm.File[key]
即為我們要找的檔案,因而組合起來就是c.Ctx.Request.MultipartForm.File["uploadname"]
。
可以看到,有什麼不懂的直接看原始碼也是可以解決問題的= =
現在再不選檔案直接點提交試試:
小總結
上傳是Web中最常見的功能,如果上傳功能存在設計、編碼缺陷,就容易形成上傳漏洞,從而成為致命的安全問題。攻擊者可以通過上傳指令碼木馬,實現檢視/篡改/刪除原始碼和任意塗鴉網頁,可以連線和操作對應的資料庫,還可以通過作業系統漏洞、配置缺陷、資訊洩露進行提權,獲取作業系統提權。
今天這裡只講跨目錄上傳檔案漏洞,所以只提到要對檔案的路徑進行標準化校驗。在實際的WEB上傳模組中,檔案的字尾名,檔案的型別,檔案的大小都是需要嚴格把關的(白名單機制)。