BUUCTF_WEB_[De1CTF 2019]SSRF Me 题解

[De1CTF 2019]SSRF Me

1.查看提示:
image-20230829102629610

2.打开网页:
image-20230829102651916

观察代码,感觉是flask注入,整理代码:

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
111
112
113
114
115
116
117
#! /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"
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)

3.代码审计:

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#! /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:
# 有参初始化一个Task对象
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
# 将客户端的ip地址进行md5加密
self.sandbox = md5(ip)
# 判断该ip地址是否存在
if (not os.path.exists(self.sandbox)):
# SandBox For Remote_Addr
# 如果该ip地址不存在,则为该ip地址创建一个目录,使该ip地址能够访问该网站文件
os.mkdir(self.sandbox)

def Exec(self):
# 申请一个result字典
result = {}
# {'code': 500}
result['code'] = 500
if (self.checkSign()):
# md5(secert_key + param + action)=sign
if "scan" in self.action:
# 打开result.txt文件,并用tmpfile指代该文件
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
# 获取param路径下的文件内容,并存储到resp中
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
# 读取到param文件下的内容时,将该文件内容打印,但是该打印的内容无法呈现到网页中,只能呈现在编译器中
# 所以无法利用其查看resp的内容
print(resp)
# 将读取的文件内容写入tmofile中
tmpfile.write(resp)
# 关闭文件tmpfile
tmpfile.close()
result['code'] = 200

if "read" in self.action:
# 再次读取result.txt文件
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
# 将读取的文件内容存储到result字典中
result['data'] = f.read()

if result['code'] == 500:
result['data'] = "Action Error"
else:
# 如果action中没有scan则返回错误
result['code'] = 500
result['msg'] = "Sign Error"
return result


def checkSign(self):
# 对该对象的action和param进行md5加密,判断加密结果和该对象的sign是否相等
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
# generate Sign For Action Scan.

# 访问url/geneSig时,该网页支持get和post请求
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
# request.args.get()获取参数param的值
param = urllib.unquote(request.args.get("param", ""))
# 设置参数action的值为scan
action = "scan"
# 执行getSign()函数
return getSign(action, param)

# 访问url/De1ta时,该网页支持GET和POST请求
@app.route('/De1ta', methods=['GET', 'POST'])
def challenge():
# 从cookie中获取参数action的值
action = urllib.unquote(request.cookies.get("action"))
# 从get方法中获取参数param的值
param = urllib.unquote(request.args.get("param", ""))
# 从cookie中获取sign的值
sign = urllib.unquote(request.cookies.get("sign"))
# ip存储客户端的ip地址
ip = request.remote_addr
if (waf(param)):
# 查看输入的param中是否存在非法字符
return "No Hacker!!!!"
# 有参数申请一个Task类,用task存储该对象
task = Task(action, param, sign, ip)
# 返回一个json文件流到网页上,即显示task.Exec()文件内容
# 即显示result存储的内容
return json.dumps(task.Exec())

# 访问url/的网址时,执行index()函数
@app.route('/')
def index():
# 打开并读取code.txt文件
return open("code.txt", "r").read()


def scan(param):
socket.setdefaulttimeout(1)
try:
# 读取param路径下的文件内容
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"


def getSign(action, param):
# 返回secert_key + param + action的md5结果
return hashlib.md5(secert_key + param + action).hexdigest()


# 对content内容进行md5加密
def md5(content):
return hashlib.md5(content).hexdigest()

# 对输入的参数进行过滤
def waf(param):
# 去除param首位空格,并将该字符串全部字符小写化
check = param.strip().lower()
# 检测字符串是否以gopher和file开头
# 即过滤了gopher和file两个协议
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False


if __name__ == '__main__':
# main函数描述了该代码通过访问的路由来控制显示的网页内容
app.debug = False
app.run(host='0.0.0.0', port=80)

知识点介绍:

参考:Flask Flask request.args.get 获取所有参数(Python)|极客教程 (geek-docs.com)

urllib.unquote(string):将url编码的字符串进行解码

