[代码审计]极致CMS底层代码剖析(FrPHP框架)
Posted Y4tacker
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[代码审计]极致CMS底层代码剖析(FrPHP框架)相关的知识,希望对你有一定的参考价值。
文章目录
写在前面
为什么会写到这个呢,其实最重要的原因是我不太想去挖低级漏洞了,更希望能够从框架底层去挖漏洞提交,因此产生了这篇文章,当然由于各个版本的Frphp核心逻辑不变,我这里用较低版本号的1.6.6去进行分析
FrPHP
目录结构
- common 包含系统部分配置、核心公共函数集、错误渲染页面
- db 数据库语句的封装
- Extend 拓展类所在目录
- lib MVC基类
- Fr.php FrPHP框架核心加载文件
接下来我们就从各个目录去分析关键的底层代码
Fr.php
这是自动加载引用类的机制,这里我们来讲一下spl_autoload_register函数,当然我觉得了解这个得先给师父们讲一下__autoload
函数
__autoload函数
首先我们写一个index.php
<?php
/**
* Created by Y4tacker
**/
function __autoload( $class ) {
$file = $class . '.class.php';
if ( is_file($file) ) {
require_once($file);
}
}
$obj = new yyds();
$obj->doHack();?>
再写一个yyds.class.php
<?php
/**
* Created by Y4tacker
**/
class yyds {
function doHack() {
echo 'hacker';
}
}
接下来运行index.php
就会输出hacker
,在index.php
中,由于没有包含yyds.class.php
,在实例化yyds
时,自动调用__autoload
函数,参数$class
的值即为类名yyds
,此时yyds.class.php
就被引进来了。
好处是可以避免书写过多的引用文件,同时也使整个系统更加灵活。
spl_autoload_register
这个函数与__autoload
极为相似
<?
function yyds( $class ) {
$file = $class . '.class.php';
if (is_file($file)) {
require_once($file);
}
}
spl_autoload_register( 'yyds' );
$obj = new yyds();
$obj->doHack();?>
运行这个同样会输出hacker
,将__autoload
换成yyds
函数。但是yyds
不会像__autoload
自动触发,这时spl_autoload_register()
告诉PHP碰到没有定义的类就执行yyds()
run
在如今各大框架当中,我们不难发现其在根目录下的index.php
,会有类似这样的代码。它最后一行通过调用run
方法,来实现整个MVC的操作,具体为什么就是封装好了一个小框架
<?php
// 应用目录为当前目录
define('APP_PATH', __DIR__ . '/');
// 开启调试模式
//define('APP_DEBUG', true);
//定义项目目录
define('APP_HOME','Home');
//定义项目模板文件目录
define('HOME_VIEW','template');
//定义项目模板公共文件目录
define('Tpl_common','');
//定义项目控制器文件目录
define('HOME_CONTROLLER','c');
//定义项目模型文件目录
define('HOME_MODEL','m');
//定义项目默认方法
define('DefaultAction','jizhi');
//取消log
define('StopLog',false);
//定义静态文件路径
define('Tpl_style','/static/');
// 加载框架文件
require(APP_PATH . 'FrPHP/Fr.php');
// 就这么简单~
接下来我们回到Frp.php
,看看它的run()
方法
public function run()
{
spl_autoload_register(array($this, 'loadClass'));
$this->setDbConfig();
$this->setReporting();
$this->removeMagicQuotes();
//$this->unregisterGlobals();
$this->route();
}
如果碰到没有定义的类就通过loadClass
函数引入,接下来几行都是配置相关和路由方面,我们来看看loadClass
public function loadClass($className)
{
$classMap = $this->classMap();
if (isset($classMap[$className])) {
// 包含内核文件
$file = $classMap[$className];
} elseif (strpos($className, '\\\\') !== false) {
// 包含应用(application目录)文件
$file = APP_PATH . str_replace('\\\\', '/', $className) . '.php';
if (!is_file($file)) {
return;
}
} else {
return;
}
include $file;
// 这里可以加入判断,如果名为$className的类、接口或者性状不存在,则在调试模式下抛出错误
}
首先调用$this->classMap()
引入所有类名
// 内核文件命名空间映射关系
protected function classMap()
{
return [
'FrPHP\\lib\\Controller' => CORE_PATH . '/lib/Controller.php',
'FrPHP\\lib\\Model' => CORE_PATH . '/lib/Model.php',
'FrPHP\\lib\\View' => CORE_PATH . '/lib/View.php',
'FrPHP\\db\\DBholder' => CORE_PATH . '/db/DBholder.php',
];
}
之后通过像我们上面那样去执行类的引入与方法执行,就不多提了接下来,通过define定义常量引入数据库的配置文件
public function setDbConfig()
{
if ($this->config['db']) {
define('DB_HOST', $this->config['db']['host']);
define('DB_NAME', $this->config['db']['dbname']);
define('DB_PREFIX', $this->config['db']['prefix']);
define('DB_USER', $this->config['db']['username']);
define('DB_PASS', $this->config['db']['password']);
define('DB_PORT', $this->config['db']['port']);
if(DB_HOST=='' || DB_NAME=='' || DB_USER=='' || DB_PASS==''){
exit('<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />数据库无法链接,如果您是第一次使用,请先执行<a href="/install/">安装程序</a><br /><br /><a href="http://jizhicms.com" target="_blank">极致CMS建站程序 jizhicms.com</a>');
}
}
}
再接下来,根据我们设定的APP_DEBUG
的值来开启对应设定的错误级别
public function setReporting()
{
if (APP_DEBUG === true) {
//error_reporting(E_ALL);
error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
ini_set('display_errors','On');
} else {
error_reporting(E_ALL);
ini_set('display_errors','Off');
ini_set('log_errors', 'On');
}
}
最后调用了removeMagicQuotes
删除敏感字符,其实就是调用了stripslashes
去删除反斜杠而已
public function removeMagicQuotes()
{
if (get_magic_quotes_gpc()) {
$_GET = isset($_GET) ? $this->stripSlashesDeep($_GET ) : '';
$_POST = isset($_POST) ? $this->stripSlashesDeep($_POST ) : '';
$_COOKIE = isset($_COOKIE) ? $this->stripSlashesDeep($_COOKIE) : '';
$_SESSION = isset($_SESSION) ? $this->stripSlashesDeep($_SESSION) : '';
}
}
最后通过route
函数来开启session,我个人理解是这样的
Common
Error.php
抛出异常的页面,里面有几个关键函数,我们可以看一看,可能对以后漏洞挖掘拓展思路
第六行,如果参数可控的话,可能会造成反射性XSS的产生,当然根据这个系统来说暂时不可控
<title><?php echo $msg?></title>
当然后面还有一个防止任意命令执行的函数,逻辑很简单这就不说了
function highlight_code($code){
if (preg_match("/<\\?(php)?[^[:graph:]]/", $code)) {
$code = highlight_string($code, TRUE);
} else {
$code = preg_replace("/(<\\?php )+/", "", highlight_string("<?php ".$code, TRUE));
}
return $code;
}
Functions.php
首先来看M函数,如果name为空则返回Model的实例,否则返回传入参数对应的实例
function M($name=null) {
if(empty($name)){
$path = 'FrPHP\\lib\\\\Model';
return $path::getInstance();
}
$name = ucfirst($name);
if($name==''){
return '缺少模型类!';
}else{
$table = $name;
$name = APP_HOME.'\\\\'.HOME_MODEL.'\\\\'.$name.'Model';
if(!class_exists($name)){
$path = 'FrPHP\\lib\\\\Model';
return $path::getInstance($table);
}else{
return $name::getInstance($table);
}
}
}
之后后面接了一堆函数,大概就是一些数据库操作相关的,比较类似挑一个来分析,如果condithions不为数组,那么直接拼接,如果为数组那么就遍历键值对,将value拼接,这里很简单,如果传入函数的参数没有过滤我们也很容易通过sql注入获取敏感信息
public function getCount($conditions=null){
$where = '';
if(is_array($conditions)){
$join = array();
foreach( $conditions as $key => $value ){
$value = '\\''.$value.'\\'';
$join[] = "{$key} = {$value}";
}
$where = "WHERE ".join(" AND ",$join);
}else{
if(null != $conditions) $where = "WHERE ".$conditions;
}
$table = self::$table;
$sql = "SELECT count(*) as Frcount FROM {$table} {$where}";
$result = $this->db->getArray($sql);
return $result[0]['Frcount'];
}
其余的部分是与一些过滤相关的函数,有兴趣自己读,这篇文章目的只是为了帮助梳理底层逻辑
db
DBholder.php
这个类对于大家来说其实没必要去读,当然我是非常非常建议初学者去读一读这个里面的代码,因为你会知道简单的PDO的封装,更重要的是能够帮助你初步体验到单例模式,并且能感受到它的便捷,这里呢我也就不带着大家去读了,给大家一点自己发挥的余地
Extend
这个目录下的pay、phpmailer、phpqrcode属于系统一些拓展功能,我们不去管他,ArrayPage.php属于分页模型,个人感觉是与漏洞挖掘无关的不推荐大家去读了
compressimage.php
接下来是compressimage.php,看名字就知道是压缩图片的一个工具类,可能有触发phar反序列化的用途,首先是初始化传入了src参数这在之后会被getimagesize所调用getimagesize() 函数用于获取图像大小及相关信息,他既可以传入本地图片,也可以传入远程图片,能很负责任的告诉大家他能出发phar反序列化,当然是否能构造pop链另说
public function openImage()
{
list($width, $height, $type, $attr) = getimagesize($this->src);
$this->imageinfo = array(
'width' => $width,
'height' => $height,
'type' => image_type_to_extension($type, false),
'attr' => $attr
);
$fun = "imagecreatefrom" . $this->imageinfo['type'];
$this->image = $fun($this->src);
}
其他函数也没啥利用点可说
DatabaseTool.php(建议跳过)
我们重点去关注writeToFile
函数,函数所需参数如下
private function writeToFile($tables = array(), $ddl = array(), $data = array())
通常来说我们的tables参数一定是可控的,我们因此可以想一下,是否可以通过写入文件,来执行我们自定义的sql语句呢,且不说是否成功
foreach ($tables as $table) {
echo '备份表:' . $table . '<br>';
$str .= "-- ----------------------------\\r\\n";
$str .= "-- Table structure for {$table}\\r\\n";
$str .= "-- ----------------------------\\r\\n";
$str .= "DROP TABLE IF EXISTS `{$table}`;\\r\\n";
$str .= $ddl[$i] . "\\r\\n";
$i++;
//echo '备份成功!<br/>';
}
好吧完美失败了,当我没说,这个类大家也可以跳过了
DB_API.php
大多也是直接拼接获取sql语句,其他的就不分析了,发现完全没用也暂时无利用点可说,那我们直接到下一个模块
public function goInc($conditions,$field,$vp=1){
$where = "";
if(is_array($conditions)){
$join = array();
foreach( $conditions as $key => $value ){
$value = '\\''.$value.'\\'';
$join[] = "{$key} = {$value}";
}
$where = "WHERE ".join(" AND ",$join);
}else{
if(null != $conditions)$where = "WHERE ".$conditions;
}
$values = "{$field} = {$field} + {$vp}";
$sql = "UPDATE {$this->table} SET [代码审计]极致CMS1.9.5存在文件上传漏洞