PHP ftp_raw() 函数

PHP ftp_raw() 函数用于向FTP服务器发送原始命令并返回服务器的响应数组。

高级功能:此函数允许执行标准FTP函数之外的自定义命令,适合需要与FTP服务器进行低级交互的场景。

语法

ftp_raw(resource $ftp, string $command): array

参数说明

参数 描述
ftp 必需。FTP连接的标识符,由ftp_connect()ftp_ssl_connect()返回
command 必需。要发送到FTP服务器的原始命令字符串

返回值

  • 返回一个数组,包含服务器对命令的响应行
  • 数组的每个元素是服务器返回的一行响应文本
  • 如果连接无效或发生错误,可能返回空数组或抛出警告

常见FTP命令

命令 描述 等效PHP函数
USER username 指定登录用户名 ftp_login()的一部分
PASS password 指定登录密码 ftp_login()的一部分
PWD 打印工作目录 ftp_pwd()
CWD path 改变工作目录 ftp_chdir()
LIST [path] 列出目录内容 ftp_rawlist()
NLST [path] 名称列表 ftp_nlist()
PASV 进入被动模式 ftp_pasv()
RETR file 检索(下载)文件 ftp_get()
STOR file 存储(上传)文件 ftp_put()
DELE file 删除文件 ftp_delete()
MKD path 创建目录 ftp_mkdir()
RMD path 删除目录 ftp_rmdir()
RNFR oldname 重命名从 ftp_rename()的一部分
RNTO newname 重命名为 ftp_rename()的一部分
SIZE file 获取文件大小 ftp_size()
MDTM file 获取文件修改时间 ftp_mdtm()
SYST 获取系统类型 ftp_systype()
QUIT 断开连接 ftp_quit()ftp_close()

示例

示例1:基本用法 - 发送原始FTP命令

<?php
// 连接FTP服务器
$ftp_server = "ftp.example.com";
$ftp_user = "username";
$ftp_pass = "password";

$ftp_conn = ftp_connect($ftp_server);
if (!$ftp_conn) {
    die("无法连接到 $ftp_server");
}

// 使用原始命令登录
echo "使用原始命令登录...\n";
$response = ftp_raw($ftp_conn, "USER $ftp_user");
print_r($response);

$response = ftp_raw($ftp_conn, "PASS $ftp_pass");
print_r($response);

// 检查是否登录成功
$response = ftp_raw($ftp_conn, "SYST");
echo "服务器系统类型: ";
print_r($response);

// 获取当前目录
$response = ftp_raw($ftp_conn, "PWD");
echo "当前目录: ";
print_r($response);

// 列出目录内容
$response = ftp_raw($ftp_conn, "LIST");
echo "目录列表: \n";
foreach ($response as $line) {
    echo "  $line\n";
}

// 关闭连接
$response = ftp_raw($ftp_conn, "QUIT");
print_r($response);

ftp_close($ftp_conn);
?>

示例2:自定义FTP客户端类

<?php
class RawFTPClient {
    private $conn;
    private $debug = false;

    public function __construct($server, $port = 21, $timeout = 90) {
        $this->conn = ftp_connect($server, $port, $timeout);
        if (!$this->conn) {
            throw new Exception("无法连接到FTP服务器: $server");
        }

        $this->log("连接到服务器: $server");
    }

    public function login($username, $password) {
        $this->sendCommand("USER $username");
        $response = $this->sendCommand("PASS $password");

        // 检查登录是否成功(响应代码 230)
        if ($this->getResponseCode($response[0]) == 230) {
            $this->log("登录成功: $username");
            return true;
        }

        throw new Exception("登录失败: " . implode("\n", $response));
    }

    public function sendCommand($command) {
        $this->log("发送命令: $command");
        $response = ftp_raw($this->conn, $command);
        $this->log("服务器响应: " . implode(" | ", $response));
        return $response;
    }

    public function getSystemType() {
        $response = $this->sendCommand("SYST");
        if (count($response) > 0) {
            // 移除响应代码,只返回系统类型
            return trim(substr($response[0], 4));
        }
        return "未知";
    }

    public function getDetailedList($path = "") {
        $command = $path ? "LIST $path" : "LIST";
        return $this->sendCommand($command);
    }

    public function getSimpleList($path = "") {
        $command = $path ? "NLST $path" : "NLST";
        return $this->sendCommand($command);
    }

    public function getFileSize($filename) {
        $response = $this->sendCommand("SIZE $filename");
        if (count($response) > 0) {
            $code = $this->getResponseCode($response[0]);
            if ($code == 213) {
                // 213 后面是文件大小
                return intval(trim(substr($response[0], 4)));
            }
        }
        return -1;
    }