1
2
3
4
5
import urllib.parse

str = '%E6%B3%95%E5%9B%BD%E7%BA%A2%E9%85%92'

print(urllib.parse.unquote(str))

输出:

1
法国红酒
使用request.args.get获取指定参数的值

例子:假设我们有一个URL为/user?name=John&age=25,我们想要获取到对应参数的值

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask, request

app = Flask(__name__)

@app.route('/user', methods=['GET'])
def get_user():
name = request.args.get('name')
age = request.args.get('age')

return f"Name: {name}, Age: {age}"

if __name__ == '__main__':
app.run()

输出:

1
2
在上面的代码中,我们定义了一个路由/user,并指定该路由只支持GET方法。
在get_user函数中,我们使用request.args.get方法获取了URL参数中名为name和age的值,并将其拼接成字符串返回。当我们访问/user?name=John&age=25时,页面会显示Name: John, Age: 25
使用request.args.get获取所有参数的值

例子:有时候我们并不知道URL参数的具体名称,或者URL参数的数量是可变的。这时,我们可以使用request.args.get方法的另一种形式,即不传递参数名,直接获取所有参数的值

1
2
3
4
5
6
7
8
9
10
11
12
13
from flask import Flask, request

app = Flask(__name__)

@app.route('/user', methods=['GET'])
def get_user():
args = request.args.get()

return f"All Params: {args}"

if __name__ == '__main__':
app.run()

输出:

1
在上述代码中,我们使用request.args.get方法获取所有URL参数的值,并将其拼接成字符串返回。例如,当我们访问/user?name=John&age=25&city=New+York时,页面会显示All Params: ImmutableMultiDict([('name', 'John'), ('age', '25'), ('city', 'New York')])。
需要注意的是,request.args.get方法返回的是一个ImmutableMultiDict对象,它类似于字典但是不可修改。我们可以使用其提供的方法来获取参数值,比如args.getlist(‘name’)可以获取名为name的参数的所有值

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask, request

app = Flask(__name__)

@app.route('/user', methods=['GET'])
def get_user():
args = request.args.get()
name = args.getlist('name')

return f"Name: {name}"

if __name__ == '__main__':
app.run()

输出:

1
在上述代码中,我们使用args.getlist方法获取名为name的参数的所有值,并将其拼接成字符串返回。当我们访问/user?name=John&name=Tom&name=Jack时,页面会显示Name: ['John', 'Tom', 'Jack']
hashlib.md5():
1
2
3
4
5
import hashlib         #导入hashlib模块

md = hashlib.md5() #获取一个md5加密算法对象
md.update('how to use md5 in hashlib?'.encode('utf-8')) #制定需要加密的字符串
print(md.hexdigest()) #获取加密后的16进制字符串

输出结果:

1
d26a53750bc40b38b65a520292f69306
str.strip([chars]):移除在字符串str中的首尾字符chars,默认移除空格
1
2
3
4
5
6
str = "0000hello0000".strip('0')
print(str)
# hello
str = " hello ".strip()
print(str)
# hello
str.lower():将字符串str所有字符转小写
1
2
3
str = "AAAAAsssDDD".lower()
print(str)
# aaaaasssddd
Python startswith() 方法用于检查字符串是否是以指定子字符串开头,如果是则返回 True,否则返回 False。如果参数 beg 和 end 指定值,则在指定范围内检查:

参考:Python startswith()方法 | 菜鸟教程 (runoob.com)

image-20230829131547193

image-20230829131601809

urllib.urlopen(path):
1
2
3
4
from urllib.request import urlopen

myURL = urlopen("https://www.runoob.com/")
print(myURL.read())

解释:

1
2
3
4
以上代码使用 urlopen 打开一个 URL,然后使用 read() 函数获取网页的 HTML 实体代码。

read() 是读取整个网页内容,我们可以指定读取的长度
如读取长度为300:read(300)
REMOTE_ADDR:

