深入escape_string for Mysql

http://zone.wooyun.org/content/2413

继续发老文档,这片是我09年分析PHP宽字节绕过addslashes等常用SQL注入过滤语句时写的分析文档。刚才发struts那篇文档,这篇一并发上来,充实一下PHP区的内容。 

同样请各位注意,这篇文档的内容是09年的,当时甚至还有用PHP4的,可能有部分过时,查看时请注意分辨。 

Author: wofeiwo#80sec.com 
Date: 2009-7-28 

一、背景  
  在日常的web程序编写过程中,经常需要处理字符集的问题。在PHP+Mysql的环境下,通常会使用SET NAMES来设定Mysql数据库当前所使用的字符集。 
  但是这就很容易造成一个问题:通过字符集转换进行注射攻击,通常这样的代码能够绕过PHP 函数addslashes或mysql_escape_string的过滤。因为addslashes、mysql_escape_string没有考虑到宽字符,一旦使用就会造成误过滤。能够被巧妙运用并造成SQL注射攻击。 
  因此,为了更好的安全性需要,我们需要使用脚本语言提供的real_escape_string API对用户输入到Mysql数据库中执行的语句或变量进行安全性的过滤。这是一个非常良好的习惯。Real_escape_string函数理论上会按照当前字符集的设置,有效处理单、宽字符,不放过任何一个敏感字符(如单引号,双引号,反斜线等)。但是,在现实的运用过程中,却常常并非如此。 
  让我们来看看如下一份代码。 
<?php 
$mysqli = new mysqli("localhost", "user", "pass", "test", 3306); 

