代码审计方法和技巧
一次性进群,长期免费索取教程,没有付费教程。
教程列表见微信公众号底部菜单
进微信群回复公众号:微信群;QQ群:460500587
微信公众号:计算机与网络安全
ID:Computer-network
代码审计(Code audit)就是检查源代码中的安全缺陷,检查程序源代码是否存在安全隐患,或者有编码不规范的地方,通过自动化工具或者人工审查的方式,对程序源代码逐条进行检查和分析,发现这些源代码缺陷引发的安全漏洞,并提供代码修订措施和建议。
代码审计是一种以发现程序错误,安全漏洞和违反程序规范为目标的源代码分析。软件代码审计是对编程项目中源代码的全面分析,旨在发现错误,安全漏洞或违反编程约定。 它是防御性编程范例的一个组成部分,它试图在软件发布之前减少错误。
通过阅读一份源码,对其进行各类漏洞挖掘,这样的过程便统称为审计。在审计中,你不但需要知道各类漏洞的原理,还需要良好的审计环境。在面对大型开源程序时,信息量往往十分巨大,所以工具的分析和检索是必不可少的。
一、常用的审计工具
好工具才能带来高效率,首先来了解一下常用的审计工具。
1、Notepad
虽然Notepad(记事本)拥有简洁的界面,但因为其效率低下,使用的人并不多。
2、Seay PHP
Seay PHP代码审计工具支持单个关键词扫描、批量函数扫描、批量正则匹配。相对纯文本而言,提高了不少效率,如图1所示。
图1 Seay PHP界面
3、CodeXploiter
这是一款国外的代码审计工具,其特点在于能够初步判定存在问题的代码位置和存在的问题,再结合手工查看即可判定问题是否存在,十分便捷。
4、抓包改包工具
这些工具对有渗透经验的朋友来说应该不会陌生,Burp Suite和Fiddler比较知名,如图2和图3所示。
图2 Burp Suite
图3 Fiddler
5、字符转换工具
字符转换工具很多,在这里推荐小葵多功能转换工具,支持将普通编码转换为URL/SQL_En/Hex/Asc/MD5_32/MD5_16/Base64等格式的编码,非常实用,如图4所示。
图4 小葵多功能转换工具
6、常用的火狐插件
对于火狐浏览器,部分朋友可能还不清楚它的价值。在渗透中,火狐浏览器以及它的拓展插件是必不可少的工具,它在PHP审计中也是个很得力的助手。下面介绍几款常用的插件。
火狐插件的安装很简单,单击菜单中的“管理您的附加组件”按钮,然后在搜索框里搜索要安装的插件名称即可,如图5所示。
图5 安装和管理火狐插件
(1)Firebug。Firebug在浏览网页的同时又可作为功能丰富的开发工具,使用者可以对任何网页的CSS、HTML和JavaScript进行实时编辑、调试和监控,如图6所示。
图6 Firebug调试工具
(2)Live HTTP headers。该工具用来对数据包进行捕获,如图7所示。
图7 Live HTTP headers
(3)Hackbar。Hackbar包含了一些常用的工具,如SQL、XSS、POST请求、加密等,如图8所示。
图8 Hackbar
善用这些插件,可以极大地提高效率,刚开始使用时,大家可以多花一些时间来熟悉它们,毕竟,磨刀不误砍柴工。
二、SQL注入
随着PHP被广泛使用,PHP的安全问题越来越被关注。而最常见的搭配就是PHP+MySQL,接下来我们探讨一下PHP审计中MySQL注入的挖掘。
1、注入的原理
顾名思义,SQL注入就是通过把SQL命令插入到Web表单提交、输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的SQL命令的目的。SQL注入是当今网络上最普遍的一种攻击方法。
为了更好地了解其原理,下面来看一段问题代码。
从代码中可以看到,先将参数id的值以GET方式传递给变量id,然后直接将变量id代入了SQL语句进行查询。而当提交如单引号这样的特殊字符时,便会出现语法错误,从而产生报错。
因此,一个最简单也最典型的SQL注入漏洞出现了。提交单引号后的结果如图9所示。
图9 提交单引号
2、常见的注入
在审计中,最常出现的注入便是GET注入、POST注入和Cookie注入。而POST注入也是最容易被忽略的,有时可能因为传递的参数较多,常常忽略某个参数的过滤,从而导致了注入。在实际开发中,往往有的开发人员使用了REQUEST传参,却只对GET进行了过滤,因此可以换一种方式提交数据进行注入(在黑盒中更加普遍)。下面来看两段问题代码。
因为REQUEST默认情况下包含了$_GET,$_POST和$_COOKIE的数组,虽然对GET方式进行了过滤,但仍可以用POST或者Cookie的方式来提交数据,从而绕过了过滤进行注入。
POST传递是位于数据包中的。因此,注入时便需要用到抓包改包工具,这里直接用火狐插件代替,如图10所示。
图10 POST注入
很多开发人员在开发中对数组的过滤不严,往往只是过滤了value,而忽略了key,下面来看一段代码。
可以看到上述代码对id的value进行了过滤,却忽略了对key的过滤,并且将key代入了查询,因此可以对key进行注入,如图11所示。
图11 数组key的注入
这里的注入格式为××.test. php?id[注入语句]=1。
3、http头注入
http客户程序向服务器发送请求的时候必须指明请求类型,从而产生了http头。常见的http头如下所述。
Host:初始URL中的主机和端口。
Referer:包含一个URL,用户从该URL代表的页面出发访问当前请求的页面。
User-Agent:浏览器类型。
Accept:浏览器可接收的MIME类型。
Accept-Language:浏览器所希望的语言种类。
Connection:表示是否需要持久连接。
Content-Length:表示请求消息正文的长度。
Cookie:这是最重要的请求头信息之一。
而在审计中,常见的http头可能被污染的参数如下。
User-agent,浏览器类型(少)。
Referer,来源(少)。
X-Forwarded-For,获取IP(多)。
client_ip,获取IP(多)。
这里给出一段X-Forwarded-For注入的问题代码。
在数据包中加上X_FORWARDED_FOR参数,输入单引号,结果如图12所示。
图12 http头注入
4、二次注入
随着安全问题日趋被重视,一些简单的SQL注入在大中型开源程序中已基本销声匿迹了。而出现更多的则是二次注入,相对于一次注入漏洞而言,二次注入漏洞更难以被发现,但是它却具有与一次注入攻击漏洞相同的攻击威力。
下面来看某个开源商场系统的漏洞实例。
这里receiver_name是可控的,下面来跟踪insert函数。
getInsertVal中还嵌套了几个函数,不过并没有做什么过滤,故此省略。从下面的代码中来找找出库的地方。
入库、出库都没有进行过滤,因此可以判定存在二次注入。再回到insert函数,可以看到查询语句如下。
INSERT INTO cart_trade('tradeid','uid','uname','addtime','status',
'totalfee','itemfee','postfee','man','coupon','expresswayid','post
type','receiver_name','receiver_province','receiver_city','receiver_
district','receiver_address','receiver_zip','receiver_link','memo','pay
ment','istax','tax_company')values('1418809144596',2,'test',1418809144,
'WAIT_PAY',6300,5300,1000,'',0,2,1,'注入语句','110000','1','','1','1','1',
'','cod',0,'')
可以构造receiver=', 1, 1, 1, user(), 1, 1, 1, 1, 0, 2)#,逃逸出单引号并闭合,用#注释掉后面多余的语句,提交过程如图13所示。
图13 提交过程
提交的数据包如图14所示。
图14 提交的数据包
成功提交后,切到个人中心,然后查看订单,如图15所示,可以看到出库的地方已经“躺着”注入出的数据了。
图15 成功二次注入
5、过滤的绕过
在开发中,开发人员想尽办法杜绝注入的情况出现,而通常采用的办法便是过滤。常见的过滤大致分为正则和关键词的过滤,相对关键词的过滤,正则过滤的方式则更为高效。但是并没有绝对安全的方法,任何防御都存在被绕过的风险,而常见的绕过如下。
大小写混合。
替换关键字。
使用编码。
使用注释。
等价函数与命令。
使用特殊符号。
http参数控制。
整合绕过。
接下来将展示几个对关键词进行过滤的简单bypass(绕过)方法。
(1)过滤代码
preg_match('/(and|or|union|where|limit)/I',$id)
绕过方法:对关键词and,or,union,where,limit进行了过滤,构造代码类似于11||(select user from users group by user_id having user_id=1 )= 'admin'即可绕过。
(2)过滤代码
preg_match('/select|order|insert|update|eval|document|delete|injection|j
ection|link|\'|\%|\/\*|\*|\.\.\/|\.\/|\,|\.|--|\"|and,$str)
绕过方法:仅对小写的注入关键词进行了过滤,大写即可绕过。
三、XSS审计
XSS攻击是近些年盛行的一种攻击方式,恶意攻击者往Web页面里插入恶意html代码,当用户浏览该网页时,嵌入其中的html代码会被执行,从而达到恶意攻击用户的特殊目的。而PHP中对XSS的审计又是怎样的呢?
这是一个Discuz的历史漏洞了,Discuz第一时间在X3.1版本的一个补丁中修复了这一漏洞,不过对于仍然使用着Discuz X3.1旧版本(其实绝大多数都在使用旧版本,因为补丁发布的时候X3.1已经发布很久了)及以下版本的网站来说,这个漏洞依然有效。下面我们来体验一下这个漏洞的审计过程。
有关这个漏洞的代码在\upload\source\function\下的function_discuzcode.php中。
119 if($allowbbcode) {
120 if(strpos($msglower, 'ed2k://') != FALSE) {
121 $message = preg_replace("/ed2k:\/\/(.+?)\//e", "parseed2k('\\1')",
$message);
122 }
123 }
很显然,这段代码用于检测是否启用ed2k协议并在第121行对ed2k链接进行了处理。为了让大家更清晰地理解这些PHP代码,这里假设您对PHP的掌握处于入门阶段,对涉及的一些API做一个简单介绍。121行中的preg_replace函数原型如下。
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed
$subject [, int $limit = -1 [, int &$count ]] )
// preg_replace 执行一个正则表达式的搜索和替换:搜索subject中匹配pattern的部分,用replacement进行替换
对于刚刚接触代码审计的初学者来说,可能会感觉自己对代码的掌握程度不够,没关系,每种语言的官方手册对每个函数都有详细解释以供开发者学习。对于有一定经验的审计者来说,开源项目的手册或说明文中仍有很多重要的部分,而且同一厂商过去的漏洞也可能为审计引导一个方向,不要羞于站在巨人的肩膀上!
这个函数调用parseed2k()函数对$message进行正则处理,下面来跟踪处理函数parseed2k()。
320 function parseed2k($url) {
321 global $_G;
322 list(,$type, $name, $size,) = explode('|', $url);
//用来读取连接中的类型,名称与大小
323 $url = 'ed2k://'.$url.'/';
324 $name = addslashes($name);
325 if($type == 'file') {
326 $ed2kid = 'ed2k_'.random(3);
327 return '<a id="'.$ed2kid.'" href="'.$url.'" target="_
blank">'.dhtmlspecialchars(urldecode($name)).'
('.sizecount($size).')</a><script language="javascript">
$(\''.$ed2kid.'\').innerHTML=htmlspecialchars(unescape
(decodeURIComponent(\''.$name.'\')))+\' ('.sizecount
($size).')\';</script>';
328 } else {
329 return '<a href="'.$url.'" target="_blank">'.$url.'</a>';
330 }
331 }
从这段代码中可以看出,parseed2k()并没有对参数$size进行安全处理,甚至没有对$size进行类型转换(暂且认为这是程序员的疏忽),因此函数sizecount($size)中传入的是字符串类型的$size变量。
下一步跟进sizecount()函数,它在同目录下的function_core.php中:
1601 function sizecount($size) {
1602 if($size >= 1073741824) {
1603 $size = round($size/1073741824 * 100)/100 . 'GB';
1604 } else if($size >= 1048576) {
1605 $size = round($size / 1048576 * 100) / 100 .' MB';
1606 } else if($size >= 1024) {
1607 $size = round($size / 1024 * 100) / 100 . 'KB';
1608 } else {
1609 $size = $size . 'Bytes';
1610 }
1611 return $size;
1612 }
这段代码用来对文件大小进行划分,字符串类型的$size的值在与Number类型比较时,会被强制转换成Number类型后再进行比较。如果传入的$size并不是纯数字字符串,那么$size的值会被转换成NaN(Not a Number),不会触发前三个if语句,直接进入else语句,而else中的函数并没有对$size进行类型转换,直接与'Bytes'进行了配对,配对后的字符串被最终返回给function_discuzcode.php中121行的$message,然后被输出。
第1609行代码在×3.1补丁中被替换为:
1609 $size = intval($size) . ' Bytes';
intval函数将$size转换为整型,因此避免了对于$size的XSS攻击。
下面来实际测试一下:
在Discuz X3.1或以下版本的论坛中发帖时插入这样一句:
ed2k://|file|xss|'+alert(123)+'|xss/
图16所示的对话框证明了漏洞的存在。
图16 XSS对话框
顺便提一下,这个漏洞因为格式限制不能包含各种引号。不要灰心,这里可以用document.write(String.fromCharCode(... ...));的方式写入html标签,如<script src=...></script>,这里的属性src不需要引号即可加载外部JS文件,进而利用这个漏洞。
通过简单地分析可以发现,程序员为了简化代码(其实打完补丁之后并没有简化)让字符串类型的$size通过强制类型转换与整型比较,然后直接将$size与表示文件大小、单位的字符串进行连接,这种简化是一个很不好的习惯,在编写代码时应避免利用强制类型转换来比较不同类型变量,这种方法往往会被攻击者利用(就像这里一样)。
四、变量覆盖
关于变量覆盖,首先要了解PHP的特性。PHP是一种类型松散的语言,它根据变量的值自动地把变量转换为正确的数据类型。变量覆盖就是指攻击者在攻击时给予其特定的值,并覆盖原有的固定值,从而引发一些安全问题。下面介绍常见的变量覆盖。
1、变量初始化
此类变量覆盖需要在register_global=on时才能发生,下面来看“乌云某白帽子”的一个漏洞。
这里变量where并没有进行初始化,而是直接代入了查询语句,从而导致变量覆盖,在这里引发了注入,注入格式为?action=list&where=注入语句。
2、危险函数引发的变量覆盖
extract()函数的作用是从数组中把变量导入到当前的符号表中。当函数中type参数为默认值、传递的变量同名时,会进行覆盖,从而引发其他安全问题。下面来看某开源程序代码。
第4行的extract($_REQUEST)命令导致了变量覆盖,因此我们可以直接覆盖掉$table,并补全语句,从而进行注入。
五、命令执行
命令执行是PHP中常见的一种漏洞,这种漏洞的危害较大,直接威胁到服务器的安全。在PHP中,命令执行往往发生在eval()、assert()、system()、exec()、shell_exec()、passthru()、escapeshellcmd()这些高危函数上。因为开发者的疏忽,这些函数所执行的命令有时会出现用户可控的情况,从而导致攻击者提交恶意代码达到攻击目的。下面将对其进行分析。
1、常见的命令执行函数
(1)eval()
该函数是把字符串按照PHP代码来执行。语法格式:
eval(phpcode);
下面是一段问题代码:
这是一段很简单的代码,可以看到代码中将参数com的值传递给变量com,然后直接将变量com的值当作PHP代码来执行,于是漏洞便产生了。当令参数com为phpinfo();时,结果如图17所示。
图17 执行phpinfo命令
(2)system()
该函数类似C语言的system()函数,用来执行指令,并输出结果,语法格式:
system(string command, int [return_var]);
下面是一段问题代码:
<?php
$com=$_GET['com'];
$result=system($com);
echo $result;
?>
当参数com为whoami时,结果如图18所示。
图18 执行whoami命令
当参数com为ping baidu.com时,结果如图19所示。
图19 执行ping命令
(3)array_map()
该函数返回用户自定义函数作用后的数组。回调函数接收的参数数目应该和传递给array_map()函数的数组数目一致,语法格式:
array_map(function, array1, array2, array3...)
下面是一段问题代码:
<?php
$callback = $_GET[callback];
$array1 = array(0, 1, 2, 3);
$array2 = array_map($callback, $array1);
?>
令上述代码中的参数callback为phpinfo,结果如图20所示。
图20 执行phpinfo命令
2、动态函数
在实际开发中,有的程序员想动态调用某些函数,却往往会忽略动态函数的风险。
下面是一段问题代码:
<?php
function A($data){
echo "A:".$data;
}
function B($data){
echo "B:".$data;
}
if(isset($_GET['test_func'])){
$test_func = $_GET['test_func'];
$com = $_GET['com'];
$test_func($com); //动态调用
}
?>
在上述代码中,程序员原意是想动态调用A函数和B函数,所以把变量test_func作为函数名,并且可控。但这其实等同于可以执行任意函数,当直接令参数test_func为system,参数com为ping baidu.com时,结果如图21所示。
图21 直接执行ping命令
六、上传绕过
熟悉渗透的朋友一定知道文件上传是getshell的主要途径之一,是用来获取Web权限的重要漏洞方式,也常常是Web渗透的最后一关,可见其重要性。下面便来剖析常见的文件上传绕过漏洞。
1、 JavaScript绕过
先来看一段实例代码:
下面来查看mkhtml函数。
可以看到mkhtml调用的是一段JavaScript代码,我们再回到uploadfile函数中。
这段代码判断当文件类型不合法时便调用mkhtml,但无论调用是否失败,都会执行上传代码,因此只要禁用JavaScript就能知道上传文件的路径了。
这里直接改包代替(因为JavaScript是客户端脚本语言,只对浏览器进行了限制),如图22所示。
图22 改包代替
2、文件头验证绕过
问题代码如下。
<?php
if($_FILES[userfile][type] != "image/gif")
{
echo "对不起,我们只允许上传GIF格式的图片!!";
exit;
}
$dir = PreviousFile/;
$PreviousFile = $dir.basename($_FILES[userfile][name]);
if(move_uploaded_file($_FILES[userfile][tmp_name], $PreviousFile))
{
echo "文件是有效的,成功上传!!!";
} else {
echo "文件上传错误!!!请重新上传!!!! ";
}
?>
上面的代码对文件类型进行了判断,只允许了image/gif这种类型。但是人们仍可以伪造GIF89A这样的文件头进行上传。
3、逻辑问题
实例代码如下。
问题出在后缀判断和rename函数上,先来看一下后缀判断。
if($split_values[0] == strtolower($split_img[1]) && $split_values[1] ==
"allow")
当上传××.jpg.php时:
$split_values[0]=××
$split_values[1]=jpg
$split_values[2]=php
但可以看到if语句并没有判断$split_values[2],因此成功绕过,进入rename函数。
rename($user_dat['usrdir'] . “/” . $_FILES[$whichfile]['name'], $user_
dat['usrdir'] . "/" . $split_img[0] . "." . strtolower($split_img[1]));
这里会将之前上传的××.jpg.php改名为××.jpg。但根据rename函数特性,当二次上传同名文件时,例如××.jpg.php,紧接着会进入流程,尝试被改名为××.jpg,但因为××.jpg已经存在了,所以成功上传了××.jpg.php。
七、文件包含
文件包含也是PHP中常见的一种漏洞,其结果往往就是getshell,其危害极大。那什么是文件包含呢?它往往出现在include()、 include_once()、require()、require_once()、fopen、file_get_contents这些加载文件的函数上。因为对文件名没有过滤,导致攻击者可以包含任意文件或特定文件,从而达到攻击目的。
1、漏洞成因
问题代码如下。
<?php
if ($_GET['dir']) {
include $_GET['dir'];
} else {
include 'test.php';
}
?>
这段代码的初衷应该是想调用某文件的样式和功能。但因为这里dir为用户可控,所以可以调用任意文件。而问题就在于此,如果攻击者上传一个尾部有PHP恶意代码的图片,如upload/××.jpg,再访问?dir=upload/××.jpg,那么恶意代码就会被引入当前文件并执行,从而达到攻击目的。
当然,文件包含并不仅限于包含上传的文件,也可以包含一些配置文件。
?dir=.htaccess
?dir=../../../../../../web.config
?dir=../../../../../../../../../var/log/apache/error.log
?dir=../../../../../../../../../proc/config.gz(需root权限)
?dir=../../../../../../../../../etc/shadow(需root权限)
2、绕过限制
在实际开发中,开发者为了避免受其害,对包含的路径做了很多限制,如下面这段代码。
<?php
if($_GET['dir']){
include("inc/".$_GET['dir'].".htm");
}
?>
从这里可以看到,开发者对目录和后缀名都进行了控制。但人们可以提交../轻松绕过对目录的限制,同时用%00截断绕过对后缀的限制。如?dir=../../../../../../../etc/passwd,从而包含恶意文件。
%00截断需要magic_quotes_gpc=off,PHP版本小于5.3.4时才能实现。
当然,对于上述代码,还有其他方法绕过其限制,如路径长度截断(PHP版本小于5.2.8,Linux下文件名长度大于4096字节,Windows下长度大于256字节)、点号截断(PHP版本小于5.2.8,只适用于Windows系统,点号长度须大于256字节)等。
再来看一段对目录进行过滤的代码。
<?php
if($_GET['dir']){
$str=str_replace("../","./",$_GET['dir']);
include("data/".$str);
}
?>
Print.php:
<?php
echo "test";
?>
这段过滤代码是用str_replace函数将../替换成./,从而使攻击者无法用../跳出目录。不过当提交.../时,因为会将../替换成./,所以又再次变成了../,从而跳出了目录。因此当人们提交?dir=.../print.php时,就成功包含了文件。
测试结果如图23所示。
图23 包含本地文件
3、任意文件读取
file_get_contents是最常见的文件读取函数,用来把整个文件读入一个字符串中。语法格式:
file_get_contents(path, include_path, context, start, max_length)
也就是我们常说的任意文件读取,控制要读取文件的路径,从而达到攻击目的,例如读取一些数据库配置文件等。
先来看一段代码:
<?php
if($_GET['dir']){
$file=file_get_contents($_GET['dir']);
echo $file;
}
?>
提交?dir=/data/web.config,结果如图24所示。
图24 读取本地文件
八、写在最后
随着网络的普及,商业网站、政府网站、个人博客不计其数。而搭建网站的门槛也变得越来越低,搭建过程开始变得模式化、智能化,很多并不懂网站开发的人也可以使用开源软件搭建属于自己的网站,并且因为开源软件价格低廉,很多企业、政府也会选择安全性高、口碑好的开源软件进行网站搭建。因此,开源软件的安全性显得尤为重要。如今随着PHP被广泛使用,PHP在开源市场的地位越来越高,这里以小结的形式来讲讲开源审计的经验。
在审计一开始,首先应该通读全局文件,看看有没有做一些全局过滤,并且大致了解程序的结构。如果做了全局过滤,那可以尝试对过滤代码进行bypass,一旦bypass成功便是全盘沦陷。
在审计中,应特别留意用户可控的参数。而对于可控参数的查找,可以检索一些传参数组,使审计更加高效,常见的传参数组如表1。
表1 常见传参数组
当找到可控参数时,就可以分析其进行了几次传递,有没有进入查询语句,经历了几个函数。而说到函数,在PHP审计中,高危函数的查找也是极其高效的方法之一,常见的高危函数如表2所示。
表2 常见高危函数
在开源程序中,出现更多的是二次漏洞。可以想象一下,假如现在有很多物品需要带走,但一次带不了那么多,那可以分两次拿。二次漏洞也是如此,把一次攻击分两次进行,但能达到一样的目的,并且这种漏洞的隐蔽性较高,在大中型开源软件中也常常出现,同时这一类漏洞相对一次漏洞而言更耗脑力,更考验审计者的耐心和体力。
当然代码审计中出现的漏洞远不止本文所说的这些,还有如拒绝服务、CSRF、平行权限、Cookie验证绕过等。
微信公众号:计算机与网络安全
ID:Computer-network