2024全网最全面及最新且最为详细的网络安全技巧四 之 sql注入以及mysql绕过技巧 (4)———— 作者:LJS

  • 目录

    4.13 利用hpp和php特性绕过waf(本小节图片无法加载出来,待后续学有余力解决)

    4.14 齐博整站注入漏洞

    4.15 如何构造一个出错的zip

    4.16 字符编码及SQL注入

    0×01 MYSQL中的宽字符注入

    0×02 GB2312与GBK的不同

    0×03 mysql_real_escape_string解决问题?

    0×04 宽字符注入的修复

    0×05 iconv导致的致命后果

    0×06 总结


  • 4.13 利用hpp和php特性绕过waf(本小节图片无法加载出来,待后续学有余力解决)

  • 在做题之前,我们先来了解一些需要用到的基础知识。
  • 对于传入的非法的 $_GET 数组参数名,PHP会将他们替换成 下划线 。经过fuzz,有以下这些字符:

  • 当我们使用HPP(HTTP参数污染)传入多个相同参数给服务器时,PHP只会接收到后者的值。(这一特性和中间件有关系)

  • 通过 $_SERVER['REQUEST_URI'] 方式获得的参数,并不会对参数中的某些特殊字符进行替换。

  • 这里的代码中有两个waf。
  • 第一个WAF在代码 第29行-第30行 ,这里面采用了 dowith_sql() 函数,跟进一下 dowith_sql() 函数,该函数主要功能代码在 第19-第26行 ,如果 $_REQUEST 数组中的数据存在 select|insert|update|delete 等敏感关键字或者是字符,则直接 exit() 。如果不存在,则原字符串返回。
  • 而第二个WAF在代码 第33行-第39行 ,这部分代码通过 $_SERVER['REQUEST_URI'] 的方式获取参数,然后使用 explode 函数针对 & 进行分割,获取到每个参数的参数名和参数值。然后针对每个参数值调用 dhtmlspecialchars() 函数进行过滤。
  • 跟进一下 dhtmlspecialchars() 函数,发现其相关功能代码在 第3行-第14行 ,这个函数主要功能是针对 '&', '"', '<', '>', '(', ')' 等特殊字符进行过滤替换,最后返回替换后的内容。从 第44行和第45行 的代码中,我们可以看到这题的参数都是通过 REQUEST 方式获取。我们可以先来看个例子:
  • 第一次 $_REQUEST 仅仅只会输出 i_d=2 的原因是因为php自动将 i.d 替换成了 i_d 。而根据我们前面说的第二个特性,PHP取最后一个参数对应的值,因此第一次 $_REQUEST 输出的是2。
  • 第二次 $_REQUEST 会输出 i_d=select&i.d=2 是因为 $_SERVER['REQUEST_URI'] 并不会对特殊的符号进行替换,因此结果会原封不动的输出。所以这题的payload可以根据下面这个思维导图进行构造:
  • 我们通过页面请求 i_d=padyload&i.d=123

  • 当数据流到达第一个WAF时,php会将参数中的某些特殊符号替换为下划线。因此便得到了两个 i_d ,所以此时的payload变成了 i_d=payload&i_d=123

  • 前面我们介绍了,如果参数相同的情况下,默认 第二个参数传入的值 会覆盖 第一个参数传入的值 。因此此时在第一个WAF中 i_d=123 ,不存在其他特殊的字符,因此绕过了第一个WAF。

  • 当数据流到达进入到第二个WAF时,由于代码是通过 $_SERVER['REQUEST_URI'] 取参数,而我们前面开头的第三个知识点已经介绍过了 $_SERVER['REQUEST_URI'] 是不会将参数中的特殊符号进行转换,因此这里的 i.d 参数并不会被替换为 i_d ,所以此时正常来说 i.di_d 都能经过第二个WAF。

  • 第二个WAF中有一个 dhtmlspecialchars() 函数,这里需要绕过它,其实很好绕过。绕过之后 i_d=payload&i.d=123 便会进入到业务层代码中,执行SQL语句,由于这里的SQL语句采用拼接的方式,因此存在SQL注入。

  • 因此最后payload如下:
  • http://127.0.0.1/index.php?submit=&i_d=-1/**/union/**/select/**/1,flag,3,4/**/from/**/ctf.users&i.d=123

  • 4.14 齐博整站注入漏洞

  • 一个比较有意思的点,因为安全策略造成的注入。
  • 就拿齐博整站系统为例。
  • 看到/member/userinfo.php,112到114行:
  • //过滤不健康的字
    $truename=replace_bad_word($truename);
    $introduce=replace_bad_word($introduce);
    $address=replace_bad_word($address);
  • 这几句过滤代码,意思是想过滤一些“不和谐”的词语。那我们看看这个replace_bad_word函数。
  • function replace_bad_word($str){
    	global $Limitword;
    	@include_once(ROOT_PATH."data/limitword.php");
    	foreach( $Limitword AS $old=>$new){
    		strlen($old)>2 && $str=str_replace($old,trim($new),$str);
    	}
    	return $str;
    }
    
    /*@include_once(ROOT_PATH."data/limitword.php");:尝试包含定义敏感词汇的文件。@ 符号用于抑制包含文件时可能出现的警告或错误,如果文件不存在或者无法包含,会安静地跳过。
    foreach( $Limitword AS $old=>$new):遍历全局变量 $Limitword,将每个敏感词汇 $old 替换为对应的 $new。
    strlen($old)>2 && $str=str_replace($old,trim($new),$str);:
    strlen($old)>2 确保只有长度大于2的敏感词才进行替换操作。
    str_replace($old,trim($new),$str) 将字符串 $str 中的 $old 替换为 $new。trim($new) 用于去除替换文本的首尾空白(如果有)。
    */
  • 实际上是一个str_replace,将旧的“不和谐”词语替换成新的“和谐”词语。那么替换列表从哪来?看看data/limitword.php:
  • <?php 
    $Limitword['造反']='造**';
    $Limitword['法轮功']='法**功';
  • 就这两个,$Limitword并没有初值。而且include之前有global $Limitword。齐博cms是将GET和POST变量注册成全局变量了,所以global取到的可以是$_POST[Limitword]或$_GET[Limitword]。也就是说$Limitword是我们可以控制的。
  • 也就是说,我们可以控制str_replace函数的三个参数。ecshop曾经的一个注入漏洞( 访问的文章审核中... - FreeBuf网络安全行业门户 ),原理相同,再说一遍。
  • 某变量$str全局做过转义,导%00转义成了\0.我们能够控制str_replace的前两个参数,则str_replace('0'\, ''\, $str),再将0转换成空,则留下了\,这个\可以转义其后的',使之变成',导致单引号逃逸出转义符的限制,造成注入。
  • 这里的原理是一样的,只不过齐博cms之前对一些变量做了过滤:
  • function filtrate($msg){
        //$msg = str_replace('&','&amp;',$msg);  // 注释掉的代码,通常用于转义 & 符号
        //$msg = str_replace(' ','&nbsp;',$msg); // 注释掉的代码,通常用于将空格转换成 &nbsp;
        $msg = str_replace('"','&quot;',$msg);  // 替换双引号 " 为 &quot;
        $msg = str_replace("'",'&#39;',$msg);   // 替换单引号 ' 为 &#39;
        $msg = str_replace("<","&lt;",$msg);    // 替换小于号 < 为 &lt;
        $msg = str_replace(">","&gt;",$msg);    // 替换大于号 > 为 &gt;
        $msg = str_replace("\t","   &nbsp;  &nbsp;",$msg); // 将制表符替换为空格
        //$msg = str_replace("\r","",$msg);      // 注释掉的代码,通常用于删除回车符
        $msg = str_replace("   "," &nbsp; ",$msg);  // 将连续多个空格替换为一个 &nbsp;
        return $msg;  // 返回处理后的字符串
    }
    
  • 将’等敏感字符过滤了。就算有一个\可以转义一个',但是这里连'都引入不了。
  • 没关系,这个文件是更新用户信息用的,执行的语句类似update table set a='111',b='222' where uid=1,我们引入一个\在111的最后,转义掉111后面的引号,这样222就逃逸出了限制,我们的sql注入语句就可以放在222的位置执行了
  • 这个和之前discuz7.2的faq.php那个注入类似:
  • 所以,完美构造了一个SQL注入。
  • 先注册一个用户:
  • avatar

  • 记下自己的uid,以便一会更新数据:
  • avatar

  • 可以先测试一下是否存在注入:
  • avatar

  • 报错了,说明注入是存在的。
  • 简单构造一下,向 http://172.16.60.160/qibo_v7/member/userinfo.php?job=edit&step=2 发送数据包:truename=xxxx%0000&Limitword[000]=&email=123@qq.com&provinceid=,address=(select user()) where uid=2%23
  • avatar

  • 将address注入成为user()。Where后面加上uid=2,防止把其他人的address也注入了。
  • 查看个人注入的地址,即可见注入获得的结果:
  • avatar

  •  

 

 

  • 4.15 如何构造一个出错的zip

  • 怎么制造一个只能解压一半的压缩包(即解压到一半出错的)?
  • 这个问题其实需要看具体情况,看解压的那个程序的容忍程度。
  • 我这里就以两个解压的程序作为例子:
  • Windows下的7zip

  • PHP自带的ZipArchive库

  • 先说7zip。7zip的容忍度很低,只要压缩包中某一个文件的CRC校验码出错,就会报错退出。
  • 如何修改压缩包里文件的CRC校验码呢?可以使用010editor。
  • 我们先准备两个文件,一个PHP文件1.php,一个文本文件2.txt,其中1.php是webshell。
  • 然后将这两个文件压缩成shell.zip。
  • 然后我们用010editor打开shell.zip,可以看到右下角有这个文件的格式信息,它被分成5部分
  • 我们打开第4部分,其中有个deCrc,我们随便把值改成其他的值,然后保存。
  • 此时用7zip解压就会出错,解压出的1.php是完好的,2.txt是一个空文件。
  • 我们再用PHP自带的ZipArchive库(代码如图4)测试这个zip,发现解压并没有出错,这也说明ZipArchive的容忍度比较高。
  • 那么我们又如何让ZipArchive出错呢?最简单的方法,我们可以在文件名上下功夫。
  • 比如,Windows下不允许文件名中包含冒号(:),
  • 我们就可以在010editor中将2.txt的deFileName属性的值改成“2.tx:”。
  • 此时解压就会出错,但1.php被保留了下来。
  • 在Linux下也有类似的方法,我们可以将文件名改成5个斜杠(/)。
  • 此时Linux下解压也会出错,但1.php被保留了下来
  •  

 

 

  • 4.16 字符编码及SQL注入

  • 尽管现在呼吁所有的程序都使用unicode编码,所有的网站都使用utf-8编码,来一个统一的国际规范。但仍然有很多,包括国内及国外(特别是非英语国家)的一些cms,仍然使用着自己国家的一套编码,比如gbk,作为自己默认的编码类型。也有一些cms为了考虑老用户,所以出了gbk和utf-8两个版本。
  • 我们就以gbk字符编码为示范,拉开帷幕。gbk是一种多字符编码,具体定义自行百度。但有一个地方尤其要注意:
  • 通常来说,一个gbk编码汉字,占用2个字节。一个utf-8编码的汉字,占用3个字节。在php中,我们可以通过输出
  • echo strlen("和");
  • 来测试。当将页面编码保存为gbk时输出2,utf-8时输出3。
  • 除了gbk以外,所有ANSI编码都是2个字节。ansi只是一个标准,在不用的电脑上它代表的编码可能不相同,比如简体中文系统中ANSI就代表是GBK。
  • 以上是一点关于多字节编码的小知识,只有我们足够了解它的组成及特性以后,才能更好地去分析它身上存在的问题。
  • 说了这么多废话,现在来研究一下在SQL注入中,字符编码带来的各种问题。
  • 0×01 MYSQL中的宽字符注入

  • 这是一个老话题了,也被人玩过无数遍。但作为我们这篇文章的序幕,也是基础,是必须要提的。
  • 我们先搭建一个实验环境。
  • 源码很简单(注意先关闭自己php环境的magic_quotes_gpc):
  • <?php
    // 连接数据库部分,注意使用了gbk编码,请将数据库信息填写进去
    $conn = mysql_connect('localhost', 'root', 'toor!@#$') or die('数据库连接失败!'); // 连接数据库,如果连接失败则输出错误信息并终止脚本
    mysql_query("SET NAMES 'gbk'"); // 设置数据库编码为 gbk,确保正确显示中文
    mysql_select_db('test', $conn) or die("连接数据库失败,未找到您填写的数据库"); // 选择数据库,如果选择失败则输出错误信息并终止脚本
    
    // 获取GET参数id,如果不存在则默认为1,并对其进行转义处理
    $id = isset($_GET['id']) ? addslashes($_GET['id']) : 1;
    
    // 构建SQL查询语句,根据传入的id查询news表中对应tid的新闻
    $sql = "SELECT * FROM news WHERE tid='{$id}'";
    
    // 执行SQL查询,并将结果存储在$result中,如果执行失败则输出错误信息并终止脚本
    $result = mysql_query($sql, $conn) or die(mysql_error());
    
    ?>
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="gbk" /> <!-- 设置HTML文档的字符编码为gbk -->
    <title>新闻</title>
    </head>
    <body>
    <?php
    // 从查询结果中获取一行数据,以关联数组的形式存储在$row中
    $row = mysql_fetch_array($result, MYSQL_ASSOC);
    
    // 输出新闻标题和内容,注意:这里的输出没有经过任何过滤或转义处理,请确保数据的安全性
    echo "<h2>{$row['title']}</h2><p>{$row['content']}<p>\n";
    
    // 释放$result占用的内存,清理资源
    mysql_free_result($result);
    ?>
    </body>
    </html>
    
  • SQL语句是SELECT * FROM news WHERE tid='{$id}',就是根据文章的id把文章从news表中取出来。
  • 在这个sql语句前面,我们使用了一个addslashes函数,将$id的值转义。这是通常cms中对sql注入进行的操作,只要我们的输入参数在单引号中,就逃逸不出单引号的限制,无法注入,如下图:
  • 那么怎么逃过addslashes的限制?众所周知addslashes函数产生的效果就是,让'变成\',让引号变得不再是“单引号”,只是一撇而已。一般绕过方式就是,想办法处理\'前面的\
  • 1.想办法给\前面再加一个\(或单数个即可),变成\\',这样\被转义了,'逃出了限制
  • 2.想办法把\弄没有。
  • 我们这里的宽字节注入是利用mysql的一个特性,mysql在使用GBK编码的时候,会认为两个字符是一个汉字(前一个ascii码要大于128,才到汉字的范围)。如果我们输入%df'看会怎样:
  • 我们可以看到,已经报错了。我们看到报错,说明sql语句出错,看到出错说明可以注入了。
  • 为什么从刚才到现在,只是在'也就是%27前面加了一个%df就报错了?而且从图中可以看到,报错的原因就是多了一个单引号,而单引号前面的反斜杠不见了。
  • 这就是mysql的特性,因为gbk是多字节编码,他认为两个字节代表一个汉字,所以%df和后面的\也就是%5c变成了一个汉字“運”,而'逃逸了出来。
  • 因为两个字节代表一个汉字,所以我们可以试试%df%df%27:
  • 不报错了。因为%df%df是一个汉字,%5c%27不是汉字,仍然是\'
  • 那么mysql怎么判断一个字符是不是汉字,根据gbk编码,第一个字节ascii码大于128,基本上就可以了。比如我们不用%df,用%a1也可以:
  •  
  • %a1%5c他可能不是汉字,但一定会被mysql认为是一个宽字符,就能够让后面的%27逃逸了出来。
  • 于是我可以构造一个exp出来,查询管理员账号密码:
  •  
  •  
  • 0×02 GB2312与GBK的不同

  • 曾经有一个问题一直困扰我很久。
  • gb2312和gbk应该都是宽字节家族的一员。但我们来做个小实验。把内容管理系统中set names修改成gb2312:
  • 007.jpg

  •  
  • 结果就是不能注入了:
  • 008.jpg

  •  
  • 有些同学不信的话,也可以把数据库编码也改成gb2312,也是不成功的。
  • 为什么,这归结于gb2312编码的取值范围。它的高位范围是0xA1~0xF7,低位范围是0xA1~0xFE,而\是0x5c,是不在低位范围中的。所以,0x5c根本不是gb2312中的编码,所以自然也是不会被吃掉的。
  • 所以,把这个思路扩展到世界上所有多字节编码,我们可以这样认为:只要低位的范围中含有0x5c的编码,就可以进行宽字符注入。
  • 0×03 mysql_real_escape_string解决问题?

  • 部分cms对宽字节注入有所了解,于是寻求解决方案。在php文档中,大家会发现一个函数,mysql_real_escape_string,文档里说了,考虑到连接的当前字符集。
  • 009.jpg

  •  于是,有的cms就把addslashes替换成mysql_real_escape_string,来抵御宽字符注入。我们继续做试验,内容管理系统v1.2:,就用mysql_real_escape_string来过滤输入:
  • 0010.jpg

  •  我们来试试能不能注入:
  • 0011.jpg

  • 一样没压力注入。为什么,明明我用了mysql_real_escape_string,但却仍然不能抵御宽字符注入。
  • 原因就是,你没有指定php连接mysql的字符集。我们需要在执行sql语句之前调用一下mysql_set_charset函数,设置当前连接的字符集为gbk。
  • 0012.jpg

  •  就可以避免这个问题了:
  • 0013.jpg

  •  
  • 0×04 宽字符注入的修复

  • 在3中我们说到了一种修复方法,就是先调用mysql_set_charset函数设置连接所使用的字符集为gbk,再调用mysql_real_escape_string来过滤用户输入。
  • 这个方式是可行的,但有部分老的cms,在多处使用addslashes来过滤字符串,我们不可能去一个一个把addslashes都修改成mysql_real_escape_string。我们第二个解决方案就是,将character_set_client设置为binary(二进制)。
  • 只需在所有sql语句前指定一下连接的形式是二进制:
  • mysql_query("SET character_set_connection=gbk, character_set_results=gbk,character_set_client=binary",$conn);
  • 这几个变量是什么意思?
  • 当我们的mysql接受到客户端的数据后,会认为他的编码是character_set_client,然后会将之将换成character_set_connection的编码,然后进入具体表和字段后,再转换成字段对应的编码。
  • 然后,当查询结果产生后,会从表和字段的编码,转换成character_set_results编码,返回给客户端。
  • 所以,我们将character_set_client设置成binary,就不存在宽字节或多字节的问题了,所有数据以二进制的形式传递就能有效避免宽字符注入。
  • 比如,我们的内容管理系统v2.0版本更新如下
  • 0014.jpg

  •  
  •  已经不能够注入了:
  • 0015.jpg

  • 在我审计过的代码中,大部分cms是以这样的方式来避免宽字符注入的。这个方法可以说是有效的,但如果开发者画蛇添足地增加一些东西,会让之前的努力前功尽弃。  
  • 0×05 iconv导致的致命后果

  • 很多cms,不止一个,我就不提名字了,他们的gbk版本都存在因为字符编码造成的注入。但有的同学说,自己测试了这些cms的宽字符注入,没有效果呢,难道是自己姿势不对?
  • 当然不是。实际上,这一章说的已经不再是宽字符注入了,因为问题并不是出在mysql上,而是出在php中了。
  • 很多cms(真的很多哦,不信大家自己网上找找)会将接收到数据,调用这样一个函数,转换其编码:
  • iconv('utf-8', 'gbk', $_GET['word']);
  • 目的一般是为了避免乱码,特别是在搜索框的位置。
  • 比如我们的内容管理系统v3.0
  • 0016.jpg

  •  
  • 我们可以看到,它在sql语句执行前,将character_set_client设置成了binary,所以可以避免宽字符注入的问题。但之后其调用了iconv将已经过滤过的参数$id给转换了一下。
  • 那我们来试试此时能不能注入:
  • 0017.jpg

  • 居然报错了。说明可以注入。而我只是输入了一个錦'。这是什么原因?
  • 我们来分析一下。“錦“这个字,它的utf-8编码是0xe98ca6,它的gbk编码是0xe55c
  •  
  • 有的同学可能就领悟了。\的ascii码正是5c。那么,当我们的錦被iconv从utf-8转换成gbk后,变成了%e5%5c,而后面的'被addslashes变成了%5c%27,这样组合起来就是%e5%5c%5c%27,两个%5c就是\,正好把反斜杠转义了,导致’逃逸出单引号,产生注入。
  • 这正利用了我之前说的,绕过addslashes的两种方式的第一种:\转义掉。
  • 那么,如果我是用iconv将gbk转换成utf-8呢?
  • 0020.jpg

  •  我们来试试:
  • 0021.jpg

  •  
  • 果然又成功了。这次直接用宽字符注入的姿势来的,但实际上问题出在php而不是mysql。我们知道一个gbk汉字2字节utf-8汉字3字节,如果我们把gbk转换成utf-8,则php会每两个字节一转换。所以,如果\'前面的字符是奇数的话,势必会吞掉\'逃出限制。
  • 那么为什么之前utf-8转换成gbk的时候,没有使用这个姿势?
  • 这跟utf-8的规则有关,UTF-8的编码规则很简单,只有二条:
  • 1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
    2)对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode码。
  • 从2我们可以看到,对于多字节的符号,其第2、3、4字节的前两位都是10,也就是说,\(0x0000005c)不会出现在utf-8编码中,所以utf-8转换成gbk时,如果有\则php会报错:
  • 0022.jpg

  • 但因为gbk编码中包含了\,所以仍然可以利用,只是利用方式不同罢了。
  • 总而言之,在我们处理了mysql的宽字符注入以后,也别认为就可以高枕无忧了。调用iconv时千万要小心,避免出现不必要的麻烦。
  • 0×06 总结

  • 在逐渐国际化的今天,推行utf-8编码是大趋势。如果就安全性来说的话,我也觉得使用utf-8编码能够避免很多多字节造成的问题。
  • 不光是gbk,我只是习惯性地把gbk作为一个典型的例子在文中与大家说明。世界上的多字节编码有很多,特别是韩国、日本及一些非英语国家的cms,都可能存在由字符编码造成的安全问题,大家应该有扩展性的思维。
  • 总结一下全文中提到的由字符编码引发的安全问题及其解决方案:
  • gbk编码造成的宽字符注入问题,解决方法是设置character_set_client=binary。

  • 矫正人们对于mysql_real_escape_string的误解,单独调用set names gbk和mysql_real_escape_string是无法避免宽字符注入问题的。还得调用mysql_set_charset来设置一下字符集。

  • 谨慎使用iconv来转换字符串编码,很容易出现问题。只要我们把前端html/js/css所有编码设置成gbk,mysql/php编码设置成gbk,就不会出现乱码问题。不用画蛇添足地去调用iconv转换编码,造成不必要的麻烦。

  • 这篇文章是我对于自己白盒审计经验的一点小总结,但自己确实在很多方面存在欠缺,本文中所提到的姿势难免存在纰漏和错误,希望有相同爱好的同学能与我指出,共同进步。

 

  • 9
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值