    public function getFileModificationTime($filename) {
        $response = $this->sendCommand("MDTM $filename");
        if (count($response) > 0) {
            $code = $this->getResponseCode($response[0]);
            if ($code == 213) {
                // 返回格式: 213 YYYYMMDDHHMMSS[.sss]
                $timestamp = trim(substr($response[0], 4));
                return $this->parseFTPTimestamp($timestamp);
            }
        }
        return 0;
    }

    public function createDirectory($dirname) {
        $response = $this->sendCommand("MKD $dirname");
        $code = $this->getResponseCode($response[0]);
        return $code == 257; // 257 表示目录创建成功
    }

    public function deleteDirectory($dirname) {
        $response = $this->sendCommand("RMD $dirname");
        $code = $this->getResponseCode($response[0]);
        return $code == 250; // 250 表示请求的文件操作完成
    }

    public function rename($from, $to) {
        $response = $this->sendCommand("RNFR $from");
        $code = $this->getResponseCode($response[0]);
        if ($code != 350) {
            return false; // 350 表示需要进一步的信息
        }

        $response = $this->sendCommand("RNTO $to");
        $code = $this->getResponseCode($response[0]);
        return $code == 250;
    }

    public function setPassiveMode($passive = true) {
        $command = $passive ? "PASV" : "PORT";
        $response = $this->sendCommand($command);
        $code = $this->getResponseCode($response[0]);
        return $passive ? $code == 227 : $code == 200;
    }

    public function enableDebug($debug = true) {
        $this->debug = $debug;
    }

    public function close() {
        $this->sendCommand("QUIT");
        ftp_close($this->conn);
        $this->log("连接已关闭");
    }

    private function getResponseCode($responseLine) {
        // FTP响应以3位数字代码开头
        if (strlen($responseLine) >= 3 && is_numeric(substr($responseLine, 0, 3))) {
            return intval(substr($responseLine, 0, 3));
        }
        return 0;
    }

    private function parseFTPTimestamp($timestamp) {
        // 格式: YYYYMMDDHHMMSS 或 YYYYMMDDHHMMSS.sss
        $timestamp = substr($timestamp, 0, 14);
        if (strlen($timestamp) == 14) {
            $year = substr($timestamp, 0, 4);
            $month = substr($timestamp, 4, 2);
            $day = substr($timestamp, 6, 2);
            $hour = substr($timestamp, 8, 2);
            $minute = substr($timestamp, 10, 2);
            $second = substr($timestamp, 12, 2);
            return mktime($hour, $minute, $second, $month, $day, $year);
        }
        return 0;
    }

    private function log($message) {
        if ($this->debug) {
            echo "[FTP] " . date('Y-m-d H:i:s') . " - $message\n";
        }
    }

    public function __destruct() {
        if ($this->conn) {
            $this->close();
        }
    }
}

// 使用示例
try {
    $ftp = new RawFTPClient('localhost');
    $ftp->enableDebug(true);

    // 登录
    $ftp->login('user', 'pass');

    // 获取系统信息
    echo "服务器类型: " . $ftp->getSystemType() . "\n";

    // 获取详细列表
    $list = $ftp->getDetailedList();
    echo "目录内容:\n";
    foreach ($list as $item) {
        echo "  $item\n";
    }

    // 获取文件大小
    $size = $ftp->getFileSize('test.txt');
    echo "文件大小: " . ($size > 0 ? "$size 字节" : "未知") . "\n";

    // 设置被动模式
    $ftp->setPassiveMode(true);

    // 创建目录
    if ($ftp->createDirectory('new_folder')) {
        echo "目录创建成功\n";
    }

    // 关闭连接
    $ftp->close();

} catch (Exception $e) {
    echo "错误: " . $e->getMessage() . "\n";
}
?>

示例3:扩展FTP功能 - 实现FTP服务器信息探测

<?php
class FTPScanner {
    private $conn;

    public function connect($server, $port = 21) {
        $this->conn = @ftp_connect($server, $port, 5);
        return $this->conn !== false;
    }

