De1CTF-2019-SSRF-Me

[De1CTF 2019]SSRF Me

1.题目:

2.过程:

进入题目,大概是一坨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
#! /usr/bin/env python
# encoding=utf-8
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"

@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
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
※action = "scan"

调用了getSign:

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

也就是说,访问/geneSign,会根据secert_key、param、action生成MD5的签名

action的默认值为scan

1
2
3
4
5
6
7
8
9
@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()) \

walf:

1
2
3
4
5
6
def waf(param):
check = param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False

class task和Exec:

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

在核对签名后,如果scan在action中,则scan读取param并存放到result.txt中

如果read在action中,则read读取并显示result.txt……大概是这个意思

想read就要伪造签名,这里存在一种攻击方式:

哈希长度扩展攻击

想要了解这种攻击方式,就要了解哈希:

1.HASH:

Hash,一般翻译做散列、杂凑,或音译为哈希,是把任意长度的输入(又叫做预映射pre-image)通过散列算法变换成固定长度的输出,该输出就是散列值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。基于Merkle–Damgård结构。

2.例如:MD5

一般md5在签名时会是这样的:

md5(key+data)

我们不知道key,但是data是可以控制的

如果同时我们知道了key的长度就可以无需key,计算出md5(key+data+attach)

attach就是我们任意附加的字段。

md5在加密时,data按照512位分块处理,最后一块不满512位的处理:

  1. data+padding+length(data):总长度为64字节;
  2. length(data):8字节,64位长,描述data原始信息的长度,按照小端储存;
  3. padding:填充,根据“\80”+(“\x00”)*x,使得达到448位长度。

这样,数据就可以分块计算了。

MD5规定了有初始计算向量,向量依次与每个数据块计算,每计算一次,得到新的向量

新的向量会覆盖掉初始向量,成为下个数据块的计算向量。最后的计算向量处理后就是最终的md5值

而我们已知的md5(key+data)就相当于已知前一个数据块计算后得到的计算向量

key长度已知的情况下,我们只要人为补充padding,就可以使attach成为下一个数据块内容,

key参与下计算的向量一定是正确的,也就确保拿已知md5继续计算下去得到的md5是正确的。

我们拿到md5值后,就可以完成验证,完成哈希长度扩展攻击攻击了。

hashpump

em……这个本地实现还挺难搞的……,hashpump工具可以轻松完成这些工作🐕!

image-20210321105703270

使用也十分简介明了,已知md5,data,key的长度,附加字段,就可以生成对应md5和填充好的数据

解题

image-20210321110037572

可以看到key的长度,同时根据hint,param为flag.txt,共24位。attach自然是read

1
2
3
4
5
6
7
8
9
@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()) \

向/De1ta页面的cookie字段提交sign和action即可:

1
2
3
4
5
6
7
8
9
10
11
import requests

url = 'http://9c8d9029-c3f0-495e-a6b1-d806896df4a2.node3.buuoj.cn/De1ta?param=flag.txt'

##将生成\x转换成%
cookies = {
'sign': '2b865fed1aca31784de5754a868533a3',
'action':'scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read'
}
res = requests.get(url=url, cookies=cookies)
print(res.text)

image-20210321110421364

ok

作者

inanb

发布于

2021-03-21

更新于

2021-08-23

许可协议


:D 一言句子获取中...