红日安全代码审计学习(一)

红日安全代码审计学习

项目地址:

https://github.com/hongriSec/PHP-Audit-Labs#

记录一下自己的学习过程

顺便加上一些自己的理解

Part1

in_array函数缺陷

github地址:in_array函数缺陷

in_array :(PHP 4, PHP 5, PHP 7)

功能 :检查数组中是否存在某个值

定义bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] )

$haystack 中搜索 $needle ,如果第三个参数 $strict 的值为 TRUE ,则 in_array() 函数会进行强检查,检查 $needle 的类型是否和 $haystack 中的相同。如果找到 $haystack ,则返回 TRUE,否则返回 FALSE

in_array()第三个参数未设置为true时,是弱匹配。

1
7shell.php 

==

1
7 
1
1,1 and if(ascii(substr((select database()),1,1))=112,1,sleep(3)));#

==

1
1  

利用场景:

文件上传、SQL注入白名单绕过

修复建议:

1、第三个参数设置为 true

2、使用正则匹配来处理变量

filter_var函数缺陷

github地址:filter_var函数缺陷

filter_var : (PHP 5 >= 5.2.0, PHP 7)

功能 :使用特定的过滤器过滤一个变量

定义mixed filter_var ( mixed $variable [, int $filter = FILTER_DEFAULT [, mixed $options ]] )

1
2
$url = filter_var($_GET['url'],FILTER_VALIDATE_URL);
var_dump($url);

我们用FILTER_VALIDATE_URL过滤器做测试。

可以使用 :// 来绕过。

由于原文没有讲清楚为什么可以绕过,本人测试了一下应该是因为filter_var函数在使用FILTER_VALIDATE_URL过滤器时,只要参数中含有 :// 就视作为有效的URL。

http://www.baidu.com 是有效的URL

ftp://www.baidu.com是有效URL

0://www.baidu.com也是有效URL,注意这个,这里可以作为一个SSRF的小trick

www.baidu.com是无效的URL

利用场景:

XSS绕过

XSS场景可以使用Javascript伪协议进行绕过

1
javascript://comment%250aalert(1)

// 在JavaScript中表示单行注释, 对**%** 百分号编码成 %25,我们这里用了字符 %0a ,该字符为换行符,所以 alert 语句与注释符 // 就不在同一行

程序将浏览器发来的payload:

1
javascript://comment%250aalert(1)

先解码成:

1
javascript://comment%0aalert(1)

存储在可回显在页面上变量中,然后用户点击就会触发alert函数,弹窗。

SSRF trick

看这篇文章

https://www.anquanke.com/post/id/101058

修复建议:

对于XSS的话,最好是过滤关键词+实体化编码。

对于ssrf的话,建议正则匹配吧。

实例化任意对象漏洞

[红日安全]代码审计Day3 - 实例化任意对象漏洞

这个漏洞类似于php任意代码执行了

实例化类的类名和传入类的参数均在用户的控制之下,攻击者可以通过该漏洞,调用PHP代码库的任意构造函数。即使代码本身不包含易受攻击的构造函数,我们也可以使用PHP的内置类 SimpleXMLElement 来进行 XXE 攻击,进而读取目标文件的内容,甚至命令执行。

先看看 SimpleXMLElement 类的定义:

SimpleXMLElement :(PHP 5, PHP 7)

功能 :用来表示XML文档中的元素,为PHP的内置类。

data:格式正确的XML字符串,当参数data_is_urlTrue时,传入一个URL字符串

options:(可选)用于指定其他Libxml参数。

SimpleXMLElement 导致的XXE攻击:

利用场景:

XXE

修复建议:

可以修改代码,不要用这种可控的参数形式。

然后提一下对XXE漏洞进行修复,可以禁止加载XML实体对象。

strpos使用不当引发漏洞

