[Zer0pts2020]Can you guess it?

Oyst3r 于 2023-10-05 发布

1.ctf 之国庆专辑哈哈哈哈哈,这道题一开始进去的界面是一个框框,一开始以为是一个 sql 注入,但点击 source 竟然给了源码,二话不说开始审计吧

<?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>

2.下面具体说一下这段代码都干了什么

1.源码一开始有个正则匹配,主要就是不能检测到config.php/(这个/属于可有可没有,因为正则表达式后面有一个*,这个代表的意思是0个到无穷个,也就是说有无穷个/也是可以的),这个倒是好绕过,后面随便加个东西就行
2.然后检测是否用source这个get传参,如果有的话就截取最后的这个文件
tip:这里大家可以先了解一下$_SERVER['PHP_SELF']东西还有basename()这个函数,简单解释一下就是$_SERVER['PHP_SELF']这个会返回与网站根目录相对的路径,然后basename()这个函数的话会输出$_SERVER['PHP_SELF']这个路径的最末尾的文件
3.$secret变量的话,就是获取了一个随机的数字,而且数字很复杂,很大,没有规律,这个用到的是和操作系统一样获取随机数的方法,所以这这一步肯定是安全的,然后就是guess(也就是页面中那个框框),它让我们输入一个和这个$secret变量一模一样的数字,而且用到了hash_equals这个东西来比较,不是平时用的===(这个我知道是为了防止时序攻击,所以这个是真的安全,若有漏洞就是0day,所以这个肯定也是没法绕过的)

3.okok 大概解释了一遍这个源码的流程,总而言之,肯定不能靠下面那个 guess 去获得 flag,因为这样写真的是最安全的写法,但大家有没有想过,如果这个题的预期解是靠 guess 的话,那上面为什么要大费周章的去写那样的代码呢,所以这个突破点肯定是上面的那些代码,然后再仔细分析一遍

4.这个一开始进去的页面应该是一个 index.php,然后经过 highlight_file(basename($_SERVER[‘PHP_SELF’]));这个之后会获得 index.php,然后会将 index.php 的源码显示出来,那我们可不可以把 config.php 里面的东西给展示一下呢?然后我也是胡乱测试一下啊,发现一个神奇的事情,就是在访问/index.php/config.php 的时候,服务器也会默认我们访问的是 index.php,所以 source 这个参数还是可以用的,然后搭了一个本地靶场,测试的时候发现虽然服务器也会默认我们访问的是 index.php,但是 SERVER[‘PHP_SELF’]这个东西竟然还是/index.php/config.php,这就找到突破点了,应该就是这两个地方起冲突了,导致我们有机会,那么我就写了第一个 payload

http://2db62442-ccea-4f10-afca-1fcc59c0b854.node4.buuoj.cn:81/index.php/config.php/?source

然后它就提示

这很正常啊,我理解,因为这里有个正则欸,检测到了 config.php/,所以肯定不行,然后现在问题就很明显了,要找到一个字符,既能绕过正则匹配(这个正则就是个纸老虎,只要不以 config.php/结尾就行),也能让这个 basename()函数正常的匹配到 config.php

5.比如下面这个 payload,肯定可以绕过正则,但是报错了,这也很正常,因为 basename()匹配到的是 1

http://2db62442-ccea-4f10-afca-1fcc59c0b854.node4.buuoj.cn:81/index.php/config.php/1?source

然后就去搜这个 basename 有没有啥办法可以绕过啊,结果搜到了 php 官方其实已经给过这个 bug

With the default locale setting "C", basename() drops non-ASCII-chars at the beginning of a filename.
在使用默认语言环境设置时,basename() 会删除文件名开头的非 ASCII 字符

然后 github 也有师傅给出了找这个字符的脚本

<?php
highlight_file(__FILE__);
$filename = 'index.php';
for($i=0; $i<255; $i++){
    $filename = $filename.'/'.chr($i);
    if(basename($filename) === "index.php"){
        echo $i.'<br>';
    }
    $filename = 'index.php';
}
?>

6.然后就很简单了,随便加一个里面的字符就行(ascii 值为 47、128-255 的字符均可以绕过 basename()),下面是最终 payload

http://2db62442-ccea-4f10-afca-1fcc59c0b854.node4.buuoj.cn:81/index.php/config.php/%ff?source