文章总结: 该文档揭示了利用开源大语言模型公开的chattemplate进行提示词注入的攻击方法。核心原理是针对已知的对话模板结构制作有效载荷,文章以DeepSeek-R1为例,演示了通过bostoken后注入系统指令、滥用think标签伪造思考过程等多种手法。关键发现在于此类攻击具有持久性,即使初始意图被识别,注入的载荷仍会在后续对话中持续影响模型行为。 综合评分: 82 文章分类: 漏洞分析,AI安全,WEB安全,实战经验
如何用chat_template通杀开源模型?
AugustTheodor AugustTheodor
银遁安全团队
2026年6月29日 08:15 广东
在小说阅读器读本章
去阅读
前言
本文转自如下公众号:
简单讲讲上下文机制原理、以及chat_template与系统提示词注入的关系。
也就是上周的直播内容。
由于本系列的顺序现在已经乱七八糟加上我最近想讲这个,放在前面——自然,我也知道很多渗透最关注的是这个。
漏洞原理
我们知道当前主流LLM的原理是“根据文字生文字”,在LLM的工作过程中,用户输入的每个词被当做一个向量来表示,LLM会根据我们输入的所有词来对后续词汇进行预测,直到段落输出结束。
这里就存在一个很大的问题:在LLM训练完成之后,如果我们要给LLM添加功能、设定角色,这些功能和角色也需要和用户的输入放在一起,被一并输入至LLM。而LLM无法判断哪一部分输入是系统的,哪一部分输入是用户的。
听起来是不是很耳熟?在传统安全中,以“用户输入”和“系统指令”的不清为攻击面而形成的攻击类型并非第一次出现。传统安全中一系列以注入为核心的漏洞如SQL注入和SSRF、XSS都算是其中一员。
值得注意的是,上下文机制虽然和提示词注入漏洞直接相关,但提示词注入这个漏洞本身是一个大类,手法比较多,一些本文中没有提到的手法会在之后讲解。
上下文机制
在实际讲解提示词注入手法之前,我们需要先了解一下LLM的上下文机制。
首先,各位读者应该都能隐隐约约把上下文跟“历史对话”关联到一起,并且事实上,这确实是上下文机制的核心原理。
假设我们在和部署在云端的LLM对话,在每轮次对话中:
1.我们输入的内容会以文字的形式被上传到云端
2.本轮次输入会被以特定格式与历史问答内容(请把与LLM交互的过程想象为写一本永不完结的书)拼接到一起,然后输入到LLM里
那么这里就有一个很关键的问题:用户、LLM(和系统)几方的输入之间是怎么拼接的,拼接格式是什么样的?
Chat Template
说到拼接格式,就要了解chat template。大部分LLM在训练阶段都会被规定一个固定的模式规则,或者说,对于同一个LLM的训练数据必定要倾向于有一个固定的格式——如果训练者预期一个足够好的训练结果。
我们拿deepseek-r1举例。它的拼接格式可以在huggingface社区或者ollama里找到。
以下是deepseek-r1的huggingface页面:
https://huggingface.co/deepseek-ai/DeepSeek-R1?chat_template=default&format=true[1]
我们可以通过查看模型的tokenizer_config.json文件以寻找template。这是它的tokenizer_config.json文件位置:
https://huggingface.co/deepseek-ai/DeepSeek-R1/blob/main/tokenizer_config.json
它的语法template是用Jinja2写的,如下:
{%- if not add_generation_prompt is defined -%} {%- set add_generation_prompt = false -%}{%- endif -%}{%- set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt="", is_first_sp=true) -%}{%- for message in messages -%} {%- if message["role"] == "system" -%} {%- if ns.is_first_sp -%} {%- set ns.system_prompt = ns.system_prompt + message["content"] -%} {%- set ns.is_first_sp = false -%} {%- else -%} {%- set ns.system_prompt = ns.system_prompt + "\n\n" + message["content"] -%} {%- endif -%} {%- endif -%}{%- endfor -%}{{- bos_token -}}{{- ns.system_prompt -}}{%- for message in messages -%} {%- if message["role"] == "user" -%} {%- set ns.is_tool = false -%} {{- "<|User|>" + message["content"] -}} {%- endif -%} {%- if message["role"] == "assistant" and "tool_calls" in message -%} {%- set ns.is_tool = false -%} {%- for tool in message["tool_calls"] -%} {%- if not ns.is_first -%} {%- if message["content"] is none -%} {{- "<|Assistant|><|tool▁calls▁begin|><|tool▁call▁begin|>" + tool["type"] + "<|tool▁sep|>" + tool["function"]["name"] + "\n" + "```json" + "\n" + tool["function"]["arguments"] + "\n" + "```" + "<|tool▁call▁end|>" -}} {%- else -%} {{- "<|Assistant|>" + message["content"] + "<|tool▁calls▁begin|><|tool▁call▁begin|>" + tool["type"] + "<|tool▁sep|>" + tool["function"]["name"] + "\n" + "```json" + "\n" + tool["function"]["arguments"] + "\n" + "```" + "<|tool▁call▁end|>" -}} {%- endif -%} {%- set ns.is_first = true -%} {%- else -%} {{- "\n" + "<|tool▁call▁begin|>" + tool["type"] + "<|tool▁sep|>" + tool["function"]["name"] + "\n" + "```json" + "\n" + tool["function"]["arguments"] + "\n" + "```" + "<|tool▁call▁end|>" -}} {%- endif -%} {%- endfor -%} {{- "<|tool▁calls▁end|><|end▁of▁sentence|>" -}} {%- endif -%} {%- if message["role"] == "assistant" and "tool_calls" not in message -%} {%- if ns.is_tool -%} {{- "<|tool▁outputs▁end|>" + message["content"] + "<|end▁of▁sentence|>" -}} {%- set ns.is_tool = false -%} {%- else -%} {%- set content = message["content"] -%} {%- if "</think>" in content -%} {%- set content = content.split("</think>")[-1] -%} {%- endif -%} {{- "<|Assistant|>" + content + "<|end▁of▁sentence|>" -}} {%- endif -%} {%- endif -%} {%- if message["role"] == "tool" -%} {%- set ns.is_tool = true -%} {%- if ns.is_output_first -%} {{- "<|tool▁outputs▁begin|><|tool▁output▁begin|>" + message["content"] + "<|tool▁output▁end|>" -}} {%- set ns.is_output_first = false -%} {%- else -%} {{- "<|tool▁output▁begin|>" + message["content"] + "<|tool▁output▁end|>" -}} {%- endif -%} {%- endif -%}{%- endfor -%}{%- if ns.is_tool -%} {{- "<|tool▁outputs▁end|>" -}}{%- endif -%}{%- if add_generation_prompt and not ns.is_tool -%} {{- "<|Assistant|><think>\n" -}}{%- endif -%}
其中变量的值在文件上面被定义:
按照上面的模版,则一条处理完毕后的提示词示例长这样:
<|begin▁of▁sentence|>系统提示
<|User|>用户输入
<|Assistant|> <|tool▁calls▁begin|> <|tool▁call▁begin|>function <|tool▁sep|>工具名称 ```json {"参数": "工具参数概括"} ``` <|tool▁call▁end|> <|tool▁calls▁end|><|end▁of▁sentence|>
<|tool▁outputs▁begin|> <|tool▁output▁begin|> 工具输出 <|tool▁output▁end|><|tool▁outputs▁end|>
<|User|>用户追问
<|Assistant|>助手回复<|end▁of▁sentence|>
利用chat template注入开源模型
由于开源模型的chat_template基本都已知,所以针对chat_template制作的POC对开源模型有额外的效果。
还是用deepseek做演示。经过测试,大多数chat template中的内容对商业部署的deepseek有效,也对以deepseek为基座二次训练的模型有效。
1 系统提示词注入
虽然deepseek中没有专门的系统提示词标签(这也导致大部分系统提示词标签类型的提示词注入对deepseek没什么用),但看chat template中可以看到规定的系统提示词在bos_token也就是<|begin▁of▁sentence|>后:
所以我们可以把payload放在这个标志后进行注入:
2 其它标签利用
这里的利用范围绝对比我写的要广,比如说——tool调用也是可以注入的。
可以用think标签伪装LLM思考过程。
这个payload我称之为天地同豆,欧耶。
同时,由于用户的对话会在上下文被压缩之前持续存在,即使LLM在第一段对话中识破了用户的意图——在后文中,用户曾使用的payload仍然会对LLM产生影响,比如这里:
References
[1] https://huggingface.co/deepseek-ai/DeepSeek-R1?chat_template=default&format=true: https://huggingface.co/deepseek-ai/DeepSeek-R1?chat_template=default&format=tru
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:银遁安全团队 AugustTheodor AugustTheodor《如何用chat_template通杀开源模型?》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论