从0到1:CTFer成长之路
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

3.2 Python的安全问题

因为Python实现各种功能非常简单、快速,所以应用越来越普遍。同时由于Python的特性问题如反序列化、SSTI等十分有趣,因此CTF比赛中也开始对Python的特性问题进行利用的考察。本节将介绍CTF比赛的Python题目中常见的考点,介绍相关漏洞的绕过方式;结合代码或例题进行分析,让读者在遇到Python代码时快速找到相关漏洞点,并进行利用。由于Python 2与Python 3部分功能存在差异,实现可能有些区别。下面的内容中,如果没有其他特殊说明,则Python 2和Python 3在相关漏洞的原理上并没有区别。

3.2.1 沙箱逃逸

CTF的题目中存在一种让用户提交一段代码给服务端、服务端去运行的题型,出题者也会通过各种方式过滤各种高风险库、关键词等。对于这类问题,我们根据过滤程度由低到高,逐一介绍绕过的思路。

3.2.1.1 关键词过滤

关键词过滤是最简单的过滤方式,如过滤“ls”或“system”。Python是动态语言,有着灵活的特性,这种情况非常容易绕过。例如:

对于字符串,我们还可以加入拼接、倒序或者base64编码等。

3.2.1.2 花样import

在Python中,想使用指定的模块最常用的方法是显式import,所以很多情况下import也会被过滤。不过import有多种方法,需要逐一尝试。

另外,如果可以控制Python的代码,在指定目录中写入指定文件名的Python文件,也许可以达到覆盖沙箱中要调用模块的目的。比如,在当前目录中写入random.py,再在Python中import random时,执行的就是我们的代码。例如:

这里利用的是Python导入模块的顺序问题,Python搜索模块的顺序也可通过sys.path查看。如果可以控制这个变量,我们可以方便地覆盖内置模块,通过修改该路径,可以改变Python在import模块时的查找顺序,在搜索时优先找到我们可控的路径下的代码,达成绕过沙箱的目的。例如:

除了sys.path,sys.modules是另一个与加载模块有关的对象,包含了从Python开始运行起被导入的所有模块。如果从中将部分模块设置为None,就无法再次引入了。例如:

如果将模块从sys.modules中剔除,就彻底不可用了。不过可以观察到,其中的值都是路径,所以可以手动将路径放回,然后就可以利用了。

同理,这个值被设置为可控模块也可能造成任意代码执行。

如果可控的是ZIP文件,也可以使用zipimport.zipimporter实现上面的效果,不再赘述。

3.2.1.3 使用继承等寻找对象

在Python中,一切都是对象,所以我们可以使用Python的内置方法找到对象的父类和子类,如[].__class__是<class'list'>,[].__class__.__mro__是(<class'list'>,<class'object'>),而[].__class__.__mro__[-1].__subclasses__()可以找到object的所有子类。

比如,第40项是file对象(实际的索引可能不同,需要动态识别),可以用于读写文件。

Python中直接使用不需要import的函数,如open、eval属于全局的module__builtins__,所以可以尝试__builtins__.open()等用法。若函数被删除了,还可以使用reload()函数找回。

3.2.1.4 eval类的代码执行

eval类函数在任何语言中都是一个危险的存在,我们可以在Python中尝试,可以通过exec()(Python 2)、execfile()、eval()、compile()、input()(Python 2)等动态执行一段Python代码。

3.2.2 格式化字符串

CTF的Python题目中会涉及Jinja2之类的模板引擎的注入。这些漏洞常常由于服务器端没有对用户的输入进行过滤,就直接带入了服务器端对相关页面的渲染过程中。通过注入模板引擎的一些特定的指令格式,如{{1+1}}返回了2,我们可以得知漏洞存在于相关Web页面中。类似这种特性不仅限于Web应用中,也存在于Python原生的字符串中。

3.2.2.1 最原始的%

如下代码实现了登录功能,由于没有对用户的输入进行过滤,直接带入了print的输出过程,从而导致了用户密码的泄露。