    public function scanServer() {
        if (!$this->conn) {
            return ['error' => '未建立连接'];
        }

        $info = [
            'server' => '',
            'features' => [],
            'system' => '',
            'welcome' => '',
            'supported_commands' => []
        ];

        try {
            // 获取欢迎信息(通常发送空命令或等待初始响应)
            $response = ftp_raw($this->conn, "NOOP"); // NOOP 是空操作命令
            $info['welcome'] = implode("\n", $response);

            // 获取系统类型
            $response = ftp_raw($this->conn, "SYST");
            if (count($response) > 0) {
                $info['system'] = $response[0];
            }

            // 获取服务器特性(FEAT 命令)
            $response = ftp_raw($this->conn, "FEAT");
            if (count($response) > 0) {
                // 移除第一行(211-Features:)和最后一行(211 End)
                array_shift($response); // 移除 "211-Features:"
                array_pop($response);   // 移除 "211 End"

                foreach ($response as $line) {
                    $line = trim($line);
                    if (!empty($line)) {
                        $info['features'][] = $line;
                    }
                }
            }

            // 测试支持的FTP命令
            $commands = [
                'ABOR', 'ACCT', 'ALLO', 'APPE', 'CDUP', 'CWD', 'DELE', 'HELP',
                'LIST', 'MKD', 'MODE', 'NLST', 'NOOP', 'PASS', 'PASV', 'PORT',
                'PWD', 'QUIT', 'REIN', 'REST', 'RETR', 'RMD', 'RNFR', 'RNTO',
                'SITE', 'SIZE', 'STAT', 'STOR', 'STOU', 'STRU', 'SYST', 'TYPE',
                'USER', 'XCUP', 'XMKD', 'XPWD', 'XRMD'
            ];

            foreach ($commands as $cmd) {
                $response = ftp_raw($this->conn, $cmd);
                if (count($response) > 0) {
                    $code = intval(substr($response[0], 0, 3));
                    // 500 或 502 表示命令不被识别
                    if ($code != 500 && $code != 502) {
                        $info['supported_commands'][$cmd] = [
                            'response' => $response[0],
                            'code' => $code
                        ];
                    }
                }
            }

            // 获取STAT信息(服务器状态)
            $response = ftp_raw($this->conn, "STAT");
            if (count($response) > 0) {
                $info['status'] = implode("\n", $response);
            }

            // 尝试获取HELP信息
            $response = ftp_raw($this->conn, "HELP");
            if (count($response) > 0) {
                $info['help'] = implode("\n", $response);
            }

        } catch (Exception $e) {
            $info['error'] = $e->getMessage();
        }

        return $info;
    }

    public function testAnonymousAccess() {
        // 测试匿名登录
        $response = ftp_raw($this->conn, "USER anonymous");
        if (count($response) > 0 && strpos($response[0], '331') === 0) {
            $response = ftp_raw($this->conn, "PASS anonymous@example.com");
            if (count($response) > 0 && strpos($response[0], '230') === 0) {
                return [
                    'success' => true,
                    'message' => '匿名登录成功',
                    'response' => $response[0]
                ];
            }
        }

        return [
            'success' => false,
            'message' => '匿名登录失败'
        ];
    }

    public function bruteForceTest($usernames, $passwords) {
        $results = [];

        foreach ($usernames as $user) {
            foreach ($passwords as $pass) {
                $response = ftp_raw($this->conn, "USER $user");
                if (count($response) > 0 && strpos($response[0], '331') === 0) {
                    $response = ftp_raw($this->conn, "PASS $pass");
                    if (count($response) > 0 && strpos($response[0], '230') === 0) {
                        $results[] = [
                            'username' => $user,
                            'password' => $pass,
                            'success' => true
                        ];
                        // 找到后退出当前用户测试
                        break;
                    }
                }
            }
        }

        return $results;
    }

    public function close() {
        if ($this->conn) {
            @ftp_raw($this->conn, "QUIT");
            @ftp_close($this->conn);
        }
    }

    public function __destruct() {
        $this->close();
    }
}

// 使用示例
$scanner = new FTPScanner();

if ($scanner->connect('localhost')) {
    echo "=== FTP服务器扫描 ===\n\n";

    // 扫描服务器信息
    $info = $scanner->scanServer();

    echo "1. 欢迎信息:\n";
    echo $info['welcome'] . "\n\n";

    echo "2. 系统类型:\n";
    echo $info['system'] . "\n\n";

    echo "3. 服务器特性 (" . count($info['features']) . " 个):\n";
    foreach ($info['features'] as $feature) {
        echo "   - $feature\n";
    }
    echo "\n";

    echo "4. 支持的命令 (" . count($info['supported_commands']) . " 个):\n";
    foreach ($info['supported_commands'] as $cmd => $data) {
        echo "   - $cmd (响应代码: {$data['code']})\n";
    }

    echo "\n5. 匿名登录测试:\n";
    $anon = $scanner->testAnonymousAccess();
    echo $anon['message'] . "\n";

    // 注意:暴力破解仅用于授权的安全测试
    // echo "\n6. 简单凭证测试:\n";
    // $users = ['admin', 'user', 'test', 'ftp'];
    // $passwords = ['password', '123456', 'admin', 'test'];
    // $results = $scanner->bruteForceTest($users, $passwords);
    // foreach ($results as $result) {
    //     if ($result['success']) {
    //         echo "找到有效凭证: {$result['username']}/{$result['password']}\n";
    //     }
    // }

    $scanner->close();
} else {
    echo "无法连接到FTP服务器\n";
}
?>

