0%

php代码审计学习笔记1

php代码审计学习笔记1

课程是某牛的,笔记只供个人学习

感觉就是从代码层面复习了一遍漏洞原理

环境配置及审计工具介绍

环境就用phpstudy

看代码的话我用的是notepad++

审计工具可以用seay源代码审计系统(基于正则

Fortify SCA也是一款不错的商业化代码审计工具(靠内置的规则空间匹配

rips审计工具也不错,可以看到用户输的一些点

以及一款数据库监控工具 vMysqlMonitoring.exe

审计靶场:VAuditDemo ZVulDrill

代码审计的思路及流程

MVC架构

MVC是一种设计创建Web程序的一种模式,其同时提供了HTML,CSS和JAVASCRIPT的完全控制。

image-20220824105406833

MVC关系

image-20220824105546784

常见的php框架

image-20220824105649145

审计思路

1
2
MVC框架常见的一个处理流程
获取请求 全剧过滤 模块文件 c函数内容 m函数内容 v显示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
常见网站目录架构
主目录

模块目录
插件目录
上传目录
模板目录
数据目录
配置目录

配置文件

公共函数的文件
安全过滤的文件

数据库结构

入口文件
1
2
3
4
通读全文
函数集文件,配置文件,安全过滤文件,index文件
优点:了解程序的架构及业务逻辑,挖掘更高质的漏洞
缺点:耗费时间多,工程较大
1
2
3
敏感关键字回溯参数
优点:快速挖掘想要的漏洞,最常用的方法。
缺点:覆盖不到逻辑漏洞的挖掘,不能了解程序的基本架构
1
2
查找可控变量
可以用rips审计工具的查找用户输入功能
1
2
功能点定向审计
程序安装,文件上传,文件管理,登录验证,备份恢复,找回密码

PHP核心配置详解

PHP.INI配置文件也就是所谓的核心配置

下面是一些常亮

image-20220824183645573

基本配置-语法

1
2
3
大小写敏感
directive = value(指令 = 值)
foo=bar ≠ FOO=bar
1
2
运算符
|、&、~、!

image-20220824185643247

1
2
3
4
空值的表达方式
foo = ;
foo = none;
foo = "none";这个就表示这么一个字符串了,而不是空值

基本配置-安全模式

1
2
3
4
安全模式
safe_mode = off
安全模式,用来限制文档的存取、限制环境变量的存取,限制外部程序的执行。
本特性已在PHP5.4.0被移除
1
2
3
限制环境变量存取
safe_mode_allowed_env_vars = string
指定php程序可以改变的环境变量的前缀,当这个选项的值为空时,那么php可以改变任何环境变量。
1
2
外部程序执行目录
safe_mode_exec_dir = "E:\Local Test\WWW"
1
2
3
4
5
禁用函数
disable_function
为了更安全的运行php,可以用此指令来禁止一些敏感函数的使用,当你想用本指令禁止一些危险函数时,切忌把dl()函数也加到禁止的列表,攻击者可以利用dl()函数加载自定义的php扩展来突破disable_function。配置禁用函数时可以使用逗号分隔函数名。
同理
disable_classes是禁用类
1
2
3
4
com组件
com.allow_dcom = false
php设置在安全模式下,仍旧允许攻击者使用com()函数来创建系统组件来执行任意命令,我推荐关闭这个函数来防止出现此漏洞。
使用COM()函数需要在php.ini中配置extension=php_com_dotnet.dll,如果php VERSION<5.4.5则不需要

基本配置-控制变量

1
2
3
4
5
6
7
8
全局变量注册开关
register_globals = off
php.ini的register_globals选项的默认值预设为off,在4.2版本之前是默认开启的,当register_globals的设定为On时,程序可以接受来自服务器的各种环境变量,包括表单提交的变量,这是对服务器来讲是不安全的,所以我们不能让它注册为全局变量。
register_globals = off时,服务器端获取数据的时候用$_GET['name']来获取数据
register_globals = On时,服务器使用POST或者GET提交的变量,都将自动使用全局变量的值来接受值。
如下:
开启这个开关之后,我们只要传入一个_SESSION[username]=aa,就可以doeverthing
关掉之后,我们就不能获取权限了

image-20220824194314976

1
2
3
魔术引号过滤
magic_quotes_gpc = on 本特性已在PHP5.4.0已经移除
magic_quotes_gpc = Off 在php.ini里面是默认关闭的,如果他打开后将自动把用户提交对sql的查询语句进行转换,如果设置成ON的话,php会吧所有的单引号,双引号,反斜杠和空字符加上反斜杠进行转义,他会影响HTTP请求的数据有(GET,POST,Cookies),开启他会提高网站的安全性,当然,您也可以使用addslashes来转义提交的HTTP请求数据,或者用stripsashes来删除转义

基本配置-远程文件

1
2
3
是否允许远程包含文件
allow_url_include = off
该配置为on的情况下,可以直接包含远程文件,若包含的变量为可控的情况下,可以直接控制变量来执行PHP代码
1
2
3
是否允许打开远程文件
allow_url_open = on
允许本地PHP文件通过调用URL重写来打开和关闭写权限,默认的封装协议提供的ftp和http协议来访问文件。

基本配置-目录权限

1
2
3
HTTP头部版本信息
expose_php = off
防止了通过http头部泄露的php版本信息
1
2
3
文件上传临时目录
upload_tmp_dir =
上传文件临时保存目录,如果不设置的话,则采用系统的临时目录
1
2
3
用户可访问目录
open_basedir = E:\Local Test\WWW
能够控制PHP脚本只能访问指定的目录,这样能够避免PHP脚本访问,不应该访问的文件,一定程度上限制了phpshell的危害

基本配置-错误信息

1
2
3
内部错误选项
display_errors = on
表明显示PHP脚本的内部错误,网站发布后便宜关闭PHP的错误回显,在调试的时候通常把PHP错误显示打开
1
2
3
错误报告级别
error_reporting = E_ALL & ~E_NOTICE
这个设置的作用是将错误级别跳到最高,显示所有问题,方便排错。

代码调试

1
2
3
4
5
6
7
8
9
10
11
12
13
代码调试小技巧

echo
最简单的输出调试方法,一般用来输出变量值或者不确定执行到哪个分支。

print_r(true为1,false为空),var_dump,debug_zval_dump
这个主要是输出变量的数据值,特别是数组和对象数据,一般我们在查看接口的返回值或者不确定的变量,都可以使用这个api,debug_zval_dump输出结果和var_dump类似,唯一增加的一个值是refcount,记录一个变量被引用了多少次

debug_print_backtrace
可以查看输出的调用栈信息

exit()
停止程序,无法运行后面代码

审计中涉及的超全局变量

1
2
全局变量:全局变量就是在函数外面定义的变量。不能在函数中直接使用。因为他的作用域不会到函数内部。所以在函数内部使用的时候常常看到类似global$a;
超全局变量:超全局变量作用域在所有脚本都有效。注意,除了$_GET,$_POST,$_SERVER,$_COOKIE等之外的超全局变量保存在$GLOBALS数组中。

$GLOBALOS

1
2
3
4
5
6
7
8
<?php
$name = 123;
function test(){
$name = 456;
}

test();
echo $name;

以上代码输出的是123

1
2
3
4
5
6
7
8
9
<?php
$name = 123;
function test(){
global $name;
$name = 456;
}

test();
echo $name;

这样就是输出的456,其中global起的是一个参数传递的作用。

1
2
3
4
5
6
7
8
<?php
$name = 123;
function test(){
$GLOBALS[name] = 4567;
}

test();
echo $name;

输出4567,变量的作用域设置的全局

$_POST$_GET

post是向服务器传送数据。将表单内各个字段与其内容放置在HTML HEADER内一起传送到ACTION属性所指的URL地址,用户看不到这个过程。

get是从服务器上获取数据,把参数数据队列加到提交表单的ACTION属性所指的URL中,值和表单内各个字段一一对应,在URL中可以看到

$_REQUEST

request可以接受get和post的数据,但是比较慢,所以我们尽量不要使用request

$_SERVER

这种超全局变量保存关于报头,路径和脚本位置的信息等。

我整理了其中的参数,也方便以后查阅

可以var_dump下来看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
$_SERVER['PHP_SELF'] #当前正在执行脚本的文件名,与 document root相关。

$_SERVER['argv'] #传递给该脚本的参数。

$_SERVER['argc'] #包含传递给程序的命令行参数的个数(如果运行在命令行模式)。

$_SERVER['GATEWAY_INTERFACE'] #服务器使用的 CGI 规范的版本。例如,“CGI/1.1”。

$_SERVER['SERVER_NAME'] #当前运行脚本所在服务器主机的名称。

$_SERVER['SERVER_SOFTWARE'] #服务器标识的字串,在响应请求时的头部中给出。

$_SERVER['SERVER_PROTOCOL'] #请求页面时通信协议的名称和版本。例如,“HTTP/1.0”。

$_SERVER['REQUEST_METHOD'] #访问页面时的请求方法。例如:“GET”、“HEAD”,“POST”,“PUT”。

$_SERVER['QUERY_STRING'] #查询(query)的字符串。

$_SERVER['DOCUMENT_ROOT'] #当前运行脚本所在的文档根目录。在服务器配置文件中定义。

$_SERVER['HTTP_ACCEPT'] #当前请求的 Accept: 头部的内容。

$_SERVER['HTTP_ACCEPT_CHARSET'] #当前请求的 Accept-Charset: 头部的内容。例如:“iso-8859-1,*,utf-8”。

$_SERVER['HTTP_ACCEPT_ENCODING'] #当前请求的 Accept-Encoding: 头部的内容。例如:“gzip”。

$_SERVER['HTTP_ACCEPT_LANGUAGE']#当前请求的 Accept-Language: 头部的内容。例如:“en”。

$_SERVER['HTTP_CONNECTION'] #当前请求的 Connection: 头部的内容。例如:“Keep-Alive”。

$_SERVER['HTTP_HOST'] #当前请求的 Host: 头部的内容。

$_SERVER['HTTP_REFERER'] #链接到当前页面的前一页面的 URL 地址。

$_SERVER['HTTP_USER_AGENT'] #当前请求的 User_Agent: 头部的内容。

$_SERVER['HTTPS'] — 如果通过https访问,则被设为一个非空的值(on),否则返回off

$_SERVER['REMOTE_ADDR'] #正在浏览当前页面用户的 IP 地址。

$_SERVER['REMOTE_HOST'] #正在浏览当前页面用户的主机名。

$_SERVER['REMOTE_PORT'] #用户连接到服务器时所使用的端口。

$_SERVER['SCRIPT_FILENAME'] #当前执行脚本的绝对路径名。

$_SERVER['SERVER_ADMIN'] #管理员信息

$_SERVER['SERVER_PORT'] #服务器所使用的端口

$_SERVER['SERVER_SIGNATURE'] #包含服务器版本和虚拟主机名的字符串。

$_SERVER['PATH_TRANSLATED'] #当前脚本所在文件系统(不是文档根目录)的基本路径。

$_SERVER['SCRIPT_NAME'] #包含当前脚本的路径。这在页面需要指向自己时非常有用。

$_SERVER['REQUEST_URI'] #访问此页面所需的 URI。例如,“/index.html”。

$_SERVER['PHP_AUTH_USER'] #当 PHP 运行在 Apache 模块方式下,并且正在使用 HTTP 认证功能,这个变量便是用户输入的用户名。

$_SERVER['PHP_AUTH_PW'] #当 PHP 运行在 Apache 模块方式下,并且正在使用 HTTP 认证功能,这个变量便是用户输入的密码。

$_SERVER['AUTH_TYPE'] #当 PHP 运行在 Apache 模块方式下,并且正在使用 HTTP 认证功能,这个变量便是认证的类型。

$_FILE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$_FILES数组内容如下: 
$_FILES['myFile']['name'] 客户端文件的原名称。
$_FILES['myFile']['type'] 文件的 MIME 类型,需要浏览器提供该信息的支持,例如"image/gif"。
$_FILES['myFile']['size'] 已上传文件的大小,单位为字节。
$_FILES['myFile']['tmp_name'] 文件被上传后在服务端储存的临时文件名,一般是系统默认。可以在php.ini的upload_tmp_dir 指定,但 用 putenv() 函数设置是不起作用的。
$_FILES['myFile']['error'] 和该文件上传相关的错误代码。['error'] 是在 PHP 4.2.0 版本中增加的。下面是它的说明:(它们在PHP3.0以后成了常量)
UPLOAD_ERR_OK
值:0; 没有错误发生,文件上传成功。
UPLOAD_ERR_INI_SIZE
值:1; 上传的文件超过了 php.ini 中 upload_max_filesize 选项限制的值。
UPLOAD_ERR_FORM_SIZE
值:2; 上传文件的大小超过了 HTML 表单中 MAX_FILE_SIZE 选项指定的值。
UPLOAD_ERR_PARTIAL
值:3; 文件只有部分被上传。
UPLOAD_ERR_NO_FILE
值:4; 没有文件被上传。
值:5; 上传文件大小为0.

通过HTTPCookies方式传递给当前脚本的变量的数组

$HTTP_COOKIE_VARS和$_COOKIE是不同的变量,PHP处理他们的方式不同

$HTTP_COOKIE_VARS包含相同的信息(4.1.0已废弃)但它不是一个超全局变量

$_SESSION

是一个数值,其中的value值可以我们设置,类似cookie

$_ENV

$_ENV包含服务器端环境变量的数组,可在PHP程序的任何地方直接访问

$_ENV只是被动的接收服务器端的环境碧昂量转换为数组元素。

SQL注入

基础

数字型的注入,当输入的参数是整型时,则可认为是数字型注入。如年龄,id,页码,都可以是,而且不需要单引号进行闭合。

首先我们自己新建一个数据库来进行一个测试

image-20220826202223548

这是一个名为test的数据库,以及一个user表

随后自己写一个查询页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$id = $_GET['id'];
$conn = mysql_connect('127.0.0.1','root','root');#建立一个连接,返回一个连接标识
mysql_select_db('test',$conn);#选择数据库
$sql = "SELECT * FROM USER WHERE id =$id";
$result = mysql_query($sql) or die(mysql_error());#执行一次sql查询
while($row = mysql_fetch_array($result)#返回一个关联数组){
echo 'ID'.$row['id'].'<br>';
echo 'USERNAME'.$row['username'].'<br>';
echo 'PASSWORD'.$row['password'].'<br>';
echo 'PHONE'.$row['phone'].'<br>';
}
mysql_close($conn);
echo '<br>';
echo $sql;

image-20220826205026879

可以看到就像是一个基础的sql注入靶场

or1=1就可以看到注入成功

image-20220826205517820

注入利用的方式

查询数据,读写文件,执行命令

1
2
写入webshell
union select 1,2,3,十六进制编码的一句话 into outfile 'C:/Localhost/WWW/3.php'

登录处注入的例子

login.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form action="login.php" method="post">
账号:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="点击登录" name="login">
</form>>
</body>
</html>

login.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
if (!isset($_POST['login'])){
echo 'fail';#首先看是否有数据从那边传过来
}else{
$username = $_POST['username'];
$password = $_POST['password'];
$conn = mysql_connect("127.0.0.1","root","root");#连接数据库
mysql_select_db("test",$conn);#选择数据库
$sql = "SELECT *FROM USER WHERE USERNAME='".$username."'";#拼接sql语句
$result = mysql_query($sql) or die('执行失败'.mysql_errno());#执行sql语句
while($row = mysql_fetch_array($result)){
$db_username = $row['username'];
$db_password = $row['password'];
}#拿到数据库的结果整理
if($db_password == $password && $db_username = $username){#对比数据
echo "成功";
}else{
echo "失败";
}
}


这里的用户名就存在万能密码注入,or 1=1

HTTP头注入

image-20220827131024547

宽字节注入和二次注入

宽字节注入

intval()#是强制转换类型,强制将其中的内容转换成整型。常用于数字型

add#函数在指定的预定义字符前添加反斜杠。这些字符是单引号(’)、双引号(”)、反斜线(\)与NUL(NULL字符)。

而宽字节注入的思路就是,单引号逃逸

1
2
3
4
5
因为%df的关系,\的编码%5c被吃掉了,也就失去了转义的效果,直接被带入到mysql中,然后mysql在解读时无视了%a0%5c形成的新字节,那么单引号便重新发挥了效果

常见的宽字节编码:GB2312,GBK,GB18030,BIG5,Shift_JIS

如果数据库使用的的是GBK编码而PHP编码为UTF8就可能出现注入问题,原因是程序员为了防止SQL注入,就会调用我们上面所介绍的几种函数,将单引号或双引号进行转义操作,转义无非便是在单或双引号前加上斜杠(\)进行转义 ,但这样并非安全,因为数据库使用的是宽字节编码,两个连在一起的字符会被当做是一个汉字,而在PHP使用的UTF8编码则认为是两个独立的字符,如果我们在单或双引号前添加一个字符,使其和斜杠(\)组合被当作一个汉字,从而保留单或双引号,使其发挥应用的作用。但添加的字符的Ascii要大于128,两个字符才能组合成汉字 ,因为前一个ascii码要大于128,才到汉字的范围 ,这一点需要注意。

image-20220827133732973

demo

test.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$id = addslashes($_GET['id']);
$conn = mysql_connect('127.0.0.1:3306','root','root');
mysql_select_db('test',$conn);
mysql_query("SET NAMES 'GBK'");
$sql = "SELECT * FROM user WHERE id = '$id'";
$result = mysql_query($sql) or die(mysql_error());
while($row = mysql_fetch_array($result)){
echo 'ID'.$row['id'].'<br>';
echo 'USERNAME'.$row['username'].'<br>';
echo 'PASSWORD'.$row['password'].'<br>';
echo 'PHONE'.$row['phone'].'<br>';
}
mysql_close($conn);
echo '<br>';
echo $sql;

image-20220827135455087

成功宽字节注入(payload 1%20%df%27%20union%20select%201,database(),3,4%20--+

修复建议:

使用mysql_ser_charset(GBK)指定字符集

使用mysql_real_escape_string进行转义

二次注入

1
2
3
4
5
6
二阶注入:
1.攻击者在http请求中提交恶意输入(输入的时候过滤了,但是进入数据库的时候还原了)
2.恶意输入保存在数据库中
3.攻击者提交第二次http请求
4.未处理第二次http请求,程序在检索存储在数据库中的恶意输入,构造sql语句
5.如果攻击成功,在第二次请求响应中返回结果

demo

reg.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
if(!empty($_POST['submit'])){
$id = addslashes($_POST['id']);
$username = addslashes($_POST['username']);
$password = addslashes($_POST['password']);
$phone = addslashes($_POST['phone']);
$conn = mysql_connect('127.0.0.1','root','root');
mysql_select_db('test');
$sql = "INSERT INTO user(id,username,password,phone) VALUES('$id','$username','$password','$phone');";
$result = mysql_query($sql) or die("失败".mysql_error());
if ($result){
echo "注册成功";
}else{
echo "注册失败";
}
}else{
echo 'not';
}
?>
<form action="reg.php" method="post">
id:<input type="text" name="id"><br>
username:<input type="text" name="username"><br>
password:<input type="text" name="password"><br>
phone:<input type="text" name="phone"><br>
<input type="submit" name="submit" value="点击提交">
</form>

search.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
if(!empty($_POST['submit'])){
$id = $_POST['id'];
$conn = mysql_connect('127.0.0.1','root','root');
mysql_select_db('test');
$sql = "SELECT * FROM user WHERE id=".$id.";";
$result = mysql_query($sql) or die("失败1".mysql_error());
while($row = mysql_fetch_array($result)){
$username = $row['username'];
$sql = "SELECT * FROM USER WHERE username='".$username."';";
$result = mysql_query($sql) or die("失败2".mysql_error());
while($row = mysql_fetch_array($result)){
echo "ID:".$row['id']."<br>";
echo "USERNAME:".$row['username']."<br>";
echo "PASSWORD:".$row['password']."<br>";
echo "PHONE:".$row['phone']."<br>";
}

}
}
?>
<form action="search.php" method="post">
search ID:<input type="text" name="id">
<input type="submit" name="submit" value="点击搜索">
</form>

我们的payload就可以在username那里加上'union select 1,2,database(),4;#

image-20220827151005452

再查询0

image-20220827151016017

但是为什么感觉就是再讲漏洞原理啊喂(#`O′)

思路:先将注入语句插入到数据库,注册、留言板等功能都具有insert数据库的操作,然后再使用update的地方触发插入到数据库的注入语句

1
UPDATE 表名称 SET 列名称 = 新值 WHERE 列名称 = 某值'union select ......#

代码执行漏洞

1
2
3
思路:
用户能够控制函数输入
存在可执行代码的危险函数

image-20220827151921288

常见的一些函数

eval()和assert()函数就不说了,直接把中间的参数当做php代码执行

1
2
3
4
5
回调函数:
mixed call_user_func(callble $callback[,mixed $parameter[,mixed $...]])
$callback是要调用自定义的函数名称
$parameter是自定义函数的参数
call_user_func — 把第一个参数作为回调函数调用 第二个参数当做回调函数的参数
1
2
3
<?php
$b = "phpinfo()";
call_user_func($_GET['a'],$b);

image-20220827165236507

常见的回调函数:

call_user_func(),call_user_func_array(),array_map()等

这里不全,后面我自己会补充。

1
2
3
4
5
6
7
动态执行函数
1.定义一个函数
2.将函数名(字符串)赋值给一个变量
3.使用变量名代替函数名调用动态执行函数

$_GET['a']($_GET['b']);#简单例子
感觉就和免杀才开始那种一样,a做函数名,b做函数参数。
1
2
3
4
5
preg_replace函数

mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )

搜索 subject 中匹配 pattern 的部分, 以 replacement 进行替换。

一般$pattern是正则

$pattern存在/e模式修正符修饰 允许代码执行,下面是其他修正符的含义

1
2
3
4
5
6
1、/g 表示该表达式将用来在输入字符串中查找所有可能的匹配,返回的结果可以是多个。如果不加/g最多只会匹配一个
2、/i 表示匹配的时候不区分大小写,这个跟其它语言的正则用法相同
3、/m 表示多行匹配。什么是多行匹配呢?就是匹配换行符两端的潜在匹配。影响正则中的^$符号
4、/s 与/m相对,单行模式匹配。
5、/e 可执行模式,此为PHP专有参数,例如preg_replace函数。
6、/x 忽略空白模式。
1
2
3
4
5
6
反向引用

对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中
,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,
最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 '\n' 访问,其中 n 为一个标识特定缓冲区的
一位或两位十进制数。

这里举一个简单的例子

1
2
3
4
5
<?php
echo $cmd = $_GET['cmd'];
$str = '<php>phpinfo()</php>';

preg_replace("/<php>(.*?)$cmd","\\1",$str);

上面的意思大概就是,从str中匹配前面有<php>的后面条件自己设定的字符,用//1来进行替换,但是我们又是/e模式,执行了第二个语句,那么就成功rce。

我们这里的payload就可这样<\/php>/e因为如果有特殊符号要转义,所以前面有个斜杠,匹配成功之后,就会被//1引用,

1
2
3
4
5
6
修复方案
尽量不要执行外部的应用程序或命令
使用自定义函数或函数库来代替外部应用程序或命令的应用
使用escappeshellarg函数来处理命令的参数
使用safe_mode_exec_dir来指定可执行的文件路径
将执行函数的参数做白名单限制,在代码或配置文件中限制某些参数

(96条消息) php preg_replace /e 模式 漏洞分析_quan9i的博客-CSDN博客_preg_replace漏洞

这篇文章感觉不错,我搬运过来

preg_replace详解

/e在php5.5的时候就被废弃

第一个案例

1
2
3
4
5
<?php
function test($str){}
echo preg_replace("/s*(.*)s*/ies", "\\1", $_GET["h"]);
show_source(__FILE__);
?>

那么我们可以看出其大致含义就是匹配出的任意内容,都用()包含起来,作为子字符串,存在缓冲区,\1是访问括号内的内容,此时我们上面那个代码,我们就可以构造如下payload进行rce

对于payload中${}的解释,它这里其实是一个可变变量
https://www.php.net/manual/zh/language.variables.variable.php
我们输入了${phpinfo()},那么它呢因为被括号包裹了,就会存进缓冲区,此时他是符合那个过滤函数的,所以要更替为第二个语句,而这里用到了/e,就相当于将第二个语句给执行了,就相当于eval(xx),第二个语句也就是//1,而//1的含义是${phpinfo()},他这个时候总的语句呢就是eval(${phpinfo()}),这玩意就相当于变量里面套变量,所以我们需要先执行里面的,也就是${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true),此时我们呢再执行这个eval${1},这玩意能正常执行出来,由于我们没有给1赋值,他会给个警告,但是没问题,不影响我们的语句,所以这个时候我们的rce就实现了

第二个案例

1
2
3
4
5
<?php
$data=$_GET['data'];
preg_replace('/<data>(.*)<\/data>/e','$ret="\\1";',$data);
show_source(__FILE__);
?>

payload如下

1
?data=<data>${phpinfo()}</data>

分析原因,此时括号内的内容是${phpinfo()},传到了缓冲区,执行后面语句时会调用,那么后面语句此时就相当于$ret=${phpinfo()},肯定会先执行phpinfo(),然后呢返回了1,此时语句是$ret=1,加上我们的/e是执行语句的,所以这里执行的就等同于eval($ret=1);我们本地测试一下输出结果

image-20220827183314670所以,这就是执行报错的原因所在,有没有解决办法呢,当然有,再加个{},此时执行结果如下

image-20220827183326474

这是为啥呢,我们不妨再来分析一下,此时存入缓冲区的是{${phpinfo()}},此时由于\1是反向引用,再加上/e,实际的执行语句就是eval($ret={${phpinfo()}})执行过里面之后,得到的就是eval($ret={1}),而里面会当做变量来执行,也就是eval($ret=${1}),此时就执行出来了。

第三个案例

1
2
3
4
5
6
7
8
9
10
11
12
<?
<?
function test($str){
}
preg_replace("/s*(.*)s*/ies", "test(\\1)", $_GET["h"]);
show_source(__FILE__);
/**
*eval("\${phpinfo()}");
*eval("test(\"{\${phpinfo()}}\");");
*${phpinfo()}
*/
?>

payload如下

?h=${phpinfo()}

此时执行的就是eval(test(${phpinfo()}),,而后为eval(test(1)),里面那个是空的,所以执行结果为空,没有错误,可以正常执行

一些正则

image-20220827170249923

image-20220827171538419

image-20220827180110497

命令执行漏洞

1
2
3
挖掘思路
用户能够控制函数输入
存在可执行代码的危险函数
1
2
3
4
类型
代码层过滤不严
系统的漏洞造成命令注入
调用的第三方组件存在代码执行漏洞

image-20220827190243561

1
2
system函数 自带回显的,不用输出也可
system(string $command, int &$return_var = ?): string
1
2
passthru函数 也是自带回显
passthru ( string $command [, int &$return_var ] ) : void
1
2
3
4
5
6
7
8
9
10
Exec函数  不自带回显
exec ( string $command [, array &$output [, int &$return_var ]] );

$command:表示要执行的命令。

$output:如果提供了 output 参数, 那么会用命令执行的输出填充此数组, 每行输出填充数组中的一个元素。 数组中的数据不包含行尾的空白字符,例如 \n 字符。 请注意,如果数组中已经包含了部分元素,exec() 函数会在数组末尾追加内容。如果你不想在数组末尾进行追加, 请在传入 exec() 函数之前 对数组使用 unset() 函数进行重置。

$return_var:如果同时提供 output 和 return_var 参数, 命令执行后的返回状态会被写入到此变量。

一般来说,我们只要写第一个参数,也就是$command。
1
2
3
4
shell_exec函数  不自带回显的
string shell_exec(string $cmd)
$cmd 要执行的命令
反引号``则调用此函数
1
2
3
4
5
6
过滤函数
Escapeshellcmd()
过滤整条命令

Escapeshellarg()
过滤整个参数

XSS漏洞

1
2
3
挖掘思路
没有过滤的参数,传入到输出函数中
搜索内容,发表文章,留言,评论回复,资料设置
1
2
3
4
反射xss常见场景
将前端获取的内容,直接输出到浏览器页面
将前端获取的内容,直接输出到HTML标签
将前端获取的内容,直接输出到<script>标签
1
2
3
<?php
$content = $_GET['content'];
echo $content;//这就是没有过滤直接输出到浏览器的xss
1
2
3
4
5
6
<?php
$content = $_GET['content'];
?>
<input type="text" value="<?php echo $content;?>">
//payload是 123?>"><script>alert(`xss`)</script>#
//闭合了标签

image-20220827201453222

1
2
3
4
5
6
7
8
9
<?php
$content = $_GET['content'];
?>
<script>
var xss = '<?php echo $content?>';
document.write(xss);
</script>

//payload是123%27;</script><script>alert(`xss`)</script>

存储其实和反射差不多,只不过大多是存在数据库的,需要别人点击去触发。

image-20220827203219636

dom既可能是存储型,也可能是反射型

1
2
3
4
5
6
7
8
9
10
<?php
$content = $_GET['content'];
?>
<input type="text" id="text" value="<?php echo $content;?>">
<dic id="print"></dic>
<script type="text/javascript">
var text = document.getElementById("text");
var print = document.getElementById("print");
print.innerHTML = text.value;
</script>

image-20220827210315089

image-20220827210740514

csrf漏洞

1
2
3
4
5
6
挖掘思路
后台功能模块:管理后台,会员中心,添加用户等
被引用的核心文件里面没有验证token和referer相关的代码

没带token:可以直接请求这个页面
没带referer:返回相同的数据

这个简单,我就不写代码了,就是后端只验证了session,但是没有验证origin或者referer等参数,或者没有加上csrftoken,导致这么一个漏洞。

文件上传

1
2
3
4
5
6
7
文件上传可控点
content-length,即上传内容大小
MAX_FILE_SIZE,即上传内容的最大长度
filename,即上传文件名
content-type,即上传文件内心
请求保重的乱码字段,即上传文件的内容
有可能存在请求包中的可控点还有上传路径,只是上面的示例中没有出现
1
2
3
挖掘思路
上传点都调用同一个上传类,直接全局搜索上传函数
黑盒寻找上传点,代码定位
1
2
3
4
5
6
代码案例
name:客户端的原始上传文件名称
Type:上传文件的MIME类型
Tmp_name:服务器端用来保存上传文件的临时文件夹
Error:上传文件时的错误信息
Size:上传文件的大小,单位是

upload.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
header("Content-type:text/html;charset=utf-8");
$upload_dir = "D:\phpStudy\PHPTutorial\WWW\daima\upload";
if(isset($_FILES['file'])){
$upload_name = $upload_dir."\\".$_FILES['file']['name'];
move_uploaded_file($_FILES['file']['tmp_name'],$upload_name);
echo "Type:".$_FILES['file']['type']."<br>";
echo "Size:".$_FILES['file']['size']/1024 . "<br>";
echo "Name".$_FILES['file']['name']."<br>";
}else{
echo "上传失败";
}

upload.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" name="上传文件">
<input type="hidden" name="MAX_FILE_SIZE" name="4096">
</form>
</body>
</html>

下面的绕过讲师只是提一下,后面我可能自己会总结

1
2
3
文件上传绕过-客户端
用firebug将form表单中的onsubmit事件删除
上传木马文件,burp拦截数据包,修改扩展名
1
2
3
4
5
6
7
文件上传绕过-服务端
黑白名单过滤
修改mime类型
截断上传攻击
.htaccess文件攻击
解析漏洞,文件包含漏洞
目录验证

目录穿越以及文件包含

1
2
3
4
5
<?php

if(isset($_GET['file'])){
readfile(''.$_GET['file']);
}

image-20220828162149341

1
2
3
4
5
目录穿越绕过方式
进行url编码
进行16位unicode编码
进行双倍url编码
进行超常utf-8 unicode编码

image-20220828162321840

1
2
3
4
5
6
目录穿越修复方案
在url中不要使用文件名称作为参数
检查使用者输入的文件名是否有目录阶层字符
在php.ini文件中设置open_basedir来指定文件的目录
使用realpath函数来展开文件路径中的"./"、"../"等字符,然后返回绝对路径名称
使用basename函数来返回不包含路径的文件名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
文件包含-挖掘经验

模块加载,cache调用,传入的参数拼接包含路径

include()
使用此函数,只有代码执行到此函数时才将文件包含进来,发生错误时只警告并继续执行

inclue_once()
功能和前者一样,区别在于当重复调用同一文件时,程序只调用一次

require()
使用此函数,只要程序执行,立即调用此函数包含文件,发生错误时,会输出错误信息并立即终止程序。

require_once()
功能和前者一样,区别在于当重复调用同一文件时,程序只调用一次
1
2
远程包含
RFI允许攻击者包含远程文件,远程文件包含需要设置allow_url_include = On 四个文件都支持HTTP、FTP等协议,相对本地文件包含更容易利用
1
2
3
4
本地包含-利用方式
%00截断

记录错误日志文件

远程包含-利用方式

image-20220828170533205

任意文件读取及删除漏洞

1
2
3
4
5
6
7
8
9
10
11
12
文件读取-挖掘经验
allow_url_fopen选项激活了URL形式的fopen封装协议是的可以访问URL对象例如文件,默认的封装协议提供ftp和http协议来访问远程文件,一些扩展库例如zlib可以能会注册更多的封装协议

fopen()
file_get_contents()
fread
fgets
fgetss
file
fpassthru
parse_ini_file
readfile
1
2
文件删除-挖掘经验
unlink($file,$cont)

变量覆盖漏洞

1
2
3
漏洞危害
比如说文件上传页面的白名单,我们可以覆盖白名单导致文件上传
用户注册页面控制没覆盖的未初始化变量导致sql

image-20220828172043564

1
常见的遍历方式释放代码,可能导致变量覆盖漏洞
1
2
3
4
5
6
7
8
9
10
<?php

$a = 1;
foreach(array('_COOKIE','_POST','_GET') as $_request){
foreach($$_request as $_key => $_value){
$$_key = addslashes($_value);
}
}

echo $a;
1
2
3
4
5
extract()变量覆盖
int extract( $array , extract_rules,prefix )
$array 关联的数组,受第二个和第三个参数的影响
extract_rules 对待非法/数字和冲突的键名的方法将根据去除标记
prefix 尽在第二个参数特殊时需要,添加前缀

image-20220829141621485

1
2
3
4
parse_str()变量覆盖
void parse_str(string $encoded_string [,array &$result])
incoded_string输入的字符串
result 变量将会以数组元素的形式存入到这个数组,作为替代
1
2
3
4
5
import_request_variables()
(PHP 4>= 4.1.0 , php5 < 5.4.0)
bool import_request_variables(string $types[,string $prefix])
$type 指定需要导入的变量。可以用字母'G','P','C'来表示get,post,cookie
$prefix 变量名前缀
1
2
3
4
5
修复方案
在php.ini文件中设置register_globals=off
使用原始变量数组,如$_POST,$_GET等数组变量进行操作
不适用foreach语句来遍历$_GET变量,而改用[(index)]来指定
验证变量是否存在,注册变量前先判断变量是否存在

反序列化漏洞

1
2
serialize()
unserialize()
1
2
漏洞成因
反序列化对象中存在魔术方法,而且魔术方法中的代码可以被控制,漏洞根据不同的代码可以导致各种攻击,如代码注入、sql注入、目录遍历等等
1
2
3
4
序列化的不同结果
public
private
protect
1
2
3
4
5
6
7
8
<?php
class test{
private $test1 = "dhk 2002";
public $test2 = "dhk 2002";
protected $test3 = "dhk 2002";
}
$test = new test();
echo serialize($test);

image-20220829144838482

1
2
3
漏洞本质
unserialize函数的变量可控
php文件中存在可利用的类,类中有魔术方法

image-20220829150623976

PHP弱类型

1
2
3
4
变量类型
标准类型:布尔boolen、整型integer、浮点floadt、字符string
复杂类型:数组array、对象object
特殊类型:资源resource

image-20220829155633444

image-20220829155648037

image-20220829155656537

image-20220829155706905

PHP伪协议

1
2
3
4
file://协议
用于访问本地系统文件
allow_url_fopen:off/on
allow_url_include:off/on
1
2
3
4
php://filter
读取源代码并进行base64编码输出
allow_url_fopen:off/on
allow_url_include:off/on
1
2
3
4
php://input
可以访问请求的原始数据的只读流
allow_url_fopen:off/on
allow_url_include:on
1
2
3
4
data://
数据
allow_url_fopen:on
allow_url_include:on