把複雜的資料類型壓縮到一個字串中
serialize() 把變數和它們的值編碼成文本形式
unserialize() 恢複原先變數
eg:
代碼如下 |
複製代碼 |
$stooges = array('Moe','Larry','Curly'); $new = serialize($stooges); print_r($new); echo "<br />"; print_r(unserialize($new)); 結果: a:3:{i:0;s:3:"Moe";i:1;s:5:"Larry";i:2;s:5:"Curly";} Array ( [0] => Moe [1] => Larry [2] => Curly ) |
當把這些序列化的資料放在URL中在頁面之間會傳遞時,需要對這些資料調用urlencode(),以確保在其中的URL元字元進行處理:
代碼如下 |
複製代碼 |
$shopping = array('Poppy seed bagel' => 2,'Plain Bagel' =>1,'Lox' =>4); 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()進行處理:
代碼如下 |
複製代碼 |
$new_cart = unserialize(stripslashes($cart)); //如果magic_quotes_gpc開啟 $new_cart = unserialize($cart); |
如果magic_quotes_runtime是啟用的,那麼在向檔案中寫入序列化的資料之前必須用addslashes()進行處理,而在讀取它們之前則必須用stripslashes()進行處理:
代碼如下 |
複製代碼 |
$fp = fopen('/tmp/cart','w'); fputs($fp,addslashes(serialize($a))); fclose($fp); //如果magic_quotes_runtime開啟 $new_cat = unserialize(stripslashes(file_get_contents('/tmp/cart'))); //如果magic_quotes_runtime關閉 $new_cat = unserialize(file_get_contents('/tmp/cart')); |
在啟用了magic_quotes_runtime的情況下,從資料庫中讀取序列化的資料也必須經過stripslashes()的處理,儲存到資料庫中的序列化資料必須要經過addslashes()的處理,以便能夠適當地儲存。
代碼如下 |
複製代碼 |
mysql_query("insert into cart(id,data) values(1,'".addslashes(serialize($cart))."')"); $rs = mysql_query('select data from cart where id=1'); $ob = mysql_fetch_object($rs); //如果magic_quotes_runtime開啟 $new_cart = unserialize(stripslashes($ob->data)); //如果magic_quotes_runtime關閉 $new_cart = unserialize($ob->data); |
當對一個對象進行還原序列化操作時,PHP會自動地調用其__wakeUp()方法。這樣就使得對象能夠重建立立起序列化時未能保留的各種狀態。例如:資料庫連接等。
用例子給你說明一下
代碼如下 |
複製代碼 |
<?php //聲明一個類 class dog { var $name; var $age; var $owner; function dog($in_name="unnamed",$in_age="0",$in_owner="unknown") { $this->name = $in_name; $this->age = $in_age; $this->owner = $in_owner; } function getage() { return ($this->age * 365); } function getowner() { return ($this->owner); } function getname() { return ($this->name); } } //執行個體化這個類 $ourfirstdog = new dog("Rover",12,"Lisa and Graham"); //用serialize函數將這個執行個體轉化為一個序列化的字串 $dogdisc = serialize($ourfirstdog); 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";} /* ----------------------------------------------------------------------------------------- 在這裡你可以將字串 $dogdisc 儲存到任何地方如 session,cookie,資料庫,php檔案 ----------------------------------------------------------------------------------------- */ //我們在此登出這個類 unset($ourfirstdog); ?> b.php <?php ?> <?php //聲明一個類 class dog { var $name; var $age; var $owner; function dog($in_name="unnamed",$in_age="0",$in_owner="unknown") { $this->name = $in_name; $this->age = $in_age; $this->owner = $in_owner; } function getage() { return ($this->age * 365); } function getowner() { return ($this->owner); } function getname() { return ($this->name); } } /*還原作業 */ /* ----------------------------------------------------------------------------------------- 在這裡將字串 $dogdisc 從你儲存的地方讀出來如 session,cookie,資料庫,php檔案 ----------------------------------------------------------------------------------------- */ $dogdisc='O:3:"dog":3:{s:4:"name";s:5:"Rover";s:3:"age";i:12;s:5:"owner";s:15:"Lisa and Graham";}'; //我們在這裡用 unserialize() 還原已經序列化的對象 $pet = unserialize($dogdisc); //此時的 $pet 已經是前面的 $ourfirstdog 對象了 //獲得年齡和名字屬性 $old = $pet->getage(); $name = $pet->getname(); //這個類此時無需執行個體化可以繼續使用,而且屬性和值都是保持在序列化之前的狀態 print "Our first dog is called $name and is $old days old<br>"; ?> |
序列化與還原序列化文法解析不一致帶來的安全隱患
. PHP string serialize() 相關源碼分析
------------------------------------
代碼如下 |
複製代碼 |
static inline void php_var_serialize_string(smart_str *buf, char *str, int len) /* {{{ */ { smart_str_appendl(buf, "s:", 2); smart_str_append_long(buf, len); smart_str_appendl(buf, ":\"", 2); smart_str_appendl(buf, str, len); smart_str_appendl(buf, "\";", 2); } |
通過上面的程式碼片段可以看到 serialize() 對 string 序列化處理方式如下:
代碼如下 |
複製代碼 |
$str = 'ryatsyne'; var_dump(serialize($str)); // $str serialized string output // s:8:"ryatsyne"; |
ii. PHP string unserialize() 相關源碼分析
---------------------------------------
unserialize() 函數對 string 的還原序列化則分為兩種,一種是對 `s:` 格式的序列化 string 進行處理:
代碼如下 |
複製代碼 |
switch (yych) { ... case 's': goto yy9; ... yy9: yych = *(YYMARKER = ++YYCURSOR); if (yych == ':') goto yy46; goto yy3; ... yy46: yych = *++YYCURSOR; if (yych == '+') goto yy47; if (yych <= '/') goto yy18; if (yych <= '9') goto yy48; goto yy18; yy47: yych = *++YYCURSOR; if (yych <= '/') goto yy18; if (yych >= ':') goto yy18; yy48: ++YYCURSOR; if ((YYLIMIT - YYCURSOR) < 2) YYFILL(2); yych = *YYCURSOR; if (yych <= '/') goto yy18; if (yych <= '9') goto yy48; if (yych >= ';') goto yy18; yych = *++YYCURSOR; if (yych != '"') goto yy18; ++YYCURSOR; { size_t len, maxlen; char *str; len = parse_uiv(start + 2); maxlen = max - YYCURSOR; if (maxlen < len) { *p = start + 2; return 0; } str = (char*)YYCURSOR; YYCURSOR += len; if (*(YYCURSOR) != '"') { *p = YYCURSOR; return 0; } // 確保格式為 s:x:"x" YYCURSOR += 2; *p = YYCURSOR; // 注意這裡,*p 指標直接後移了兩位,也就是說沒有判斷 " 後面是否為 ; INIT_PZVAL(*rval); ZVAL_STRINGL(*rval, str, len, 1); return 1; |
另一種是對 S: 格式的序列 string 進行處理(此格式在 serialize() 函數序列化處理中並沒有定義):
代碼如下 |
複製代碼 |
static char *unserialize_str(const unsigned char **p, size_t *len, size_t maxlen) { size_t i, j; char *str = safe_emalloc(*len, 1, 1); unsigned char *end = *(unsigned char **)p+maxlen; if (end < *p) { efree(str); return NULL; } for (i = 0; i < *len; i++) { if (*p >= end) { efree(str); return NULL; } if (**p != '\\') { str[i] = (char)**p; } else { unsigned char ch = 0; for (j = 0; j < 2; j++) { (*p)++; if (**p >= '0' && **p <= '9') { ch = (ch << 4) + (**p -'0'); } else if (**p >= 'a' && **p <= 'f') { ch = (ch << 4) + (**p -'a'+10); } else if (**p >= 'A' && **p <= 'F') { ch = (ch << 4) + (**p -'A'+10); } else { efree(str); return NULL; } } str[i] = (char)ch; } (*p)++; } str[i] = 0; *len = i; return str; } // 上面的函數是對 \72\79\61\74\73\79\6e\65 這樣十六進位形式字串進行轉換 ... switch (yych) { ... case 'S': goto yy10; // 處理過程與 s: 相同 if ((str = unserialize_str(&YYCURSOR, &len, maxlen)) == NULL) { return 0; } // 處理過程與 s: 相同 |
從上面的程式碼片段可以看到 unserialize() 對序列化後的 string 還原序列化處理如下:
代碼如下 |
複製代碼 |
$str1 = 's:8:"ryatsyne";'; $str2 = 's:8:"ryatsyne"t'; $str3 = 'S:8:"\72\79\61\74\73\79\6e\65"'; var_dump(unserialize($str)); // $str1, $str2 and $str3 unserialized string output // ryatsyne; |
iii. 文法解析處理不一致導致的安全隱患
-----------------------------
從上述分析過程可以看到 PHP 在還原序列化 string 時沒有嚴格按照序列化格式 s:x:"x"; 進行處理,沒有對 " 後面的是否存在 ; 進行判斷,同時增加了對十六進位形式字串的處理,這樣前後處理的不一致讓人很費解,同時由於 PHP 手冊中對此沒有詳細的說明,大部分程式員對此處理過程並不瞭解,這可能導致其在編碼過程中出現疏漏,甚至導致嚴重的安全問題。
回到文章開頭提到的 IPB 漏洞上,利用這個 funny feature of PHP 可以很容易的 bypass safeUnserialize() 函數的過濾:)
代碼如下 |
複製代碼 |
* mixed safe_unserialize(string $serialized) * Safely unserialize, that is only unserialize string, numbers and arrays, not objects * * @license Public Domain * @author dcz (at) phpbb-seo (dot) com */ static public function safeUnserialize( $serialized ) { // unserialize will return false for object declared with small cap o // as well as if there is any ws between O and : if ( is_string( $serialized ) && strpos( $serialized, "\0" ) === false ) { if ( strpos( $serialized, 'O:' ) === false ) { // the easy case, nothing to worry about // let unserialize do the job return @unserialize( $serialized ); } else if ( ! preg_match('/(^|;|{|})O:[+\-0-9]+:"/', $serialized ) ) { // in case we did have a string with O: in it, // but it was not a true serialized object return @unserialize( $serialized ); } } return false; } // a:1:{s:8:"ryatsyne"tO:8:"ryatsyne":0:{}} // 只要構造類似的序列化字串就可以輕易突破這裡的過濾了 |