注意事项

  • ftp_raw() 是一个低级函数,需要了解FTP协议和命令语法
  • 发送的命令必须符合FTP协议规范,包括正确的命令和参数格式
  • 某些FTP服务器可能限制或禁用某些命令
  • 命令响应可能因服务器实现而异,需要适当解析
  • 使用原始命令可能绕过标准函数的安全检查,需谨慎使用
  • 某些命令(如PASS)可能以明文传输密码,存在安全风险
  • 建议优先使用标准的FTP函数,除非有特殊需求

最佳实践建议

推荐做法
  • 优先使用标准的FTP函数,它们更安全且易于使用
  • 使用ftp_raw()前,先用FEAT命令检查服务器支持的功能
  • 正确解析FTP响应代码,根据代码判断操作是否成功
  • 实现命令执行的重试机制和错误处理
  • 记录发送的命令和接收的响应,便于调试
  • 对用户输入的命令进行严格的验证和过滤
  • 使用SSL/TLS连接(ftp_ssl_connect())保护敏感命令
避免做法
  • 不要在不了解FTP协议的情况下随意发送原始命令
  • 避免发送可能破坏服务器或泄露信息的命令
  • 不要依赖原始命令的响应格式,不同服务器可能不同
  • 避免在公共代码中硬编码敏感的原始命令
  • 不要忽略命令执行的错误响应
  • 避免频繁发送原始命令,可能被服务器视为攻击
  • 不要在不安全的网络中使用原始命令传输敏感信息

常见问题解答

在以下情况下考虑使用ftp_raw()

  1. 执行非标准命令:当需要执行PHP FTP函数不支持的FTP命令时
  2. 服务器特定功能:使用服务器特有的扩展命令,如SITE命令
  3. 高级调试:需要查看原始FTP协议交互进行调试时
  4. 特殊需求:需要精确控制FTP会话流程时
  5. 兼容性处理:处理特定FTP服务器的非标准行为时
  6. 安全测试:在授权范围内进行FTP服务器安全评估时

示例:使用SITE CHMOD 755 filename设置文件权限(如果服务器支持)。

FTP响应代码由3位数字组成,每位数字有特定含义:

数字位置 含义 常见值
第一位 响应类型 1-正面初步响应
2-正面完成响应
3-中间正面响应
4-暂时负面完成响应
5-永久负面完成响应
第二位 响应类别 0-语法
1-信息
2-连接
3-认证和计费
4-未指定
5-文件系统
第三位 具体响应 更详细的响应信息
// 解析响应代码示例
function parseFTPResponse($responseLine) {
    if (strlen($responseLine) >= 3) {
        $code = intval(substr($responseLine, 0, 3));
        $message = trim(substr($responseLine, 4));

        return [
            'code' => $code,
            'message' => $message,
            'type' => floor($code / 100), // 第一位
            'category' => floor(($code % 100) / 10), // 第二位
            'success' => $code < 400 // 1xx, 2xx, 3xx 表示成功
        ];
    }
    return null;
}

使用ftp_raw()可能带来以下安全风险:

风险类型 描述 防范措施
命令注入 用户输入可能包含恶意FTP命令 严格验证和过滤所有输入,避免直接拼接命令
信息泄露 敏感信息(如密码)可能以明文传输 使用SSL/TLS连接(ftp_ssl_connect()
服务器攻击 恶意命令可能破坏服务器或数据 限制可执行的命令范围,使用最小权限账户
拒绝服务 频繁发送命令可能导致服务器过载 实现速率限制和适当的延迟
协议漏洞 利用FTP协议本身的漏洞 保持FTP客户端和服务器更新
// 安全的命令构建示例
function buildSafeCommand($baseCommand, $params = []) {
    // 白名单验证允许的命令
    $allowedCommands = ['LIST', 'NLST', 'PWD', 'SYST', 'FEAT'];

    if (!in_array($baseCommand, $allowedCommands)) {
        throw new Exception("不允许的命令: $baseCommand");
    }

    // 对参数进行安全过滤
    $safeParams = array_map('escapeshellarg', $params);

    return $baseCommand . (!empty($safeParams) ? ' ' . implode(' ', $safeParams) : '');
}

相关函数

  • ftp_connect() - 建立FTP连接
  • ftp_ssl_connect() - 建立SSL-FTP连接
  • ftp_login() - 登录FTP服务器
  • ftp_rawlist() - 返回目录的详细列表
  • ftp_exec() - 在FTP服务器上执行命令(如果支持)
  • ftp_systype() - 返回远程FTP服务器的系统类型
  • ftp_set_option() - 设置各种FTP运行时选项