[红日安全]代码审计Day4 - strpos使用不当引发漏洞`

结合具体场景代码

代码在 第8行第9行 使用 strpos 函数来防止输入的参数含有 < 和 > 符号,猜测开发者应该是考虑到非法字符注入问题。

strpos — 查找字符串首次出现的位置

作用:主要是用来查找字符在字符串中首次出现的位置。

结构:int strpos ( string $haystack , mixed $needle [, int $offset = 0 ] )

1
2
3
4
<?php
var_dump(strpos('abcd','a')); #0
var_dump(strpos('abcd','x')); #false
?>

上面场景代码中核心判断代码是

1
(!strpos($user,'<') || !strpos($user,'>')) && (!strpos($pass,'<') || !strpos($pass,'>'))

作者考虑到可能有黑客会在登陆点进行SQL注入测试,所以判断了一下是否出现 ‘<’** 或 **’>’ 若出现则strpos函数获得出现位置,再配合 ! 取反得到false

先不说strpos函数的问题,这段代码的逻辑就有问题,如果$user只出现了>或<号但$pass没有出现>或<号,那么依然可以判定成功。

例如这样

再说回strpos函数的绕过,因为strpos函数是取字符位置,但是如果取到了位置为0,那么通过!取反得到的就会是true了,就会导致判断成功,代码如下

利用场景:

ctf印象中见过

修复建议:

代码逻辑优化

或者登录验证那块不要这么写了(真要防止SQL注入,这样写会不会太儿戏了。。。)

escapeshellarg与escapeshellcmd使用不当

[红日安全]代码审计Day5 - escapeshellarg与escapeshellcmd使用不当

原理:

escapeshellarg()

将转码任何已经存在的单引号

例如:

1
2
3
4
<?php
$test = "127.0.0.1' -v -d a=1";
echo(escapeshellarg($test));
?>

输出:

1
'127.0.0.1'\'' -v -d a=1'

这里稍微解释一下

其实是变成了三个字符串的连接

1
'127.0.0.1'
1
\' #这里是escapeshellarg函数的效果 原来的单引号前面加上\转义
1
'-v -d a=1'

escapeshellcmd函数

escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。

反斜线(\)会在以下字符之前插入: &#;|*?~<>^()[]{}$`, \x0A\xFF'" 仅在不配对儿的时候被转义。

当**escapeshellarg()escapeshellcmd()**一起使用时就会出现参数逃逸漏洞

1
2
3
4
5
<?php
$test = "127.0.0.1' -v -d a=1";
$test2 = escapeshellarg($test);
echo(escapeshellcmd($test2));
?>

输出:

1
'127.0.0.1'\\'' -v -d a=1\'

依然是变成了三个部分

1
'1270.0.1' #没有发生转义 单引号是配对的
1
\\''  #从之前的\' 变成 \\' 这回是反斜线被转义了 所以单引号是多出来的正好与之前的第三部分的第一个单引号配对了
1
-v -d a=1\' #因为第二部分配对了 导致第三部分的单引号多余了 所以根据函数规则 未匹配的单引号会加上反斜线转义

利用场景:

参数逃逸导致远程代码执行

ctf也有遇到过

修复建议:

不建议大家同时使用 escapeshellcmd()escapeshellarg() 函数对参数进行过滤

正则使用不当导致的路径穿越问题

[红日安全]代码审计Day6 - 正则使用不当导致的路径穿越问题

preg_replace:(PHP 4, PHP 5, PHP 7)

功能 : 函数执行一个正则表达式的搜索和替换

定义mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )

搜索 subject 中匹配 pattern 的部分, 如果匹配成功将其替换成 replacement

本质上是对正则表达式的绕过

preg_replace 中的 pattern 部分 ,该正则表达式并未起到过滤目录路径字符的作用。[^a-z.-_] 表示匹配除了 a 字符到 z 字符、**.** 字符到 _ 字符之间的所有字符。

1
../../ config.php

即可删除config.php文件

利用场景:

本质是对正则的绕过,只要正则写的不完善,都可以进行绕过尝试。

CTF中也有出现过。

修复建议:

结合业务修改完善正则表达式。

parse_str函数缺陷

[红日安全]代码审计Day7 - parse_str函数缺陷

这个函数的缺陷实际上导致的是一个变量覆盖漏洞。

parse_str

功能 :parse_str的作用就是解析字符串并且注册成变量,它在注册变量之前不会验证当前变量是否存在,所以会直接覆盖掉当前作用域中原有的变量。

定义void parse_str( string $encoded_string [, array &$result ] )

如果 encoded_string 是 URL 传入的查询字符串(query string),则将它解析为变量并设置到当前作用域(如果提供了 result 则会设置到该数组里 )。

利用场景:

白盒审计

ctf遇到过

修复建议:

在注册变量前先判断变量是否存在

preg_replace函数之命令执行

[红日安全]代码审计Day8 - preg_replace函数之命令执行

preg_replace:(PHP 5.5)

功能 : 函数执行一个正则表达式的搜索和替换

定义mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )

搜索 subject 中匹配 pattern 的部分, 如果匹配成功以 replacement 进行替换

  • $pattern 存在 /e 模式修正符,允许代码执行
  • /e 模式修正符,是 **preg_replace() ** 将 $replacement 当做php代码来执行

总结就是 php5.5版本以下,preg_replace函数有一个/e模式,开启了就会导致代码执行。

经典问题案例:

1
2
3
4
5
6
7
8
9
header("Content-Type: text/plain");

function complexStrtolower( $regex, $value){
return preg_replace('/('. $regex.')/ei','strtolower("\1")',$value);
}

foreach ($_GET as $regex => $value){
echo complexStrtolower($regex, $value)."n";
}

preg_replace 使用了 /e 模式,导致可以代码执行,我们可以控制第一个和第三个参数,第二个参数固定为 ‘strtolower(“\1”)’ 字符串。

上面的命令执行,相当于 eval(‘strtolower(“\1”);’)

如何进行命令执行其他恶意代码?

这里要用到一个PHP的小trick反向引用

看了几篇文章,感觉讲的不是很清楚,这里解释一下

1
2
3
4
5
反向引用
对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,
所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。
缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 '\n' 访问,
其中 n 为一个标识特定缓冲区的一位或两位十进制数。
1
2
3
public static function camelize($word) {
return preg_replace('/(^|_)([a-z])/e', 'strtoupper("\2")', $word);
}

上面这段代码,就是捕获匹配的 (^|_)([a-z]) 部分。它们从 1 开始编号,因此您有反向引用 1 和 2。

1是正则 **(^|_) ** 所匹配到的东西,2是 ([a-z]) 所匹配到的东西。这里还有一个隐藏点 0 是整个匹配的字符串 很多文章没讲到这个。

回到上面的代码中

1
preg_replace('/('. $regex.')/ei','strtolower("\1")',$value);

第二个参数中的\\1,实际上就是\1,配合反向引用的规则,\1就是指第一个子匹配项。

那上面的命令执行,就相当于 eval(‘strtolower(“满足正则表达式的$value”);’)

官方 payload 为: /?.*={${phpinfo()}}

.* 正则匹配任意多个字符

1
2
原先的语句: preg_replace('/(' . $regex . ')/ei', 'strtolower("\\1")', $value);
变成了语句: preg_replace('/(.*)/ei', 'strtolower("\\1")', {${phpinfo()}});

但是这个payload是有问题的

以**.作为参数名是非法的,会自动转化成_**

图片来自:https://www.cnblogs.com/HelloCTF/p/13184476.html

稍微修改以下payload即可

1
\S*=${phpinfo()}

\S* 正则匹配任意多个非空白符

1
`\C`、`\D`、`\H`、`\N`、`\S`、`\V`、`\X` 都能代替

匹配到 {${phpinfo()}} 或者 ${phpinfo()} ,才能执行 phpinfo 函数,这是一个小坑。这实际上是 PHP可变变量 的原因。在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。

1
2
3
4
5
<?php $a = "hello";
echo "a=$a";
echo "\n";
echo 'a=$a';
?>

输出:

1
2
a=hello 
a=$a

为什么要匹配到 {${phpinfo()}} 或者 ${phpinfo()} ,才能执行 phpinfo 函数,这是一个小坑。这实际上是 PHP可变变量 的原因。在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。 ${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true)。

利用场景:

PHP版本有限制,可能见到的机会不多。

ctf出现过作为考点的题目

修复建议:

避免使用 /e 模式修正符

str_replace函数过滤不当

[红日安全]代码审计Day9 - str_replace函数过滤不当

str_replace :(PHP 4, PHP 5, PHP 7)

功能 :子字符串替换

定义mixed str_replace ( mixed $search , mixed $replace , mixed $subject [, int &$count ] )

