$_SERVER['PHP_SELF']与basename()函数造成的漏洞


[Zer0pts2020]Can you guess it?

进入题目:

点击查看源码:

源码为:

<?php
include 'config.php'; // FLAG is defined in config.php

if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
  exit("I don't know what you are thinking, but I won't let you read it :)");
}

if (isset($_GET['source'])) {
  highlight_file(basename($_SERVER['PHP_SELF']));
  exit();
}

$secret = bin2hex(random_bytes(64));
if (isset($_POST['guess'])) {
  $guess = (string) $_POST['guess'];
  if (hash_equals($secret, $guess)) {
    $message = 'Congratulations! The flag is: ' . FLAG;
  } else {
    $message = 'Wrong.';
  }
}
?>
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Can you guess it?</title>
  </head>
  <body>
    <h1>Can you guess it?</h1>
    <p>If your guess is correct, I'll give you the flag.</p>
    <p><a href="?source">Source</a></p>
    <hr>
<?php if (isset($message)) { ?>
    <p><?= $message ?></p>
<?php } ?>
    <form action="index.php" method="POST">
      <input type="text" name="guess">
      <input type="submit">
    </form>
  </body>
</html>

这是一个猜字符给flag的游戏。从获得FLAG那里开始溯源:

if (hash_equals($secret, $guess)) {
    $message = 'Congratulations! The flag is: ' . FLAG;
} 

hash_equals:比较两个字符串,无论它们是否相等,本函数的时间消耗是恒定的。 本函数可以用在需要防止时序攻击的字符串比较场景中,例如,可以用在比较 crypt() 密码哈希值的场景。

这个函数是非常安全的,再来看$secret:

$secret = bin2hex(random_bytes(64));

random_bytes(int $length):生成适合于加密使用的任意长度的加密随机字节字符串,例如在生成salt、密钥或初始化向量时,一般配合bin2hex()函数使用。

bin2hex():把ASCII字符串转换为十六进制值

这道题生成的随机字符串是64位,也是非常难爆破的,所以在hash_equals和random_bytes上做文章是几乎不可能的。

再往上看:

if (isset($_GET['source'])) {
    highlight_file(basename($_SERVER['PHP_SELF']));
    exit();
}

如果存在source参数,则读取路径中的文件名部分。

再来看看$_SERVER[‘PHP_SELF’]

$_SERVER['PHP_SELF'] 将返回相对于网站根目录的路径及 PHP 程序名称。

所以我们的思路就来了:

当我访问index.php时,我可以在后面加上一些东西,比如/index.php/config.php,这样的话下面这个代码匹配到config.php后将执行:

if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
  exit("I don't know what you are thinking, but I won't let you read it :)");
}

// “\/*”匹配0次或多次“/”

并输出I don't know what you are thinking, but I won't let you read it :)

但当我们访问/index.php/config.phpa时,这样访问的却仍然是index.php:

)但此时经过basename()后,传进highlight_file()函数的文件名就变成了config.phpa,如果能绕过那个正则,让basename()返回config.php的话,就可以得到config.php源码了,而题目告诉FLAG就在config.php里。所以说,那个随机数就是个障眼法。

那么我们就想办法绕过那个正则,结尾\/*$的意思是出现0或多个“/”然后结束字符串,所以此正则本意是不允许config.php作为$_SERVER[‘PHP_SELF’]的结尾,但我们可以利用空字符串绕过正则:basename()会去掉文件名中的不可见字符,使用超过ascii码范围的字符就可以绕过:

ASCII值范围为0-255,但ASCII表范围为0-127,所以ASCII码并没有规定编号为128~255的字符,也就是我们传入128以上的数值,即可绕过正则,比如128 -> 0x80

https://bugs.php.net/bug.php?id=62119 找到了basename()函数的一个问题,它会去掉文件名开头的非ASCII值:

var_dump(basename("xffconfig.php")); // => config.php
var_dump(basename("config.php/xff")); // => config.php

所以最终payload为:

/index.php/config.php/%80?source

这样/index.php/config.php运行的是index.php,但是basename()获取到的是config.php,然后再通过?source即可读取到config.php的源码:

如下图得到flag:


Author: WHOAMI
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source WHOAMI !
评论
  TOC