PHP中的一些tricks

php中的一些trucks

关于preg_replace

1
preg_replace($pattern, $replacement, $subject)      正则搜索subject中匹配pattern的部分(默认是所有),以replacement进行替换。

第一个参数$pattern

这个参数是一个正则表达式,用来在$subject进行正则匹配

php正则表达式的定界符可以是”/“和”#”,在正则表达式中使用到定界符时需要用”"来转义。

在定界符之后还可以添加php正则表达式修饰符,这些修饰符包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
i 忽略大小写,匹配不考虑大小写

m 多行独立匹配,如果字符串不包含[\n]等换行符就和普通正则一样。

s 设置正则符号 . 可以匹配换行符[\n],如果没有设置,正则符号.不能匹配换行符\n。

x 忽略没有转义的空格

e eval() 对匹配后的元素执行函数。(php7移除)

A 前置锚定,约束匹配仅从目标字符串开始搜索

D 锁定$作为结尾,如果没有D,如果字符串包含[\n]等换行符,$依旧依旧匹配换行符。如果设置了修饰符m,修饰符D 就会被忽略。

S 对非锚定的匹配进行分析

U 非贪婪,如果在正则字符量词后加“?”,就可以恢复贪婪

X 打开与perl 不兼容附件

u 强制字符串为UTF-8编码,一般在非UTF-8编码的文档中才需要这个。建议UTF-8环境中不要使用这个。

第二个参数$replacement

这个参数是用于替换的字符串或字符串数组。只有当$pattern是数组的时候这个参数为数组才有效,会对应数组中的元素进行替换。

$replacement中可以包含向后引用的\\n$n,其含义是被匹配到的第n个捕获子组捕获到的文本。而\\0$0则表示完整的模式匹配文本

1
2
3
4
echo preg_replace('/(\d+)(\W)(\d+)/', '${0}', '123,456')   ==> 123,456
echo preg_replace('/(\d+)(\W)(\d+)/', '${1}', '123,456') ==> 123
echo preg_replace('/(\d+)(\W)(\d+)/', '${2}', '123,456') ==> ,
echo preg_replace('/(\d+)(\W)(\d+)/', '${2}', '123,456') ==> 456

如果要在$replacement中使用反斜线'\',必须使用四个'\\\\'
首先这是php的字符串,'\\\\'经过转义后成为了'\\',再经过正则表达式引擎后才被认为是一个原文反斜线
简单的说,perg_replace将两个反斜线转化成一个反斜线

1
2
3
4
echo preg_replace('/a/','\,','a')      ==> \,
echo preg_replace('/a/','\\,','a') ==> \,
echo preg_replace('/a/','\\\,','a') ==> \,
echo preg_replace('/a/','\\\\,','a') ==> \,

一个例子:

1
2
3
4
5
6
7
8
<?php
highlight_file(__FILE__);
$str = addslashes($_GET['option']);
$file = file_get_contents('option.php');
$file = preg_replace('|\$option=\'.*\';|', "\$option='$str';", $file);

file_put_contents('option.php', $file);
?>

这里可以有多种绕过姿势:
第一种:使用\'逃逸单引号

如果我们传入\';phpinfo();//,经过addslashes()后的字符串为\\\';phpinfo();//,而这个字符串经过preg_replace后将前两个反斜线合并为一个反斜线后变成\\';phpinfo();//,成功引入了一个单引号。

如果原来的option.php内容为<?php $option='123'; ?>,经过这次替换则变成了<?php $option='\\';phpinfo();//'; ?>,成功写入shell。

第二种:使用$0的知识点进行二次替换

第一次传入;phpinfo();,option.php内容变成<?php $option=';phpinfo();'; ?>
第二次传入%00$0,如果传入的是%00,即\0,则经过addslashes后变成了\\0
preg_replace的时候便成为了preg_replace('|\$option=\'.*\';|', "\$option='\\0';", "<?php \$option=';phpinfo();'; ?>")
此时\\0表示完整的模式匹配文本,即"\$option='\$option=';phpinfo();';';"。即可写入shell。
使用$0也是同样的原理。

第三种:使用%0a换行之后再进行二次替换

由于preg_replace是一行一行的进行匹配的,所以可以先使用%0a进行换行,然后进行二次替换。

