次完整的XPath注入攻击应该包括使用特殊构造的查询来提取一个XML数据库内的数据或者信息。作为一门新的技术,XPath注入在一定程度上和SQL注入漏洞有着惊人的相似之处,通过下面的文字,我们将进一步来了解这种新型渗透技术。
在温习前人的相关研究成果之前,我们将介绍一些理论性的研究背景,这样有助于我们更好地理解这种手法的关键。首先我们要了解的便是XML标准和XPath语言(XML是The Extensible Markup Language(可扩展标识语言)的简写,是由全球信息网委员会(WWW)推广并发展而来的)。
所谓的XML文档就是通过XML的标准来对数据进行描述的一种形式,为了能更好地了解XML的工作原理,我们用下面的这个例子来进行说明。
view plainprint?
//这一行显示的是XML的版本,当前是1.0
//描述person这一根元素
Jaime
Blasco
12345678w
Eazel S.L
//以上四行属于person的子元素,如name、surname等
//该行是上面根元素的结束
大家可能已经注意到,XML是一种允许快速、简易地提取并描述数据的语言,并且具有简单直接等优点。知道了XML的工作原理,我们还需要一种特定的机制规范我们对这些数据的利用,这是我们的XPath语言发挥作用的前提条件。
XPath语言
XPath是XML路径语言的简称,是通过“引用数据”的方式,从XML文档来读取各种信息。XPath能够直接用于程序开发,比如微软的.net和Macromedia ColdFusion这类框架对XPath缺省是支持的。XPath对现有XML文档的分段提取的方法主要是由XML分析器生成节点树的形式来进行提取的。生成的树中包含有许多不同的节点,例如source、element、attribute、 text、comments、processing instruction等。
关于XPath,有一个很基本的要素就是表达式,作用相当于XPath的语言指令。这些表达式代表着操作,其中最重要的一种便是路径表达式。比如“/person/name”,就是通过轮流的方式遍历引用了全部的根元素及其下级的子元素。这样,XPath表达式会返回一个空节点列表或者包含一个或多个节点的元素列表。XML的另一个重要的机制是筛选,筛选是通过谓词来实现的,通过谓词可以对匹配的节点进行进一步的筛选。比如“/person/govt_id_number[@private=”if”]”,展示的是读取person中govt_id_number的 private属性为if的下面所有子元素的集合。
此外,我们应该区分XML里面的条件运算符。And运算符负责把不同的谓语封装进括号中;or运算符则代表管道字符 “|”;negation则使用了保留字NOT。如上面所叙述,我们介绍的一些简单的XPath语法规则有助于我们理解后面将要列举的注入示例,因为这些规则在程序实现的过程中发挥了巧妙的作用。
下面我们继续了解一些XPath语言的知识。我们可以使用双斜杠“//”来选择递减排列下来的全部节点,如“ //user/name”可以读取全部的用户名。此外,node()是另外一种在XPath中,读取任意节点的方法,如“//user/node() o //user/child::node()”,将读出任意用户其下的所有节点(在这里,我们每个用户都有三个text()类型的节点)。同样,我们可以指定相应的节点类型,比如Text()为文本节点,Comment()为注释节点,processinginstruction()可以处理指令节点。
接下来我们将要介绍的语法部分就是最重要的谓语部分。比如“//user[position()=n] /name”,这个表达式能够获得用户名称为n的节点;再比如“//user[position()=1]/child::node() [position()=2]”,将读出第一个用户的第二个节点(这里是password)。
最后,我们通过介绍三个用来测试的有用的函数来结束这部分的讲述,它们分别是count(node-set)函数,返回参数node-set中的节点个数,比如count(//user/child::node()),将会返回全部user的全部节点总数;stringlength(string)函数,取字符串的长度,如果参数被省略则计算上下文节点的string-value值的长度,比如 stringlength(//user[position()=1]/child::node()[position()=1]),将会返回user的第一个节点包含的一个字符串的长度;substring(string, number, number)函数,取子字符串,注意字符串的位置从0开始,比如substring((//user[position()=1] /child::node()[position()=1),2,1),将会返回user的第一个节点(name)的第二个字母。
分析有漏洞的程序
接下来我们将一起来研究一些隐含XPath注入漏洞的程序,这里的程序仅仅是用于教学目的而编写的。在开始之前,我们要强调一下,本文的所有例子已经顺利通过MONO平台下C#的调试,因此可以方便地跨平台开发一些.net软件。Monodevelop常常用于设计和 XSP——一个轻型的并且支持asp.net程序的Web服务器。
我们看下面的示例(为了更好的熟悉我们接下来要分析的程序,以下将继续沿用下面的这个XML文档作为例子)。程序index.aspx的代码如下。
view plainprint?
<%@ Page Language="C#" %>
Access to System:
当我们连接到XSP服务器的时候,我们将看到如图1所示的页面,这个页面模拟了一个限制用户访问某些受限数据的验证界面(仅仅允许注册用户访问)。现在,让我们来思考一下如何让程序非常规的运行,以使我们达到一定的目的。我们可以看到,上面有几个数据输入的地方,像 username和password这些,它们可以由字母和数字字符组成,甚至包括一些特殊的字符。但是如果我们在用户名的地方输入一个普通的逗号,会发生什么情况呢?如图2所示。
图1
图2
我们要留意地是以下的代码:
System.Xml.XPath.XPathException:
Error during parse of string(//user[name/text()=''' and password/text()='']/account/text())--->Mono.Xml.XPath.yyParser.yyException: irrecoverable syntax error
由此我们可以知道程序是用.net编写,并运行在XSP(MONO)环境下,因为Mono.Xml.XPath泄露了这些信息。而且,在这种情况下,我们可以很容易地破坏该程序的原有逻辑,因为这个错误提示页面已经把整个XPath查询完全暴露给我们:
string(//user[name/text()='' and password/text()='']/account/text())
那么我们不妨设想,当我们输入“' or 1=1 or ''='”来登录的话,又会出现什么情况?作为一条查询,它将会是这样子:
string(//user[name/text()='' or 1=1 or ''='' and password/text()='']/account/text())
这样,我们特殊的输入导致了查询发生改变,这条查询语句将导致程序从XML文档读取并返回第一个账户的名称。我相信,看过以上这些以后,不少人已经注意到这一类型的攻击相比SQL注入攻击有一定的相似性,SQL注入同样可以用于类似的程序,如:
Select * From users where name = '' and passwd = ''
攻击者会利用’ or 1=1-中的“-”来注释掉后面的查询语句。但是在XPath里,没有类似的效果,因此我们必须另外找到一种机制来注释掉后面的那些查询“碎片”。我们很早就知道我们的表达式“' or 1=1 or ''= –”利用了两个or运算符来取消掉and运算符的作用,从而使得查询总是返回TRUE的结果。就这样,当我们输入上述字符串以后,我们将获得管理员的访问权,因为管理员administrator是XML文档中的第一条记录。现在,作为一名用户,我们已经获得了系统的访问授权,但除此以外,我们还能够做些什么呢?
获取XML数据库访问权
现在,可能你已经开始怀疑我们前面讲述的理论知识不仅仅是为了更好地理解这种攻击,没错,从这里开始,我们要把我们的研究重心转移到获取整个XML数据库上面来。为了实现这一目的,我们需要利用好我们仅有的工具,即XPath语言和程序对我们查询的各种响应(包括那些授权或者未授权的访问,即我们分别看见的TRUE或者FALSE)。举例子说明一下,假设我们要得到第一个用户名的长度,输入:
' or string-length(//user[position()=1]/child::node()[position()=1])=4 or ''='
上面的意思是等于我们在碰运气一样地询问漏洞程序,第一个用户名是否由4个字符组成。程序拒绝我们访问则表示程序对我们刚才的询问返回了否定的答案(false)。有了这个机制,我们便可以尝试更多的可能性,最终将猜出长度为5。
' or string-length(//user[position()=1]/child::node()[position()=1])=5 or ''=' //授权(返回TRUE)
再来看另一个例子,我们要得到第一个用户的用户名的首字母,我们使用如下的查询:
' or substring((//user[position()=1]/child::node()[position()=1]),1,1)="a" or ''='
这次我们询问程序,第一个用户名称的首字母是否为a,结果服务器返回false。在若干次可能性尝试以后,我们最终得到首字母为j,服务器返回true。
自动化处理
你或许觉得整个过程就是一个消耗大量时间并且十分烦琐的手工过程,但是,如果我们能开发出一个小工具能够自动为我们干这些活的话,获取整个XML数据库绝对不成问题。此外,由于我们这里的测试并非“盲”注入(因为我们事先已经了解了XML文档的结构),所以我们很有可能更容易、快捷地开发自己相应的程序。
根据来自于程序的错误提示所提供的信息,我们可以重建XML文档的结构,比如下面这样:
因此,我们的程序应该以递归的方式横跨所有的节点,并包括所有字符及其组合而成的字符串。为了本次测试,我专门用C#写了一个小程序,该程序能从本文所提到的示例程序对应的XML文档中提取全部信息,代码详见本期光盘的杂志相关。
在编写我们自己的程序或者分析别人的程序的时候,我们应该事先熟悉该程序数据在鉴定的过程中的传递。为了实现这一点,我们可以查看HTML源代码或者使用一个本地的代理(如WebScarab)。例如以下的线索分析:
__VIEWSTATE=DA0ADgIFAQUDDgINAA4CBQEFCQ4CDQ0PAQEEVGV4dAFOJyBvciBzdHJpbmctbGVuZ3RoKC8vdXNlcltwb3NpdGlvbigpPTFdL2NoaWxkOjpub2RlKClbcG9zaXRpb24oKT0xXSk9NCBvciAnJz0nAAAAAA0NDwECAAABDUFjY2VzcyBEZW5pZWQAAAAADQ0PAQIAAAEAAAAAAA4BAQZDaGVjazE%3D&TextBox1=test&TextBox2=test&__EVENTTARGET=Button1&__EVENTARGUMENT=HTTP/1.0 200 OK
在以上的字符串里面,我们能够提取出许多变量,这些变量对于我们写的工具来说,在连接服务器的时候是必不可少的。图 3便是我们猜解程序运行时的截图。从中可以看到,为了读取整个数据库,服务器必须响应数量庞大的请求包。但这不并构成什么问题,因为我们可以改进自己程序的代码以降低查询请求包,以防我们遭遇某种二进制数据搜索的情况,或者服务器被询问到给定的字符是在我们指定的字符之前还是之后的情况,使得程序也能应付得来。我们暂时把这个任务留给有兴趣深入探讨的朋友。
图3
如何防范这类的攻击
在本文的最后部分,我们将来一同探讨避免这一类或者类似的攻击的各种方法。在目前许多防范这类攻击的方法中,过滤用户输入是其中十分有效的一种。过滤包括质疑一切用户发送给我们程序的数据以及滤掉全部被认为是对我们程序有害的字符。过滤可以在客户端和服务端两边实现,如果可能的话,建议两者同时进行过滤。
此外,目前还有一种方法便是参数化查询,查询中的表达式不会直接被执行,当应用了参数化查询以后,这些查询会被重新编译执行,而不会随便让用户的输入直接在表达式中执行,把用户的输入和程序的逻辑分离开了。最后一种办法就是使用专门被设计来针对这类型攻击的一些产品,比如像由Daniel Caz?zulino开发的一款安全防护工具。
总结
本文和大家讨论了XPath中的代码的注入攻击,随着XML技术的广泛传播和发展,一旦程序使用了未受保护的XML和XPath规则,这类型的攻击将会严重威胁web安全。