近期「收割」了一批感兴趣的书,空闲时会阅读一下。不过很快就会发现,没法随时随地阅读它们,因为我总是想要记笔记。
顺带一提,我用的是 Calibre 统一管理书籍,手机(安卓)用的是 Moon+ Reader、iPad 用的是 KyBook 3。Calibre 可以启动内容服务器,同个局域网下让移动设备访问所有的书籍并下载,很是方便。Moon+ Reader 还可以直接通过 OPDS(Open Publication Distribution System,开放式出版发布系统)下载书籍。
这一篇阅读的是《黑客攻防技术宝典:Web 实战篇》(The Web Application Hacker's Handbook: Finding and Exploiting Security Flaws)。
《黑客攻防技术宝典:Web 实战篇》・在读笔记
近期「收割」了一批感兴趣的书,空闲时会阅读一下。不过很快就会发现,没法随时随地阅读它们,因为我总是想要记笔记。
顺带一提,我用的是 Calibre 统一管理书籍,手机(安卓)用的是 Moon+ Reader、iPad 用的是 KyBook 3。Calibre 可以启动内容服务器,同个局域网下让移动设备访问所有的书籍并下载,很是方便。Moon+ Reader 还可以直接通过 OPDS(Open Publication Distribution System,开放式出版发布系统)下载书籍。
这一篇阅读的是 《黑客攻防技术宝典:Web 实战篇》(The Web Application Hacker's Handbook: Finding and Exploiting Security Flaws)。
第一章
早期的万维网仅由 Web 站点构成。
Web 1.0 时代,Web 站点的唯一功能就是陈列信息,也就是静态文档。作为读者,你无法对该它作出任何影响,只能去看它。
到了 Web 2.0 时代,互联网变成了一个功能强大的交互式软件或服务平台。你可以注册登录、转账、发布照片等。
现在,网站几乎都是 Web 应用程序。安全问题变得至关重要且复杂。
作者表示,就算使用 SSL 或者遵循 PCI 标准扫描 Web 站点,也会遇到这些漏洞:
SSL(Secure Sockets Layer,安全套接层),会加密浏览器和网站服务器之间的通信通道。现在则有另一个名字:TLS(Transport Layer Security,传输层安全协议),不过本质是一样的。
PCI 通常指的是 PCI DSS(Payment Card Industry Data Security Standard,支付卡行业数据安全标准)。这是由 Visa、MasterCard 等主流信用卡公司联合制定和推行的一套安全规则和要求。它规定了任何处理、存储或传输信用卡信息的商家或机构 必须 遵守的安全标准。所有接受信用卡支付的线上电商、线下实体店、支付处理公司等,都必须遵守 PCI DSS 标准。定期进行的「PCI 扫描」就是为了检查商家的系统是否符合这些规定。
- 不完善的身份验证
- 登录环节有漏洞
- 不完善的访问控制
- 登录之后的权限管理混乱。例如普通用户可以查看其他用户的隐私数据(比如订单、私信),甚至可以执行管理员才能做的操作(比如删除别人的账户)
- SQL 注入
- 利用输入框,向网站的数据库下达恶意命令
- 跨站点脚本(XSS)
- 攻击者在网站上留下恶意代码,当其他用户访问时,这段代码会在他们的浏览器里执行
- 信息泄露
- 网站泄露了太多内部技术细节
- 跨站点请求伪造(CSRF)
- 强迫一个已登录的用户,在不知情的情况下,向网站发送一个他本不想发送的请求
这些问题里,最核心的问题是:服务器永远不能信任客户端、永远假设客户端发来的请求都是暗藏恶意的。
-
用户可以拦截和修改浏览器与服务器之间传递的所有数据,包括请求参数、Cookie 等。任何在浏览器端做的安全检查(比如「价格不能是负数」)都可以被轻松绕过
-
用户可以不按照开发者设计的流程操作。比如一个购物流程是「加入购物车 -> 填写地址 -> 支付」,用户可能直接跳到第 3 步,或者在一个步骤里提交 100 次请求
-
攻击者不一定用普通的浏览器(Chrome、Firefox)来访问你的网站。他们有各种专业工具(比如 Postman、Burp Suite),可以构造出浏览器绝对无法发出的、稀奇古怪的请求
这意味着 Web 安全的真正战场不在于加固传输通道(SSL 的工作),而在于服务器后端本身必须建立一套「不信任」和「严格审查」的机制。
Web 应用安全问题之所以如此普遍和顽固,就是因为整个开发生态系统存在一系列系统性缺陷,导致即使是善意的开发者也很容易写出不安全的代码。
-
不成熟的安全意识。这很简单理解:写网站的程序员通常不是安全专家。IT 安全人员可能很懂防火墙,但不懂 SQL 注入;Web 开发者可能每天忙于用第三方工具包拼凑出功能,但很少有时间去深入研究安全基础,甚至会错误地假设他们使用的框架是绝对安全的
-
独立开发。公司的网络基础设施通常是购买业界领先地成熟产品,按照标准流程部署,相对可靠。但 Web 应用几乎都是自己从头开发的。即使使用了第三方组件,也是通过新代码「粘合」起来的。这意味着每个网站都是一个独特的实验品,不像工业化标准产品那样经过千锤百炼
-
欺骗性的简化。现在有很多强大的开发框架(如 Liferay),提供了大量现成的代码组件。一个新手程序员可以在短时间内「拖拉拽」出一个功能强大的网站,但他根本不理解其内部运行机制和潜在风险。能用 ≠ 安全。这种模式导致大量使用相同框架的网站,一旦框架本身爆出漏洞,就会「一损俱损」
-
迅速发展的威胁形势。Web 攻击和防御是一个高速发展的领域。今天还被认为是安全的防御方法,明天可能就被新的攻击技巧破解。一个开发团队在项目开始时学习了最新的安全知识,但等他们花了一两年时间开发完产品并上线时,黑客们已经发明出了全新的攻击武器
-
资源与时间限制。大多数项目都有严格的预算和上线日期。功能开发和稳定性是首要任务。安全问题因为「看不见摸不着」,优先级通常被排在最后。很多时候,只有到项目快上线了,才找人匆匆忙忙做个「渗透测试」,这种测试往往只能发现最表面的问题
-
技术上强其所难。很多 Web 核心技术,例如 JavaScript,都诞生于互联网早期,当时的设计目标也都只是为了显示一些简单的静态文档。但现在,我们用这些技术去构建极其复杂的应用,意味着技术本身被「强行」用在了远超其最初设想的场景中,自然会暴露出各种无法预料的安全漏洞
-
对功能的需求不断增强。为了吸引用户,网站的功能变得越来越复杂。一个简单的登录功能,现在会被要求加上「密码恢复」、「用短信登录」、「记住我」等多种选项。每一个新增的功能,都为黑客提供了一个新的潜在攻击点。功能越多,需要保护的地方就越多,防御的难度呈指数级增长
在 Web 应用出现之前,安全防御的重点都放在网络边界上。过去的安全思路很简单:只要防火墙够强,公司的网络就是安全的。而 Web 应用要被用户使用,公司必须在「城墙」上开一个大门(允许 HTTP / HTTPS 流量通过)。而且这个应用为了实现功能,还必须连接到最核心的数据库和后台系统。既然防火墙已经被合法地打穿,那么真正的安全边界就不再是那道「旧城墙」,而是 Web 应用程序本身。应用处理用户输入的每一行代码,都成了新的前线和边界。
第二章
Web 应用程序大多采用以下三层安全机制来处理用户的访问,对应三个本质性的问题:
-
身份验证:你是谁?
确认一个未知访客的真实身份,并赋予他一个已知的「身份标签」。Web 应用程序会使用「登录」来进行这个操作。你可以输入用户名和密码,或者使用指纹、人脸识别、客户端证书等,系统会去数据库内核对、确认你的身份是合法用户。
-
会话管理:我如何记住你是谁?
在无状态的 HTTP 协议上,为用户创建一个持续的、被认可的「登录状态」,避免每次操作都需要重新验证身份的麻烦。当你登录成功后,服务器会给你一个令牌(Token),通常以 Cookie 或者其他形式保存在你的浏览器里。当你每次浏览网站的不同页面、提交不同请求时,浏览器都会自动带上这个令牌。服务器看到这个令牌,就会知道你刚才登录过了、不需要再登录一遍。
-
访问控制:你能做什么?
在确认了你的身份并维持了你的登录状态之后,对你的每一次具体操作进行权限检查,确保你只在被允许的范围内活动。例如,你作为一个普通用户只能查看自己的邮件、修改自己的个人资料,但是不能去访问管理员后台、查看其他人的邮件。因为系统的权限设置规定了,普通用户角色不能执行管理员操作,也不能访问不属于自己的数据。
这三层安全机制至关重要,任意一层存在缺陷都可能使攻击者自由访问:
- 如果身份验证有缺陷,那么攻击者可以伪造一张完全合法的通行证,并且拥有被冒充者的所有权限。之后的两层再怎么坚固也没有用
- 如果会话管理有缺陷,那么攻击者根本不需要去验证身份,他大可以直接去偷或者复制一个通行证、冒充别人在 Web 应用程序里随意执行别人的所有权限
- 如果访问控制有缺陷,那么攻击者只需要一个最低权限的账户,就能访问整个系统的所有功能和数据
身份验证
身份验证是「信任」的起点和基石。它是 Web 应用安全中最基础、最关键,却又最容易被误解和错误实现的部分。
身份验证不是一个非黑即白的东西,它有一个从弱到强的「安全光谱」。选择哪种方式,取决于应用的重要性和它所保护的数据价值。最基础的是用户名 + 密码,适用于绝大多数场景,也是最基础的防线;在「知道密码」的基础上,高价值应用 —— 例如电子银行 —— 会增加「你拥有什么(手机验证码、令牌)」或者「你是什么(指纹、面容 ID)」;政府、军队或者特定企业内部系统会将认证凭证与物理硬件绑定,虽然牺牲了便利性,但是大大提升了破解难度。
不过真正的攻击面并不是这些,而是身份验证机制的支持功能。例如,攻击者可以尝试在注册时,使用包含恶意代码的用户名来注册,从而影响到 Web 应用程序内部;修改密码时如果不要求输入旧密码的话,攻击者可以通过一些方式 —— 像 XSS—— 来劫持用户登陆后的会话、直接修改密码;最值得注意的是账户恢复功能,安全问题是可以通过社交网络查到的、重置令牌不够随机的话就会被预测到、验证码没有次数限制的话就会被暴力破解。
为什么看似简单的登录功能,却是重灾区?这些问题都需要我们考虑到:
- 当用户登录时,返回的错误信息是「用户名不存在」还是「密码错误」?如果是前者,攻击者就可以利用这个功能来枚举出所有的合法用户名,为后续的密码攻击做准备。一个安全的实现应该统一返回「用户名或密码错误」
- 身份验证流程往往涉及多个步骤和状态。例如,一个需要两步验证的流程,攻击者能否在完成第一步验证后,直接跳过第二步,访问一个需要登录才能访问的页面?这就是逻辑缺陷,攻击者利用了开发者没有周全考虑到的流程漏洞
- 身份验证机制最终还是要被人使用。系统是否强制用户设置强密码?用户是否会因为密码重用、导致在一个不安全的网站泄露密码,而那个密码又可以用来登录你的高安全应用?
会话管理
身份验证只是一瞬间的动作,而会话管理则是在用户登录后的整个访问期间,持续地、安全地记住用户身份的机制。这个机制的命脉完全系于一个叫做「令牌」的东西上,因此,令牌的安全性就等同于整个会话的安全性。
最传统、最常见的会话管理模型是使用 Session:用户登陆成功后,服务器会在自己内部创建一个「档案」,里面存放着该用户的所有状态信息,这个「档案」便是会话。服务器创建了「档案」之后,会生成一个独一无二、极其随机的字符串作为这个「档案」的编号,即令牌。服务器将这个令牌发给用户的浏览器,浏览器通常会将其存到 Cookie 里。之后,用户的浏览器每次向服务器发请求,都会自动带上存有这个令牌的 Cookie。服务器收到请求后,看到令牌,就拿着这个编号去自己的「档案室」里查找对应的「档案」,从而恢复用户的所有状态信息,并处理他们的请求。
问题在于,令牌是维持登录状态的唯一凭证,那么攻击者就不再需要用户的密码,他们只需要想办法得到用户的令牌。手段如下:
- 如果应用程序生成的会话令牌不够随机(例如,只是简单地基于时间戳或用户 ID 进行编码),攻击者就可以分析规律,预测出其他用户的合法令牌,从而冒充他们
- 窃取令牌的方式有很多:
- 攻击者在页面注入恶意脚本,读取并发送你的 Cookie
- 如果用户在不安全的公共 Wi-Fi 上访问一个没有使用 HTTPS 的网站,网络中的任何人都可以「偷听」到用户的网络流量,并从中窃取会话 Cookie
- 攻击者使用了用户未锁屏的电脑
- 攻击者先访问网站,让服务器生成一个会话令牌。然后他把这个令牌通过链接发给用户(例如
http://网站.后缀/?SESSIONID=...)。用户点击链接后,用自己的账号密码成功登录。如果此时服务器没有为用户 重新生成一个新的、更安全的会话令牌,那么攻击者预先知道的那个令牌就和用户成功登录的账户绑定了
还有其他模型,比方说无状态会话,最典型的代表便是 JWT(JSON Web Token),它的原理是将状态信息保存在客户端而非服务器上。这种方式服务器端开销小、易于扩展;缺点是一旦令牌发出,在它过期之前就很难让它失效。
访问控制
访问控制是安全体系的最后一道、也是最复杂的一道防线。它的复杂性决定了它必然是漏洞的重灾区,因此也成为了攻击者最有价值的攻击目标。
首先,访问控制的定位是「后置检查」,也就是说它仅关心「已登录的用户可以做什么」。为什么访问控制必然复杂?在一个真实的应用中,权限绝对不是简单的「是」或「否」。我们可以将访问控制分为几种类型,每一种都会引入大量复杂的逻辑:
-
垂直访问控制
基于角色和权限的控制。不同级别的用户能访问的功能完全不同。例如管理员可以访问
/admin/dashboard,而普通用户不能。当角色非常多(超级管理员、区域管理员、编辑、版主、普通用户……)且权限可以灵活组合时,逻辑会变得极其复杂,开发者很容易在某个角色的权限判断上出错。 -
水平访问控制
基于数据所有权的控制。同一角色的用户之间,只能访问属于自己的数据。
这是 最常见、最致命的漏洞来源。对于每一个需要操作数据的请求(如
GET /orders/101,POST /profiles/202),服务器都 必须 增加一步检查:「当前登录的用户是否是订单 101 的主人?」或「当前登录的用户是否就是用户 202 本人?」。 -
上下文相关的访问控制
权限还依赖于当前的状态或上下文。打个比方,在一个竞拍应用中,只有在竞拍进行中你才能出价。这种逻辑将状态机(State Machine)引入了权限判断,使得代码的判断分支急剧增多。
对于攻击者而言,开发者总是会犯错的。
开发者可能会错误假设:所有用户都会按照设计的流程来。因此在前端界面只给用户显示「编辑我的帖子」按钮,这样用户就不可能去编辑别人的帖子了。但是真实情况是攻击者根本不通过开发者的前端界面、直接捕获点击「编辑我的帖子」时发出的请求,例如 PUT /api/posts/123。然后手动修改请求中的帖子 ID,改成 PUT /api/posts/456(别人的帖子 ID),再重新发送。如果后端开发者相信了前端的「保护」,而忘记在后端 API 上进行所有权检查,那么这次攻击就成功了。这就是经典的 IDOR(Insecure Direct Object Reference,不安全的直接对象引用)漏洞。
也有可能,开发者给 10 个端口添加了检查,却在第 11 个端口忘记这么干了。
输入的多样性和不可信性
「用户输入」不单单是用户在表单里填写的内容。像我们先前说的,从安全的角度看,所有从客户端发送到服务器的数据,都是用户输入,都是 不可信 的。这包括:
- 表单字段(用户名、密码、搜索框、评论区)
- URL 参数(
/product?id=123)、URL 路径(/user/profile/123) - Cookies、User-Agent、Referer 等所有头部信息
- 服务器端生成,但客户端可修改的数据。这一点很重要,也很容易被开发者忽略。例如,一个隐藏表单字段
<input type="hidden" name="price" value="100">。开发者以为价格是服务器定的,但攻击者可以轻易地拦截请求,把100改成1
验证策略必须因地制宜。一些场景该严格就严格,一些则该宽松就宽松。
如何去防御呢?作者给出了五种防御策略:
-
拒绝已知的不良输入
创建黑名单,禁止任何包含列表中内容的数据通过。
这是最脆弱的方法。理由很简单,黑名单很容易被「绕过」:
- 大小写绕过:
SELECT->SeLeCt - 等价语句绕过:
or 1=1->or 2=2 - 编码绕过:
<->%3c - 插入无关字符绕过:
SELECT FROM->SELECT/*foo*/FROM
黑名单只能作为辅助,永远都不能作为主要的防御手段。它注定会失败,因为你永远无法穷举所有不好的东西。
- 大小写绕过:
-
接受已知的正常输入
创建白名单,只允许符合规则的数据通过。
在适用的情况下,这是最强大、最有效的方法。最适用于格式固定的数据,如手机号、邮政编码、产品 ID 等。但是不适用于需要丰富表达的自由文本,如文章评论。
-
净化
不拒绝,也不完全接受,而是对输入进行无害化处理。
一种非常实用和必要的妥协方案,主要处理那些无法使用白名单的自由文本。
较为经典的例子是防止 XSS 攻击。假设用户输入了
<script>alert(1)</script>。显示前你必须将其进行 HTML 编码,将其转换为无害的文本<script>alert(1)</script>。这样浏览器就只会把它当成普通文字显示出来,而不会执行它。 -
安全数据处理
与其检查输入是否「安全」,不如让处理过程本身对「危险」免疫。
最佳实践,从根本上解决特定问题。
经典案例为防止 SQL 注入。与其使用黑名单过滤
SELECT、--等字符,不如直接使用参数化查询。这种方法将用户输入的数据和 SQL 命令本身完全分离开来,即时用户输入了恶意的 SQL 代码,数据库也只会把它当成普通的字符串数据来处理,而不会执行它。 -
语法检查
检查输入的业务含义是否合法,而不仅仅是格式。
比方说,用户修改购物车商品数量,他提交的请求
product_id=101&quantity=-1。从语法上来看,-1是一个有效的整数,但从业务逻辑上看,数量不能为负数。另一个例子是,用户 A 尝试访问order_id=102,虽然102是合法的订单号格式,但这个订单属于用户 B,所以这也是非法的。
很多初学者会犯的错误是,在应用的最外层写一个「超级过滤器」,试图把所有「坏」东西都挡在门外。作者解释道,这种一刀切的入口过滤是无效的。数据在系统内部流动和转换时会产生新的风险,所以必须在每个处理环节进行针对性的验证。就像是一个对 HTML 页面完全安全的字符串 O'Malley,对于 SQL 查询来说却是十分危险的存在(因为有单引号)。
我们应当应用边界确认 —— 数据流传的每一个组件,都应将其输入视为不可信,并执行针对自己上下文的验证。
- 用户 -> Web 服务器(外部边界):进行基础格式校验(白名单)
- Web 服务器 -> 数据库(信任边界):进行 SQL 转义或使用参数化查询
- Web 服务器 -> 后端 API(信任边界):进行 XML / JSON 编码
- Web 服务器 -> 用户浏览器(信任边界):进行 HTML 编码
每个组件只负责保护自己,不信任任何上游组件传来的数据。
我学习爬虫的时候,教师提到一个很有意思的点:「爬虫是永无止境的,因为会有反爬虫、反反爬虫、反反反爬虫……」我认为这跟开发者和攻击者的关系很类似(但是性质恶劣许多),为了可以「反反反爬虫」,我们需要了解攻击者如何「反反爬虫」我们。高级攻击者会利用我们防御逻辑的漏洞来绕过防御。
- 当我们的防御措施是「删除」或者「替换」某个恶意字符串时,攻击者可以通过构造嵌套的恶意代码,利用你「删除」这个动作来重新组合出攻击代码
- 攻击者提交编码后的恶意输入(
%27 OR 1=1)。因为该字符串没有单引号,所以我们的过滤器会放行。这个字符串接着会被传递给后端的 SQL 查询模块。SQL 模块在执行前,会进行 URL 解码,%27变回了'。最终,恶意代码' OR 1=1被执行,攻击成功。这是利用了规范化这一概念
处理攻击者
一个成熟的应用程序,必须建立在「我一定会被攻击」这个残酷的假设之上。因此,安全机制不仅仅是建立一套静态的防御工事,更要设计一套动态的应对体系。当攻击发生时,应用需要能够以受控的方式去处理、响应、记录并挫败攻击者。
-
处理错误
无论开发者多小心,意料之外的错误总会发生,尤其是在遭受恶意攻击时。一个关键的防御机制,就是优雅且安全地处理这些错误。
在生产环境中,应用程序绝对不应该在返回给用户的页面中,显示任何系统生成的原始错误消息、堆栈跟踪(stack trace)或调试信息。这些看似无害的调试信息,在攻击者眼中却是藏宝图。它们会轻易地暴露你所使用的技术栈(如
Microsoft SQL Server Error)、数据库结构(如column 'username' not found)、服务器的内部文件路径等等。在某些极端情况下,有缺陷的错误处理机制甚至可能成为攻击者直接窃取数据的渠道。正确的做法是「对用户,要含糊其辞;对日志,要事无巨细」:
- 当发生无法处理的错误时,应该向用户展示一个统一的、友好的通用错误页面(例如,「抱歉,服务暂时出现问题,请稍后再试」)。这个页面不包含任何技术细节。
- 在服务器的后台,应用程序应该使用
try-catch块等机制捕获这个错误的 全部 详细信息,包括完整的堆栈跟踪、请求参数等,并将其完整地记录到内部的日志文件中,以便开发者进行分析和修复。
无法预料的错误往往是应用程序防御体系中存在未知漏洞的信号。安全地处理它们,既能防止信息泄露,又能为我们修复漏洞提供线索。
-
维护审计日志
如果说错误日志记录的是应用的「意外」,那么审计日志记录的就是应用的「日常」。当安全事件(不幸地)发生后,审计日志就扮演着「黑匣子」的角色,是调查和追溯攻击过程的唯一依据。
一个有效的审计日志,应该能帮助我们回答以下问题:攻击者利用了什么漏洞?他访问了哪些不该访问的数据?他执行了哪些未授权的操作?他是谁(IP 地址、账户)?
在任何注重安全的应用程序中,日志应当记录所有重要的安全相关事件,这至少应该包括:
- 所有身份验证事件,无论是成功还是失败的登录、密码修改等
- 关键的业务交易,比如信用卡支付、转账操作
- 被访问控制机制明确拒绝的越权访问尝试
- 任何包含了明显攻击字符串(如
or 1=1--)的恶意请求
但这里存在一个悖论:日志本身也可能成为一个安全风险。因为日志中可能记录了用户的敏感请求参数,甚至是会话令牌。如果日志文件保护不当,被攻击者读取到,就可能直接导致整个应用被攻破。因此,日志文件必须受到严格的访问控制保护,甚至最好是存放在一个独立的、高度安全的日志服务器上,并确保其不可被篡改(例如写入到一次性写入的介质中)。
-
向管理员发出警报
警报系统的目标是在攻击者得手之前或得手之时,就立即通知管理员,以便采取实时干预。
一个好的警报系统,关键在于在「准确性」和「信噪比」之间找到平衡。如果误报太多,管理员很快就会对警报变得麻木,从而忽略真正的攻击。因此,警报应该聚焦于那些高度可疑或明确恶意的「反常事件」,例如:
- 某个 IP 地址或用户在短时间内发起了海量的请求,这很可能是自动化扫描或暴力破解的迹象
- 某个账户的交易金额或频率远超正常范围
- 请求中包含了已知的攻击字符串
- 请求修改了普通用户根本不应该能修改的数据,比如隐藏表单字段中的商品价格或用户 ID
书中提出了一个非常深刻的观点:外部的防火墙(WAF)无法理解你应用的业务逻辑。一个 WAF 也许能识别出请求参数里的
SELECT * FROM是 SQL 注入,但它绝对无法判断GET /orders/102对于当前登录的用户A来说,是一次越权访问。但你的应用程序本身最清楚这些业务逻辑。因此,最有效的警报,是与应用自身的访问控制机制紧密结合。当访问控制逻辑拒绝了一个越权请求时,这不仅是一次成功的防御,更是一个极高质量的、几乎零误报的警报信号。
-
应对攻击
作者承认,没有任何应用是完美无缺的,总会有一些漏洞潜藏其中。既然无法做到完美,那我们至少可以增加攻击者发现和利用这些漏洞的成本。
现实中的攻击,很少有一击即中的。攻击者需要系统性地进行大量探测,发送成百上千个精心构造的请求,来试探应用是否存在各种漏洞。这个过程就像在黑暗中摸索一扇没有上锁的门。
应对攻击的机制,就是在他摸索的过程中,不断地给他制造障碍。例如:
- 当应用识别到某个 IP 或用户正在进行系统性探测时,可以故意放慢对他们请求的响应速度,从 100 毫秒延长到 5 秒。这会极大地拖慢自动化扫描工具的效率
- 当检测到明确的攻击行为时,可以立即终止当前用户的会话,强制他重新登录。这可以打断攻击者的攻击链
- 在更极端的情况下,可以暂时封禁攻击者的 IP 地址或用户账户
这些措施虽然无法阻挡最顶尖、最有耐心的攻击者,但它们可以有效地劝退大量的「脚本小子」和随意攻击者。
管理应用程序
应用程序的管理功能(如用户管理、角色分配、系统配置)是为管理员等高权限用户设计的。由于这些功能常常与普通用户功能构建在同一个 Web 应用内,它们就成为了一个极具吸引力且高价值的攻击面。攻击者的核心目标是利用管理功能实现权限提升。
攻击者有四条主要路径来攻破管理功能:
- 通过暴力破解、凭证填充等手段直接获取管理员账户的登录权限。因为管理员账户数量少,目标明确,所以是常见的攻击起点
- 应用程序的普通用户功能和管理功能之间,可能存在访问控制缺陷。攻击者以低权限用户身份登录后,尝试直接访问管理功能的 URL(例如
GET /admin/create-user)。如果后端没有对该端点进行严格的权限校验,攻击者就能执行未授权的管理操作 - 攻击者在普通用户功能中(如个人资料、评论区)提交包含恶意 JavaScript 的输入。当管理员在管理后台查看这些被污染的数据时,恶意脚本会在管理员的浏览器中执行。这能导致管理员的会话令牌被窃取,或者让攻击者以管理员身份执行任意操作
- 管理功能本身被设计用来执行危险操作,例如文件上传 / 下载、执行系统诊断命令等。这些功能内部如果存在漏洞(如路径遍历、命令注入),其破坏力会远超普通功能中的同类漏洞,可能直接导致服务器被完全控制
管理功能之所以频繁出现漏洞,根本原因在于它们往往 未经充分的安全测试。测试人员可能没有获得管理员权限,或者开发者错误地假设「管理员是可信的」,从而在开发管理界面时放松了安全警惕。这种假设是致命的,因为攻击者的目标正是利用其他漏洞,让自己成为那个「可信的」管理员。
第三章
HTTP
跳过。
解刨 Web 功能
一个 Web 应用并非铁板一块,而是由众多服务端和客户端技术共同协作构成的复杂生态系统。攻击的第一步,就是理解这个生态的组成,识别出每一个可能被利用的组件。
作者介绍了(当时)几种主流的后端技术栈和前端技术,并介绍了它们各自的特点和风险。但因为这本书有点年头了,也就不说了。
编码方案
Web 应用最初被设计为处理基于文本的数据,为了能够安全地传输特殊字符(如 &、=、空格)和二进制数据,人们设计了多种编码方案。然而,攻击者恰恰利用了这些编码机制,将恶意的攻击载荷「伪装」成看似无害的数据,从而穿透防御措施。
-
URL 编码
URL 编码是处理 Web 请求时最常见、最基础的编码形式。它的存在是为了将任意字符转换为符合 URL 规范的、可安全传输的 US - ASCII 字符集。其规则很简单,即以
%符号加上字符的两位十六进制 ASCII 码。从攻击者的角度看,URL 编码是绕过简单输入过滤器的第一手段。假设一个 Web 应用防火墙或一段简单的代码,试图通过查找字符串
<script>来阻止 XSS 攻击。攻击者可以提交编码后的载荷%3cscript%3e。对于这个简单的过滤器来说,它看到的只是一串不包含<script>的普通字符,于是便会放行。然而,当这个请求到达 Web 服务器的应用程序容器(如 Tomcat、IIS)时,容器会自动执行第一步规范化 ——URL 解码,将%3c和%3e还原为<和>,从而使恶意的脚本得以在后续处理中生效。 -
Unicode 编码
Unicode 编码的出现是为了表示世界上所有的字符,其编码方式(如 UTF-8)远比 ASCII 复杂。这也为攻击者提供了更广阔的绕过空间。
攻击者可以利用 Unicode 中多种方式表示同一个或相似字符的特性来绕过防御。例如,一个过滤器可能严格禁止了半角的
<字符,但攻击者可能会尝试提交一个全角的<字符。如果后端的某个组件在处理过程中,会「智能」地将全角字符转换为半角字符(一种规范化),那么这次攻击就成功绕过了过滤器。Unicode 编码为这类基于「字符混淆」的攻击提供了丰富的素材。 -
HTML 编码
与前两者主要被攻击者利用不同,HTML 编码(或称 HTML 实体编码)主要是作为一种 核心的防御机制。它的目的是在 HTML 页面中安全地显示用户提交的内容,确保浏览器将其作为纯文本对待,而不是作为 HTML 标签来解析。
当应用需要显示用户提交的
<script>字符串时,它必须先将其进行 HTML 编码,转换为<script>。浏览器在渲染时,看到<就会把它当作一个普通的「小于号」字符显示出来,而不是一个标签的开始。这是防御存储型和反射型 XSS 攻击最关键的一步。当然,攻击者也会反过来利用它。他们会尝试各种不常见的编码组合(如混合使用十进制
<和十六进制<)来探测应用后端的解码和过滤逻辑是否存在漏洞,试图找到一种能绕过过滤器,但最终仍能被浏览器正确解码并执行的组合。 -
Base64 与十六进制编码
开发者有时会使用 Base64 或十六进制编码来传输二进制数据,或者试图「隐藏」一些不想让普通用户直接看到的参数。
对于攻击者来说,最重要的一点是:编码不是加密。它是一种完全公开、可逆的转换。在渗透测试中,任何看起来像 Base64(由
A-Z, a-z, 0-9, +, /组成,有时以=结尾)或十六进制(由0-9, a-f组成)的字符串,都应该被立即解码。这些被「隐藏」的参数背后,往往是未经严格校验的敏感数据或控制应用逻辑的关键变量,是极佳的攻击入口。 -
远程和序列化框架
现代富客户端应用为了简化开发,常常使用一些远程框架(如 Java 序列化对象,Flex AMF 等)。这些框架会自动将客户端对象「序列化」成一种特定的、通常是二进制的格式,然后传输给服务器,服务器再将其「反序列化」还原为对象。
这个过程对开发者来说是透明的,但对攻击者来说,这是一个巨大的、不透明的黑盒。这种自定义的、复杂的编码格式无法被传统的 Web 防火墙或过滤器所理解。如果攻击者能够逆向分析出这种序列化格式的规则,他就可以在客户端构造一个恶意的对象。当服务器端在执行 反序列化 操作,将这段数据还原成对象时,就可能触发严重的安全漏洞,最坏的情况下可导致 远程代码执行。这便是所谓的「不安全反序列化」漏洞,是近年来危害最大的一类漏洞。
第四章
先前的三个章节,作者讲完了理论知识,而该章节开始我们要讲如何实践。
应用程序映射
在发起真正的攻击之前,攻击者会对目标进行全面、细致的侦查。
Web 抓取或爬虫是内容枚举的起点。自动化工具从一个初始 URL 开始,解析页面中的链接和表单,然后递归地访问这些新发现的资源,直到无法找到新内容为止。
这种自动化方法速度快,能快速发现通过标准链接连接的可见内容。一个值得注意的技巧是检查 robots.txt 文件。这个文件本意是告诉搜索引擎哪些目录不应被索引,但有时开发者会错误地将一些敏感的、不希望被公开访问的后台路径写在里面,这无异于给攻击者提供了一份藏宝图。
然而,单纯依赖自动化抓取是远远不够的,并且存在诸多严重限制。它常常无法处理由复杂 JavaScript 动态生成的导航菜单;无法解析 Flash 等客户端插件中的链接;在遇到需要特定格式输入的多阶段表单时会卡住;并且可能因为将所有功能都指向同一个 URL(仅通过 POST 参数区分)而遗漏大量功能。更危险的是,一个无差别的自动化爬虫可能会意外触发「删除用户」之类的危险功能,对系统造成实际损害。
为了克服自动化抓取的种种弊端,更专业、更有效的方法是 用户导向的抓取。
这种方法的核心是,测试人员像一个真实用户一样,通过浏览器正常地浏览和使用应用程序的各项功能。与此同时,所有的网络流量都通过一个 拦截代理工具(如 Burp Suite)进行。这个代理工具会「被动地」记录下浏览器发出的每一个请求和收到的每一个响应,并根据这些真实流量来构建应用程序的站点地图。
这种人机结合的方式拥有巨大优势。由人来处理复杂的登录、表单提交流程和 JavaScript 交互,保证了数据的有效性和会話的持续性;由工具来负责记录、分析和发现响应中隐藏的链接(例如 HTML 注释中的链接),保证了侦察的全面性。测试人员还可以有选择地避免触发那些危险的管理功能。这是进行应用程序映射的首选和标准方法。
很多时候,应用程序中最脆弱的部分,恰恰是那些没有在任何地方被链接的「隐藏」或「遗忘」的功能。发现这些内容是侦察阶段的关键:
- 通过 猜测 来发现内容。攻击者使用一个包含成千上万个常用目录和文件名的字典,通过自动化工具(如 Burp Intruder)向服务器发起大量请求(例如
GET /admin,GET /backup,GET /test.php)。分析服务器的响应码是这里的关键:200 OK意味着资源存在;403 Forbidden或401 Unauthorized同样意味着资源存在,只是被访问控制所保护;302 Found(重定向到登录页)也表明这是一个需要登录才能访问的有效资源。通过这种方式,可以暴力破解出未被链接的管理后台或测试页面。 - 通过公开信息推测。这项工作依赖于分析和推理。首先,通过观察已知页面的命名规律(如
ViewUser.aspx,AddUser.aspx),可以推测可能存在EditUser.aspx或DeleteUser.aspx。其次,通过观察 URL 中的数字规律(如id=101,id=102),可以尝试遍历 ID 来发现更多数据。最后,审查所有客户端代码(HTML 注释、JavaScript 脚本)也常常能发现被开发者遗忘的后端 API 接口或敏感信息。 - 利用公共资源。有时候,一个功能现在虽然没有链接,但过去可能有。搜索引擎(如 Google Hacking)和 Web 档案(如 Wayback Machine)是两个强大的信息来源。通过高级搜索语法,可以找到已被搜索引擎索引但已从官网移除的页面、包含敏感信息的文档、或者第三方网站上讨论该应用的帖子,这些都可能暴露隐藏的功能或过去的漏洞。
- 利用 Web 服务器。Web 服务器本身也可能存在配置错误或漏洞,例如开启了「目录列表」功能,这会直接将某个目录下的所有文件暴露给攻击者。此外,许多常用的第三方组件(如 phpMyAdmin)都有默认的安装路径,攻击者可以通过扫描这些默认路径来判断应用是否使用了这些可能存在已知漏洞的组件。
最后,我们需要转变一个思维定式:不要只把应用看作是一系列「页面」的集合,而要把它看作是一系列「功能」的集合。在一些现代应用中,所有操作可能都指向同一个 URL(如 /api/service),而具体执行何种功能则是由 POST 请求中的一个参数(如 action=deleteUser)决定的。在这种情况下,传统的 URL 路径蛮力破解会完全失效,攻击者必须转而对 action 这个 参数的值 进行破解。
与此类似,应用中还可能存在 隐藏参数。例如,一个正常的请求 GET /shop,如果被攻击者加上了 ?debug=true 这个参数,服务器的响应中就可能包含详细的调试信息,甚至关闭某些安全检查。发现这些隐藏参数同样需要通过自动化工具,使用常用参数名列表进行系统性的探测。
分析应用程序
在完成了初步的内容枚举后,下一步是进行应用程序分析。这个阶段的目标,是把前一阶段收集到的海量原始信息,转化为有价值的、可用于攻击的情报。这个过程要求我们理解应用的核心功能、行为模式和技术选型,从而识别出它暴露给外界的关键受攻击面。
-
我们需要系统性地盘点所有数据进入应用程序的渠道,因为每一个入口点都是一个潜在的攻击向量。
用户输入不仅限于 URL 查询字符串和 POST 请求体中的参数。一个完整的入口点清单还必须包括 URL 的文件路径本身,尤其是在采用 REST 风格的应用中(例如
/shop/product/123里的123就是一个参数)。同样,每一个 HTTP Cookie 也都是由用户提交的、可被篡改的输入。更隐蔽的入口点存在于其他 HTTP 请求头中。许多应用会记录或处理
User-Agent和Referer头。例如,应用可能会根据User-Agent头来判断用户是来自 PC 还是移动设备,从而加载不同的界面和代码逻辑。攻击者可以通过伪造这个头部,来探索为移动端设计的、可能防御措施较弱的功能。一些位于代理或负载均衡器后的应用,会信任X-Forwarded-For头来获取客户端的真实 IP 地址,如果攻击者可以控制这个头,就可能绕过基于 IP 的访问控制,甚至在日志中注入恶意数据。最后,还存在一些带外通道。例如,一个 Web 邮件应用,它不仅通过 HTTP 接收输入,还通过 SMTP 接收邮件。攻击者可以通过发送一封精心构造的恶意邮件,来攻击 Web 界面显示邮件时的解析逻辑。
-
通过分析应用的各种行为特征,我们通常可以精确地识别出其后端的技术栈。
最直接的线索来自于服务器返回的明确信息,例如
Server响应头(Apache/2.4.41)、HTML 源码中的注释(``)、特定的 Cookie 名称(JSESSIONID表明是 Java,PHPSESSID表明是 PHP)以及 URL 中的文件扩展名(.aspx表明是 ASP.NET,.jsp表明是 Java)。即使开发者隐藏了这些明确的标识,攻击者依然可以通过 HTTP 指纹识别 技术,通过分析服务器对各种畸形或不常见请求的独特响应方式,来推断其类型。
识别出具体的技术和版本号后,攻击者就可以去公开的漏洞库(如 CVE)中查找该版本是否存在已知的、可被利用的漏洞。尤其重要的是识别 第三方代码组件。现代应用很少从零开始构建,大多会集成各种开源或商业的第三方组件(如论坛、购物车、富文本编辑器等)。这些组件被广泛使用,一旦其中一个被发现存在漏洞,影响范围会非常广。攻击者常常通过组件的特定文件名、路径或行为特征来识别它们,然后进行针对性的攻击。
-
通过仔细审查 HTTP 请求的细节,可以推断出后端可能正在发生什么:
- 当你看到一个 URL 参数
&OrderBy=name,你应该立刻想到,后端极有可能正在执行一个 SQL 查询,并且这个参数的值被直接拼接到了ORDER BY子句中,这是一个潜在的 SQL 注入点 - 当你看到参数
?template=user_profile.tpl,你应该怀疑,后端可能正在根据这个参数的值去读取一个模板文件并包含到响应中,这是一个潜在的路径遍历漏洞,或许可以读取到服务器上的任意文件 - 当你看到一个隐藏的表单字段
<input type="hidden" name="edit" value="false">,你必须问自己:如果我把它改成true会发生什么?这可能就是一个用于开启编辑模式的、未经验证的访问控制开关
此外,通过观察应用处理数据的一致性,可以推断其内部逻辑。如果在 A 功能点,应用会将你的输入
>编码为>,那么可以推断在 B 功能点,它可能也采用了相同的编码逻辑。反之,如果发现应用中某个部分的 UI 风格或参数命名与其他部分格格不入,那它很可能是一个后来集成的、防护措施可能也与主应用不一致的薄弱环节。 - 当你看到一个 URL 参数
-
分析的最终产出,是一份清晰的、可执行的 攻击计划。这个计划将应用的功能模块与潜在的漏洞类型一一对应起来。整个过程的逻辑链条是:
- 枚举内容与功能:我看到了一个登录页面(
/auth/Login) - 分析功能:这是一个身份验证功能,涉及数据库交互,处理用户名和密码输入,并且设置会话状态
- 映射到攻击向量:因此,我应该针对这个功能,系统性地测试以下漏洞:
- 用户名枚举
- 暴力破解和弱密码
- SQL 注入
- 会话令牌的可预测性
对应用的每一个功能点(文件上传、社交分享、数据查询等)都重复这个「枚举 -> 分析 -> 映射」的过程,就构成了一张完整的受攻击面地图。这份地图会告诉你,应该把时间和精力优先投入到哪些最有可能存在严重漏洞、或者一旦攻破回报最高的功能上去。
- 枚举内容与功能:我看到了一个登录页面(
第五章
Web 应用有时会向客户端发送一些数据,并期望客户端在后续请求中原封不动地将这些数据再发送回来。开发者这样做,通常是为了减轻服务器的会话管理负担、简化在负载均衡环境下的部署,或是为了方便地集成第三方组件。然而,这背后隐藏着一个根本性的安全问题:任何被发送到客户端的数据,无论其形式如何,都完全处于用户的控制之下。 认为这些数据不会被篡改,是导致大量漏洞产生的根源。
最直接的数据传送方式,就是将数据以明文形式放在请求的各个部分:
- 隐藏表单字段(
<input type="hidden">)。这是最典型的例子。开发者常常将商品价格、用户 ID 等信息放在隐藏字段中。他们错误的假设是,「隐藏」即意味着「不可修改」。然而,攻击者只需查看 HTML 源码就能看到这些字段,并通过浏览器开发者工具或拦截代理工具在提交表单前,轻易地将价格499修改为1,甚至是-499 - HTTP Cookie。与隐藏字段类似,开发者认为 Cookie 由浏览器管理,普通用户无法直接编辑。但对于使用拦截代理工具的攻击者来说,修改 Cookie 的值与修改任何其他请求参数毫无区别。一个名为
DiscountAgreed=25的 Cookie,可以被轻易地修改为DiscountAgreed=99,从而获取未授权的折扣 - 开发者有时会将控制逻辑的参数放在 URL 中,例如用在一个图片加载链接或是一个 POST 表单的
action属性里,认为用户不会注意到或无法修改。这种想法是错误的,因为拦截代理可以看到并修改 HTTP 请求的每一个字节,包括 URL 的任何部分 - 有些应用会检查
Referer头来强制用户遵循一个特定的操作流程(例如,必须从第二步页面才能访问第三步页面)。这是一种极其脆弱的控制方式,因为Referer头和User-Agent一样,只是 HTTP 请求中的一个普通头部,攻击者可以通过代理工具将其修改为任意期望的值,从而「欺骗」服务器,让它以为请求来自一个合法的来源。认为 HTTP 头部比 URL 参数更难篡改,是一种需要被纠正的错误观念
当开发者意识到明文传输数据不安全时,他们可能会采取下一步措施:对数据进行 模糊处理,使其变得难以阅读,例如进行某种自定义编码或加密。这种方式确实提高了攻击的门槛,但远非无法破解。对于这类数据,攻击者有多种应对策略:
-
逆向工程:如果模糊算法比较简单,攻击者可以通过分析多个输入和输出样本,尝试逆向破解出算法本身
-
寻找「预言机」:在应用的其他地方,可能存在某个功能,可以将用户输入的任意明文,处理成模糊后的密文。攻击者可以利用这个功能作为「预言机」,来生成自己想要的任意攻击载荷的加密版本
「预言机」是密码学和信息安全领域一个非常重要的概念:应用程序中存在的、可以被攻击者利用的某个功能