作者:xd_xd
作者博客:http://xdxd.love/
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转成浮点数,则可以成功识别科学计数法。
我的感觉是这应该是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’这个逻辑了。
以下内容引用自《php7内核剖析》:
PHP是弱类型语言,不需要明确的定义变量的类型,变量的类型根据使用时的上下文所决定,也就是变量会根据不同表达式所需要的类型自动转换,比如求和,PHP会将两个相加的值转为long、double再进行加和。每种类型转为另外一种类型都有固定的规则,当某个操作发现类型不符时就会按照这个规则进行转换,这个规则正是弱类型实现的基础。 除了自动类型转换,PHP还提供了一种强制的转换方式:
无论是自动类型转换还是强制类型转换,不是每种类型都可以转为任意其他类型。
4.1.3 转换为整型
其它类型转为整形的转换规则:
具体处理:
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。
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 --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。