前言 2010 年 IBM X-Force 趋势及风险报告显示,近年来 Web 应用安全漏洞是大的安全危险。随着对 Web 应用安全的重视,很多研发团队逐步将应用渗透扫描测试引入到软件开发生命周期中。敏捷开发过程中,为了降低代码重构带来的安全风险,很多团队期望能在各个 Sprint 后期自动化执行现有全部的扫描脚本,以保证当前 Sprint 没有影响前面 Sprint 的交付成果。这听起来非常理想,但实践过程中往往难度会比较大。 首先我们来简单了解 Web 应用渗透测试的基本原理。渗透测试往往通过手工探索或者自动爬取的方式分析出被测试应用中的 URL,然后基于已知安全漏洞的原理修改 HTTP 请求,将修改后的请求发送到被测试应用服务器,根据 Web 应用服务器的 HTTP 响应信息判断漏洞是否存在。因此,简单来说,渗透测试是一种“录放”式的测试。为了保证测试的准确性,我们必须要保证所录制的 HTTP 请求的有效性。这正是扫描脚本重用所面临的大挑战。 正如我们所熟知,很多 Web 应用为了防止重复提交会在页面中增加会话令牌,如果测试工具重新播放这个 URL,它请求主体内的令牌已过期,服务器会拒绝这类请求而跳转到错误页面。很多Web应用为了防止恶意用户修改页面的 Cookie 或者隐藏参数等,还会采用 Hash 值校验的方式。服务器端在向客户端发送 Cookie 或者隐藏参数的时候,会基于这些参数生成一个 Hash 值,将 Hash 值也作为参数发送到客户端。服务器端处理客户端发送来的请求时,会重新基于这些参数生成新的 Hash 值,然后判断新 Hash 值跟客户端提交过来的原 Hash 参数值是否相同,如果不同则认为页 面遭到恶意篡改而拒绝服务响应。此外,很多 Web 应用为了防止跨站请求伪造(CSRF)攻击,会在 HTTP 响应中动态添加随机命名的参数,攻击者难以伪造这些随机参数,因此无法伪造出合格的 HTTP 请求。以上种种技术在防止 Web 应用遭到恶意攻击的同时,自然增加了渗透测试的难度。利用渗透测试工具所探索到的此类 Web 应用的请求,相对服务器来说都是不合法的过期请求,因此此类 Web 应用的安全测试脚本难以被重用。 IBM Rational AppScan 标准版(下文简称为 AppScan)产品是业界的 Web 应用安全测试工具。它提供了丰富的参数管理功能,能够帮助用户通过自定义的方式从 HTTP 响应中识别出所需要的参数,其参数定制功能还可以帮助检测出格式较为复杂的参数。AppScan 支持动态跟踪参数的参数值变化,并将新的参数值更新到相应的 HTTP 请求中。因此,通过 AppScan 的定制参数特性,我们可以跟踪到页面内的 Hash 参数、Token 令牌的变化,并自动更新到所录制的 HTTP 请求中。这意味着,我们所录制的扫描脚本中的 HTTP 请求在下一次执行的时候仍能保持有效,所以,即便 Web 应用中存在某些格式复杂的特殊参数,我们所录制的扫描脚本依然能够被重用。下文我们将深入介绍 AppScan 的定制参数功能,并通过案例的方式跟读者分享如何利用定制参数提高测试脚本的重用性。 定制参数功能简介 AppScan 允许用户管理从 Web 应用所接受到的参数和 Cookie。通常情况下,AppScan 在探索阶段会帮助 用户自动检测出可能是会话标识的 Cookie 和参数。当页面中的参数格式比较复杂的情况下,AppScan 允许用户定制参数以识别出所有会话相关的 Cookie 和参数。在进一步讨论 AppScan 处理 Cookie 和参数之前,我们简单回顾一下 HTTP 请求和响应的基本知识。 HTTP 请求包括三部分:请求行(Request Line),头部(Headers)和数据体(Body)。其中,请求行由请求方法(Method),请求网址 Request-URI 和协议(Protocol)构成,而请求头包括多个属性,数据体则可以被认为是附加在请求之后的文本或二进制文件。HTTP 响应与之类似,包括:响应状态行,头部,以及数据体。下面我们通过一个简单的 HTTP POST 请求来了解 Cookie 和参数。 清单 1. HTTP POST 请求示例 POST /examples/servlets/servlet/SessionExample;jsessionid= 0ABAA3CD7E6538C52FEF00729E673C3D HTTP/1.0 Cookie: JSESSIONID=0ABAA3CD7E6538C52FEF00729E673C3D; UnicaNIODID=wbXVlLVSIiK-W76vO58; ePassLanguage=Localeen_US&CharsetUTF-8 Content-Length: 30 Accept: image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/x-ms-application, application/x-ms-xbap, application/vnd.ms-xpsdocument, application/xaml+xml, application/msword, */* Referer: http://localhost:8080/examples/servlets/servlet/SessionExample Content-Type: application/x-www-form-urlencoded Host: localhost:8080 Pragma: no-cache Connection: Keep-Alive User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Win32) Accept-Language: en-US dataname=name&datavalue=jeremy 从这段 HTTP POST 请求容易看出,HTTP 请求头部 Cookie 中有 JSESSIONID、UnicaUIODID、ePassLanguage,HTTP 请求主体中包含两个参数 dataname 和 datavalue。通常情况下,参数往往存在于三种位置:HTTP 请求主体(Request Body)内、HTTP 请求查询(Request URI Query)内、以及 HTTP 请求路径(Request URI Path)内。请求查询中参数例子比较常见,HTTP GET 请求主要通过请求查询来传递参数,例如: /examples/servlets/servlet/SessionExample?dataname=name&datavalue=jeremy 请求路径传递参数比较少见,有些网站为了便于搜索引擎爬取索引,会通过 URL Rewrite 的方式,将参数置于请求路径中,另外一个例子是 Web 应用为了防止客户端禁用 Cookie,有些应用服务器会将 jsessionid 通过 URI 传递。如上述 HTTP POST 示例所示: /examples/servlets/servlet/SessionExample;jsessionid=0ABAA3CD7E6538C52FEF00729E673C3D
图 1. Rational AppScan 内置定制参数 JSESSIONID
参数传递的灵活性增加了渗透扫描工具检测跟踪参数的难度。AppScan 提供了功能强大的定制参数功能,帮助用户检测到比较复杂的参数。上图展示了 AppScan 内置的一个定制参数,用以自动识别出 URI 中的 Session ID。可以看出,定制参数功能基于正则表达式来识别参数。引用名称指的是定制参数的名称。模式指的是用以识别参数的正则表达式,模式能识别成组的参数。值组索引值指的是哪个组包含的是参数的值。名称组索引指的是哪个组包含的是参数的名称。位置指的是参数的位置(请求主体/请求路径/请求查询)。条件模式中可以设定包含该参数的整体(请求主体/请求路径/请求查询)的正则表达式,仅当该模式匹配时,AppScan 才会创建此参数。响应模式中可以设置识别 HTTP 响应中的参数值的正则表达式,当用于识别 HTTP 请求中的参数值的正则表达式不同于识别 HTTP 响应中的参数值的正则表达式时,我们必须提供给 AppScan 响应模式的正则表达式。例如,如果 HTTP 请求中跟踪的参数格式是 <SessionID>value</SessionID> ,而 HTTP 响应中的相同参数的格式是 <Jsessionid>value</Jsessionid> ,则必须定义响应模式来识别 HTTP 响应中的 Session ID。 案例简介 上文跟读者简单介绍了扫描脚本重用的价值,以及 AppScan 的定制参数功能。下面本文将通过一个案例介绍如何利用 AppScan 的定制参数来提高扫描脚本的重用性。 笔者在实际工作中遇到一个扫描脚本重用问题。未完成授权的用户登录系统后,需要供系统给予的验证码完成下一步授权验证,如果没有验证码或者验证码过期则需要先申请授权验证码。申请授权验证码时需要提供当前用户的 ID、被授权资源的 ID,以及系统动态生成的校验参数,授权验证完成后这个授权验证码即被销毁,该用户也变为完成授权的用户,再也没有访问以上页面资源的权限。考虑到这个功能是新增功能,相关功能模块还未稳定,在以后的迭代周期中需要重新运行这段扫描脚本。因此,笔者试图针对这个功能设计出可重复运行的扫描脚本。 案例所述应用中的存在若干交互的链接,我们将采用手工探索的方式进行录制扫描脚本,为了提高脚本的重用性,AppScan 允许用户导出手工探索的链接清单。由于仅未完成授权的用户才能访问该功能界面,所以我们需要录制未完成授权用户的登录方法,并将该登录序列进行导出(我们可直接修改导出的登录方法文件,替换其中的登录账号信息以支持新的登录用户)。这样,如果需要重复扫描这段功能模块,我们可以新建扫描文件,导入前期录制好的手工探索链接清单,以及新的未完成授权用户的登录方法。但 AppScan 在导入手工探索链接清单时候,会执行里面所有的链接,以验证这些链接依然有效。我们发现申请授权验证码链接无法通过 AppScan 的合法性检验。通过使用手工探索后,我们可以通过 AppScan 看到申请授权验证码的请求内容如下。 清单 2. 申请授权验证码的 HTTP 请求 POST /selfservices/passcode HTTP/1.1 Accept: image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/x-ms-application, application/x-ms-xbap, application/vnd.ms-xpsdocument, application/xaml+xml, application/msword, */* Content-Type: application/x-www-form-urlencoded Host: demo Connection: Keep-Alive Cache-Control: no-cache User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Win32) Accept-Language: en-US Cookie: ... Content-Length: 131 action=REQUEST_PASSCODE&resourceid=0003273610&userid=2700005805&curtime= 1305183922792&ADCE4375=da2a8144c72054f6a932230c86bd0988 观察发现,resourceid、userid、curtime 等参数是通过隐藏参数的方式,位于申请授权验证码的表单内部。比较特别的是 ADCE4375 参数,比较这个链接的几个 HTTP 请求后发现,它的名称是随机生成的。 清单 3. 申请授权验证码的 Form 表单 <form method="post" action="/selfservices/passcode" id="REQUEST_PASSCODE_FORM"> <input type="hidden" name="action" value="REQUEST_PASSCODE" /> <input type="hidden" name="resourceid" value="0003273610" /> <input type="hidden" name="userid" value="2700005805" /> <input type="hidden" name="curtime" value="1305183919218" /> <input type="hidden" name="ADCE4375" value="d4be19775b0f346c32453c8be95d39c4" /> ... </form> 进一步分析发现,resourceid 即为被授权资源的 id;userid 为当前用户的 id;curtime 为系统时间;ADCE4375 为系统提供的验证参数。应用服务器端基于 resourceid、userid,以及 curtime 等参数生成了动态校验参数 ADCE4375 的值,以防恶意用户篡改 resourceid 等字段信息。由于校验参数名称是动态的,这同时有利于避免 CSRF 攻击。如果应用服务器发现校验参数不合格,则直接拒绝服务响应。 因此,如果想重复运行这段扫描脚本,应用服务器会发现校验参数等不合法,直接拒绝服务响应,导致前期导出的这段手工探索链接清单无法被重用。针对以上案例中 HTTP 请求中参数的分析,我们发现 resourceid 和 userid 跟当前登录用户相关,curtime 的值每次请求都会变化,动态校验参数的名称和值每次请求都会变化。我们可以利用 AppScan 设置参数跟踪,自动检测并跟踪 HTTP 响应中的上述参数的参数值,并将新的参数值更新到 HTTP 请求中。这样的话,当重复执行以上请求的时候,HTTP 请求中的参数都是合法参数,这个 HTTP 请求即是有效请求,能够被重复执行。