分类目录归档:安全分析

一条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