一次完整的 XPath 注入攻击应该包括使用特殊构造的查询来提取一个 XML 数据库内的
数据或者信息。作为一门新的技术,XPath 注入在一定程度上和 SQL 注入漏洞有着惊人的相
似之处,通过下面的文字,我们将进一步来了解这种新型渗透技术。
在温习前人的相关研究成果之前,我们将介绍一些理论性的研究背景,这样有助于我们
更好地理解这种手法的关键。首先我们要了解的便是 XML 标准和 XPath 语言(XML 是 The
Extensible Markup Language(可扩展标识语言)的简写,是由全球信息网委员会(WWW)
推广并发展而来的) 。
所谓的 XML 文档就是通过 XML 的标准来对数据进行描述的一种形式,为了能更好地了
解 XML 的工作原理,我们用下面的这个例子来进行说明。
<?xml version="1.0"?>
//这一行显示的是 XML 的版本,当前是 1.0
<person> //描述 person 这一根元素
<name>Jaime</name>
<surname>Blasco</surname>
<govt_id_number private="if">
12345678w</govt_id_number>
<company>Eazel S.L</company>
//以上四行属于 person 的子元素,如 name、surname 等
</person> //该行是上面根元素的结束
大家可能已经注意到,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()类型的节点) 。同样,我们可以指定
相应的节点类型,比如 T ext()为文本节点,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 的代码如下。
<%@ Page Language="C#" %>
<html>
<head>
<script runat="server">
void Button1_OnClick(object Source, EventArgs e){
System.Xml.XmlDocument XmlDoc = new System.Xml.XmlDocument();
XmlDoc.Load("datos.xml");
System.Xml.XPath.XPathNavigator nav = XmlDoc.CreateNavigator();
System.Xml.XPath.XPathExpression expr = nav.Compile(
"string(//user[name/text()='"+TextBox1.Text+"' and password/text()
='"+TextBox2.Text+"']/account/text())");
String account=Convert.ToString(nav.Evaluate(expr));
if (Check1.Checked) {
cadena.Text = expr.Expression;
} else {
string.Text = "";
}
if (account=="") {
Label1.Text = "Access Denied";
} else {
Label1.Text = "Access Granted\n" + "You have logged in as: "
+ account;
}
}
</script>
</head>
<body>
<body BGCOLOR="#3d5c7a">
<br clear="all">
<font color="white"><center>
<h3>Access to System:</h3></center>
<center><form id="ServerForm" runat="server">
<p>User:</p>
<asp:TextBox id="TextBox1" runat"server"></asp:TextBox>
<p>Password:</p>
<asp:TextBox id="TextBox2" runat="server"></asp:TextBox>
<p>
<button id=Button1 runat="server" OnServerClick="Button1_OnClick">
Enter</button>
<br>
<br>
<asp:CheckBox id=Check1 runat="server" Text="XPath Debug" />
<font color = "red"><h2><asp:Label id="Label1" runat="server">
</asp:Label></h2></font>
<span id=Span1 runat="server" />
</form>
</center>
</font>
<br clear="all">
<br><br><br>
<font color="#11ef3b">
<asp:Label id="cadena" runat="server"></asp:Label>
</font>
</body>
</html>
当我们连接到 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 文档的结构,比如下面
这样:
<user>
<name></name>
<password></password>
<account></account>
</user>
因此,我们的程序应该以递归的方式横跨所有的节点,并包括所有字符及其组合而成的
字符串。为了本次测试,我专门用 C#写了一个小程序,该程序能从本文所提到的示例程序
对应的 XML 文档中提取全部信息。
在编写我们自己的程序或者分析别人的程序的时候,我们应该事先熟悉该程序数据在鉴
定的过程中的传递。为了实现这一点,我们可以查看 HTML 源代码或者使用一个本地的代理
(如 WebScarab) 。例如以下的线索分析:
__VIEWSTATE=DA0ADgIFAQUDDgINAA4CBQEFCQ4CDQ0PAQEEVGV4dAFOJyBvciBzdHJpbmct
bGVuZ3RoKC8vdXNlcltwb3NpdGlvbigpPTFdL2NoaWxkOjpub2RlKClbcG9zaXRpb24oKT0xXSk9NCB
vciAnJz0nAAAAAA0NDwECAAABDUFjY2VzcyBEZW5pZWQAAAAADQ0PAQIAAAEAAAAAAA4BAQZD
aGVjazE%3D&TextBox1=test&TextBox2=test&__EVENTTARGET=Button1&__EVENTARGUMENT=H
TTP/1.0 200 OK
在以上的字符串里面,我们能够提取出许多变量,这些变量对于我们写的工具来说,在
连接服务器的时候是必不可少的。图 3 便是我们猜解程序运行时的截图。从中可以看到,为
了读取整个数据库,服务器必须响应数量庞大的请求包。但这不并构成什么问题,因为我们
可以改进自己程序的代码以降低查询请求包,以防我们遭遇某种二进制数据搜索的情况,或
者服务器被询问到给定的字符是在我们指定的字符之前还是之后的情况,使得程序也能应付
得来。我们暂时把这个任务留给有兴趣深入探讨的朋友。
图 3
如何防范这类的攻击
在本文的最后部分,我们将来一同探讨避免这一类或者类似的攻击的各种方法。在目前
许多防范这类攻击的方法中,过滤用户输入是其中十分有效的一种。过滤包括质疑一切用户
发送给我们程序的数据以及滤掉全部被认为是对我们程序有害的字符。过滤可以在客户端和
服务端两边实现,如果可能的话,建议两者同时进行过滤。
此外,目前还有一种方法便是参数化查询,查询中的表达式不会直接被执行,当应用了
参数化查询以后,这些查询会被重新编译执行,而不会随便让用户的输入直接在表达式中执
行,把用户的输入和程序的逻辑分离开了。最后一种办法就是使用专门被设计来针对这类型
攻击的一些产品,比如像由 Daniel Cazzulino 开发的一款安全防护工具。
总结
本文和大家讨论了 XPath 中的代码的注入攻击,随着 XML 技术的广泛传播和发展,一旦
程序使用了未受保护的 XML 和 XPath 规则,这类型的攻击将会严重威胁 web 安全。