/* check connection */ 
if (mysqli_connect_errno()) { 
    printf("Connect failed: %s\n", mysqli_connect_error()); 
    exit(); 


$mysqli->query('SET NAMES gbk'); //使用set names设置字符集 
$city = chr(0xbf).chr(0x5c); //0xbf5c是个有效的gbk字符,模拟用户输入 
$city = $mysqli->real_escape_string ($city);//使用real_escape进行过滤 


/* this query will fail, cause we didn't escape $city */ 
if (!$mysqli->query("INSERT into myCity(name) VALUES ('$city')")) { 
    print "INSERT into myCity (name) VALUES ('$city')\n"; 
    printf("Error: %s\n", $mysqli->error); 


var_dump($city); 

var_dump($mysqli->client_encoding()); 

$mysqli->close(); 
?>

  这份代码的结构很常见,它的执行结果如下: 
INSERT into myCity (name) VALUES ('縗\') 
Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''縗\')' at line 1 
string(3) "縗\" 
string(6) "latin1"

  我们可以看到,虽然使用SET NAMES设置了数据库的字符集,但是real_escape_string函数却没有起到他该有的作用:按照当前字符集,过滤敏感字符。反而还是和addslashes一样,一个字节一个字节过滤,使得“0xbf5c”(縗)被过滤成了“0xbf5c5c”(縗\)。从而转义了后面的单引号,破坏了sql语句。 

二、原因  
  再仔细查看上面一段程序的结果。会发现mysql的client_encoding()函数返回的字符集是”latin1”。很明显,real_escape_string是按照了这个字符集对参数进行过滤,所以才导致了以上的这些问题。 
  在mysql手册中,对set names的作用如下表示: 
SET NAMES 'x'语句与这三个语句等价: 

mysql> SET character_set_client = x; 
mysql> SET character_set_results = x; 
mysql> SET character_set_connection = x;

  看上去完美无缺,所有mysql涉及到的三种字符集都被修改了(客户端字符集,连接字符集,输出结果字符集)。但是为何还是不起作用? 
  通过跟踪PHP中mysql_real_escape_string的代码,发现他是直接调用了mysql提供的mysql_real_escape_string这个API: 
ulong STDCALL 
mysql_real_escape_string(MYSQL *mysql, char *to,const char *from, 
       ulong length) 

  if (mysql->server_status & SERVER_STATUS_NO_BACKSLASH_ESCAPES) 
    return (uint) escape_quotes_for_mysql(mysql->charset, to, 0, from, length); 
  return (uint) escape_string_for_mysql(mysql->charset, to, 0, from, length); //这里使用的是struct mysql中的charset成员来作为判断当前字符集的 
}

  从代码中可以看到它是通过mysql这个结构体中charset来判断当前使用的字符集设置的。而使用sql语句对mysql环境变量进行设置,无论如何设置,都无法改变客户端内存中mysql结构体的charset成员的值。 

三、解决方案  
  因此,为了修改此结构体,从而使mysql_real_escape_string函数真正生效,只有通过PHP提供的mysql_set_charset或mysqli_set_charset函数来进行: 
int STDCALL mysql_set_character_set(MYSQL *mysql, const char *cs_name) 

  struct charset_info_st *cs; 
  const char *save_csdir= charsets_dir; 

  if (mysql->options.charset_dir) 
    charsets_dir= mysql->options.charset_dir; 

  if (strlen(cs_name) < MY_CS_NAME_SIZE && 
     (cs= get_charset_by_csname(cs_name, MY_CS_PRIMARY, MYF(0)))) 
  { 
    char buff[MY_CS_NAME_SIZE + 10]; 
    charsets_dir= save_csdir; 
    /* Skip execution of "SET NAMES" for pre-4.1 servers */ 
    if (mysql_get_server_version(mysql) < 40100) 
      return 0; 
    sprintf(buff, "SET NAMES %s", cs_name); //set names语句 
    if (!mysql_real_query(mysql, buff, (uint) strlen(buff))) 
    { 
      mysql->charset= cs; //修改了结构体的charset 
    } 
  }


  从代码中可以看出,这两个函数不仅仅执行了SET NAMES SQL语句,还增加了关键的一步:“mysql->charset= cs;”。 修改了mysql结构体中的charset为当前字符集,这样才能使mysql_real_escape_string真正起作用。 
  我们再来测试一下,修改上文测试代码中的: 
$mysqli->query('SET NAMES gbk');
  修改为: 
$mysqli->set_charset('gbk');
  然后再看看执行结果: 
string(2) "縗" 
string(3) "gbk"

  成功执行,而client_encoding也变成了gbk,说明我们的mysql_real_escape_string函数真正按照预期起作用了。 

四、扩展  
  通过以上方法的确可以解决此字符集转换注入问题,但是,mysqli_set_charset函数对PHP和Mysql有版本要求,必须当mysql版本大于5,PHP版本大于5.0.5时,此函数才有效。至于另一个mysql_set_charset函数,则更要求PHP版本大于5.2.3时才能有效。这对于某些环境下的老版本应用程序可能并不适用。
  这里提供另一个解决方案,可以在Mysql 4.1以上版本有效(注:这也是Discuz当年用的方法)。使用: 
SET character_set_client=binary;
  当使用此语句时,将客户端连接设置为binary,则无论用户输入中是否有敏感字符,mysql在解析时,都只会将其当做二进制数据,而不会当做转义字符或特殊字符处理,因此不会破坏sql语句。应用程序只需要在从mysql数据库取出数据后,在输出时按照当前字符集输出即可。 
   
  我们再来测试一下,修改上文测试代码中的: 
$mysqli->query('SET NAMES gbk');
  修改为: 
$mysqli->query('SET character_set_client=binary;');
  然后再看看执行结果: 
string(3) "縗\" 
string(6) "latin1"

  可见虽然反斜杠被输入进了sql语句,但是并没有被当做转义字符处理。无法破坏sql语句结构,可以安全运行。 
  然而此方法也有另外的问题,比如上例中插入的数据就比正常插入的数据多出了一个反引号。而当使用like语句时,类似” like ‘%$_GET[‘user_input’]%’ ”这样的语句,如果当时的显示是gbk编码,而用户输入的是一个单字符,而此单字符又是gbk双字符编码中的一部分时,则此语句可能会匹配到非预期的内容。此外,使用binary还可能会造成其他不可知的问题。 

五、总结  
  希望各位在编写程序时,按照当时环境和需求,选择以上两种方式中的一种方式进行字符集数据库操作。通常情况下,我推荐使用mysql_set_charset设置字符集的方案,只有在环境不允许的情况下,才推荐使用第二种binary编码的方案。但是无论在什么情况下,都禁止使用”SET NAMES”来作为设置字符集的操作,因为从安全上说,这一设置根本没有起到任何作用。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值