当前位置:首页 > PHP教程 > php应用 > 列表

php序列化serialize() 与反序列化unserialize()

发布:smiling 来源: PHP粉丝网  添加日期:2015-04-11 16:25:58 浏览: 评论:0 

本文章以php中serialize() 和 unserialize()函数来引出我们在正常开发应用中对于序列化使用方法与使用序列化时的一些安全问题,希望对各位会带来帮助,把复杂的数据类型压缩到一个字符串中.

serialize() 把变量和它们的值编码成文本形式

unserialize() 恢复原先变量

代码如下:

  1. $stooges = array('Moe','Larry','Curly'); 
  2. $new = serialize($stooges); 
  3. print_r($new); 
  4. echo "<br />"
  5. print_r(unserialize($new)); 
  6. //结果: 
  7. a:3:{i:0;s:3:"Moe";i:1;s:5:"Larry";i:2;s:5:"Curly";} 
  8. Array ( [0] => Moe [1] => Larry [2] => Curly ) 

当把这些序列化的数据放在URL中在页面之间会传递时,需要对这些数据调用urlencode(),以确保在其中的URL元字符进行处理,代码如下:

  1. $shopping = array('Poppy seed bagel' => 2,'Plain Bagel' =>1,'Lox' =>4); 
  2. echo '<a href="next.php?cart='.urlencode(serialize($shopping)).'">next</a>'

margic_quotes_gpc和magic_quotes_runtime配置项的设置会影响传递到unserialize()中的数据,如果magic_quotes_gpc项是启用的,那么在URL、POST变量以及cookies中传递的数据在反序列化之前必须用stripslashes()进行处理,代码如下:

  1. $new_cart = unserialize(stripslashes($cart)); 
  2. //如果magic_quotes_gpc开启 
  3. $new_cart = unserialize($cart); 

如果magic_quotes_runtime是启用的,那么在向文件中写入序列化的数据之前必须用addslashes()进行处理,而在读取它们之前则必须用stripslashes()进行处理,代码如下:

  1. $fp = fopen('/tmp/cart','w'); 
  2. fputs($fp,addslashes(serialize($a))); 
  3. fclose($fp); 
  4. //如果magic_quotes_runtime开启 
  5. $new_cat = unserialize(stripslashes(file_get_contents('/tmp/cart'))); 
  6. //如果magic_quotes_runtime关闭 
  7. $new_cat = unserialize(file_get_contents('/tmp/cart')); 

在启用了magic_quotes_runtime的情况下,从数据库中读取序列化的数据也必须经过stripslashes()的处理,保存到数据库中的序列化数据必须要经过addslashes()的处理,以便能够适当地存储,代码如下:

  1. mysql_query("insert into cart(id,data) values(1,'".addslashes(serialize($cart))."')"); 
  2. $rs = mysql_query('select data from cart where id=1'); 
  3. $ob = mysql_fetch_object($rs); 
  4. //如果magic_quotes_runtime开启 
  5. $new_cart = unserialize(stripslashes($ob->data)); 
  6. //如果magic_quotes_runtime关闭 
  7. $new_cart = unserialize($ob->data); 

当对一个对象进行反序列化操作时,PHP会自动地调用其__wakeUp()方法,这样就使得对象能够重新建立起序列化时未能保留的各种状态,例如,数据库连接等.

