一、PHP代码执行代码审计
首先讲一下PHP代码执行漏洞和命令执行漏洞的区别,PHP代码执行指的是将php代码植入到web应用程序中通过web容器来执行,执行的是php代码,命令执行是指讲包含系统命令的恶意输入提交给系统,执行的是系统命令。
接下来说一下几个常见的PHP代码执行漏洞常见函数。
1.eval()函数
# eval
(PHP 4, PHP 5, PHP 7)
eval — 把字符串作为PHP代码执行
### 说明
**eval** ( string `$code` ) : [mixed]
把字符串 `code` 作为PHP代码执行。
**Caution**
函数**eval()**语言结构是 *非常危险*的, 因为它允许执行任意 PHP 代码。 *它这样用是很危险的。* 如果您仔细的确认过,除了使用此结构以外 别无方法, 请多加注意,*不要允许传入任何由用户 提供的、未经完整验证过的数据* 。
### 参数[ ¶]
需要被执行的字符串
代码不能包含打开/关闭 [PHP tags](https://www.php.net/manual/zh/language.basic-syntax.phpmode.php)。比如, *'echo "Hi!";'* 不能这样传入: *'<?php echo "Hi!"; ?>'*。但仍然可以用合适的 PHP tag 来离开、重新进入 PHP 模式。比如 *'echo "In PHP mode!"; ?>In HTML mode!<?php echo "Back in PHP mode!";'*。
除此之外,传入的必须是有效的 PHP 代码。所有的语句必须以分号结尾。比如 *'echo "Hi!"'* 会导致一个 parse error,而 *'echo "Hi!";'* 则会正常运行。
*return* 语句会立即中止当前字符串的执行。
代码执行的作用域是调用 **eval()** 处的作用域。因此,**eval()** 里任何的变量定义、修改,都会在函数结束后被保留。
### 返回值[ ¶]
**eval()** 返回 **`NULL`**,除非在执行的代码中 *return* 了一个值,函数返回传递给 *return* 的值。 PHP 7 开始,执行的代码里如果有一个 parse error,**eval()** 会抛出 ParseError 异常。在 PHP 7 之前, 如果在执行的代码中有 parse error,**eval()**返回 **`FALSE`**,之后的代码将正常执行。无法使用 [set_error_handler()](https://www.php.net/manual/zh/function.set-error-handler.php) 捕获 **eval()** 中的解析错误。
这里需要注意的是php eval函数接收的参数语句必须是以;结尾的,如果不是的话会返回语法错误。
漏洞代码如下所示:
p1
这里会接收一个id的参数,然后赋值给shiyanlou变量,最后用echo输出值,注意php代码是从右往左执行,所以输入phpinfo之后会先执行然后再给shiyanlou赋值。
结果如下:
p2
2.assert函数
assert — 检查一个断言是否为 FALSE
PHP 5
bool assert ( mixed $assertion [, string $description ] )
PHP 7
bool assert ( mixed $assertion [, Throwable $exception ] )
assert() 会检查指定的 assertion 并在结果为 FALSE 时采取适当的行动。
如果 assertion 是字符串,它将会被 assert() 当做 PHP 代码来执行。 assertion
是字符串的优势是当禁用断言时它的开销会更小,并且在断言失败时消息会包含 assertion 表达式。 这意味着如果你传入了 boolean
的条件作为 assertion,这个条件将不会显示为断言函数的参数;在调用你定义的 assert_options()
处理函数时,条件会转换为字符串,而布尔值 FALSE 会被转换成空字符串。
断言这个功能应该只被用来调试。 你应该用于完整性检查时测试条件是否始终应该为 TRUE,来指示某些程序错误,或者检查具体功能的存在(类似扩展函数或特定的系统限制和功能)
assert 危险点在于断言语句是字符串,那将会当成php代码来执行。
p3
结果如下所示:
p4
3.preg_replace()函数
该函数解释如下:
PHP 函数 preg_replace()
语法
mixed preg_replace (mixed pattern, mixed replacement, mixed string [, int limit [, int &$count]] );
定义和用法
preg_replace()函数就像POSIX中的函数ereg_replace(),除了可以使用正则表达式的匹配模式和替换input参数。 可选的输入参数limit指定有多少匹配应该发生。 如果可选参数$count传递这个变量就会填充替代品的数量。
返回值
替换发生后,将返回修改后的字符串。
如果没有找到匹配的,字符串将保持不变
出现的问题在于如果匹配字符模式串里出现参数e修饰符时,replacement 的值会被当成php代码来执行。
漏洞代码如下:
<?php
$id = $_GET['id'];
preg_replace("/<j>(.*)<j>/e",'\1',$id);
?>
利用代码如下:
p5
- call_user_func() 函数
该函数主要用来调用其他函数用。。
### 官方说明:
(PHP 4, PHP 5, PHP 7)
call_user_func — 把第一个参数作为回调函数调用
#### 说明
第一个参数 `callback` 是被调用的回调函数,其余参数是回调函数的参数。
#### 参数
* `callback`
将被调用的回调函数([callable]
* `parameter`
0个或以上的参数,被传入回调函数。
**Note**:请注意,传入**call_user_func()**的参数不能为引用传递。
漏洞代码如下:
<?php
call_user_func($_GET['id'],$_GET['data']);
?>
利用方法如下:
p6
5.动态函数
php这么开放的嘛还能动态函数。。。
p7
利用方法和前面一样,就不截图了。
二.PHP命令注入漏洞
1.第一类函数主要是直接执行系统命令的函数,类似system()、exec()、shell_exec()、passthru()可以直接传入命令执行并返回结果,其中system()最简单,不需要输出函数,会自动打印命令执行结果,所以这里我们先讲解system()函数
p8
这种利用比较简单,如下:
p9
其他几个函数差不多,区别就是需要自己手动打印出来而已。
2.第二类函数主要是popen()、proc_open()函数,区别于第一种的是他没有回显,echo不出来结果的,一般是要重导向至一个文件再cat出来才行。
popen()、proc_open()
resource popen ( string $command , string $mode )
打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生
第一个参数 $command 会被当作命令执行。
第二个参数 $mode 决定执行模式,有两个取值r 或者 w ,来指明是读还是写.
漏洞代码如下:
<?php
$bash = $_GET['bash'];
popen("$bash",'r');
?>
利用代码如下:
p10
3.第三类函数只有pcntl_exec(),此函数是php的多进程扩展,在处理大量任务的情况下会使用到,使用此函数需要额外安装.
函数解析如下:
void pcntl_exec ( string $path [, array $args [, array $envs ]] )
以给定参数执行程序:
path : path必须时可执行二进制文件路径或一个在文件第一行指定了一个可执行文件路径标头的脚本(比如文件第一行是#!/usr/local/bin/perl的perl脚本)。
args: args是一个要传递给程序的参数的字符串数组。
envs : envs是一个要传递给程序作为环境变量的字符串数组,这个数组是 key => value格式的,key代表要传递的环境变量的名称,value代表该环境变量值。
三.xss漏洞代码审计
xss代码审计的时候主要寻找没有进行输出处理的函数。
1.反射性xss
漏洞代码如下:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>XSS</title>
</head>
<body>
<form action="" method="get">
<input type="text" name="input">
<input type="submit">
</form>
<br>
<?php
$XssReflex = $_GET['input'];
echo 'output:<br>'.$XssReflex;
?>
</body>
</html>
明显就是通过get方法来接收一个input变量的值,然后通过echo直接输出了,并没有经过任何的过滤。
漏洞利用代码就是最典型的如下所示喽。
p11
2.存储型xss
漏洞代码如下:
<span style="font-size:18px;"><meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<html>
<head>
<title>XssStorage</title>
</head>
<body>
<h2>Message Board<h2>
<br>
<form action="XssStorage.php" method="post">
Message:<textarea id='Mid' name="desc"></textarea>
<br>
<br>
Subuser:<input type="text" name="user"/><br>
<br>
<input type="submit" value="submit" onclick='loction="XssStorage.php"'/>
</form>
<?php
if(isset($_POST['user'])&&isset($_POST['desc'])){
$log=fopen("sql.txt","a");
fwrite($log,$_POST['user']."\r\n");
fwrite($log,$_POST['desc']."\r\n");
fclose($log);
}
if(file_exists("sql.txt"))
{
$read= fopen("sql.txt",'r');
while(!feof($read))
{
echo fgets($read)."</br>";
}
fclose($read);
}
?>
</body>
</html></span>
从代码中可以看到他就是接收两个post的方法,然后直接写入文件里面,再从文件里面直接echo文件内容而已。问题和上面是一样的,也是不进行输出处理。
咿,我突然想明白他为啥这么写了,因为这样不用写入数据库。。。。却又实现了储存型xss的机制。。。
漏洞利用代码和前面也差不多。。。
p12
防御手段如下所示:
A.PHP直接输出html的,可以采用以下的方法进行过滤:
1.htmlspecialchars函数
2.htmlentities函数
3.HTMLPurifier.auto.php插件
4.RemoveXss函数
B.PHP输出到JS代码中,或者开发Json API的,则需要前端在JS中进行过滤:
1.尽量使用innerText(IE)和textContent(Firefox),也就是jQuery的text()来输出文本内容
2.必须要用innerHTML等等函数,则需要做类似php的htmlspecialchars的过滤
C.其它的通用的补充性防御手段
1.在输出html时,加上Content Security Policy的Http Header
(作用:可以防止页面被XSS攻击时,嵌入第三方的脚本文件等)
(缺陷:IE或低版本的浏览器可能不支持)
2.在设置Cookie时,加上HttpOnly参数
(作用:可以防止页面被XSS攻击时,Cookie信息被盗取,可兼容至IE6)
(缺陷:网站本身的JS代码也无法操作Cookie,而且作用有限,只能保证Cookie的安全)
3.在开发API时,检验请求的Referer参数
(作用:可以在一定程度上防止CSRF攻击)
(缺陷:IE或低版本的浏览器中,Referer参数可以被伪造)
四、csrf代码审计
p13
过程如上所示,csrf的过程没啥好代码审计的,解决方法就是设置一个csrf_token,然后直接服务器端登录的时候校验token值对不对,对的话再给登录。
五、sql注入代码审计
普通sql注入代码审计以前搞很多了,这次代码审计一个宽字节注入。
举个常规的例子如下所示:
宽字节注入产生的原因有两个
A:php和mysql设置的字符集不一样,比如一个utf-8,一个GBK等
B:设置的宽字符集可能吃掉转义符号
主要思想是当出现'被转义的时候,我们采用一个%df(反正比ascii码大于128)加在%27前面,然后'被转义成/'的时候,就会出现%df%5c一起变成一个汉字的情况,于是就变成(汉字'),就实现了绕过。
六、文件包含代码审计
主要学习四个函数include,include_once,require,require_once
include和require区别在于如果找不到文件的话,那就直接出警告,而require是出致命错误。
include和include_once的区别是如果文件已经包含过了,once就不会再次包含了
漏洞代码如下:
<?php
if (isset($_GET['file'])) {
include($_GET['file']);
}
else{
echo "使用 file 参数包含文件";
}
?>
p15
这里get到一个点是,被包含的文件无论是什么格式,只要被包含进来都会以php来解析,所以上传图片木马的时候,只要被包含进来,就算直接以jpg作为后缀也能执行成功。
远程文件包含需要allow_url_include = On开启才能行。
六、PHP文件上传代码审计
PHP文件上传函数
move_uploaded_file(file,newloc):
move_uploaded_file() 函数将上传的文件移动到新位置, 若成功,则返回 true,否则返回 false。
参数 描述
file 必需。规定要移动的文件。
newloc 必需。规定文件的新位置
漏洞代码如下:
<?php
if (isset($_POST['submit'])) {
echo "文件名:".$_FILES['upfile']['name']."<br />";
if ($_FILES['upfile']['error'] == 0) {
$dir = "./upload/".$_FILES['upfile']['name'];
move_uploaded_file($_FILES['upfile']['tmp_name'],$dir);
echo "文件保存路径:".$dir."<br />";
echo "上传成功...<br />";
}
}
?>
未对格式做过滤,然后是有做限制的代码如下:
<?php
if (isset($_POST['submit'])) {
echo "文件名:".$_FILES['upfile']['name']."<br />";
switch ($_FILES['upfile']['type']) {
case 'image/jpeg':
$flag = 1;
break;
default:
die("上传失败,请确保文件格式为图片.....");
break;
}
if ($_FILES['upfile']['error'] == 0) {
$dir = "./upload/".$_FILES['upfile']['name'];
move_uploaded_file($_FILES['upfile']['tmp_name'],$dir);
echo "文件保存路径:".$dir."<br />";
echo "上传成功...<br />";
}
}
?>
七、变量覆盖函数代码审计
1.extract()函数
p16
漏洞代码如下:
<?php
$a = 1;
print_r("extract()执行之前:\$a = ".$a."<br />");
$b = array('a'=>'2');
extract($b);
print_r("extract()执行之后:\$a = ".$a."<br />");
?>
当第二个参数为空或者EXTR_OVERWRITE时,变量注册如果遇到冲突会直接覆盖掉原变量。
当第二个变量为EXTR_IF_EXISTS时,仅当原变量已存在是对其进行更新,否则不注册新变量。
p17
2.parse_strc()函数
parse_strc()函数将字符串解析为多个变量。
p18
漏洞代码如下:
该函数在注册变量之前不会验证当前变量是否已存在,如果存在会直接覆盖。
<?php
$a = 1;
print_r("parse_str()执行之前:\$a = ".$a."<br />");
parse_str("a=2");
print_r("parsr_str()执行之后:\$a = ".$a."<br />");
?>
- import_request_variables()函数
import_request_variables ( string $types , string $prefix )
将 GET/POST/Cookie 变量导入到全局作用域中, types 参数指定需要导入的变量, G代表GET,P代表POST,C代表COOKIE.
<?php
// 此处将导入 GET 和 POST 变量
// 使用“rvar_”作为前缀
import_request_variables("gP", "rvar_");
echo $rvar_foo;
?>
漏洞代码如下:
<?php
$a = 1;//原变量值为1
import_request_variables('GP'); //传入参数是注册变量
print_r($a); //输出结果会变成传入参数
?>
一般而言单独的变量覆盖函数很难利用,一般是配合其他的漏洞进行利用,因此需要了解下防御方法
将extract.php中extract()函数第二个参数修改为extr_skip
parse_str()函数的防范,只能我们自己添加判断语句,如:
<?php
$a = 1;
print_r("parse_str()执行之前:\$a = ".$a."<br />");
if(!isset($a)){
parse_str("a=2");
}
print_r("parsr_str()执行之后:\$a = ".$a."<br />");
?>
import_request_variables()函数防御
此函数是非常危险的函数,在PHP5.5之后已被官方删除!假如你任然在使用低版本的PHP环境,也建议你避免使用此函数。
八、身份认证漏洞
先上一段常见的水平越权漏洞源代码审计吧
<?php
session_start();
if(isset($_COOKIE['username']))
{
$username = $_COOKIE['username'];
if($username == root)
echo "<td width='150'><div align='middle'> 欢迎您登录系统".$_SESSION['username']."管理员!</div></td>";
if($username == shiyanlou)
echo "<td width='150'><div align='middle'> 欢迎普通用户shiyanlou".$_SESSION['username']."!</div></td>";
}
echo "<td width='150'><div align='middle'> <a href='logout.php'> 注销</a></div></td>";
?>
这就是平时常见的水平越权漏洞来着。
然后session会话固定攻击漏洞可参考[https://www.cnblogs.com/0x4D75/p/9790281.html]
然后补上一段代码审计的修补方法:
防止水平越权漏洞的话,如下:
if(isset($_COOKIE['username']))
{
$_SESSION['veri'] = $_COOKIE['username'];
header("location: main.php");
}
在添加session变量的时候对cookie保存一份,然后后面每次操作都进行校验。
if($_COOKIE['username']==$_SESSION['username'])
{
if($_COOKIE['username'] == root)
echo "<td width='150'><div align='middle'> 欢迎您登录系统".$_COOKIE['username']."管理员!</div></td>";
if($_COOKIE['username'] == shiyanlou)
echo "<td width='150'><div align='middle'> 欢迎普通用户".$_COOKIE['username']."!</div></td>";
}
else{
echo "<td width='150'><div align='middle'> 登录失败!请尝试重新登录!</div></td>";
}
session会话固定就靠设置会话超时时间,每次登录都重新设置session来解决
九 、工具
目前商业性的审计软件一般都支持多种编程语言,比如VCG、Fortify SCA,缺点就是价格比较昂贵。其他常用的代码审计工具还有findbugs、codescan、seay,但是大多都只支持Windows环境。所以针对PHP代码审计,免费的支持linux环境的 PHP 代码审计软件Rips
网友评论