[代码审计]极致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("/(&lt;\\?php&nbsp;)+/", "", 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存在文件上传漏洞

[代码审计]极致CMS1.9.5存在文件上传漏洞

[PHP代码审计]极致CMS1.9存在SQL注入漏洞

[PHP代码审计]极致CMS1.9存在SQL注入漏洞

PHP-CMS代码审计

从一个小众cms入门代码审计