第 35 课
代码审计实例之任意文件上传
课程入口(付费)
个人背景
李,本科,电子信息工程专业,毕业一年半,有JavaScript的,PHP,Python的语言基础,目前自学网络安全中。
代码审计实例之任意文件上传
文件上传知识背景文件上传这个功能在大部分系统中都存在,但是要做好一个文件上传和下载功能的话就非常吃经验,因为涉及到很多的边界条件. 首先,文件上传首先就要校验文件类型是否在允许范围内,而确定一个文件类型恰恰是最难的事情.
而在Web的场景里,浏览器在上传文件的时候,会提供一个Content-Type,作为后端的一个参考
同时上传的文件信息会在PHP的$_FILES数组里,这个数组同时也会出现Content-Type信息,因为这个信息实在是太方便了,很多人在学习的时候就会直接把它当作是文件的类型来处理,一直延续着这个习惯.
在文件上传之后,如何保存也是一个问题.我总结成了6个字,就是不解析,不执行.
任何逃离了这个设定的上传和下载功能,都会造成问题。
下载功能,让浏览器弹出下载.一般是在响应头中,设置一个Content-Disposition信息
Content-Disposition: attachment; filename=example.html
如果文件名部分,没有经过过滤,传入了一些不可见的字符例如换行符\n,会导致这一行作废,换而言之浏览器就不会弹出下载,而是依据响应头中Content-type的信息再次判断,在这里就有可能让一个下载html的行为变成了渲染html。
Content-Disposition: attachment; filename=example\n.html
所以搜索Content-Disposition: attachment;这种代码,然后查看调用的上下文,审计文件名部分是否可控,也是一个审计的技巧。
finecms5.0.8文件上传分析准备的CMS是finecms 5.0.8。
首先,这套系统存在我们刚刚说的base64图片上传功能,可以通过base64,来定位看到这个函数的注释,已经很明显地发现是一个头像上传功能,根据文件所在的路径结构finecms/dayrui/controllers/member/Account.php,不难得出这个功能应该是在会员的账号相关地方有对应的入口.
我们先接着分析,前几行根据注释都能知道是创建存放图片文件夹的代码,我们重点关注下面的上传流程.
if ($_POST['tx']) {
$file = str_replace(' ', '+', $_POST['tx']);
if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $file, $result)){
$new_file = $dir.'0x0.'.$result[2];
if (!@file_put_contents($new_file, base64_decode(str_replace($result[1], '', $file)))) {
exit(dr_json(0, '目录权限不足或磁盘已满'));
}
这里是保存文件的逻辑代码.如果你看过项目的说明,就会知道这个项目是基于CodeIgniter这个PHP框架写的.这个框架获取$_POST数组,也是有封装的,对应的是$this→input→post()
然而在这里直接出现了$_POST['tx'],这种代码,并没有按照框架的约定用$this→input→post()代替,是一个不规范的写法.就我个人的开发经验而言,当在代码中开始不遵守规范的时候,就是容易犯错的时候,所以就要对接下来的代码重点关注了。
我们看下面一行代码
preg_match('/^(data:\s*image\/(\w+);base64,)/', $file, $result)
这里有一个正则表达式,用于对base64字符串做匹配,提取信息,大家如果刚入门,可能比较怕面对正则表达式,这里我推荐一个正则表达式可视化的在线工具 https://jex.im/regulex ,可以帮助大家理解这个正则表达式。
提取一下核心的逻辑,它保存文件的核心代码只有这4行
$file = str_replace(' ', '+', $_POST['tx']);
preg_match('/^(data:\s*image\/(\w+);base64,)/', $file, $result)
$new_file = $dir.'0x0.'.$result[2];
file_put_contents($new_file, base64_decode(str_replace($result[1], '', $file)))
这4行的作用分别是:
1.替换base64字符串中的空格为加号
2.利用正则表达式提取base64字符串的信息,把匹配到的信息放入$result数组
3.拼接上传文件的路径,文件名中扩展名部分从上面正则表达式中匹配结果的第2组而来
4.把base64字符串去掉前面几个字节中和格式相关的内容,然后做base64解码,然后写入文件
这段代码,在保存文件的时候,文件名中的扩展名部分,只是把来自正则表达式检测base64中的结果第2组信息作为扩展名,并没有对于扩展名进行丝毫的验证,根据可视化工具的提示,第3组信息的位置是 image/的后面,;base64,的前面。第2组信息,是我们完全可控的,后面文件内容部分也是我们可控的,那么不难分析出这里可以存在一个任意文件上传的漏洞.
只需要构造一个类似base64图片的字符串,在这个正则表达式的第2组位置填入我们想要生成的文件的扩展名如php,然后在文件信息的位置写入PHP代码,然后把PHP代码base64编码一下,就可以形成一个payload了.
我们先确认这段代码的入口,在这里我就直接告诉大家对应的入口是在前台的会员的头像上传处.
这里我选一个1kb大小的图片作为头像,上传的时候抓包
把代表文件格式的png改成php,代表图片内容的字符串改成一个简单的PHP代码
然后用base64编码一下 PD9waHAgcGhwaW5mbygpPz4=
上传之后的路径,分析代码甚至是把项目目录下文件夹一个一个打开找,就可以得出是在/uploadfile/member/用户id/0x0.扩展名这个路径下。而这个用户id非常好获取,查看浏览器cookie,或者是从刚刚数据包中cookie信息就知道我这个用户的id是3
简单拼接一下就可以得到:http://finecms.com.test/uploadfile/member/3/0x0.php这个url
实际访问一下,就可以发现成功访问到我们的PHP文件,并且服务端解析了并运行了这个PHP文件
任意文件上传的代码审计就到这里,同学们可以在课后利用github的代码搜索功能,尝试自己挖掘一个任意文件上传或者是下载文件截断导致html代码被执行的漏洞。
0下期更新笔记内容:
PHP代码审计入门:代码审计实例3