作者:xd_xd
作者博客:http://xdxd.love/
solveme.peng.kr winter sleep
solveme是一個CTF的練習平臺,其中winter sleep題目是這樣的。
<?php
error_reporting(0);
require __DIR__.'/lib.php';
if(isset($_GET['time'])){
if(!is_numeric($_GET['time'])){
echo 'The time must be number.';
}else if($_GET['time'] < 60 * 60 * 24 * 30 * 2){
echo 'This time is too short.';
}else if($_GET['time'] > 60 * 60 * 24 * 30 * 3){
echo 'This time is too long.';
}else{
sleep((int)$_GET['time']);
echo $flag;
}
echo '<hr>';
}
highlight_file(__FILE__);
輸入一個字符串,通過is_numric的判斷,要大于5184000小于777600,最后通過sleep函數,就可以輸出flag。顯然,如果輸入一個較大的數,會sleep很長時間。需要一個數大于5184000,然后int之后又要是一個很小的數。
解決的方案是這樣的:
<?php
echo 60 * 60 * 24 * 30 * 2;
echo "\n";
echo 6e6;
echo "\n";
echo (int)'6e6';
echo "\n";
echo 60 * 60 * 24 * 30 * 3;
可以看以上腳本輸出內容:
5184000
6000000
6
7776000
使用科學計數法。
看了一些writeup,只是給出了解決的辦法,但是并沒有詳細的說明,為什么會這樣。有的地方提到說是弱類型,雖然這幾次比較存在類型的自動轉換,但是跟我理解的弱類型的自動轉換存在差異。所以想要探究一番。
黑盒測試

可以看到當接收到科學計數法表示的字符串跟一個整型變量運算(‘6e6’-0),6e6自動并不是自動轉換成了int型,而是轉換成了float,所以最終的數字是float型的6000000。最后兩行代碼可以直接的說明了問題。使用int強制轉換一個科學計數法表示的字符串,轉換過程中并不能識別科學計數法,只是把e當做普通字符了。效果跟6a6是一樣的。而用float轉成浮點數,則可以成功識別科學計數法。

feature or bug
我的感覺是這應該是php的一個bug。同一個字符串,轉換成int型和float型有著兩種解釋。正常的邏輯應該是(int)’6e6’ = (int)(float)’6e6’。這樣才比較符合正常的一個理解邏輯。
找了幾個php的版本,分別做了下測試:
測試腳本如下:
import docker
client = docker.from_env()
php_versions = ['5.3','5.4','5.5','5.6', '7.0','7.1','7.2']
for version in(php_versions):
php = "php:"+version + "-cli"
print(php)
print("echo((int)'6e6')")
print(client.containers.run("php:"+version+"-cli", '''php -r "echo((int)'6e6');"'''))
print("echo((float)'6e6')")
print(client.containers.run("php:"+version+"-cli", '''php -r "echo((float)'6e6');"''’))
結果如下:
? dockerpy python phptest.py
php:5.3-cli
echo((int)'6e6')
6
echo((float)'6e6')
6000000
php:5.4-cli
echo((int)'6e6')
6
echo((float)'6e6')
6000000
php:5.5-cli
echo((int)'6e6')
6
echo((float)'6e6')
6000000
php:5.6-cli
echo((int)'6e6')
6
echo((float)'6e6')
6000000
php:7.0-cli
echo((int)'6e6')
6
echo((float)'6e6')
6000000
php:7.1-cli
echo((int)'6e6')
6000000
echo((float)'6e6')
6000000
php:7.2-cli
echo((int)'6e6')
6000000
echo((float)'6e6')
6000000
在php7.0以前的版本中(int)’6e6’結果是6,但是在7.1以后的版本中,(int)’6e6’已經是6000000,符合(int)’6e6’ = (int)(float)’6e6’這個邏輯了。
php內核分析
以下內容引用自《php7內核剖析》:
PHP是弱類型語言,不需要明確的定義變量的類型,變量的類型根據使用時的上下文所決定,也就是變量會根據不同表達式所需要的類型自動轉換,比如求和,PHP會將兩個相加的值轉為long、double再進行加和。每種類型轉為另外一種類型都有固定的規則,當某個操作發現類型不符時就會按照這個規則進行轉換,這個規則正是弱類型實現的基礎。 除了自動類型轉換,PHP還提供了一種強制的轉換方式:
- (int)/(integer):轉換為整形 integer
- (bool)/(boolean):轉換為布爾類型 boolean
- (float)/(double)/(real):轉換為浮點型 float
- (string):轉換為字符串 string
- (array):轉換為數組 array
- (object):轉換為對象 object
- (unset):轉換為 NULL
無論是自動類型轉換還是強制類型轉換,不是每種類型都可以轉為任意其他類型。
4.1.3 轉換為整型
其它類型轉為整形的轉換規則:
- NULL:轉為0
- 布爾型:false轉為0,true轉為1
- 浮點型:向下取整,比如:(int)2.8 => 2
- 字符串:就是C語言strtoll()的規則,如果字符串以合法的數值開始,則使用該數值,否則其值為 0(零),合法數值由可選的正負號,后面跟著一個或多個數字(可能有小數點),再跟著可選的指數部分
- 數組:很多操作不支持將一個數組自動整形處理,比如:array() + 2,將報error錯誤,但可以強制把數組轉為整形,非空數組轉為1,空數組轉為0,沒有其他值
- 對象:與數組類似,很多操作也不支持將對象自動轉為整形,但有些操作只會拋一個warning警告,還是會把對象轉為1操作的,這個需要看不同操作的處理情況
- 資源:轉為分配給這個資源的唯一編號
具體處理:
ZEND_API zend_long ZEND_FASTCALL _zval_get_long_func(zval *op) { try_again:
switch (Z_TYPE_P(op)) {
case IS_NULL:
case IS_FALSE:
return 0;
case IS_TRUE:
return 1;
case IS_RESOURCE:
//資源將轉為zend_resource->handler
return Z_RES_HANDLE_P(op);
case IS_LONG:
return Z_LVAL_P(op);
case IS_DOUBLE:
return zend_dval_to_lval(Z_DVAL_P(op));
case IS_STRING:
//字符串的轉換調用C語言的strtoll()處理
return ZEND_STRTOL(Z_STRVAL_P(op), NULL, 10);
case IS_ARRAY:
//根據數組是否為空轉為0,1
return zend_hash_num_elements(Z_ARRVAL_P(op)) ? 1 : 0;
case IS_OBJECT:
{
zval dst;
convert_object_to_type(op, &dst, IS_LONG, convert_to_long);
if (Z_TYPE(dst) == IS_LONG) {
return Z_LVAL(dst);
} else {
//默認情況就是1
return 1;
}
}
case IS_REFERENCE:
op = Z_REFVAL_P(op);
goto try_again;
EMPTY_SWITCH_DEFAULT_CASE()
}
return 0;
}
4.1.4 轉換為浮點型
除字符串類型外,其它類型轉換規則與整形基本一致,就是整形轉換結果加了一位小數,字符串轉為浮點數由zend_strtod()完成,這個函數非常長,定義在zend_strtod.c中,這里不作說明。
書中提到,字符串轉換為整型,是C語言strtol()的規則,由ZEND_STRTOL函數完成的,字符串轉換成浮點數,是用zend_strtod函數完成的。
對比一下C語言的strtol和strtod

strtol不能識別科學計數法,字符串6e6轉成整型是6,而strtod可以識別科學計數法,6e6轉成浮點數是6000000。
動態調試php內核
編譯debug版php。
git clone http://git.php.net/repository/php-src.git
cd php-src
git checkout PHP-7.0
./buildconf
./configure --disable-all --enable-debug --prefix=$HOME/myphp
make
make install
gdb調試
gdb --args php -r "echo((int)'6e6');”
在類型轉換函數上下斷點:
b _zval_get_long_func

可以看到使用zend_strtol函數進行轉換。

zent_strtol 直接是使用strtoll。
調試一下7.1版本php

可以看到7.1版中使用了新的函數is_numeric_string替代strtoll。注釋中說明使用新函數是為了避免strtoll的溢出問題,自己實現了is_number_string函數來替代strtoll。然而并沒有提到科學計數法表示的字符串的問題。但是實際實現上跟strtoll有不同。妥善的處理科學計數法表示的數字。

最終的字符串轉整型的邏輯如下:

最終的處理邏輯是如果發現了小數點或者數字e,就采用zend_strtod來處理,這樣就跟字符串轉浮點數是一模一樣的處理邏輯了。所以最終的結果也就符合了(int)’6e6’ = (int)(float)’6e6’這個邏輯。
思考
那么這到底是個bug還是feature呢。最終的結果來看,php7.0及以前的版本使用strtoll轉字符串到整型,7.1以后的版本使用了strtod來轉換。所以strtoll不能識別科學計數法表示的數字是不是一個bug。
參考資料
- solveme.peng.kr的中文writeup:http://www.freebuf.com/articles/web/165537.html
- php源碼調試的文章:https://gywbd.github.io/posts/2016/2/debug-php-source-code.html
- php7內核分析:https://github.com/pangudashu/php7-internal
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/566/