第4篇 | Web应用的“内患”:注入、越权与命令执行漏洞详解

《网络安全的攻防启示录》· 第一篇章:破壁之术 · 4

堡垒往往从内部被攻破,而Web应用最大的威胁,就藏在它对待自己人的方式里。

想象一下这个场景:你在一个电商网站下单后,在浏览器地址栏里看到这样一个链接:

https://www.example.com/order.php?order_id=12345

你出于好奇,或者不小心手滑,把 12345 改成了 12346 ,按下回车。令人惊讶的是,屏幕上竟然显示了另一个用户的完整订单信息——姓名、电话、地址、购买商品一览无余。

越权访问漏洞(水平越权)场景示意图

图注: 直观展示攻击者仅通过修改ID就访问到了不属于自己的数据。

这不是虚构的故事,而是每天都在互联网上真实发生的越权访问(Broken Access Control漏洞。更令人担忧的是,这仅仅是Web应用内患的冰山一角。

作为在软件行业深耕30多年的从业者,我见证了太多这样的案例。有一次在为某金融机构做架构咨询时,他们的开发团队负责人自信地告诉我:我们系统很安全,用了最新防火墙、做了加密传输。但在后续的代码抽查中,我们发现了一个简单的SQL拼接操作——一个可能导致整个客户数据库被泄露的小疏忽

真正的安全威胁,往往不在外部的高墙之外,而在内部那些被我们视为理所当然的代码逻辑中。

今天,我们就来深入剖析Web应用的三大内患注入(Injection越权(Broken Access Control)与命令执行(Command Execution

这些漏洞之所以如此普遍且危险,不是 因为它们的技术有多么高深莫测,而是 因为它们直接利用了应用系统对自己人”——也就是正常用户、正常输入和正常业务流程——过度信任


一、 信任的崩塌:为什么这些内患如此普遍?

在深入具体漏洞之前,我们必须先探讨一个根本问题:为什么这些早在20年前就被反复提及的基础漏洞,在2024年的今天仍然大面积存在?


思考小札:

在我早期的项目管理经历中,团队最关心的指标是功能是否按时实现Time to Market)和性能是否达标。安全,往往是排在最后,甚至被牺牲掉的那个。

我常常在评审会上听到这样的声音:这个功能先上,安全问题后面再迭代优化。”“这是内网系统,没必要搞这么复杂。这种功能优先内部可信的开发文化,正是滋生这些内患漏洞的完美温床。


1.1 开发者的信任预设

大多数开发者在编码时,心中都有一个理想用户模型:用户会按照我预期的方式使用功能。

  • 当用户输入用户名时,他输入的就是用户名(zhangsan);
  • 当用户访问订单页面时,他只会看自己的订单(order_id=12345)。

这种预设本身没问题,问题在于,代码逻辑完全建立在了这个善意预设之上,没有任何防范恶意的措施。

1.2 复杂性的保护色

现代Web开发框架(如Spring Boot, Django, Ruby on Rails)和ORM(对象关系映射,如MyBatis, Hibernate)的复杂性,反而给这些基础问题提供了保护色

我们很容易产生一种错误的安全感——“框架已经帮我处理了安全问题

但真相是:框架能提供保护,但不能替代开发者的安全意识。如果你错误地使用了框架(比如在ORM中仍然拼接字符串),或者在框架之外(比如直接调用系统命令)进行了危险操作,漏洞依然会产生。

不是 框架没用,而是 我们误解了框架的责任边界,把本该自己承担的验证责任,甩给了框架。


二、 SQL注入(SQL Injection):当用户篡改了数据库指令

SQL注入是最经典、最具破坏力的内患之一。

它的原理: 攻击者通过构造特殊的输入,欺骗应用程序,使其将恶意代码作为合法的SQL查询语句的一部分,提交给数据库执行。

核心问题: 程序未能区分数据代码

2.1 案例:经典的登录绕过

一个极其不安全的登录验证SQL可能是这样拼接的:

Java

// 错误示范:通过字符串拼接构造SQL

String sql = "SELECT * FROM users WHERE username = '" + userName +

             "' AND password = '" + password + "'";

// ... 执行查询 ...

  • 正常用户: 输入 zhangsan123456
    • 执行的SQL:SELECT * FROM users WHERE username = 'zhangsan' AND password = '123456'
  • 攻击者: 在用户名框输入 admin' -- (注意:admin、单引号、空格、两个减号、空格),密码任意。
    • 执行的SQL:SELECT * FROM users WHERE username = 'admin' -- ' AND password = '...'

SQL语法中,-- (双减号) 行注释符。这意味着,从--开始到行尾的所有内容(包括密码验证)都被数据库忽略了!查询语句瞬间变成了:

SELECT * FROM users WHERE username = 'admin'

如果admin用户存在,攻击者就无需密码,直接登录了系统。

2.2 演进:从拖库Shell”

SQL注入不是 只能绕过登录。它早已演化出更高级的形态:

  • 联合查询注入 (UNION Attack): 利用UNION关键字,获取其他表的数据,比如整个users表或products表,这就是常说的“拖库”。
  • 盲注 (Blind SQLi): 当页面不返回错误信息时,攻击者通过构造逻辑判断(布尔盲注)或利用时间延迟(时间盲注),像猜谜一样一点点“猜”出数据库里的内容。
  • 堆叠查询 (Stacked Queries): 允许攻击者在一条查询中执行多条SQL语句(用分号;隔开),实现更复杂的攻击,如修改数据、删除表格,甚至在特定数据库(如SQL Server的xp_cmdshell)上执行操作系统命令(“拿Shell”)。

SQL注入攻击原理示意图

图注: 展示了攻击者的恶意输入(红色部分)如何污染了原始的SQL查询语义,导致逻辑判断失效。

2.3 防御启示:信任的隔离舱

如何防御SQL注入?不是 黑名单过滤(比如禁止'--),因为攻击者总能找到绕过的方法(如编码)。

唯一的、根本性的解决方案是:参数化查询 (Parameterized Queries),也叫预编译 (Prepared Statements)

Java

// 正确示范:使用参数化查询

// 1. SQL语句使用 '?' 作为占位符

String sql = "SELECT * FROM users WHERE username = ? AND password = ?";

// 2. 预编译SQL(数据库已"理解"SQL的结构)

PreparedStatement stmt = connection.prepareStatement(sql);

// 3. 传入参数(此时传入的内容只会被当作"数据",绝不会被当作"代码"

stmt.setString(1, userName);

stmt.setString(2, password);

// ... 执行查询 ...

这种方式,就像是为用户输入的数据准备了一个隔离舱无论攻击者输入' OR '1'='1 还是别的什么,数据库都只会把它当作一个普通字符串(数据)去匹配,而绝不会将其作为SQL指令(代码)来执行。

核心启示: 永远不要信任用户输入,必须在代码层面将数据指令彻底分离。


三、 越权漏洞:看不见的权限边界

如果说SQL注入是骗过了系统,那么越权漏洞就是绕过了系统。系统本身可能在正常工作,只是用户在自己合法的身份下,访问到了本不该属于他的资源。

核心问题: 系统验证了你是谁(认证),却没验证你能干什么(授权),或者验证得不彻底。

3.1 水平越权 vs. 垂直越权

权限攻击可以分为水平权限攻击和垂直权限攻击,越权主要分两种:

  • 水平越权 (Horizontal Privilege Escalation):
    • 定义: 攻击者访问与自己同级别权限的其他用户的资源。
    • 场景: 正如开篇的例子,用户A通过修改URL中的order_id,看到了用户B的订单。
  • 垂直越权 (Vertical Privilege Escalation):
    • 定义: 攻击者(如普通用户)执行了更高级别权限(如管理员)的操作。
    • 场景: 用户A只是普通用户,但他通过直接访问URL .../admin/delete_user.php,竟然成功打开了管理员才能访问的删除用户页面。

3.2 越权的根源:相信前端会守规矩

越权漏洞的产生,往往源于一个致命的信任预设:信任前端(或客户端)会守规矩


思考小札:

在我们评审某保险公司的理赔系统时,发现了一个经典问题:普通理赔员的界面上没有”“审批通过这个按钮(前端代码将其隐藏了)。但如果理赔员通过抓包工具,直接伪造一个包含审批动作的请求发送到后端API,系统居然就通过了!

为什么?因为后端API的开发者默认只有管理员才能看到点击那个按钮,所以他干脆就没在后端API上加权限验证

这就是典型的依赖前端控制权限导致的垂直越权。攻击者不是 攻破了系统,而是 简单地绕过了你隐藏起来的门


3.3 防御启示:以会话为准,而非请求

防御越权漏洞的核心原则:后端必须对所有资源的每一次访问,都进行严格的权限校验。

1、绝不信任用户请求中的ID

Java

// 错误示范:信任请求中的ID

String orderId = request.getParameter("order_id");

Order order = getOrderById(orderId); // 此时可能获取了别人的订单

// 正确示范:从会话中获取用户身份,并校验资源归属

String currentUserId = session.getUserId(); // 1. 从会话获取当前登录用户ID

Order order = getOrderById(orderId);

// 2. 关键校验:检查订单是否属于当前用户

if (order.getUserId().equals(currentUserId)) {

    // ... 允许访问 ...

} else {

    // ... 拒绝访问(越权)...

}

2、统一的权限拦截: 将权限校验逻辑做成统一的拦截器(Interceptor)或过滤器(Filter,在所有敏感API执行前自动运行,而不是在每个业务方法里手动写一遍,避免遗漏。

核心启示: 用户的认证状态(登录了)和授权状态(能干啥)是两回事。必须在服务端,以当前登录的身份Session/Token)为唯一信任源,去校验其对目标资源操作权限


四、 命令执行(RCE):从Web应用到操作系统的致命跳跃

这是Web“内患最危险的一种,因为它允许攻击者直接从Web应用跨界到服务器操作系统,执行任意命令。

核心问题: 应用程序将用户输入作为操作系统命令的一部分来执行。

4.1 场景:危险的便捷功能

很多Web应用确实需要执行系统命令,比如:

  • 调用 ping 命令测试网络连通性。
  • 调用 ImageMagick 库处理用户上传的图片(如缩放、裁剪)。
  • 调用系统脚本执行备份或数据处理。

问题不是执行命令而是拼接命令。

4.2 案例:图片处理的分号惨案

假设一个图片处理功能,后端代码如下:

Java

// 错误示范:拼接系统命令

String command = "convert " + userUploadedImagePath + " -resize 800x600 output.jpg";

Runtime.getRuntime().exec(command);

  • 正常用户: 上传路径 '/tmp/image.jpg'
    • 执行命令:convert /tmp/image.jpg -resize 800x600 output.jpg (功能正常)
  • 攻击者: 构造一个恶意路径 '; rm -rf / ;'
    • 执行命令:convert '; rm -rf / ;' -resize 800x600 output.jpg

Linux/Unix中,分号:命令分隔符。这条命令被系统解析成了三条:

  1. convert ' ' (执行失败或空操作)
  2. rm -rf / ( 删库跑路! )
  3. ' ' -resize 800x600 output.jpg (执行失败)

攻击者通过一个简单的分号,实现了从应用层到系统层的致命一跃

4.3 防御启示:隔离参数指令

防御命令执行漏洞的思路,与防御SQL注入如出一辙:隔离数据与指令

1、首选:避免调用系统命令。 尽量使用语言内置的库(如Java的图像处理库)来完成功能,而不是调用外部程序。

2、次选:使用参数数组(永不拼接)。

Java

// 正确示范:使用参数数组

// 指令和参数被严格分离

String[] command = {"convert", userUploadedImagePath, "-resize", "800x600", "output.jpg"};

Runtime.getRuntime().exec(command);

这样,即使用户输入 '; rm -rf / ;',它也只会被当作一个无效的文件名参数传递给convert命令,而绝不会被操作系统当作新的指令来执行。

3、最后:严格的白名单验证。 如果必须拼接(极不推荐),也要对输入进行白名单验证(例如,只允许字母、数字、下划线和点),而不是黑名单过滤(禁止;|&等)。

命令执行漏洞防御体系

图注: 展示了防御命令执行的理想流程,核心在于验证、安全构造和隔离。


五、 核心启示:安全编码的本质,是信任管理

当我们剖析完这三大内患,会发现它们都指向同一个根源:开发者在代码中赋予了不该有的信任

  • SQL注入: 信任用户输入的内容是“纯数据”,而不是“SQL指令”。
  • 越权访问: 信任用户提交的标识符(如order_id)代表了他有权访问的资源。
  • 命令执行: 信任用户输入的内容是“合法参数”,而不是“系统命令”。

因此,防御这些内患的统一心法,就是在代码层面践行零信任原则

不是 这个输入安全吗?而是 我凭什么信任这个输入?

  1. 永不信任输入(Input Validation): 视所有输入(URL参数、POST数据、HTTP头、Cookie...)为潜在威胁,进行严格的白名单验证和规范化。
  2. 分离代码与数据(Separation): 严禁使用任何形式的字符串拼接来构造查询或命令。必须使用参数化查询或参数数组。
  3. 最小权限(Least Privilege): Web应用连接数据库的账户,绝不能是rootsa,应是仅有增删改查权限的受限账户。执行系统调用的进程,应在容器或沙箱中以最低权限运行。
  4. 以服务端为准(Server-Side Authority): 绝不依赖前端(JavaScript、HTML隐藏)进行安全控制。所有权限判断必须在后端,基于可信的会话(Session)信息进行。

结语:从亡羊补牢未雨绸缪

Web应用的这些内患,本质上都是系统内部的信任管理失当。

作为从业三十余年的老兵,我见过太多亡羊补牢的紧急修复——半夜上线补丁、回滚数据、安抚客户。也参与过一些未雨绸缪的成功实践——在设计阶段就引入威胁建模,在CI/CD流水线中集成安全扫描。

我可以负责任地说,两者的成本差异是指数级的。

真正的安全,不是 发现漏洞后再去修补,而是 在设计之初、在敲下第一行代码时,就建立起不信任的思维模式。

下次当你编写处理用户输入的代码时,不妨多问自己一句:如果这个用户是恶意的,他会如何滥用我这个设计?

这个简单的思维转变,或许就是你的应用从脆弱走向坚固的关键一步。

下篇预告:

了解了Web应用的内患后,我们将在下一篇探讨《第5 | Web应用的外邪XSSCSRFSSRF漏洞详解》,看看攻击者如何从外部利用应用的正常功能(如浏览器、其他服务器)发起攻击。敬请期待。


互动时刻:

  • 本篇小结: 我们深入剖析了Web应用的三大“内患”——SQL注入、越权访问和命令执行。我们发现,这些漏洞的根源不是 技术高深,而是 对用户输入的“过度信任”。防御的核心在于零信任编码:彻底分离数据与指令,并在服务端严格校验权限。
  • 思考/讨论: 回顾您当前或最近参与的项目,是否也存在文中提到的(如SQL拼接、依赖前端做权限控制等)“信任预设”问题?您认为在团队中推动“安全左移”(在开发早期就介入安全)最大的阻力是什么?欢迎分享您的想法和计划。
  • 术语小词典 (更新)
    • SQL注入 (SQLi): 通过在输入中插入恶意SQL代码,改变数据库查询逻辑的攻击。
    • 越权 (BAC - Broken Access Control): 用户访问了其本不该有权访问的资源或功能。
    • 命令执行 (Command Execution): 攻击者通过Web应用在服务器上执行了非预期的操作系统命令。
    • 参数化查询 (Parameterized Query): 一种安全编码实践,将SQL指令模板与数据参数分离发送给数据库,从根本上防止SQL注入。
    • CI/CD (Continuous Integration/Continuous Deployment): 持续集成/持续部署,现代软件开发的自动化流程。
    • SAST (Static Application Security Testing): 静态应用安全测试,通过扫描源代码来发现漏洞。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老马爱知

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值