第一次传入a';phpinfo();%0a//,写入文件后,文件内容为

1
2
<?php $option='a\';phpinfo();
//';?>

第二次随便传入一个值,比如1,则把反斜线给去除了。此时文件内容为

1
2
<?php $option='1';phpinfo();
//';?>

php文件操作相关的tricks

上传后删除

file_put_contentsfile_get_contentscopy等读取写入操作与unlinkfile_exists等删除判断文件函数之间对于路径处理是不同的

php读取、写入文件,都会调用底层php_stream_open_wrapper_ex来打开流,而判断文件存在、重命名、删除文件等操作则无需打开文件流,这就是二者的区别。

php_stream_open_wrapper_ex在读和写文件有不同的操作,但是在最后都会使用tsrm_realpath_r函数来将filename给标准化成一个绝对路径,并且会**去除路径末尾的、////.//.**。

在这之前,会判断传入的路径是否是目录,可以有两种方式让php底层出错使得函数判断为不是目录:

  1. 可以利用多层软链接递归,使得地址栈崩掉
    /proc/self/root为根目录/的软连接,通过多层这样的软连接可以使得地址栈崩掉,函数返回-1从而判断为不是目录
  2. 控制directory为false
    如果的路径中有不存在的目录,directory就会是false,所以可以直接让函数判断为不是目录。
    /aaa/../var/www/html/index.php/.

在写文件时,会使用virtual_file_ex函数来判断传入路径是否为目录,比如/tmp/index.php/它认为是个目录,而/tmp/index.php/.则认为不是目录。
这样子,传入的a.php/.首先判断不是目录,最后又通过tsrm_realpath_r去除掉了末尾的/.,解析成为了文件a.php。需要注意的是,如果/tmp/index.php文件存在,这个方式会出错

所以,如果我们传入的是文件名中包含一个不存在的路径,写入的时候因为会处理掉”../“等相对路径,所以不会出错;判断、删除的时候因为不会处理,所以就会出现”No such file or directory”的错误。

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$name = @$_GET['name'];
$data = @$_GET['data'];
if($name)
{
$filename = 'tmp/'.$name;
file_put_contents($filename, $data);

if (file_exists($filename)) {
unlink($filename);
}
}
?>

所以这里可以使用name=a.php/.name=xxx/../a.phpname=a.php/ss/..等来绕过,windows下还可以用name=test.php:test test.ph<来绕过

如果file_put_contents的第一个参数完全可控,则还可以使用php伪协议来绕过,file_existsunlink并不能使用php伪协议

相同类型的还有:

1
2
3
4
5
6
7
8
9
10
11
12
<?php

$content = $_POST['content'];
$filename = $_POST['filename'];
$filename = "upload/".$filename;

if(preg_match('/.+\.ph(p[3457]?|t|tml)$/', $filename)){
die("Bad file extension");
}else{
file_put_contents($filename, $content);
}
?>

file_put_contents写入文件

1
file_put_contents($filename, $data)    将$data写入$filename

需要注意的是,如果$data为一个数组,则会将数组内的元素当成字符串拼接起来写入文件。

例如:

1
2
3
4
5
6
7
8
<?php
$text = $_GET['text'];
if(preg_match('[<>?]', $text)) {
die('error!');
}
file_put_contents('shell.php', $text);

?>

此时只要传入text[]=phpinfo();即可绕过。

call_user_func调用类方法

1
call_user_func($callback, $parameter)     把第一个参数作为回调函数调用,其余参数都是回调函数的参数

需要注意的是:当我们的第一个参数为数组时,会把第一个值当作类名,第二个值当作方法进行回调
例如

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class test{
static function hello(){
echo "hello!";
}
}
$classtest = "test";
// or
// $classtest = new test();
call_user_func([$classtest, 'hello']);

?>

结果就会调用类test中的hello方法,输出hello

php变量名不合法字符转换成_

在PHP中,变量名称不能使用点号.,例如$a.b是一个不合法的变量名。因此,php会自动将点替换为下划线。

除了点,一些其他字符如果出现在GET参数名中,也会被自动的替换为下划线。

1
2
3
4
5

