透过一道CTF题目学到的小知识点 – 作者:Chr1sto

0x00 正文

先放出这道题的源码

<?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]);

审计源码可以看到定义了一个trick类,trick1和trick2两个变量,当程序结束,__destruct()方法被调用

trick1被强转为字符串,接下来判断两个变量的长度,当两个变量的长度不大于5时进入下面的逻辑

接着往下,当trick1和trick2不相等并且md5值相等时才能输出flag

本质上是一道简单的题,但是关键点就在于如何绕过,让strick1被强转成字符串后的md5值还和strick2相等

那么重点来了

我们构造序列化的时候需要传的值为

trick1=0.01

trick2=0.1*0.1

为什么要传这两个值呢,别急,我们一点一点往下看

先看我构造序列化的代码

<?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");
        }
    }
} 

$chr1sto = new trick();
$chr1sto->trick1=0.01;
$chr1sto->trick2=0.1*0.1;
$s = serialize($chr1sto);
print($s);

按照我们之前分析的逻辑,当我们赋值了这两个变量,程序结束,调用析构方法,先将trick1强转为字符串

然后判断两个变量的长度是否大于5

先看一下我们序列化出的结果

1598452498.png!small

O:5:”trick”:2:{s:6:”trick1″;d:0.01;s:6:”trick2″;d:0.010000000000000002;}

有意思的事情出现了

我们赋值的trick2=0.1*0.1反序列化后的结果为0.010000000000000002

按照正常的理解不应该是0.01吗

1598452708.png!small1598452726.png!small

那为什么会这样呢

这其实是一个浮点数精度的问题

官方文档里是这么说的:

浮点数的精度
浮点数的精度有限。尽管取决于系统,PHP 通常使用 IEEE 754 双精度格式,则由于取整而导致的最大相对误差为 1.11e-16。非基本数学运算可能会给出更大误差,并且要考虑到进行复合运算时的误差传递。

此外,以十进制能够精确表示的有理数如 0.1 或 0.7,无论有多少尾数都不能被内部所使用的二进制精确表示,因此不能在不丢失一点点精度的情况下转换为二进制的格式。这就会造成混乱的结果:例如,floor((0.1+0.7)*10) 通常会返回 7 而不是预期中的 8,因为该结果内部的表示其实是类似 7.9999999999999991118…。

所以永远不要相信浮点数结果精确到了最后一位,也永远不要比较两个浮点数是否相等。如果确实需要更高的精度,应该使用任意精度数学函数或者 gmp 函数。

通俗来说,像0.1,0.7,0.58之类的浮点数十进制值转换为二进制是一个无限长的值,也就是说没办法用二进制精准表示,这个是所有语言的通病,因为计算机底层就是这么规定的,所以会出现精度丢失的问题

以下情况会出现精度丢失的问题

  • 十进制数的二进制表示形式可能不精确。
  • 使用的数字之间类型不匹配(例如,混合使用浮点型和双精度型)

同样的,这样的浮点数在参与基本运算的时候也会出现精度丢失的情况

经过多次尝试后我发现

显示输出时候会显示为

1598508729.png!small

1598509015.png!small

1598458723.png!small

这是因为php最大精度是14位

如果超出14位会衰减

看下面的例子

1598509437.png!small

1598509504.png!small

别看php现在把0.100000000000001当成了0.1

实际上数还是那个数

还要注意四舍五入的情况

1598509819.png!small

理解了这些之后让我们回到题目

传入这两个值之后会进行strlen()和md5()的判断

这两个函数也受精度缺陷的影响,所以可以绕过这两个函数

这道题就这么出来了

1598510707.png!small

另外,经过我多次尝试

php5到php7.0的php版本中

如0.1,0.7这样的浮点数单独或经过运算,序列化后打印出的字符串是0.100000000000001,0.69999999999999996这样精度丢失的样子

1598511549.png!small

特殊的是在php5.2.17中打印出来的序列化后的字符串有了更多的位数

1598511661.png!small

而在php7.1中就会序列化为0.1和0.7

1598511409.png!small

这到底是为什么我也没搞明白,有知道的师傅教教我Orz

0x01

文章的最后再加两个这道题的另外两种解法

第一种

两个变量的值都为INF

payload:

O:5:”trick”:2:{s:6:”trick1″;d:INF;s:6:”trick2″;d:INF;}

1598513194.png!small

第二种

两个变量的值都为NAN

payload:

O:5:”trick”:2:{s:6:”trick1″;d:NAN;s:6:”trick2″;d:NAN;}

1598513573.png!small

来源:freebuf.com 2020-08-27 16:45:17 by: Chr1sto

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论