hxp2020-audited
1. 前言
更新时间 | 更新内容 |
---|---|
2020-12-26 | 初稿 |
2020-12-27 | 第二种利用方式 |
上周末打了一下hxp ctf[1]. 因为和考试撞了所以就看了一个题, 也没做出来. 赛后看大佬们的wp还是挺有意思的. 通过这个题目学到了很多新知识, 记录并分享一下.
2. 正文
这是一个python pwn. 题目附件中有服务端如下(python版本是的最新python 3.9
):
1 | #!/usr/bin/python3 -u |
题目用到了python 3.8中新引入的一个特性: audit[2]. 用户可以通过 sys.addaudithook
添加一个 audit hook 函数, 之后在执行过程中遇到某些事件(如import 某个 module)时就会触发这个 audit hook 函数, 并把对应事件类型和参数传给该audit hook 函数. cpython中有一些列事件默认会触发 audit hook 函数(详见[3]). 用户也可以通过sys.audit(event, args)
来触发audit hook 函数. 这个特性的本意是为了使得python的执行过程对管理人员队更透明的.
本题中会将用户输出的代码编译执行, 不过有两个限制操作:
- sys.modules 中的module都
del
了 - 有个audit hook函数, 只允许一次
exec
, 否则就会调用__exit(1)
当时看到这儿时我就猜测是不是有什么绕过audit的骚操作, 还真搜到一个视频[4]. 视频中老哥的思路是修改内存中python解释器的关于调用audit hook那部分的代码. 在修改过程中必然要触发好几次audit hook函数, 显然无法使用.
赛后看来两位师傅的writeup[5][6]. 学到了很多.
大致思路如下:
- 修改
__exit
为一个无效函数 - 想办法拿到
os
模块 os.system("cat /flag*")
接下来我们就看看如何具体实现
2.1. 修改 __exit 为无效函数
我们使用如下代码进行测试
1 | import sys |
我们的目的是构造一段代码, 使其仅触发一次audit函数就将__exit
修改成一个无用函数.
通过查看exec()
的文档[7]可以发现下面这一段:
If the globals dictionary does not contain a value for the key
__builtins__
, a reference to the dictionary of the built-in modulebuiltins
is inserted under that key. That way you can control whatbuiltins
are available to the executed code by inserting your own__builtins__
dictionary into globals before passing it to exec().
因此我们可以使用如下语句加载sys
模块(来自fab1ano师傅的思路[5])
1 | sys = __builtins__["__loader__"].load_module('sys') |
我们可以构造代码如下
1 | tmp = """ |
执行发现可以成功将__exit
替换:
1 | [*] exec (<code object <module> at 0x7ffff77d85b0, file "<user input>", line 2>,) |
至此我们就可以任意次触发 audit 函数而不用担心程序退出了, 接下来我们要做的解决sys.modules
被删除的问题.
2.2. 想办法拿到os
模块
关于sys.modules
的作用[8]中是这么说的:
Python import 的步骤
python 所有加载的模块信息都存放在 sys.modules 结构中,当 import 一个模块时,会按如下步骤来进行
如果是 import A,检查 sys.modules 中是否已经有 A,如果有则不加载,如果没有则为 A 创建 module 对象,并加载 A
如果是 from A import B,先为 A 创建 module 对象,再解析A,从中寻找B并填充到 A 的 dict 中
我们更新测试代码, 加入删除sys.modules
代码:
1 | import sys |
此时再使用之前的解法就会报错.
sys.modules
中的__main__
被删除了, 所以这个语句sys.modules['__main__']
会报错.os
模块也被从sys.modules
中删除了, 所以此时执行import os
python会找到os.py
执行并导入模块, 这个过程中会触发多次 audit hook函数.
下面时报错信息.
1 | [*] exec (<code object <module> at 0x7ffff77d85b0, file "<user input>", line 2>,) |
我们无法引用sys.modules["__main__"]
了, 对此fab1ano师傅的做法是使用gc
(又学到了).
而且通过搜索gc
中的对象, 可以将__main__
和os
都定位到.
最终代码如下:
1 | tmp = """ |
成功 bypass 并执行命令;
1 | Searching for modules __main__ and os in garbage collector objects |
2.3. another way
hstocks师傅使用的另外一种方法来引入 sys
和 os
模块, 这个方法好像在沙箱逃逸时挺常用的, 不过classes
的索引在不同版本的python中好像不一样, 具体原理还不清楚.
1 | classes = ''.__class__.__base__.__subclasses__() |
写了个脚本, 用来搜索可以用这个方法导入的模块. (因为不同python版本的偏移可以不一样)
1 | classes = ''.__class__.__base__.__subclasses__() |
定位到__main__
模块的方法是通过触发报错然后在调用栈(tarceback)上搜索(又学到了)
代码如下:
1 | try: |
3. 结语
通过这个题目学到了很多骚操作.
- 通过修改函数绕过 audit
- 通过
__builtins__['__loader__'].load_module('sys')
导入模块 - 通过
gc
绕过对sys.modules
的删除 - 通过
''.__class__.__base__.__subclasses__()
来导入模块 - 通过报错然后搜索traceback来定位
__main__
而且还有个很有意思的事情, python的文档中说明object.__setattr__
, object.__getattr__
和 object.__delattr__
三个事件默认会触发 audit hook的, 但是我在 python 3.9.0 下测试发现这三个事件都不会触发 audit hook 函数, 很奇怪, 测试代码如下:
1 | import sys |
4. 参考
- hxp CTF 2020 – ctftime.org
- Python Runtime Audit Hooks – PEP 578
- Audit events table – python doc
- Bypassing Python3.8 Audit Hooks – youtube
- writeup by fab1ano – github
- writeup by hstocks – github
- exec()– python doc
- Python沙箱逃逸的n种姿势 – 先知社区
- sys.modules – python doc
- sys.exc_info – python doc
- Python沙箱逃逸总结 – HatBoy