file

虽然 PHP 是世界上最好的语言,但是也有一些因为弱类型语言的安全性问题出现。WordPress 历史上就出现过由于 PHP 本身的缺陷而造成的一些安全性问题,如 CVE-2014-0166 中的 Cookie 伪造就是利用了 PHP Hash 比较的缺陷。 当然一般这种情况实战中用到的不是很多,但是在 CTF 竞赛中却是一个值得去考察的一个知识点,特此记录总结之。

精度绕过缺陷

理论

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

下面看一个有趣的例子:

echo 1.0000000000001; // 13 位小数
echo "<br>";
echo 1.000000000000001; // 15 位小数
echo "<br>";
echo 2018.9999999999; // 10 位小数
echo "<br>";
echo 2018.99999999999; // 11 位小数
echo "<br>";
echo floor((0.1 + 0.7) * 10);
echo "<br>";
echo((0.1 + 0.7) - 0.7999999999999999); // 16 位小数
echo "<br>";
echo((0.1 + 0.7) - 0.799999999999999); // 15 位小数

file

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

实践

问鼎杯 2017 老眼昏花

网上很多 write-up 感觉就像是看着答案写 write-up,个人感觉真正的 write-up 中应该体现自己的思考在里面。

题目描述

file

题目言简意赅,让我们把 2017 这个值传递给服务器。

考察点
  • PHP 浮点精确度
write-up

what year is this? 所以第一反应是直接给 year 参数赋值为 2017:

?year=2017

然而结果如下:

file

有提示了,说明 year 这个参数是对的,但是 2017 中不可以出现 7,这里如果不了解 PHP 精度的话,肯定是对 2017 进行各种编码绕过, 但是这里对编码也进行过滤了:

file

所以最后一种可能就是利用 PHP 精度来绕过:

?year=2016.99999999999

file

类型转换的缺陷

理论

PHP 提供了 is_numeric 函数,用来变量判断是否为数字。PHP 弱类型语言的一个特性,当一个整形和一个其他类型行比较的时候,会先把其他类型 intval 数字化再比。

实践

is_numeric() 用于判断是否是数字,通常配合数值判断。

案例代码

error_reporting(0);
$flag = 'flag{1S_numer1c_Not_S4fe}';
$id = $_GET['id'];
is_numeric($id) ? die("Sorry....") : NULL;
if ($id > 665) {
    echo $flag;
} 

考察点

  • PHP 类型转换缺陷

write-up

分析下代码:首先对 GET 方式提交的参数 id 的值进行检验。id 通过 is_numeric 函数来判断是否为数字,如果为数字的话,GG。如果不是数字的话,和 665 进行比较,id 的值大于 665 的时候输出 flag

乍看上去又好像不可能这里,但是如果知道 PHP 弱类型语言的一个特性,当一个整形和一个其他类型行比较的时候,会先把其他类型 intval 数字化再比。这个特性的话就可以很好的绕过。

http://localhost/?id=666gg

file

松散比较符的缺陷

理论

PHP 比较相等性的运算符有两种,一种是严格比较,另一种是松散比较。

如果比较一个数字和字符串或者比较涉及到数字内容的字符串,则字符串会被转换成数值并且比较按照数值来进行。

file

严格比较符

严格比较符,会先判断两种字符串的类型是否相等,再比较。

===   //全等
!==   //不全等

file

松散比较符

松散比较符,会先将字符串类型转换成相同,再比较。

==   //等于
!=   //不等

file

PHP 会根据变量的值,自动把变量转换为正确的数据类型。这一点和 C 和 C++ 以及 Java 之类的语言明显不同。虽然这样 PHP 方便了程序员,但是随之而来却会带来一些安全性的问题。

一个简单的例子

$a = null;
$b = false;
echo $a == $b;
echo "<br>";
$c = "";
$d = 0;
echo $c == $d;

由于 PHP 对变量自动转换的特性,这里面的 $a == $b$c == $d 均为真。所以页面输出的结果为:

file

一个深入的例子

var_dump(0 == 'gg'); // true
echo "<br>";

var_dump(0 === 'gg'); // false
echo "<br>";

var_dump(1 == 'gg'); // false
echo "<br>";

var_dump(1 == '1gg'); // true
echo "<br>";

var_dump(1 == 'gg1'); // false
echo "<br>";

var_dump('0e123' == '0e456'); // true
echo "<br>";

var_dump('0e123' == '0eabc'); // false

下面结合 PHP 相等性比较缺陷再解释下会好懂一点:

var_dump(0 == 'gg');  // true
var_dump(0 === 'gg'); // false
var_dump(1 == 'gg');  // false

0 与 gg 进行松散性质的不严格比较,会将 gg 转换为数值,强制转换,由于 gg 是字符串,转化的结果是 0,所以输出 true

0 与 gg 进行严格性质的严格比较,这里的 gg 是字符串类型,和 int 类型的 0 不相等,所以输出 false

0 与 gg 进行松散性质的不严格比较,会将 gg 转换为数值,强制转换。由于 gg 是字符串,转化的结果是 0,不等于 1,所以输出 false

var_dump(1 == '1gg'); // true 
var_dump(1 == 'gg1'); // false

1 与 1gg 进行松散性质的不严格比较,这里 1gg 被强制转换为 int 类型的时候会从字符串的第一位开始做判断进行转换,这里的 1gg 第一位是 1,所以 1gg 被转换为 1,所以输出 true

1 与 gg1 进行严格性质的严格比较,字符串 gg1 的第一位不是数字,所以它被强制转换为 0,所以输出 false

var_dump('0e123' == '0e456');  // true
var_dump('0e123' == '0eabc');  // false

这里比较特殊,字符串中出现了 0e,PHP 手册介绍如下:

当一个字符串被当作一个数值来取值,其结果和类型如下:

如果该字符串没有包含 .eE 并且其数字值在整型的范围之内(由 PHP_INT_MAX 所定义),该字符串将被当成 integer 来取值。其它所有情况下都被作为 float 来取值。

该字符串的开始部分决定了它的值。如果该字符串以合法的数值开始,则使用该数值。否则其值为 0(零)。合法数值由可选的正负号,后面跟着一个或多个数字(可能有小数点),再跟着可选的指数部分。指数部分由 eE 后面跟着一个或多个数字构成。

0e1230e456 相互不严格性质比较的时候,会将 0e 这类字符串识为科学技术法的数字,0 的无论多少次方都是零,所以相等, 输出 true

0e1230eabc 相互进行不严格性质比较的时候,本应该将 0e 这类字符串识为科学技术法的数字, 但是这里的 0e 后面跟着的是 abc,数学中科学计数的指数不可以包含字母。所以这里字符串中虽然是 0e 开头,但是后面的 abc 却不符合科学技法的规范,所以输出是 false

实践

md5 绕过(Hash 比较缺陷)

南京邮电大学网络攻防训练平台中一道比较经典的 md5 collision 题,关于这道题目的 WriteUp 网上很多,但是真正深入分析的少之又少~~

题目描述

$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";
}

考察点

  • 简单的 PHP 代码审计
  • PHP 弱类型的 Hash 比较缺陷

write-up

从源码中可以得输入一个 a 的参数的变量,a 首先不等于 QNKCDZO 并且 a 的 md5 值必须等于 QNKCDZO 加密后的 md5 值。

乍一看好像不可能存在这样的值,但是这里 QNKCDZO 加密后的 md5 值为 0e830400451993494058024219903391 这里是 0e 开头的,在进行等于比较的时候,PHP 把它当作科学计数法,0 的无论多少次方都是零。 所以这里利用上面的弱类型的比较的缺陷来进行解题:

?a=s155964671a

file

字符串加密后 md5 为 0exxxx 的字符串(x 必须是 10 进制数字)列表:

字符串 md5
QNKCDZO 0e830400451993494058024219903391
240610708 0e462097431906509019562988736854
aabg7XSs 0e087386482136013740957780965295
aabC9RqS 0e041022518165728065344349536299
s878926199a 0e545993274517709034328855841020
s155964671a 0e342768416822451524974117254469
s214587387a 0e848240448830537924465865611904
s214587387a 0e848240448830537924465865611904
s878926199a 0e545993274517709034328855841020
s1091221200a 0e940624217856561557816327384675
s1885207154a 0e509367213418206700842008763514

sha1 md5 加密函数漏洞缺陷

理论

md5()sha1() 对一个数组进行加密将返回 NULL。

实践

Boston Key Party CTF 2015: Prudential

题目描述

I dont think sha1 isbroken.Prove me wrong.

题目给了一个登录框:

file

考察点

  • sha1() 函数漏洞缺陷

write-up

源代码给出如下:

<html>
<head>
    <title>level1</title>
    <link rel='stylesheet' href='style.css' type='text/css'>
</head>
<body>

<?php
require 'flag.php';
if (isset($_GET['name']) and isset($_GET['password'])) {
    if ($_GET['name'] == $_GET['password'])
        print 'Your password can not be your name.';
    else if (sha1($_GET['name']) === sha1($_GET['password']))
        die('Flag: ' . $flag);
    else
        print '<p class="alert">Invalid password.</p>';
}
?>

<section class="login">
    <div class="title">
        <a href="./index.txt">Level 1</a>
    </div>

    <form method="get">
        <input type="text" required name="name" placeholder="Name"/><br/>
        <input type="text" required name="password" placeholder="Password"/><br/>
        <input type="submit"/>
    </form>
</section>
</body>
</html>

分析一下核心登录代码如下:

if ($_GET['name'] == $_GET['password'])
    print 'Your password can not be your name.';
else if (sha1($_GET['name']) === sha1($_GET['password']))
    die('Flag: ' . $flag);

GET 类型提交了两个字段 namepassword,获得 flag 要求的条件是:

  • name != password
  • sha1(name) == sha1(password)

这个乍看起来这是不可能的,但是这里利用 sha1() 函数在处理数组的时候由于无法处理将返回 NULL 可以绕过 if 语句的验证,if 条件成立将获得 flag

构造语句如下:

?name[]=a&password[]=b

这里符合了 2 个拿到 flag 的条件:

  • a 不等于 b
  • namepassword 由于是数组,经过 sha1() 函数加密后都返回 NULL

拿到 flagI_think_that_I_just_broke_sha1

拓展总结

经过验证,不仅 sha1() 函数无法处理数组,这里 md5() 函数也有同样的问题,在处理数组的时候,都将返回 NULL

测试代码如下:

error_reporting(0);
$flag = 'flag{I_think_that_I_just_broke_md5}';
if (isset($_GET['username']) and isset($_GET['password'])) {
    if ($_GET['username'] == $_GET['password'])
        print 'Your password can not be your username.';
    else if (md5($_GET['username']) === sha1($_GET['password']))
        die($flag);
    else
        print 'Invalid password';
}

这里面的核心代码如下:

if ($_GET['username'] == $_GET['password'])

并且得满足:

if (md5($_GET['username']) === sha1($_GET['password']))

同样利用 md5() 函数无法处理数组的这个漏洞,构造 GET 请求拿到 flag

?username[]=a&password[]=b

file

字符串处理函数漏洞缺陷

理论

strcmp() 函数:比较两个字符串(区分大小写)。

用法如下:

int strcmp ( string $str1 , string $str2 )

参数 str1第一个字符串。
参数 str2第二个字符串。
如果 str1 小于 str2 返回 < 0
如果 str1 大于 str2 返回 > 0
如果两者相等,返回 0。

这个函数接受到了不符合的类型,例如数组类型,函数将发生错误。但是在 5.3 之前的 PHP 中,显示了报错的警告信息后,将 return 0!也就是虽然报了错,但却判定其相等了。

