前言
先知上一个大佬挖的洞,也有了简单的分析
https://xianzhi.aliyun.com/forum/topic/2135
我自己复现分析过程,漏洞的原理比较简单,但是漏洞的利用方式对我而言则是一种新的利用方式。本文对分析过程做一个记录。
正文
分析软件运行的流程
拿到一个需要分析的 php
程序,首先看看客户端的 http
请求是如何对应到程序中的代码的。
首先得找一个分析的开始点,就以 触发漏洞 的 请求为示例把。
GET /cash/block-printTradeBlock.html?param=eyJvcmRlckJ5IjoiaWQgbGltaXQgMCwxO3NlbGVjdCBpZigxPTIsMSxzbGVlcCgyKSkjIiB9 HTTP/1.1
Host: hack.ranzhi.top
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=2c17fafndhnfle6j4r9dskopk3; lang=zh-cn; theme=default; rid=kcvhkos22q574sqdjiha39icl5; keepLogin=false; XDEBUG_SESSION=14822
Connection: close
安装系统时我们设置 www
目录为虚拟主机目录, 所以当我们访问 /cash/block-printTradeBlock.html
时,实际上访问的是 www/cash/block-printTradeBlock.html
, 但是 cash
目录中并没有相关的文件
不过该目录下有 .htaccess
文件,通过该文件可以重写 url
, 根据规则我们知道,如果访问 cash
目录下不存在的文件,会把请求交给 index.php
处理。
.htaccess
文件参考
http://t.cn/REFVyCJ
https://www.zybuluo.com/phper/note/73726
那下面就分析 index.php
即可,下好断点,然后发送数据包
f7
跟进 loader.php
, 首先加载了一些基础类。
然后做一些初始化的操作,实例化一些基础对象,后面用来加载程序的主体
然后是设置路由方式
$app->parseRequest();
会对 处理后 url
按 -
分割,第一项作为 module_name
第二项作为 method_name
. 以上面的数据包为例。执行完后的结果如下。
此时我们已经设置好了 模块名 和 方法名, 下面回到 loader.php
。
$common->checkPriv(); # 权限校验
$app->loadModule(); # 加载相关模块的方法
进入 loadModule
方法
public function loadModule()
{
$appName = $this->appName;
$moduleName = $this->moduleName;
$methodName = $this->methodName;
/*
* 设置control的类名。
* Set the class name of the control.
**/
$className = class_exists("my$moduleName") ? "my$moduleName" : $moduleName;
/*
* 创建control类的实例。
* 根据 `$app->parseRequest()` 设置好的 模块名 实例化对应的类
* Create a instance of the control.
**/
$module = new $className();
$this->control = $module;
/*
* 使用反射机制获取函数参数的默认值
* 通过反射获取 函数的参数名称,
* 通过 `php` 的反射机制 , 获取参数名, 并且初始化好
*
* */
$defaultParams = array();
$methodReflect = new reflectionMethod($className, $methodName);
foreach($methodReflect->getParameters() as $param)
{
$name = $param->getName();
$default = '_NOT_SET';
if(isset($paramDefaultValue[$appName][$className][$methodName][$name]))
{
$default = $paramDefaultValue[$appName][$className][$methodName][$name];
}
elseif(isset($paramDefaultValue[$className][$methodName][$name]))
{
$default = $paramDefaultValue[$className][$methodName][$name];
}
elseif($param->isDefaultValueAvailable())
{
$default = $param->getDefaultValue();
}
$defaultParams[$name] = $default; # 组成一个由 defaultParams[参数名] = "" 构成的字典
}
/**
* 根据PATH_INFO或者GET方式设置请求的参数。
* 根据请求方式, 从请求数据包中获取需要的参数信息。
*/
if($this->config->requestType != 'GET')
{
$this->setParamsByPathInfo($defaultParams);
}
else
{
$this->setParamsByGET($defaultParams);
}
# 过滤数据
if($this->config->framework->filterParam == 2)
{
$_GET = validater::filterParam($_GET, 'get');
$_COOKIE = validater::filterParam($_COOKIE, 'cookie');
}
/* 调用方法,并传入参数 */
call_user_func_array(array($module, $methodName), $this->params);
return $module;
}
该函数的流程为
- 根据
$app->parseRequest()
设置好的 模块名 实例化对应的类 - 通过
php
的反射机制 , 获取方法参数名, 并且初始化好 - 根据请求方式, 从请求包中获取需要的参数信息到
$defaultParams
。 - 过滤数据
- 调用方法,并传入参数
看一看设置参数的方式
按照 -
分割 url
格式为
module_name-method_name-param1-param2
所以在该程序中 www
和 app
目录是相互对应的。
当我们请求
/dir_name/module_name-method_name-param-...-paramN.html
最后会调用 app
目录下 module_name
中的 control.php
的
module_name->method_name(param1,....,paramN)
比如
/cash/block-printTradeBlock.html
实际就是调用 app
目录下 cash
中的 control.php
的
block->printTradeBlock()
SQL 注入分析
漏洞出现在 lib/base/dao/dao.class.php
这里只对 $order
部分进行了校验, 而没有对 limit
后面的部分进行校验。
看到 printTradeBlock
在 $this->processParams()
中设置好 $this->params
可以看到 $this->params
就是 json_decode(base64_decode($_GET['param']))
然后又会调用
orderBy($this->params->orderBy)
所以我们可以控制 limit
后面的部分, SQL注入。
这套程序中还会执行 sql
语句,用的是 Pdo
用的是 $pdo->query()
, 这个函数可以一次执行多条 sql
语句。
/**
* 执行SQL语句,返回PDOStatement结果集。
* Query the sql, return the statement object.
*
* @access public
* @return object the PDOStatement object.
*/
public function query($sql = '')
{
/* 如果有错误,返回一个空的PDOStatement对象,确保后续方法能够执行。*/
/* If any error, return an empty statement object to make sure the remain method to execute. */
if(!empty(dao::$errors)) return new PDOStatement();
if($sql)
{
$sql = trim($sql);
$sqlMethod = strtolower(substr($sql, 0, strpos($sql, ' ')));
$this->setMethod($sqlMethod);
$this->sqlobj = new sql();
$this->sqlobj->sql = $sql;
}
else
{
$sql = $this->processSQL(); // 大概就是获取 sql 语句
}
$key = md5($sql);
try
{
$method = $this->method;
$this->reset();
if($this->slaveDBH and $method == 'select')
{
if(isset(dao::$cache[$key])) return dao::$cache[$key];
$result = $this->slaveDBH->query($sql);
dao::$cache[$key] = $result;
return $result;
}
else
{
if($this->method == 'select')
{
if(isset(dao::$cache[$key])) return dao::$cache[$key];
$result = $this->slaveDBH->query($sql);
dao::$cache[$key] = $result;
return $result;
}
return $this->dbh->query($sql);
}
}
catch (PDOException $e)
{
$this->sqlError($e);
}
}
这样我们的利用方式就简单了。
- 闭合 limit 语句,用
;
连接多条语句 - 没有回显, 使用 时间盲注
poc
任意文件下载 && 任意文件删除
由于可以一次执行多条 sql
语句 ,我们实质上已经可以控制数据库了。
在 app/sys/file/control.php
,有两个函数 delete
和 download
, 分别用于删除文件和下载文件。
以 delete
为例
$this->file->getById($fileID)
时间就是在 表前缀sys_file
中查找对应 id
相对应的 路径。
利用 sql
注入修改为目标路径(用 ../
进行目录跳转),然后选择相应 id
即可删除指定文件。下载文件也是类似。
getshell
删除 my.php
后 会要求我们重新安装系统。
在重装系统的最后一步,会直接使用 POST
中的值,来设置 my.php
的内容,同时 , 访问 install.php
时,是不会调用参数过滤的函数的
所以可以注入 php
代码,getshell