该函数返回一个字符串或者数组。如下:

str_replace(字符串1,字符串2,字符串3):将字符串3中出现的所有字符串1换成字符串2。

str_replace(数组1,字符串1,字符串2):将字符串2中出现的所有数组1中的值,换成字符串1。

str_replace(数组1,数组2,字符串1):将字符串1中出现的所有数组1一一对应,替换成数组2的值,多余的替换成空字符串。

本质是过滤不严谨

以上代码是将 ../ 字符替换成空,然后进行路径拼接

payload:**….//** 或者 …/./ ,在经过程序的 str_replace 函数处理后,都会变成 ../

利用场景:

任意文件读取,任意文件删除……

修复建议:

结合业务场景进行过滤

程序未恰当exit导致的问题

[红日安全]代码审计Day10 - 程序未恰当exit导致的问题

本质上代码逻辑有问题,在本应该立即exit退出的地方,没有退出函数,使得程序继续运行,出现非预期的情况。

利用场景:

重装漏洞

修复建议:

在正确的地方退出程序即可,使用 dieexit 等函数。

unserialize反序列化漏洞

[红日安全]代码审计Day11 - unserialize反序列化漏洞

这个可以说是打ctf之前经常能见到。

这里直接推几篇文章

这一篇就够了

https://blog.csdn.net/solitudi/article/details/113588692

利用场景:

构造POP链rce

ctf

修复建议:

不要把用户的输入或者是用户可控的参数值直接放进反序列化的操作中

误用htmlentities函数引发的漏洞

[红日安全]代码审计Day12 - 误用htmlentities函数引发的漏洞

htmlentities — 将字符转换为 HTML 转义字符

1
string htmlentities ( string $string [, int $flags = ENT_COMPAT | ENT_HTML401 [, string $encoding = ini_get("default_charset") [, bool $double_encode = true ]]] )

作用:在写PHP代码时,不能在字符串中直接写实体字符,PHP提供了一个将HTML特殊字符转换成实体字符的函数 htmlentities()。

注:htmlentities() 并不能转换所有的特殊字符,是转换除了空格之外的特殊字符,且单引号和双引号需要单独控制(通过第二个参数)。第2个参数取值有3种,分别如下:

  • ENT_COMPAT(默认值):只转换双引号。
  • ENT_QUOTES:两种引号都转换。
  • ENT_NOQUOTES:两种引号都不转换。

利用场景:

当参数没设置清楚会导致出现SQL注入、XSS。

修复建议:

htmlentities 这个函数使用的时候,尽量加上可选参数,并且选择 ENT_QUOTES 转换单引号和双引号 参数。

特定场合下addslashes函数的绕过

[红日安全]代码审计Day13 - 特定场合下addslashes函数的绕过

addslashes — 使用反斜线引用字符串

1
string addslashes ( string $str )

作用:在单引号(’)、双引号(”)、反斜线(\)与 NULL( NULL 字符)字符之前加上反斜线。

具体案例:

代码 第33行 ,通过 POST 方式传入 userpasswd 两个参数,通过 isValid() 来判断登陆是否合法。我们跟进一下 isValid() 这个函数,该函数主要功能代码在 第12行-第22行 ,我们看到 13行14行 调用 sanitizeInput() 针对 userpassword 进行相关处理。

跟进一下 sanitizeInput() ,主要功能代码在 第24行-第29行 ,这里针对输入的数据调用 addslashes 函数进行处理,然后再针对处理后的内容进行长度的判断,如果长度大于20,就只截取前20个字符。

滤了单引号,正常情况下是没有注入了,那为什么还能导致注入了,原因实际上出在了 substr 函数

substr — 返回字符串的子串

1
string substr ( string $string , int $start [, int $length ] )

作用:返回字符串 stringstartlength 参数指定的子字符串。

代码中length默认为20

我们里可以用他默认的长度为20,设计一个payload

正常情况输入

1
user=1234567890123456789'

会被转换成

1
user=1234567890123456789\'

但是经过substr函数的截取

又变成了

1
user=1234567890123456789\

再结合具体代码

1
select count(p) from user u where user = '1234567890123456789\' AND password = '$pass'