用例子给你说明一下,代码如下:

  1. <?php 
  2. //声明一个类 
  3. class dog { 
  4. var $name
  5. var $age
  6. var $owner
  7. function dog($in_name="unnamed",$in_age="0",$in_owner="unknown") { 
  8. $this->name = $in_name
  9. $this->age = $in_age
  10. $this->owner = $in_owner
  11. function getage() { 
  12. return ($this->age * 365); 
  13. function getowner() { 
  14. return ($this->owner); 
  15. function getname() { 
  16. return ($this->name); 
  17. //实例化这个类 
  18. $ourfirstdog = new dog("Rover",12,"Lisa and Graham"); 
  19. //用serialize函数将这个实例转化为一个序列化的字符串 
  20. $dogdisc = serialize($ourfirstdog); 
  21. print $dogdisc//$ourfirstdog 已经序列化为字符串 O:3:"dog":3:{s:4:"name";s:5:"Rover";s:3:"age";i:12;s:5:"owner";s:15:"Lisa and Graham";} 
  22. /* 
  23. ----------------------------------------------------------------------------------------- 
  24. 在这里你可以将字符串 $dogdisc 存储到任何地方如 session,cookie,数据库,php文件 
  25. ----------------------------------------------------------------------------------------- 
  26. */ 
  27. //我们在此注销这个类 
  28. unset($ourfirstdog); 
  29. ?> 
  30. b.php 
  31. <?php 
  32.  
  33. ?> 
  34. <?php 
  35. //声明一个类 
  36. class dog { 
  37. var $name
  38. var $age
  39. var $owner
  40. function dog($in_name="unnamed",$in_age="0",$in_owner="unknown") { 
  41. $this->name = $in_name
  42. $this->age = $in_age
  43. $this->owner = $in_owner
  44. function getage() { 
  45. return ($this->age * 365); 
  46. function getowner() { 
  47. return ($this->owner); 
  48. function getname() { 
  49. return ($this->name); 
  50. }//开源软件:phpfensi.com 
  51. /*还原操作   */ 
  52. /* 
  53. ----------------------------------------------------------------------------------------- 
  54. 在这里将字符串 $dogdisc 从你存储的地方读出来如 session,cookie,数据库,php文件 
  55. ----------------------------------------------------------------------------------------- 
  56. */ 
  57. $dogdisc='O:3:"dog":3:{s:4:"name";s:5:"Rover";s:3:"age";i:12;s:5:"owner";s:15:"Lisa and Graham";}'
  58. //我们在这里用 unserialize() 还原已经序列化的对象 
  59. $pet = unserialize($dogdisc); //此时的 $pet 已经是前面的 $ourfirstdog 对象了 
  60. //获得年龄和名字属性 
  61. $old = $pet->getage(); 
  62. $name = $pet->getname(); 
  63. //这个类此时无需实例化可以继续使用,而且属性和值都是保持在序列化之前的状态 
  64. print "Our first dog is called $name and is $old days old<br>"
  65. ?> 

序列化与反序列化语法解析不一致带来的安全隐患,PHP string serialize() 相关源码分析,代码如下:

  1. static inline void php_var_serialize_string(smart_str *buf, char *str, int len) /* {{{ */  
  2.   {  
  3. smart_str_appendl(buf, "s:", 2);  
  4. smart_str_append_long(buf, len);  
  5. smart_str_appendl(buf, ":\"", 2);  
  6. smart_str_appendl(buf, str, len);  
  7. smart_str_appendl(buf, "\";", 2);  

通过上面的代码片段可以看到 serialize() 对 string 序列化处理方式如下:

  1. $str = 'ryatsyne';  
  2.   var_dump(serialize($str));  
  3.   // $str serialized string output  
  4.   // s:8:"ryatsyne"; 

ii.PHP string unserialize() 相关源码分析

unserialize() 函数对 string 的反序列化则分为两种,一种是对 `s:` 格式的序列化 string 进行处理,代码如下:

  1. switch (yych) {  
  2.   ...  
  3. case 's':  goto yy9;  
  4. ...  
  5.   yy9:  
  6. yych = *(YYMARKER = ++YYCURSOR);  
  7. if (yych == ':') goto yy46;  
  8. goto yy3;  
  9. ...  
  10.   yy46:  
  11. yych = *++YYCURSOR;  
  12. if (yych == '+') goto yy47;  
  13. if (yych <= '/') goto yy18;  
  14. if (yych <= '9') goto yy48;  
  15. goto yy18;  
  16.   yy47:  
  17. yych = *++YYCURSOR;  
  18. if (yych <= '/') goto yy18;  
  19. if (yych >= ':') goto yy18;  
  20.   yy48:  
  21. ++YYCURSOR;  
  22. if ((YYLIMIT - YYCURSOR) < 2) YYFILL(2);  
  23. yych = *YYCURSOR;  
  24. if (yych <= '/') goto yy18;  
  25. if (yych <= '9') goto yy48;  
  26. if (yych >= ';') goto yy18;  
  27. yych = *++YYCURSOR;  
  28. if (yych != '"') goto yy18;  
  29. ++YYCURSOR;  
  30. {  
  31. size_t len, maxlen;  
  32. char *str;  
  33.  
  34. len = parse_uiv(start + 2);  
  35. maxlen = max - YYCURSOR;  
  36. if (maxlen < len) {  
  37.   *p = start + 2;  
  38.   return 0;  
  39. }  
  40.  
  41. str = (char*)YYCURSOR;  
  42.  
  43. YYCURSOR += len;  
  44.  
  45. if (*(YYCURSOR) != '"') {  
  46.   *p = YYCURSOR;  
  47.   return 0;  
  48. }  
  49.   // 确保格式为 s:x:"x"  
  50.  
  51. YYCURSOR += 2;  
  52. *p = YYCURSOR;  
  53.   // 注意这里,*p 指针直接后移了两位,也就是说没有判断 " 后面是否为 ;  
  54.  
  55. INIT_PZVAL(*rval);  
  56. ZVAL_STRINGL(*rval, str, len, 1);  
  57. return 1; 

另一种是对 S: 格式的序列 string 进行处理,此格式在 serialize() 函数序列化处理中并没有定义,代码如下:

  1. static char *unserialize_str(const unsigned char **p, size_t *len, size_t maxlen)  
  2.   {  
  3. size_t i, j;  
  4. char *str = safe_emalloc(*len, 1, 1);  
  5. unsigned char *end = *(unsigned char **)p+maxlen;  
  6.  
  7. if (end < *p) {  
  8.   efree(str);  
  9.   return NULL;  
  10. }  
  11.  
  12. for (i = 0; i < *len; i++) {  
  13.   if (*p >= end) {  
  14. efree(str);  
  15. return NULL;  
  16.   }  
  17.   if (**p != '\\') {  
  18. str[i] = (char)**p;  
  19.   } else {  
  20. unsigned char ch = 0;  
  21.  
  22. for (j = 0; j < 2; j++) {  
  23.   (*p)++;  
  24.   if (**p >= '0' && **p <= '9') {  
  25. ch = (ch << 4) + (**p -'0');  
  26.   } else if (**p >= 'a' && **p <= 'f') {  
  27. ch = (ch << 4) + (**p -'a'+10);  
  28.   } else if (**p >= 'A' && **p <= 'F') {  
  29. ch = (ch << 4) + (**p -'A'+10);  
  30.   } else {  
  31. efree(str);  
  32. return NULL;  
  33.   }  
  34. }  
  35. str[i] = (char)ch;  
  36.   }  
  37.   (*p)++;  
  38. }  
  39. str[i] = 0;  
  40. *len = i;  
  41. return str;  
  42.   }  
  43.   // 上面的函数是对 \72\79\61\74\73\79\6e\65 这样十六进制形式字符串进行转换  
  44.   ...  
  45.   switch (yych) {  
  46. ...  
  47. case 'S':  goto yy10;  
  48. // 处理过程与 s: 相同  
  49. if ((str = unserialize_str(&YYCURSOR, &len, maxlen)) == NULL) {  
  50.   return 0;  
  51. }  
  52. // 处理过程与 s: 相同 

从上面的代码片段可以看到 unserialize() 对序列化后的 string 反序列化处理如下:

  1. $str1 = 's:8:"ryatsyne";';  
  2.   $str2 = 's:8:"ryatsyne"t';  
  3.   $str3 = 'S:8:"\72\79\61\74\73\79\6e\65"';  
  4.   var_dump(unserialize($str));  
  5.   // $str1, $str2 and $str3 unserialized string output  
  6.   // ryatsyne; 

iii.语法解析处理不一致导致的安全隐患

从上述分析过程可以看到 PHP 在反序列化 string 时没有严格按照序列化格式 s:x:"x"; 进行处理,没有对 " 后面的是否存在 ; 进行判断,同时增加了对十六进制形式字符串的处理,这样前后处理的不一致让人很费解,同时由于 PHP 手册中对此没有详细的说明,大部分程序员对此处理过程并不了解,这可能导致其在编码过程中出现疏漏,甚至导致严重的安全问题.

回到文章开头提到的 IPB 漏洞上,利用这个 funny feature of PHP 可以很容易的 bypass safeUnserialize() 函数的过滤,代码如下:

  1. * mixed safe_unserialize(string $serialized)  
  2. * Safely unserialize, that is only unserialize string, numbers and arrays, not objects  
  3. *  
  4. * @license Public Domain  
  5. * @author dcz (at) phpbb-seo (dot) com  
  6. */  
  7. static public function safeUnserialize( $serialized )  
  8. {  
  9. // unserialize will return false for object declared with small cap o  
  10. // as well as if there is any ws between O and :  
  11. if ( is_string$serialized ) && strpos$serialized"\0" ) === false )  
  12. {  
  13. if ( strpos$serialized'O:' ) === false )  
  14. {  
  15. // the easy case, nothing to worry about  
  16. // let unserialize do the job  
  17. return @unserialize( $serialized );  
  18. }  
  19. else if ( ! preg_match('/(^|;|{|})O:[+\-0-9]+:"/'$serialized ) )  
  20. {  
  21. // in case we did have a string with O: in it,  
  22. // but it was not a true serialized object  
  23. return @unserialize( $serialized );  
  24. }  
  25. return false;  
  26. }  
  27.  
  28. // a:1:{s:8:"ryatsyne"tO:8:"ryatsyne":0:{}}  
  29. // 只要构造类似的序列化字符串就可以轻易突破这里的过滤了

Tags: serialize() unserialize()

分享到: