攻击原理
绝对路径名或者相对路径名中可能会包含文件链接(例如:软链接、硬链接、快捷方式、影子文件、别名等),或者包含特殊字符(例如:.与..),这使得验证文件路径变得困难;同时还有很多操作系统和文件系统相关的命名约定,也增加了验证文件路径的困难。
攻击影响
若不对文件路径进行验证,攻击者便可以在任意目录上传任意文件,或者利用目录遍历、等价路径等方式,读取/修改系统重要数据文件,对系统进行攻击。
防范措施
当文件路径来自非信任域时,在文件操作之前必须对文件路径进行验证,而对文件路径标准化使得验证文件路径简单起来。
在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
文件夹里新建一个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/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
软件监听抓包
在“文件上传”的表单里选择任意文件并提交上传:
将filename="1.png"
修改为filename="../1.png"
后,点击Forward
按钮,让修改后的报文送达服务器。
可以看到,上传的文件“1.png
”出现在了与upload
文件夹同级的目录中(即static
目录下)。
在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)
红框部分,r
指的是Request
,即c.Ctx.Request
。
而r.MultipartForm.File[key]
即为我们要找的文件,因而组合起来就是c.Ctx.Request.MultipartForm.File["uploadname"]
。
可以看到,有什么不懂的直接看源码也是可以解决问题的= =
上传是Web中最常见的功能,如果上传功能存在设计、编码缺陷,就容易形成上传漏洞,从而成为致命的安全问题。攻击者可以通过上传脚本木马,实现查看/篡改/删除源码和任意涂鸦网页,可以连接和操作对应的数据库,还可以通过操作系统漏洞、配置缺陷、信息泄露进行提权,获取操作系统提权。
今天这里只讲跨目录上传文件漏洞,所以只提到要对文件的路径进行标准化校验。在实际的WEB上传模块中,文件的后缀名,文件的类型,文件的大小都是需要严格把关的(白名单机制)。