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 ,添加如下代码(老惯例,仍然是声明了两个对比的控制器,并分别重写了GetPost函数):

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请求时,一个表单会发送至FileControllerPost函数响应并处理,而另一个表单会发送至SafeFileControllerPost函数响应并处理。

进行实验

在浏览器中输入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 目录下。

然而在FileControllerPost函数中,直接取了表单上传的文件名作为参数,传入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上传模块中,文件的后缀名,文件的类型,文件的大小都是需要严格把关的(白名单机制)

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值