chr(32) 空格
chr(46) .
chr(91) [
chr(128) ~ chr(159)

关于header

1
header($string)    发送原生 HTTP 头

其参数为头字符串,有两种特别的头:
一种以”HTTP/“开头的,将会被用来计算将要发送的HTTP状态码。
例如:

1
header("HTTP/1.0 404 Not Found");

第二种特殊情况是”Location:”头信息
它不仅把报文发送给浏览器,而且还将返回给浏览器一个REDIRECT(302)的状态码

需要注意的点

  1. Location和”:”之间不能有空格,否则会出现错误
  2. 在用header前不能有任何的输出(包括空白)
  3. 使用header()进行跳转的时候没有使用exit()或者是die(),导致后续的代码任然可以执行。
    如果后面存在危险函数,那么将会触发漏洞。

php可变变量与变量执行

在花括号内的代码是可以执行的

1
2
3
<?php echo "${phpinfo()}"; ?>

//这种写法在php5.4.45以下的版本中都是无法执行的,但是在之后的版本都是可行

在任何php的版本中都可以执行的方法:

1
2
3
4
5
6
"${ phpinfo()}";     //第一个字符为空格
"${ phpinfo()}"; //第一个字符为tab
"${/**/phpinfo()}"; //第一个字符为注释
"${[回车]phpinfo()}"; //第一个字符为回车
"${@phpinfo()}"; //第一个字符为@

原理:空格,tab,注释,回车是各种语法分析引擎中常见的分割字符,@是PHP语法的一个特殊的容错符号,所以可变变量内的花括号有这么一个规则,需要判断花括号内的内容是否为真正的代码,条件即是文本的第一个字符串是否为PHP语法解析引擎的分割字符和特殊的语法符号!

参考:https://blog.spoock.com/2017/07/18/php-variables-variable/

__HALT_COMPILER()函数的利用

__HALT_COMPILER()在phar反序列化的时候遇到过,是用在phar的stub文件标识

其实,__halt_compitler()是一个php函数,其作用是中断编译器的执行,即使后面的内容不符合php语法。
需要注意的是,该函数仅能够在最外层使用

也就是,__halt_compitler()的功能类似于die();?>的功能,可以让php编译器中断执行

总结描述:

1
__halt_compitler();  ==>  die();?>

php中的浮点数精度问题

php中的小数精度一般在$10^{-16}$,但是在浮点数转为字符串的时候,最多只会保存小数点后14位,小数点后第15位四舍五入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$a = 0.10000000000001;    // 14位
$b = 0.100000000000004; // 15位
$c = 0.100000000000005; // 15位
$d = 0.1000000000000009; // 16位

var_dump((string)$a);
var_dump((string)$b);
var_dump((string)$c);
var_dump((string)$d);

结果为:
string(16) "0.10000000000001"
string(3) "0.1"
string(16) "0.10000000000001"
string(3) "0.1"

php中小数精度一般在$10^{-16}$,所以超过16位小数的部分可能造成精度缺失

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$a = 0.1;

$b = 0.1000000000000001; // 16位
$c = 0.10000000000000001; // 17位
$d = 0.100000000000000001; // 18位
$e = 0.100000000000000009; // 18位

var_dump($a == $b);
var_dump($a == $c);
var_dump($a == $d);
var_dump($a == $e);

结果为:
bool(false)
bool(true)
bool(true)
bool(true)

所以,php的小数精度在转换成字符串时和平时是有差异的,利用好14和16位精度的差异就可以完成一些绕过:

1
2
3
4
5
6
7
8
9
$a = 0.1;
$b = 0.100000000000001; // 15位 转换成字符串为0.1,作为数字仍然为15位小数

var_dump($a == $b);
var_dump(md5($a) === md5($b));

结果:
bool(false)
bool(true)

php的预定义变量

PHP中有两个特殊的数字相关的预定义变量:

  • NAN 表示一个不存在的数
  • INF 表示一个无穷大的数

而这两个变量转换成字符串的时候会转成字符形式的NANINF

1
2
3
4
var_dump(1/0);     // float(INF)
var_dump(INF-INF); // float(NAN)
var_dump((string)INF); // string(3) "INF"
var_dump((string)NAN); // string(3) "NAN"
文章作者: Dar1in9
文章链接: http://dar1in9s.github.io/2020/04/10/php/php之中的tricks/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Dar1in9's Blog