接到一个授权范围内的测试任务后我习惯先打开目标主页面抓流量。这次的目标是一个部署在内网的在线文档管理系统功能覆盖文档管理、团队协作、全文检索、版本历史和评论体量不大但接口不少。第一轮浏览还没结束一个带搜索功能的接口就把我拉回了旧时光——参数拼接导致的SQL注入非常典型单引号直接触发报错。这个注入的价值很明确文档库里可能存在敏感信息能直接导出数据。但测试要求里有一条很显眼授权范围内要尽量说明漏洞影响尤其是能否从SQL注入进一步升级为命令执行。于是我把思路从“拿到数据”切换到“SQLi to RCE链路能不能打通”这才有了下文这堆思考。为了行文脱敏下文把这类在线文档管理系统统一称为“目标文档系统”不出现具体产品名称。这篇文章适合三类读者需要在实战里推进利用链的安全测试人员想从攻击视角反推加固方案的开发和安全运维以及正在学Web安全、想搞明白注入为什么能发展成RCE的学生。我会把每一步的判断依据和失败原因都写出来而不是只丢结论。1. 从SQL注入到命令执行这条攻击链到底怎么走1.1 文档系统为什么容易成为SQL注入的重灾区我在测试目标文档系统之前已经跟进过几款同类产品。整体感觉是文档管理这类系统的技术债务普遍偏高。产品迭代太快早期为了赶上线大量接口直接拼SQL后期功能堆上来老接口又没人敢动。搜索、筛选、排序、导出、批量操作用到的参数非常多每一个使用了字符串拼接的参数都是一个潜在注入点。更关键的是很多文档系统为了保证全文检索、附件管理和统计报表能正常工作数据库账号往往是高权限账号甚至直接使用root。高权限加可注入参数这条链路在起点上就已经具备了。从影响范围的角度看文档系统的特殊性还在数据资产本身。知识库、客户信息、内部设计文档、项目排期表都放在同一个库里一次注入就等于一次数据泄露。安全测试写报告时不能只写“存在SQL注入”要把数据规模、敏感字段、账号权限、能否再扩展都讲清楚。这也是为什么我在拿到注入点之后没有急着跑数据而是先想“这条注入到底能把影响推到多远”。1.2 整条利用链的思考框架从SQL注入到RCE并不是一条笔直的路中间要经过好几层判断。我习惯把链路拆成五环来看第一环确认注入点是否能稳定读取库内数据类型是字符型还是数值型回显是报错、布尔还是时间盲注。第二环判断数据库类型、版本、当前账号权限这是决定后续所有路线的前提。第三环探测数据库的文件读写能力包括 secure_file_priv、日志开关、plugin 目录等全局状态。第四环根据前两环的结果选择写Webshell、写日志、加载UDF或读源码后走二次利用。第五环验证命令执行或者拿到能继续推进的敏感数据将影响转化为报告结论。这五环不是每一环都必须走通。前面三环是决策依据最后一环才是RCE目标。很多人打不动是因为直接跳到第四环去各种试写文件语句忽略了权限和环境探测。MySQL的版本、账号的FILE权限、secure_file_priv的值直接决定了哪些路径可用、哪些路径从一开始就是死路。2. 注入点定位与权限探测核心前置排查2.1 优先翻哪些接口搜索、排序和批量操作我在目标文档系统上首先翻的是列表和搜索类接口。原因很简单这类接口参数多、逻辑杂经常为了支持模糊查询、排序、分页而直接拼SQL。keyword、search、order、sort、page、limit、dir、id、field这些参数都有嫌疑。常见的问题代码长这样$where where doc_title like % . $_GET[keyword] . %; $sql select * from doc where doc_status1 . $where;这段代码的问题非常直观keyword 参数的内容被直接拼进 SQL 字符串用户只要输入单引号就可能破坏原有的闭合条件。判断注入类型时不要一上来就丢给自动化工具。先手工试一个单引号观察页面是否报错、报错信息是否回显SQL片段再用布尔条件判断比如 keywordtest and 11 与 keywordtest and 12 的返回结果是否有差异。如果页面没有明显差异再考虑时间盲注但时间盲注尽量不要在生产环境长时间测试避免拖垮数据库。2.2 数据库权限与文件能力探测是关键检查点确认注入可控之后真正决定能否RCE的是数据库环境和权限。很多人在这一步偷懒直接跑数据结果浪费了大量时间在不可行的写文件路线上。我会按照一个固定清单去提取信息核心探测项如下探测项查询表达式示意返回结果的意义数据库版本SELECT version()决定可用函数、变量名和默认配置当前用户SELECT current_user()判断是否为 root 或高权限账号文件导入导出限制SELECT secure_file_privNULL、空字符串、指定目录对应完全不同的写文件能力日志开关与路径SELECT global.general_log, general_log_file判断 general_log 是否可开启、日志路径能否被改到Web目录插件目录SELECT plugin_dirUDF 需要写入的目录路径不可用时直接放弃这些查询在注入点里都可以通过 UNION 或报错注入拿到优先级很高。拿到结果之后要做一次过滤判断如果当前用户没有 FILE 权限所有依赖 LOAD_FILE 和 INTO OUTFILE 的路线全部划掉如果 secure_file_priv 是 NULL说明 MySQL 服务端禁止了文件导入导出INTO OUTFILE 写了也会报错如果有 SUPER 权限general_log 写文件这条路才真正可行。这一轮判断做完后面至少能省一两个小时。3. 四条从SQLi到RCE的可行路径3.1 路径一INTO OUTFILE 直接写Webshell优先判断但成功率低于预期这条路径的原理不复杂MySQL 的 SELECT ... INTO OUTFILE 可以把查询结果写入服务器本地文件。如果Web应用和数据库部署在同一台机器上又能猜到Web根目录的绝对路径我们就可以把一个包含服务端脚本语言标记的文件写到Web目录里然后通过URL访问触发命令执行。它的前置条件非常苛刻需要同时满足当前数据库账号具备 FILE 权限secure_file_priv 为空字符串或者指向Web目录能获取到Web应用的可写绝对路径Web目录本身对 MySQL 进程有写权限数据库与Web应用同机部署。判断顺序上先查 secure_file_priv再看路径。secure_file_priv 为 NULL 时后面所有步骤都不用试了。如果是空或指定目录可以尝试写入一个无恶意内容的探测文件比如 test_scan_2025.txt然后请求对应URL确认文件是否可达。确认可写并可达之后再在本地复现环境里验证完整的Webshell写入。实际利用语句形如SELECT ?php ... ? INTO OUTFILE /var/www/html/xxx.php这里不展开具体内容关键是理解写文件的本质。这条路径在真实环境里成功率往往低于预期原因很多MySQL 8.0 默认 secure_file_priv 为 NULL、云数据库默认禁止文件操作、容器化部署导致Web与数据库分离、站点路径经过重写后无法直接访问文件。所以它适合作为第一优先级判断但不适合作为唯一押注。3.2 路径二general_log 日志写文件高权限账号的捷径当账号能修改全局变量时general_log 写文件是比 INTO OUTFILE 更实用的路径。它的原理是利用 MySQL 的通用查询日志general_log 开启后所有执行的 SQL 语句都会被记录到日志文件里。如果把日志文件路径改到Web目录再执行一条带有脚本语言标记的SQL语句日志文件就变成了一个可以被Web容器解析的文件。这条路要能用有几个关键条件当前账号具备修改全局变量的权限通常是 SUPER 权限MySQL 8.0 里也可能需要 SYSTEM_VARIABLES_ADMIN知道Web目录的绝对路径MySQL 进程对目标目录有写权限数据库与Web同机部署。操作顺序大致是这样先确认能否修改全局变量执行 SET GLOBAL general_log ON然后把日志路径改到Web目录执行 SET GLOBAL general_log_file /var/www/html/temp_log.php接着执行一条包含可被Web解析标记的SQL让日志内容写进目标文件最后访问文件验证并在测试结束后恢复 original 配置、删除临时文件。需要特别提醒的是开启 general_log 会记录所有SQL日志量增长非常快对数据库性能和磁盘都是压力。安全测试中如果不小心把日志路径改到了Web目录线上日志可能直接暴露其他业务数据。这类操作风险高必须确认在授权范围内并且在测试结束后马上恢复。相比 INTO OUTFILE它的优势是不依赖 secure_file_priv但代价是需要更高权限而且日志内容容易不完整存在写入失败或解析失败的情况。3.3 路径三UDF 动态库加载老版本的高危选项UDF 的完整名称是用户自定义函数MySQL 允许通过加载动态库扩展函数。攻击者可以编译一个动态库文件上传到 plugin 目录再通过 CREATE FUNCTION 语句创建一个自定义函数调用这个函数执行系统命令。这条路径的限制条件更多我在真实项目中很少遇到能直接打通的。需要当前账号有 FILE 权限或 DUMPFILE 写入能力能上传二进制动态库文件能拿到 plugin_dir 路径并且该目录可写动态库必须匹配操作系统位数和 MySQL 版本MySQL 8.0 之后对 UDF 的加载限制变多成功率进一步下降。上传动态库通常会遇到 secure_file_priv 的限制所以最终还是要回到文件写入能力上。如果前面已经确认 INTO OUTFILE 不可用UDF 这条路的可行性也会大打折扣。不过在老版本、内网部署、管理不严格的系统里UDF 依然是经典的一条RCE路径。我的建议是把它放在路线清单里但不要抱太大指望尤其是目标使用云数据库或较新版本时。3.4 路径四先读源码再找二次漏洞最容易被低估当数据库层所有写文件路线都被堵死时SQL注入可能还能读取文件。LOAD_FILE 函数配合 FILE 权限可以读取数据库服务器本地的文本文件。即使不能写入Webshell读到应用配置文件也能带来决定性突破。我遇到过不少系统数据库注入点本身无法直接RCE但通过 LOAD_FILE 读到了站点配置文件、nginx配置、数据库连接配置。拿到数据库密码后可以直接登录数据库管理后台拿到第三方API密钥后可以控制附件存储拿到后台加密密钥后可以伪造登录态。这些都属于“SQLi to RCE”的变形——注入不直接给shell但给了一条通往后台上传功能、模板编辑功能的凭证链。这条路径特别适合两类场景一是 MySQL 当前账号有 FILE 权限但 secure_file_priv 限制写入的场景读文件通常不受写入目录限制二是数据库和Web分离、但数据库服务器上存在敏感配置文件的场景。读文件的目标一般按优先级排列Web站点配置文件、应用安装目录里的数据库配置文件、系统用户和定时任务文件、nginx和apache日志。拿到一条可行的凭证链往往比硬啃写文件更有效。4. 一次完整测试记录从有望到碰壁4.1 环境准备与工具选型为了把这套思路完整跑一遍我在本地搭了两套LNMP环境一套 MySQL 5.7一套 MySQL 8.0都部署了旧版目标文档系统。工具方面我主要用HTTP抓包工具来跟踪页面请求和参数用注入检测工具快速确认常规注入类型但关键判断全部用手工SQL。手工确认的原因很简单自动工具能证明“有漏洞”但解释不了“为什么这条路径可以、那条不行”而后面这个问题恰恰是RCE链路决策的核心。工具用途关键注意HTTP抓包工具看接口参数、响应细节适合分析请求结构不适合直接改包盲测注入检测工具快速判断注入类型和数据类型对复杂权限探测帮助有限容易误报数据库客户端在本地环境验证写文件行为避免将高危险语句放生产环境文件监控确认写入文件是否生效用于验证日志写文件和OUTFILE结果4.2 实测过程记录这次测试的过程记录如下我尽量把决策点写清楚。定位注入点很顺利。目标文档系统的搜索列表接口有一个 keyword 参数直接拼到了 SQL 查询里。手工加单引号后页面返回了数据库语法错误确认是字符型注入。注入检测工具随后确认了 UNION 注入可行但页面上只有一个标题字段有回显UNION 提取数据并不方便于是我改用报错注入来逐条取数。权限探测结果是第一个转折点。当前用户是 doc_user%权限只有 SELECT、INSERT、UPDATE、DELETE没有 FILE 权限也没有 SUPER 权限。这一步直接让 INTO OUTFILE、general_log、UDF 三条路全部进入不可行清单。为了严谨我还是执行了 secure_file_priv 查询返回结果在 MySQL 8.0 上是 NULL进一步确认写文件路线完全关闭。接下来尝试用 LOAD_FILE 读取应用配置文件但因为账号没有 FILE 权限语句直接报错。这时候我没有继续硬钻数据库权限提升而是转向了数据层面的二次利用。通过报错注入我在 information_schema 里翻到了应用配置表 doc_config里面存着后台管理员密码哈希和一组API密钥。利用这些数据登录后台后通过后台的模板编辑功能组合获得了命令执行。整体结论是SQL注入没有直接打通到RCE但通过注入拿到了关键凭证转到应用层之后才完成命令执行。这条链在真实环境里其实比直接写Webshell更常见因为现代数据库环境对文件写入的限制越来越严格而应用层往往还有更多入口可以组合利用。4.3 为什么真实环境经常断在“最后一公里”我在复盘这套流程时整理了导致 SQLi 到 RCE 中断的常见原因云数据库托管服务商通常禁用 FILE 权限secure_file_priv 设置为 NULL用户也没有 SUPER 权限容器化部署Web 和数据库分离即使数据库能写文件也写不到Web目录站点路径不可预测Nginx 做了 rewrite 或目录别名写在真实路径下的文件无法通过URL访问文件系统只读部分容器或加固系统对Web目录做了只读挂载安全防护WAF 拦截写文件语句数据库审计对 file 操作告警。断在最后一公里不等于漏洞没有影响。SQL注入本身、配置表暴露、凭证泄露每一项单独提出来都是高危问题。区别只在于影响是“数据泄露级别”还是“命令执行级别”报告里把这条利用链走到哪一步写清楚比一味追求RCE更实在。5. 防守方视角如何斩断SQLi到RCE的链条5.1 应用层断开注入入口防守方最该做的第一件事是在应用层把SQL注入的入口堵死。预编译语句是首选它能让SQL结构与参数分离用户输入无论怎么构造都不会改变查询逻辑。PHP里用 PDO 预处理Java 里用 PreparedStatement都是成熟方案。对于已有的老代码无法大规模重构时至少要把搜索、排序、分页等高风险参数做白名单校验阻止特殊字符进入SQL拼接片段。关闭详细错误回显同样重要。很多注入能快速被确认就是靠页面上直接暴露的SQL语法错误。开发阶段保留日志足够生产环境要把数据库错误信息隐藏在日志里页面只返回通用错误提示。5.2 数据库层斩断权限提升路径数据库侧的加固核心是“最小权限”。业务系统连接数据库账号绝不应该用 root 或具备 FILE、SUPER 权限的高权账号。建议至少做到业务账号只授予增删改查所需的表和字段权限不授予 FILE、SUPER、GRANT 等全局权限在 MySQL 配置中设置 secure_file_privNULL直接禁用文件导入导出日志文件必须存放在Web根目录之外防止日志写文件被利用定期升级MySQL版本修复旧版本中已知的权限绕过和UDF加载问题。这套操作对大多数团队来说并不复杂但收益极高。一旦业务账号没有 FILE 权限SQL注入即使存在也很难从数据库层翻到RCE。5.3 运行时检测与应急响应最后一步是在运行时发现攻击行为。WAF 可以拦截明显的注入特征比如报错信息、文件写入函数、sleep函数等。数据库审计日志要重点关注几次敏感操作SET GLOBAL、INTO OUTFILE、LOAD_FILE、CREATE FUNCTION这些语句的出现往往意味着攻击者正在尝试提权。文件完整性监控也可以发挥作用对Web目录的异常新文件、日志目录的路径变化做告警。应急响应预案里一旦发现可疑SQL应该立即封禁来源IP、回收数据库账号权限、检查Web目录新增文件并且结合前后日志做一次完整溯源。6. 个人经验与思考拿到 SQL 注入点之后我现在的第一反应不是急着跑数据而是先花二十分钟摸清权限边界。这套“环境判断优先于payload”的习惯帮我避开了无数次无效操作。数据库版本、当前用户、FILE权限、secure_file_priv、日志路径这五个点决定了大半个利用链的走向。另外要提醒一点不要低估数据层面的价值。很多情况下SQLi 到 RCE 不是一条直线而是先靠注入拿到一组关键凭证再转到应用层完成命令执行。把“信息收集—凭证获取—二次利用”当成一条完整的链来思考比死磕某个写文件语句要有用的多。最后给防守方一个优先级建议应用层做预编译、数据库层做最小权限这两板斧配合起来能斩断绝大多数 SQLi to RCE 的链路。我在实际项目里见过不少团队只上WAF、不管数据库账号权限结果还是被从一条老接口打穿。所以如果你时间有限先去查业务数据库账号的权限这个收益最直接。