参考:HTTP 请求头中的 Remote_Addr,X-Forwarded-For,X-Real-IP - 23云恋49枫 - 博客园 (cnblogs.com)

1
表示发出请求的远程主机的 IP 地址,remote_addr代表客户端的IP,但它的值不是由客户端提供的,而是服务端根据客户端的ip指定的,当你的浏览器访问某个网站时,假设中间没有任何代理,那么网站的web服务器(Nginx,Apache等)就会把remote_addr设为你的机器IP,如果你用了某个代理,那么你的浏览器会先访问这个代理,然后再由这个代理转发到网站,这样web服务器就会把remote_addr设为这台代理机器的IP
x_forwarded_for:
1
简称XFF头,它代表客户端,也就是HTTP的请求端真实的IP,只有在通过了HTTP 代理或者负载均衡服务器时才会添加该项,正如上面所述,当你使用了代理时,web服务器就不知道你的真实IP了,为了避免这个情况,代理服务器通常会增加一个叫做x_forwarded_for的头信息,把连接它的客户端IP(即你的上网机器IP)加到这个头信息里,这样就能保证网站的web服务器能获取到真实IP
os.path.exists():
1
2
os即operating system(操作系统),Python 的 os 模块封装了常见的文件和目录操作。os.path模块主要用于文件的属性获取,exists是“存在”的意思,所以顾名思义,os.path.exists()就是判断括号里的文件是否存在的意思,括号内的可以是文件路径
原文链接:https://blog.csdn.net/u012424313/article/details/82216092

4.分析:
(1).有代码分析得访问url/geneSign只会返回md5(secert_key + param + action)的结果,访问url/只会返回code.txt文件的内容,所以我们只能去访问url/De1ta:

1
http://25f82c99-8160-4244-bc6c-fee7c6ef602e.node4.buuoj.cn:81/De1ta

对路由器@app.route(‘/De1ta’, methods=[‘GET’, ‘POST’])下的函数进行分析后得:

我们需要令param=flag.txt,同时action中必须有scan,这样就可以调用

scan(param),去访问当前网页下的文件flag.txt(如果直接访问是访问不到的,可能是flask框架的作用)

image-20230829140947972

(2).但是如果只有scan是无法对文件内容进行显示的,只有action中包含有read时,才能进行显示文件内容:

image-20230829141123757

read可以将写入flag.txt的内容的文件result.txt再次进行读取,并存如最终需要显示的result字典中:

image-20230829141312146

所以action=scanread

(3).要触发上述内容需要绕过checkSign()的检测:
md5(secert_key + param + action)=sign

image-20230829141406740

5.最终payload的构造:

(1).secert_key:
image-20230829142324834

由于flask框架中,网页显示(执行)的都是路由器的内容,所以对于网页来说,在路由器之前所定义的secert_key在网页显示后都是唯一的,即在网页初始化的时候对secert_key赋值,之后该全局变量都是不变的

(2).参数的基本情况:

1
2
3
4
param=flag.txt
action=scanread
secert_key=xxx
sign=md5(xxxflag.txtscanread)

现在就是要获取md5(xxxflag.txtscanread)的值

(3)url/geneSign的利用:

image-20230829142858868

该路由器会返回:md5(secert_key+param+scan)的值

所以如果我们令param=flag.txtread

则该路由器返回的就是md5(xxxflag.txtreadscan)

就可以活得sign的值:
payload:

1
param=flag.txtread

输出:

1
5927506259f0daa2cdfb35867203b343

所以此时sign=5927506259f0daa2cdfb35867203b343=xxxflag.txtreadscan

(3).最终的payload:

1
2
3
4
url/De1ta
?param=flag.txt

Cookie:action=readscan;sign=5927506259f0daa2cdfb35867203b343

image-20230829143838356

flag=flag{ceb51b96-bdcf-415a-bb56-da305f851028}


BUUCTF_WEB_[De1CTF 2019]SSRF Me 题解
http://example.com/2023/08/29/2023-08-29-[De1CTF 2019]SSRF Me/
作者
South
发布于
2023年8月29日
许可协议