比如,用户输入“%(password)s”就可以获取用户的真实密码。

3.2.2.2 format方法相关

上述的例子还可以使用format方法进行改写(仅涉及关键部分):

此时若passwd="{password}",也可以实现3.2.2.1节中获取用户真实密码的目的。除此之外,format方法还有其他用途。例如,以下代码

会先把0替换为format中的参数,再继续获取相关的属性。由此我们可以获取代码中的敏感信息。

下面引用来自于http://lucumr.pocoo.org/2016/12/29/careful-with-str-format/的例子:

如果format_string为{event.__init__.__globals__[CONFIG][SECRET_KEY]},就可以泄露敏感信息。

理论上,我们可以参考上文,通过类的各种继承关系找到想要的信息。

3.2.2.3 Python 3.6中的f字符串

Python 3.6中新引入了f-strings特性,通过f标记,让字符串有了获取当前context中变量的能力。例如:

不仅限制为属性,代码也可以执行了。例如:

但是目前没有把普通字符串转换为f字符串的方法,也就是说,用户可能无法控制一个f字符串,可能无法利用。

3.2.3 Python模板注入

Python的很多Web应用涉及模板的使用,如Tornado、Flask、Django。有时服务器端需要向用户端发送一些动态的数据。与直接用字符串拼接的方式不同,模板引擎通过对模板进行动态的解析,将传入模板引擎的变量进行替换,最终展示给用户。

SSTI服务端模板注入正是因为代码中通过不安全的字符串拼接的方式来构造模板文件而且过分信任了用户的输入而造成的。大多数模板引擎自身并没有什么问题,所以在审计时我们的重点是找到一个模板,这个模板通过字符串拼接而构造,而且用户输入的数据会影响字符串拼接过程。

下面以Flask为例(与Tornado的模板语法类似,这里只关注如何发现关键的漏洞点)。在处理怀疑含有模板注入的漏洞的网站时,先关注render_*这类函数,观察其参数是否为用户可控。如果存在模板文件名可控的情况,如

配合上传漏洞,构造模板,则完成模板注入。

对于下面的例子,我们应先关注render_template_string(template)函数,其参数template通过格式化字符串的方式构造,其中request.url没有任何过滤,可以直接由用户控制。

那么直接在URL中传入恶意代码,如“{{self}}”,拼接至template中。由于模板在渲染时服务器会自动寻找服务器渲染时上下文的有关内容,因此将其填充到模板中,就导致了敏感信息的泄露,甚至执行任意代码的问题。

通过在本地搭建与服务器相同的环境,查看渲染时上下文的信息,这时最简单的利用是用{{variable}}将上下文的变量导出,更好的利用方式是找到可以直接利用的库或函数,或者通过上文提到的继承等寻找对象的手段,从而完成任意代码的执行。

3.2.4 urllib和SSRF

Python的urllib库(Python 2中为urllib2,Python 3中为urllib)有一些HTTP下的协议流注入漏洞。如果攻击者可以控制Python代码访问任意URL,或者让Python代码访问一个恶意的Web Server,那么这个漏洞可能危害内网服务安全。

对于这类漏洞,我们主要关注服务器采用的Python版本是否存在相应的漏洞,以及攻击的目标是否会受到SSRF攻击的影响,如利用某个图片下载的Python服务去攻击内网部署的一台未加密的Redis服务器。

3.2.4.1 CVE-2016-5699

CVE-2016-5699:Python 2.7.10以前的版本和Python 3.4.4以前的3.x版本中的urllib2和urllib中的HTTPConnection.putheader函数存在CRLF注入漏洞。远程攻击者可借助URL中的CRLF序列,利用该漏洞注入任意HTTP头。

在HTTP解析host的时候可以接收urlencode编码的值,然后host的值会在解码后包含在HTTP数据流中。这个过程中,由于没有进一步的验证或者编码,就可以注入一个换行符。

例如,在存在漏洞的Python版本中运行以下代码:

其功能是从命令行参数接收一个URL,然后访问它。为了查看urllib请求时发送的HTTP头,我们用nc命令来监听端口,查看该端口收到的数据。

此时向127.0.0.1:12345发送一个正常的请求,可以看到HTTP头为:

然后我们使用恶意构造的地址

可以看到HTTP头变成了:

对比之前正常的请求方式,X-injected:header行是新增的,这样就造成了我们可以使用类似SSRF攻击手法的方式,攻击内网的Redis或其他应用。

除了针对IP,这个攻击漏洞在使用域名的时候也可以进行,但是要插入一个空字节才能进行DNS查询。比如,URL:http://localhost%0d%0ax-bar:%20:12345/foo进行解析会失败的,但是URL:http://localhost%00%0d%0ax-bar:%20:12345/foo可以正常解析并访问127.0.0.1。

注意,HTTP重定向也可以利用这个漏洞,如果攻击者提供的URL是恶意的Web Server,那么服务器可以重定向到其他URL,也可以导致协议注入。

3.2.4.2 CVE-2019-9740

CVE-2019-9740:Python urllib同样存在CRLF注入漏洞,攻击者可通过控制URL参数进行CRLF注入攻击。例如,我们修改上面CVE-2016-5699的poc,就可以复现了

可以看到,HTTP头如下:

3.2.5 Python反序列化

反序列化在每种语言中都有相应的实现方式,Python也不例外。在反序列化的过程中,由于反序列化库的实现不同,在太相信用户输入的情况下,将用户输入的数据直接传入反序列化库中,就可能导致任意代码执行的问题。Python中可能存在问题的库有pickle、cPickle、PyYAML,其中应该重点关注的方法如下:pickle.load(),pickle.loads(),cPickle.load(),cPickle.loads(),yaml.load()。下面重点讨论pickle的用法,其他反序列化方法类似。

pickle中存在__reduce__魔术方法,来决定类如何进行反序列化。__reduce__方法返回值为长度一个2~5的元组时,将使用该元组的内容将该类的对象进行序列化,其中前两项为必填项。元组的内容的第一项为一个callable的对象,第二项为调用callable对象时的参数。比如通过如下exp,将生成在反序列化时执行os.system("id")的payload。在用户对需要进行反序列化的字符串有控制权时,将payload传入,就会导致一些问题。例如,将以下反序列化产生的结果直接传入pickle.loads(),则会执行os.system("id")。

pickle中存在很多opcode,通过这些opcode,构造调用栈,我们可以实现很多其他功能。比如,code-breaking 2018中涉及一道反序列化的题目,在反序列化阶段限制了可供反序列化的库,__reduce__只能实现对一个函数的调用,于是需要手工编写反序列化的内容,以完成对过滤的绕过及任意代码执行的目的。

3.2.6 Python XXE

无论什么语言,在涉及对XML的处理时都有可能出现XXE相关漏洞,于是在审计一段代码中是否存在XXE漏洞时,最主要的是找对XML的处理过程,关注其中是否禁用了对外部实体的处理。比如,对于某个Web程序,通过请求头中的Content-type判断用户输入的类型,为JSON时调用JSON的处理方法,为XML时调用XML的处理方法,而这个过程中刚好没有对外部实体进行过滤,这就导致了在用户输入XML时的XXE问题。

XXE就是XML Entity(实体)注入。Entity(实体)的作用类似Word中的“宏”,用户可以预定义一个Entity,再在一个文档中多次调用,或在多个文档中调用同一个Entity。XML定义了两种Entity:普通Entity,在XML文档中使用;参数Entity,在DTD文件中使用。

在Python中处理XML最常用的就是xml库,我们需要关注其中的parse方法,查看输入的XML是否直接处理用户的输入,是否禁用了外部实体,即审计时的重点。但是,Python从3.7.1版开始,默认禁止了XML外部实体的解析,所以在审计时也要注意版本。具体xml库存在的安全问题,读者可以查阅xml库的官方文档:https://docs.python.org/3/library/xml.html

