查看原文
其他

代码审计方法和技巧

计算机与网络安全 计算机与网络安全 2022-06-01

一次性进群,长期免费索取教程,没有付费教程。

教程列表见微信公众号底部菜单

进微信群回复公众号:微信群;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包含了一些常用的工具,如SQLXSS、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

【推荐书籍】

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存