ereg() 函数:字符串正则匹配。
strpos() 函数:查找字符串在另一字符串中第一次出现的位置,对大小写敏感。

这两个函数都是用来处理字符串的,但是在传入数组参数后都将返回 NULL

实践

Boston Key Party CTF 2015: Northeastern Univ

题目描述

Of course, a timing attack might be the answer, but Im quite sure that you can do better than that.

题目给了一个登录框:

file

考察点

  • 字符串处理函数漏洞缺陷

write-up

给出源代码如下:

<html>
<head>
    <title>level3</title>
    <link rel='stylesheet' href='style.css' type='text/css'>
</head>
<body>

<?php
require 'flag.php';

if (isset($_GET['password'])) {
    if (strcmp($_GET['password'], $flag) == 0)
        die('Flag: ' . $flag);
    else
        print '<p class="alert">Invalid password.</p>';
}
?>

<section class="login">
    <div class="title">
        <a href="./index.txt">Level 3</a>
    </div>

    <form method="get">
        <input type="text" required name="password" placeholder="Password"/><br/>
        <input type="submit"/>
    </form>
</section>
</body>
</html>

分析一下核心登录代码如下:

if (strcmp($_GET['password'], $flag) == 0)

这里使用了 == 松散比较了 $flag 和通过 GET 方式提交的 password 的值,如果想等的话,拿到 flag

这里用的是 == 松散性质的比较,再利用字符串处理数组时将会报错,在 5.3 之前的 PHP 中,显示了报错的警告信息后,将 return 0。所有这里将 password 参数指定为数组,利用函数漏洞拿到 flag

file

拓展总结

除了 strcmp() 函数外,ereg()strpos() 函数在处理数组的时候也会异常,返回 NULL

测试代码如下:

error_reporting(0);
$flag = 'flag{P@ssw0rd_1s_n0t_s4fe_By_d0uble_Equ4ls}';
if (isset ($_GET['password'])) {
    if (ereg("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE)
        echo 'You password must be alphanumeric';
    else if (strpos($_GET['password'], '--') !== FALSE)
        die($flag);
    else
        echo 'Invalid password';
}

将参数 password 赋值一个数组传递进去:

http://localhost/?password[]=gg

ereg() 函数是处理字符串的,传入数组后返回 NULLNULLFALSE,是不恒等(===)的,满足第一个 if 条件;而 strpos() 函数也是处理字符串的,传入数组后返回 NULLNULL !== FALSE,满足条件,拿到 flag

file

parse_str 函数变量覆盖缺陷

理论

parse_str 函数的作用就是解析字符串并注册成变量,在注册变量之前不会验证当前变量是否存在,所以直接覆盖掉已有变量。

void parse_str ( string $str [, array &$arr ] )

str 输入的字符串。
arr 如果设置了第二个变量 arr,变量将会以数组元素的形式存入到这个数组,作为替代。

实践

测试代码:

error_reporting(0);
$flag = 'flag{V4ri4ble_M4y_Be_C0verEd}';
if (empty($_GET['b'])) {
    show_source(__FILE__);
    die();
} else {
    $a = "www.sqlsec.com";
    $b = $_GET['b'];
    @parse_str($b);
    if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
        echo $flag;
    } else {
        exit('your answer is wrong~');
    }
}

考察点

  • parse_str 变量覆盖缺陷

write-up

找到核心代码:

@parse_str($b); 这里使用了 parse_str 函数来传递 b 的变量值。

if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO'))

这里用到的是文章上面的知识点 md5() 函数缺陷。

因为这里用到了 parse_str 函数来传递 bif 的语句的条件是拿 $a[0] 来比较的,有因为这里的变量 a 的值已经三是固定的了。

整体代码乍看起来又不可能,但是利用变量覆盖函数的缺陷这里可以对 a 的变量进行重新赋值,后面的的 if 语句再利用本文前面提到的 md5() 比较缺陷进行绕过:

http://localhost/?b=a[0]=240610708

file


原文链接