下述代码中包含两段XXE常见的payload,分别用于读取文件和探测内网,再通过Python对其中的XML进行解析。代码本身没有对外部实体进行限制,从而导致了XXE漏洞。

运行这段代码,就可以打印出/etc/passwd的内容,而且127.0.0.1:8005可以收到一个HTTP请求。

除了这种情况,有时源程序在解析完XML数据后,并不会将其中的内容进行输出,此时无法从返回结果中获取我们需要的内容。在这种情况下,我们可以利用Blind XXE作为攻击方式,同样是利用对XML实体的各种操作,攻击载荷如下所示。

先用file://php://filter获取目标文件的内容,然后将内容以http请求发送到接收数据的服务器。由于不能在实体定义中引用参数实体,因此我们需要将嵌套的实体声明放到一个外部dtd文件中,如下文的eval.dtd。

在服务器上建立监听即可实现数据的外带。同时在某些情况下,需要外带的数据中可能存在特殊字符,此时需要通过CDATA将数据进行包裹,最终实现外带。由于在互联网上有很多相关资料,故不在此处做更多介绍。

3.2.7 sys.audit

2018年6月,Python的PEP-0578新增了一个审计框架,可以提供给测试框架、日志框架和安全工具,来监控和限制Python Runtime的行为。

Python提供了对许多常见操作系统的各种底层功能的访问方式。虽然这对于“一次编写,随处运行”脚本非常有用,但使监控用Python编写的软件变得困难。由于Python本机原生系统API,因此现有的监控审计工具要么上下文信息是受限的,要么会直接被绕过。

上下文受限是指,系统监视可以报告发生了某个操作,但无法解释导致该操作的事件序列。例如,系统级别的网络监视可以报告“开始侦听在端口5678”,但可能无法在程序中提供进程ID、命令行参数、父进程等信息。

审计绕过是指,一个功能可以使用多种方式完成,监控了一部分,使用其他的就可以绕过。例如,在审计系统中专门监视调用curl发出HTTP请求,但Python的urlretrieve函数没有被监控。

另外,对于Python有点独特的是,通过操纵导入系统的搜索路径或在路径上放置文件而不是预期的文件,很容易影响应用程序中运行的代码。当开发人员创建与他们打算使用的模块同名的脚本时,通常会出现这种情况。例如,一个random.py文件尝试导入标准库random,实际上执行的是用户的random.py。

3.2.8 CTF Python案例

3.2.8.1 皇家线上赌场(SWPU 2018)

题目是一个Flask Web,通过任意文件读取获取views.py的代码:

__init__.py文件内容如下:

然后使用得到的secret_key,我们可以伪造Session,生成一个符合getflag条件的Session。

getflag的format可以直接注入一些数据,但是需要跳出g.u,题目中给了提示:为了方便,给user写了save方法,所以直接使用__globals__跳出得到flag,payload见图3-2-1。

图3-2-1

3.2.8.2 mmmmy(网鼎杯2018线上赛)

伪造JWT登入后是一个留言功能,发现输入的东西都会原原本本地打印在页面上,于是猜测这是一个SSTI。测试后发现过滤了很多东西,如“'”“"”“os”“_”“{{”等,只要出现了这些关键字,就直接打印None。虽然过滤了“{{”,但是可以使用“{%”,如“{%if 1%}1{%endif%}”会打印“1”。

我们思考需要绕过的地方。首先“__”被过滤,可以使用“[]”结合request来绕过,如“{%if()[request.args.a]%}”,URL中的“/bbs?a=__class__”。然后可以构造一个读取文件的payload:

但是报了500错误,考虑是没有chr函数。那么如法炮制,获取chr函数:

然后可以使用脚本进行盲注,见图3-2-2。

图3-2-2

图3-2-2(续)

除了盲注,还有一种方法可以直接打印明文,即使用jinja2中的print,见图3-2-3。打印结果见图3-2-4。

图3-2-3

图3-2-4