开始练习【红日团队】的PHP-Audit-Labs 代码审计 Day9
链接:https://github.com/hongriSec/PHP-Audit-Labs
感兴趣的同学可以去练习练习
预备知识:
内容题目均来自 PHP SECURITY CALENDAR 2017
Day 9 - Rabbit代码如下:
class LanguageManager {
public function loadLanguage() {
$lang = $this->getBrowserLanguage();
$sanitizedLang = $this->sanitizeLanguage($lang);
require_once("/lang/$sanitizedLang");
}
private function getBrowserLanguage() {
$lang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en';
return $lang;
}
private function sanitizeLanguage($language) {
return str_replace('../', '', $language);
}
}
(new LanguageManager())->loadLanguage();
漏洞解析 :
这一题考察的是一个 str_replace
函数过滤不当造成的任意文件包含漏洞。在代码 第14行
处,
return str_replace('../', '', $language);
程序仅仅只是将../
字符替换成空,这并不能阻止攻击者进行攻击。
例如攻击者使用payload:....//
或者..././
,在经过程序的 str_replace
函数处理后,都会变成 ../
,所以上图程序中的str_replace
函数过滤是有问题的。
str_replace () 函数:
(PHP 4, PHP 5, PHP 7)
功能:
str_replace() 函数替换字符串中的一些字符(区分大小写)。
- 如果搜索的字符串是一个数组,那么它将返回一个数组。
- 如果搜索的字符串是一个数组,那么它将对数组中的每个元素进行查找和替换。
- 如果同时需要对某个数组进行查找和替换,并且需要执行替换的元素少于查找到的元素的数量,那么多余的元素将用空字符串进行替换。
- 如果是对一个数组进行查找,但只对一个字符串进行替换,那么替代字符串将对所有查找到的值起作用。
- 注释:该函数是区分大小写的。请使用
str_ireplace()
函数执行不区分大小写的搜索。- 注释:该函数是二进制安全的。
定义:
str_replace(find,replace,string,count)
说明:
参数 | 描述 |
---|---|
find | 必需。规定要查找的值。 |
replace | 必需。规定替换 find 中的值的值。 |
string | 必需。规定被搜索的字符串。 |
count | 可选。一个变量,对替换数进行计数。 |
范例:
结果:
Array ( [0] => blue [1] => pink [2] => green [3] => yellow )
Replacements: 1
实例分析:
本次实例分析,我们选取的是
Metinfo 6.0.0
版本。
漏洞POC 本站提供安全工具、程序(方法)可能带有攻击性,仅供安全研究与教学之用,风险自负!
漏洞分析:
漏洞文件在 app/system/include/module/old_thumb.class.php
中,我们发现程序将变量$dir
中出现的../
和 ./
字符替换成空字符串(第4行处),猜想开发者应该是有考虑到路径穿越问题,所以做了此限制。具体代码如下:
public function doshow(){
global $_M;
$dir = str_replace(array('../','./'), '', $_GET['dir']);
if(substr(str_replace($_M['url']['site'], '', $dir),0,4) == 'http' && strpos($dir, './') === false){
header("Content-type: image/jpeg");
ob_start();
readfile($dir);
ob_flush();
flush();
die;
}
if($_M['form']['pageset']){
$path = $dir."&met-table={$_M['form']['met-table']}&met-field={$_M['form']['met-field']}";
}else{
$path = $dir;
}
$image = thumb($path,$_M['form']['x'],$_M['form']['y']);
if($_M['form']['pageset']){
$img = explode('?', $image);
$img = $img[0];
}else{
$img = $image;
}
if($img){
header("Content-type: image/jpeg");
ob_start();
readfile(PATH_WEB.str_replace($_M['url']['site'], '', $img));
ob_flush();
flush();
}
}
接着在第5行
处,用strstr
函数判断 $dir
变量中是否含有 http
字符串,如果有
,则读取加载$dir
变量,并以图片方式显示出来。这里猜测开发者的意图是,加载远程图片。
strstr () 函数:
(PHP 4, PHP 5, PHP 7)
功能:
strstr()
函数搜索字符串在另一字符串中是否存在,如果是,返回该字符串及剩余部分,否则返回 FALSE
。
定义:
strstr(string,search,before_search)
说明:
参数 | 描述 |
---|---|
string | 必需。规定被搜索的字符串。 |
search | 必需。规定要搜索的字符串。如果该参数是数字,则搜索匹配该数字对应的 ASCII 值的字符。 |
before_search | 可选。一个默认值为 “false” 的布尔值。如果设置为 “true”,它将返回 search 参数第一次出现之前的字符串部分。 |
范例
结果:
o world!
Hello
然而这段代码是可以绕过的。
例如
我们使用 payload:.....///http/.....///.....///.....///.....///etc/passwd
,过滤后实际就变成: ../http/../../../../etc/passwd
。
例如代码:
<?php
$dir = str_replace(array('../','./'), '', $_GET['dir']);
if(strstr($dir,'http')){
echo readfile($dir);
}else{
die("none");
}
?>
效果如下:
接下来,我们要做的就是搜索程序在哪里调用了这个文件。用phpstorm
加载整个项目文件,按住 Ctrl+Shift+F
键,搜索关键词old_thumb
,发现在 include/thumb.php
文件中调用 old_thumb
类,搜索结果如下图:
我们在include/thumb.php
文件中,可以看到 M_CLASS
定义为 old_thumb
<?php
# MetInfo Enterprise Content Management System
# Copyright (C) MetInfo Co.,Ltd (http://www.metinfo.cn). All rights reserved.
define('M_NAME', 'include');
define('M_MODULE', 'include');
define('M_CLASS', 'old_thumb');
define('M_ACTION', 'doshow');
require_once '../app/system/entrance.php';
# This program is an open source system, commercial use, please consciously to purchase commercial license.
# Copyright (C) MetInfo Co., Ltd. (http://www.metinfo.cn). All rights reserved.
?>
而 M_ACTION
定义为 doshow
。我们接着跟进到 app/system/entrance.php
文件中
// app/system/include/class/load.class.php
require_once PATH_SYS_CLASS.'load.class.php';
load::module();
在该文件的末尾(92行)可以看包含了app/system/include/class/load.class.php
文件,引入了 load
类,然后调用了load
类的module
方法。
我们跟进module
方法,并查看各个变量的赋值情况( app/system/include/class/load.class.php 文件)(113-121行):
public static function module($path = '', $modulename = '', $action = '') {
if (!$path) {
if (!$path) $path = PATH_OWN_FILE;
if (!$modulename) $modulename = M_CLASS;
if (!$action) $action = M_ACTION;
if (!$action) $action = 'doindex';
}
return self::_load_class($path, $modulename, $action);
}
上图程序最后调用了load
类的 _load_class
方法,我们跟进该方法,详细代码如下:
private static function _load_class($path, $classname, $action = '') {
$classname=str_replace('.class.php', '', $classname);
$is_myclass = 0;
if(!self::$mclass[$classname]){
if(file_exists($path.$classname.'.class.php')){
require_once $path.$classname.'.class.php';
} else {
echo str_replace(PATH_WEB, '', $path).$classname.'.class.php is not exists';
exit;
}
$myclass = "my_{$classname}";
if (file_exists($path.'myclass/'.$myclass.'.class.php')) {
$is_myclass = 1;
require_once $path.'myclass/'.$myclass.'.class.php';
}
}
if ($action) {
if (!class_exists($classname)) {
die($classname . ' ' . $action . ' class\'s file is not exists!!!');
}
if(self::$mclass[$classname]){
$newclass = self::$mclass[$classname];
}else{
if($is_myclass){
$newclass = new $myclass;
}else{
$newclass = new $classname;
}
self::$mclass[$classname] = $newclass;
}
if ($action!='new') {
if(substr($action, 0, 2) != 'do'){
die($action.' function no permission load!!!');
}
if(method_exists($newclass, $action)){
call_user_func(array($newclass, $action));
}else{
die($action.' function is not exists!!!');
}
}
return $newclass;
}
return true;
}
可以看到上图代码第27行
处实例化了一个 old_thumb
类对象,然后在第36行
处调用了 old_thumb
类的 doshow
方法,doshow
方法中的 $dir
变量就是用户可以控制的。以上便是完整的攻击过程分析,下面我们看看具体如何进行攻击。
漏洞利用:
实际上攻击的话就很简单了,因为 $dir
变量是直接通过 GET
请求 获取的,然后用 str_replace
方法处理,而 str_replace
方法处理又有问题,所以我们构造 payload 如下:
http://localhost/metInfo/include/thumb.php?dir=.....///http/.....///最终用户授权许可协议.txt
成功读取 最终用户授权许可协议.txt
文件。
修复建议:
关于修复建议,这里先抛出个问题给大家,针对这个案例,下面的修复代码是否可行?
$dir = str_replace(array('..','//'), '', $_GET['dir']);
咋一看,这个代码好像完美地修复了路径穿越问题,但是,我们在修复代码的时候一定要结合实际情况。比如在metinfo
中,程序这里原来的功能是加载远程图片,使用上面的修复代码,会导致正常的图片链接无法加载,这种修复肯定是无效的。这里给出我的修复代码,如下图:
结语
再次感谢【红日团队】