\将本来是用来闭合user的单引号转义成了正常的单引号,所以签名的单引号与$pass的前一个单引号进行了闭合。

这里我们让 pass=or 1=1# ,那么最后的sql语句如下:

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

sql注入成功。

利用场景:

白盒审计

ctf

修复建议:

结合程序逻辑进行代码优化。

从变量覆盖到getshell

[红日安全]代码审计Day14 - 从变量覆盖到getshell

第10-11行 处, Carrot 类的构造方法将超全局数组 $_GET 进行变量注册,这样即可覆盖 第8行 已定义的 $this-> 变量。而在 第16行 处的析构函数中, file_put_contents 函数的第一个参数又是由 $this-> 变量拼接的,这就导致我们可以控制写入文件的位置,最终造成任意文件写入问题。下面我们试着使用 payload

1
id=../var/www/html/shell.php&shell=',)%0a<?php phpinfo();?>//

shell.php文件中内容:

1
2
3
4
5
6
7
array(
'id' => '../var/www/html/shell.php',
'lost' => 0,
'bought' => 0,
'shell' => '\',)
<?php phpinfo();?>//',
)

这里注意,shell变量中的反斜杠\是自动加上的,是给我们传入shell参数的内容中的单引号转义的。

利用场景:

变脸覆盖的场景有写shell还有覆盖session登录等等

修复建议:

检测变量名是否为PHP原有的超全局数组,如果是则直接退出并告知变量不允许

$_SERVER[‘PHP_SELF’]导致的防御失效问题

[红日安全]代码审计Day15 - $_SERVER[‘PHP_SELF’]导致的防御失效问题

PHP自带的**$_SERVER[‘PHP_SELF’]** 参数是可以控制

PHP_SELF 指当前的页面绝对地址,比如我们的网站:http://www.test.com/redict/index.php,那么**PHP_SELF**就是 /redict/index.php 。

但有个小问题很多人没有注意到,当URLPATH_INFO的时候,比如:http://www.test.com/redict/index.php/admin,那么**PHP_SELF**就是/redict/index.php/admin 也就是说,其实 PHP_SELF 有一部分是我们可以控制的。

利用场景:

任意URL跳转

有一个关于 360webscan 的防护脚本一个历史漏洞,正是使用了 $_SERVER[‘PHP_SELF’] 这个变量,导致可以绕过360webscan防护脚本的防护,脚本的防护效果失效。

修复建议:

使用 $_SERVER[‘SCRIPT_NAME’] 代替即可

深入理解$_REQUESTS数组

[红日安全]代码审计Day16 - 深入理解$_REQUESTS数组

超全局数组 $_REQUEST 中的数据,是 $_GET$_POST$_COOKIE 的合集,而且数据是复制过去的,并不是引用。

所以很多时候仅仅是对**$_GET** 、 $_POST的传参做了限制,但利用时的变量确实使用**$_REQUEST**传入的,相当于并没有进行过滤。

利用场景:

XSS,SQL注入等

修复建议:

优化参数处理逻辑。

Raw MD5 Hash引发的注入

[红日安全]代码审计Day17 - Raw MD5 Hash引发的注入

md5 — 计算字符串的 MD5 散列值

1
string md5 ( string $str [, bool $raw_output = false ] )

如果可选的 raw_output 被设置为 TRUE,那么 MD5 报文摘要将以16字节长度的原始二进制格式返回。

案例:

我可以控制的点有两个变量,一个是 $user ,一个是 $pass ,**$pass** 经过了 md5 的处理,但是返回字段不是标准的md5值,**$user** 经过了 addslashes 函数的处理,无法引入特殊符号去闭合。

如果我们经过 $pass = md5($this->password, true); 处理之后的值逃逸出一个反斜杆,那么实际上带入到数据库的值就如下所示:

1
select count(p) from user s where password='xxxxxx\' and user='xxx#'

3

发现 md5(128, true) 最后的结果带有反斜杠。

payload如下:

1
user= OR 1=1#&passwd=128

带入到数据库查询的语句如下:

1
select count(p) from user s where password='v�a�n���l���q��\' and user=' OR 1=1#'

利用场景:

CTF遇见过

修复建议:

建议在使用 md5 函数的时候,不要将 $raw_output 字段设置为true