
1. 为什么要在意“这个请求是不是 curl 管道到 bash”如果你是开源软件的维护者官网提供了一键安装脚本写法通常是这样curl -fsSL https://example.com/install.sh | sh每天有大量请求打到服务器上日志里能看到无数条对install.sh的 GET 请求。但你想过一个问题吗这些请求到底来自浏览器里点开链接的人还是来自真实终端里执行curl | bash的人前者可能只是围观后者才是真正把脚本跑起来的安装者。这两类行为对你的安装量统计、脚本分发渠道分析、异常滥用排查意义完全不一样。另一个更常见的场景是做安全分析。假设你在运营一个对外提供脚本或接口的服务某天发现某个 IP 在不停拉取脚本你想先判断它是不是自动化脚本在批量调用。这时候一个类似Curlbash_detect的 PoC 项目能给你一些思路不依赖客户端上报、不修改用户命令只从 HTTP 请求本身的特征去推断“这次请求是不是来自 curl并且很可能被管道交给了 bash”。先给结论这种检测有价值但它只能做到概率性判断不能做到身份认证。HTTP 请求头完全由客户端控制任何一个有经验的人都能伪造。真正能用好它的地方是统计分析、安全研究、流量分类而不是作为访问控制唯一依据。这篇文章会从curl | bash请求的基本特征讲起然后给出一个最小可运行的服务端检测实现最后讨论哪些特征可靠、哪些特征容易被绕过以及生产环境里怎么用才稳妥。2. curl | bash 模式的本质与检测价值2.1 什么是 curl | bashcurl | bash是 UNIX/Linux 世界里非常常见的一种软件安装方式。它的执行过程是curl从指定 URL 下载脚本内容到标准输出管道符号|把这段内容交给bashbash 把标准输入当作脚本执行。curl -fsSL https://example.com/install.sh | sh一行命令完成“下载 执行”两个动作对使用者来说省事对提供方来说可以减少“下载后手动运行”的跳转步骤。许多知名的安装脚本、工具链引导程序都采用这种模式。2.2 服务端能看到什么从服务端视角看一次curl -fsSL https://example.com/install.sh | sh本质上是一个 HTTP GET 请求。服务端能拿到的是请求方法、路径、协议版本请求头User-Agent、Accept、Accept-Encoding、Connection、Accept-Language 等客户端 IP、端口、TLS 指纹如果走 HTTPS请求时间、是否下载完整、后续是否继续请求其他资源。服务端看不到的是这个响应体是被bash执行了还是被curl -o写入磁盘或是被more显示在终端。管道行为发生在客户端本地服务端无法直接观察。因此所有“检测 curl|bash”的方案本质上都是通过请求头特征与行为特征做推断。2.3 检测这件事的价值流量统计区分“人工访问”和“脚本安装”让安装量统计更准确。安全研究分析自动化脚本是否在批量拉取接口或安装包。风险感知当脚本被第三方平台直接curl|bash引用时维护者可以感知到异常扩散。但也要清醒它只回答“请求看起来像不像 curl|bash”不回答“这个请求是不是安全的”。3. 服务端看到的请求差异要让检测逻辑可落地先要理解 curl 默认请求头和浏览器默认请求头的差异。3.1 curl 默认请求头使用 curl 访问一个 HTTP 地址时默认请求头很少。以下是常见内容不同 curl 版本略有差异GET /install.sh HTTP/1.1 Host: example.com User-Agent: curl/8.5.0 Accept: */*curl 默认不会主动发送Accept-LanguageAccept-Encoding7.x 部分版本会发Accept-Encoding: identitySec-Fetch-*Sec-CH-UA-*Cookie除非显式指定或已有本地 cookie 文件Referer也就是说curl 的请求头非常“朴素”。3.2 现代浏览器默认请求头现代 Chrome/Firefox 请求一个静态脚本文件时请求头里通常包含GET /install.sh HTTP/1.1 Host: example.com User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Accept: */* Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q0.9,en;q0.8 Sec-Fetch-Dest: script Sec-Fetch-Mode: no-cors Sec-Fetch-Site: same-origin Sec-CH-UA: Chromium;v124, ...浏览器会发一整套“安全获取上下文”的头同时常常携带 Cookie、Referer、Sec-CH-UA。3.3 一张表看清差异特征curl 默认curl | bash现代浏览器User-Agent形如 curl/8.x.x同左Mozilla/5.0 ...Accept*/**/**/*或带具体 mimeAccept-Encoding无或 identity同左gzip, deflate, brAccept-Language通常无通常无zh-CN, zh;q0.9...Sec-Fetch-*无无有Sec-CH-UA无无有Cookie无除非指定无通常有Connection依版本同左keep-alive从这张表可以提炼出第一版检测规则。需要说明curl和curl|bash在 HTTP 请求头层面几乎没有区别。因为管道是客户端本地的操作curl 在发请求时并不知道响应体会被 bash 执行。所以真正可靠的“curl|bash 检测”必须结合请求路径、请求头组合、访问频率、IP 环境等多维信息而不是单纯看字符串。4. 环境准备与最小 HTTP 捕获服务为了验证检测思路可以先搭建一个最小 HTTP 服务把所有请求头打印出来然后分别用 curl、curl|bash、浏览器访问观察差异。4.1 环境要求任意 Linux 发行版或 macOSWindows 也可以用 WSL 或 Git BashPython 3.6用于快速起一个 HTTP 服务curl 命令一个浏览器用于对照访问。版本不需要刻意固定。本文的重点是通用思路而不是某个版本特有的头字段。4.2 用 Python 标准库写一个请求头捕获服务不需要引入任何第三方依赖直接用http.server。文件路径capture_server.py#!/usr/bin/env python3 # 文件路径capture_server.py from http.server import BaseHTTPRequestHandler, HTTPServer import sys class CaptureHandler(BaseHTTPRequestHandler): def _handle_request(self): print( * 60) print(f请求行{self.command} {self.path} {self.request_version}) print(请求头) for key, value in self.headers.items(): print(f {key}: {value}) content_length self.headers.get(Content-Length) if content_length: body self.rfile.read(int(content_length)) print(请求体, body.decode(utf-8, errorsreplace)) print( * 60) self.send_response(200) self.send_header(Content-Type, text/plain; charsetutf-8) self.end_headers() self.wfile.write(bdetect server ok\n) do_GET _handle_request do_POST _handle_request do_HEAD _handle_request if __name__ __main__: port int(sys.argv[1]) if len(sys.argv) 1 else 8080 server HTTPServer((0.0.0.0, port), CaptureHandler) print(fcapture server running at http://0.0.0.0:{port}) server.serve_forever()启动python3 capture_server.py 8080保持这个终端开着它会实时打印请求头和请求体。4.3 三种方式分别访问打开另一个终端分别执行# 方式一普通 curl curl http://127.0.0.1:8080/install.sh # 方式二curl 管道到 bash脚本内容只是无害的 echo curl http://127.0.0.1:8080/install.sh | bash # 方式三浏览器直接访问 # 在浏览器地址栏输入 http://127.0.0.1:8080/install.sh观察 capture_server.py 的输出会看到curl 的请求头很少UA 是curl/8.x.xcurl | bash的请求头与普通 curl 基本一致浏览器的请求头非常多包含Sec-Fetch-*、Sec-CH-UA、Accept-Language等。这一步能直观建立“请求头差异”的体感。5. 实现 curlbash_detect 检测逻辑掌握了差异之后就可以用 Python 实现一个 PoC 检测接口。下面的代码不是唯一实现而是一个可运行的示例。它的作用是接收 HTTP 请求根据一组预设规则打分最后输出“疑似 curl|bash”或“疑似浏览器”。5.1 特征与规则设计先定义一组容易被机器判断的特征User-Agent 是否以curl/开头Accept 是否为*/*是否缺少Accept-Languagecurl 默认通常不发送是否缺少Sec-Fetch-*现代浏览器几乎总会发送是否缺少Sec-CH-UAChromium 系浏览器会发送是否缺少Cookie多数真实用户首次访问可能没有 Cookie所以这条权重低。计分规则可以简单设计为命中一个特征加一定分数最后按总分判断类别。5.2 Flask 版本检测服务使用 Flask 便于后续扩展成接口也方便项目集成。文件路径curlbash_detect/app.py# 文件路径curlbash_detect/app.py from flask import Flask, request, jsonify app Flask(__name__) def detect_from_request(): headers request.headers ua headers.get(User-Agent, ) accept headers.get(Accept, ) has_accept_language Accept-Language in headers has_sec_fetch any(key.lower().startswith(sec-fetch) for key in headers.keys()) has_sec_ch_ua Sec-CH-UA in headers has_cookie Cookie in headers score 0 reasons [] if ua.lower().startswith(curl/): score 2 reasons.append(User-Agent 显示为 curl) if accept.strip() */*: score 1 reasons.append(Accept 为 */*) if not has_accept_language: score 1 reasons.append(缺少 Accept-Language) if not has_sec_fetch: score 2 reasons.append(缺少 Sec-Fetch-* 安全上下文头) if not has_sec_ch_ua: score 1 reasons.append(缺少 Sec-CH-UA) if not has_cookie: # 首次访问可能确实没有 Cookie权重压低 reasons.append(未携带 Cookie仅记录不计分) threshold 4 result curl-like if score threshold else browser-like return { result: result, score: score, threshold: threshold, reasons: reasons, user_agent: ua, } app.route(/detect, methods[GET, POST]) def detect(): data detect_from_request() return jsonify(data) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)这段代码的核心逻辑score从 0 开始累加UA 是 curl 且命中“缺少 Sec-Fetch”时分数会快速上升最终结果是一个概率意义上的分类不是绝对判断。5.3 启动检测服务安装 Flask 后启动pip install flask python3 curlbash_detect/app.py访问接口curl -s http://127.0.0.1:5000/detect预期输出示例{ result: curl-like, score: 7, threshold: 4, reasons: [ User-Agent 显示为 curl, Accept 为 */*, 缺少 Accept-Language, 缺少 Sec-Fetch-* 安全上下文头, 缺少 Sec-CH-UA, 未携带 Cookie仅记录不计分 ], user_agent: curl/8.5.0 }浏览器访问http://127.0.0.1:5000/detect时结果通常会是browser-like。5.4 模拟 curl|bash 的检测由于curl和curl|bash在服务端看到的请求头几乎一致可以直接用同一个 curl 命令测试curl -s http://127.0.0.1:5000/detect | bash这里 bash 执行的是 JSON 文本可能会报语法错误但服务端检测逻辑已经工作了。想更贴近真实场景可以先准备一个无害脚本文件路径detect_test.sh#!/usr/bin/env bash # 本脚本只用于演示不做任何有害操作 echo detect_test.sh executed然后执行curl -s http://127.0.0.1:5000/detect.sh | bash服务端返回的 JSON 会显示为curl-like。这说明服务端只能检测到“curl 风格请求”无法真正判断管道另一头是不是 bash。这也是这个项目定位为 PoC 的原因。6. 运行验证与结果分析6.1 验证步骤启动 capture_server.py分别用 curl、curl|bash、浏览器访问确认请求头差异启动 Flask 检测服务用 curl 访问/detect用浏览器访问/detect对比 JSON 输出用curl -s http://127.0.0.1:5000/detect | bash验证管道模式下服务端的判断结果尝试用curl -A Mozilla/5.0 ...篡改 UA观察检测结果变化。6.2 预期结果对照调用方式resultscore说明curl /detectcurl-like7 左右命中 curl 典型特征curl /detect | bashcurl-like7 左右与普通 curl 几乎一致浏览器访问 /detectbrowser-like0 或很低命中浏览器安全上下文头curl -A Mozilla/5.0... /detect可能是 browser-like视其余头而定说明 UA 可伪造6.3 结果解读这个 PoC 能说明两件事现代浏览器的请求头特征比较明显把它们从 curl 请求中区分出来并不难但要区分“curl 下载文件”和“curl|bash 执行脚本”非常困难因为服务端看不到客户端本地发生的事。因此任何宣称能百分百识别curl|bash的方案都应该谨慎看待。合理用途是当请求同时满足“UA 是 curl”“缺少浏览器特征”“路径指向 .sh”“来自陌生 IP”等条件时将其标记为可疑再由人工或更细粒度的策略复核。7. 常见问题与排查思路7.1 为什么 curl 和 curl|bash 在服务端看起来一样这是最常见的问题。因为管道操作发生在 shell 层curl 进程本身只负责 HTTP 交互。curl 发起请求时并不知道 stdout 会被 bash 读取。HTTP 协议层面不存在“我要执行脚本”这种声明所以服务端无法直接看到。7.2 User-Agent 是 curl 就一定是 curl|bash 吗不一定。可能是curl -o install.sh下载文件后手动执行CI/CD 脚本中调用 curl 拉取资源攻击者故意伪造普通浏览器 UA。所以不能只用 UA 做判断。7.3 为什么浏览器请求头里有大量 Sec-Fetch 字段Sec-Fetch-*是 Fetch Metadata 规范的一部分浏览器会自动附加在请求中用于描述请求的上下文比如目标资源类型、导航模式、站点来源。curl 不承担浏览器职责所以默认不发送。正因如此它是区分“浏览器”和“非浏览器”的强信号。7.4 检测结果为什么不稳定请求头可能被中间层修改CDN 可能追加X-Forwarded-For、Via等头企业代理可能改写 User-AgentWAF 可能删除或新增某些头。因此生产环境做检测时建议在源站直连测试或先确认中间层不会修改关键头。7.5 响应状态码与 Content-Type 是否影响检测不影响。检测只关心请求头响应如何返回是另一回事。但真实场景里install.sh的 Content-Type 通常被配置为text/plain或application/octet-stream这和浏览器是否直接渲染有关与“是不是 curl|bash”无关。7.6 能不能根据请求是否带 Referer 判断不能。curl 默认不带 Referer浏览器在地址栏直接输入 URL 时通常也不带 Referer。如果页面里通过script src引用脚本会带 Referer但这属于页面加载场景不是curl|bash场景。排查思路统一用表格问题现象可能原因排查方式解决方案Flask 服务启动失败Flask 未安装查看终端报错pip install flask检测结果都是 curl-like中间代理修改了请求头抓包对比源站与代理层请求在源站直连测试排除代理干扰浏览器结果也显示 curl-like浏览器插件修改 UA无痕模式访问确认测试环境干净无法区分 curl 和 curl|bash管道行为在客户端发生接受 PoC 的能力边界结合访问频率、文件路径等其他信号8. 检测的边界、安全注意事项与最佳实践8.1 检测边界这个检测逻辑面向的对象是“默认配置的 curl”和“默认配置的现代浏览器”。一旦客户端显式定制请求头检测就可能失效。典型绕过方式curl -A Mozilla/5.0 (Windows NT 10.0; Win64; x64) \ -H Accept-Language: zh-CN,zh;q0.9 \ -H Sec-Fetch-Dest: script \ -H Sec-Fetch-Mode: no-cors \ -H Sec-Fetch-Site: same-origin \ http://127.0.0.1:5000/detect因此项目定位为 Proof of Concept 是合适的。它用于展示“请求头组合如何泄露客户端类型”但不应该作为强安全边界。8.2 安全注意事项如果你是服务端维护者想用类似思路做访问统计或安全分析请遵守以下原则只在有合法授权的系统和流量上做分析日志中避免记录不必要的敏感头如 Cookie 原文不要把检测结果直接用于封禁用户除非有明确的策略和人工复核机制在测试环境验证规则再考虑是否上生产保持规则可配置、可回滚避免一次误判导致大面积访问异常。8.3 工程实践建议特征权重化不要只是“有/无”二值判断可以把不同特征按置信度加权加入 URL 与来源分析只有请求目标是.sh脚本、.run安装包时才启用高敏感规则结合访问节奏curl|bash 通常是一次性请求不会像浏览器那样继续请求 JS/CSS/图片定期更新特征库curl 和浏览器版本都在演进UA 格式和安全头策略会变化在代码里做好日志脱敏IP 可以匿名化Cookie 一律不记录不要把检测接口暴露为公开接口这类检测接口本身会被爬虫扫描建议限制来源 IP 或加访问凭证。8.4 如果要扩展成生产级方案可以在 Flask 示例基础上增加按 IP 维度聚合判断某个 IP 是否批量拉取脚本引入 TLS 指纹如 JA3/JA4识别“真实 curl”与“伪造 UA 的请求”与 CDN 日志、业务日志联动做多源交叉验证对命中多次的可疑来源自动进入观察名单而不是直接封禁。9. 总结与后续学习方向Curlbash_detect这个 PoC 最有价值的点不是提供了一个可以照抄的检测接口而是提醒开发者一件事HTTP 请求头是客户端可以任意捏造的任何基于请求头的“识别”都是概率推断。如果你想继续深入可以从这几个方向入手阅读 curl 文档或抓包了解不同版本 curl 的默认请求头和历史变化学习 Fetch Metadata 规范理解浏览器为什么发送Sec-Fetch-*研究 TLS 指纹了解如何在网络层辅助识别客户端类型用同样的思路搭一个请求日志分析脚本对比源站日志和 CDN 日志。建议你先在本地把 capture_server 和 detect 接口跑起来亲手用 curl、浏览器、curl|bash 三种方式各访问一次直观感受“服务端看到的世界”和“客户端实际发生的事”之间的差距。这一步比记住任何规则都有用。把这个小实践跑通之后你会对 HTTP 请求的可信边界有更清晰的判断这对日常接口开发、爬虫对抗、安全分析都会有帮助。