【Linux】内网穿透 FTP 终极复现手册 (2026 版)--cpolar

【Linux】内网穿透 FTP 终极复现手册2026 版--cpolar

  • 前言
  • [一、 核心背景与原理](#一、 核心背景与原理)
  • [二、 服务端:ftp_server.py (Kali 运行)](#二、 服务端:ftp_server.py (Kali 运行))
    • [如果复现过程中遇到 2个域名不一样](#如果复现过程中遇到 2个域名不一样)
  • 三、访问FTP服务器
  • [四、 (非必须)客户端:ftp_client.py (Windows 运行)](#四、 (非必须)客户端:ftp_client.py (Windows 运行))
  • [五、 快速复现流程总结](#五、 快速复现流程总结)

前言

因为有个靶场所需需要搭建一个 FTP 服务器,又因为比较懒不高兴开 VPS 服务器去搭建 FTP ,用内网穿透工具 cpolar 搭建了2条 tcp 隧道,复现了 ftp 但是花了很长时间;为避免各位踩坑特此整理了一个笔记。

安全性问题最重要说三遍:
操作过程中请注意,FTP 主目录下不要放敏感文件!!!
操作过程中请注意,FTP 主目录下不要放敏感文件!!!
操作过程中请注意,FTP 主目录下不要放敏感文件!!!

一、 核心背景与原理

本方案 cpolar 需要搭建两条 tcp 隧道[1](#1),在双重 NAT(内网环境)下,FTP 协议的** "双通道" **特性会导致连接失败。

  • 控制通道 (Port A): 负责指令:tcp://3.tcp.cpolar.top:11935 -> tcp://127.0.0.1:2121
  • 数据通道 (Port B): 负责文件内容。tcp://3.tcp.cpolar.top:11667 -> tcp://127.0.0.1:2120

穿透痛点:

  1. 端口不一致: cpolar 随机分配的外网端口(如 11667)与本地监听端口(如 2120)无法对应。
  2. IP 伪装失效: 服务端默认会返回内网 IP,导致公网客户端连接失败。

解决方案: 通过 Python 脚本 "暴力劫持" FTPPASV(被动模式)响应,强制将客户端引导至公网隧道出口。

IP漂移以及解决方法

  1. 复现过程中 cpolar 映射到过2个域名:3.tcp.cpolar.top3.tcp.vip.cpolar.cn
  2. 因域名不一致导致公网 IP 也不一致,可自行 ping 命令得到真实 IP
  3. 随着时间的推移,IP 也会变动,每次复现需要确定真实 IP

流量的一生:

  1. 户连 隧道A:11935 → 转发至本地 2121 (登录成功)。
  2. 用户发 ls 指令 → 服务端"撒谎"说:请连 隧道B:11667 拿数据。
  3. 用户连 隧道B:11667 → 转发至本地 2120 (获取目录成功)。

二、 服务端:ftp_server.py (Kali 运行)

功能: 重写 PASV 逻辑,实现 "协议层端口重定向"

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 运行环境: Windows / Linux / macOS (跨平台通用)
# 依赖安装: pip install pyftpdlib
# cpolar 两条 tcp 隧道一条映射 2121 端口,一条映射 2120 端口
# 复现环境时 cpolar 映射公网 IP 是 8.148.71.245 有可能随着时间变动会变动,需要调整代码
# 另外也可能 两条隧道公网 IP 不一样,

from pyftpdlib.authorizers import DummyAuthorizer  # 权限管理器
from pyftpdlib.handlers import FTPHandler          # 协议处理器
from pyftpdlib.servers import FTPServer            # 监听服务器
import logging                                     # 日志模块

# 开启 DEBUG 日志,实时查看 FTP 握手过程
logging.basicConfig(level=logging.DEBUG)

class CpolarFTPHandler(FTPHandler):
    """
    【核心黑科技】自定义处理器,拦截并重写被动模式响应
    """
    def ftp_PASV(self, line):
        # 1. 暴力劫持响应内容:
        # 强制告诉客户端:请去连接公网地址 8.148.71.245 的 11667 端口
        # 端口计算公式:45 * 256 + 147 = 11667
        self.respond('227 Entering Passive Mode (8,148,71,245,45,147).')

        # 2. 启动本地监听:
        # 虽然告诉客户端连外网,但本地服务器必须在 2120 端口等着 cpolar 转回来的流量
        try:
            self.handle_pasv()  # pyftpdlib 2.x 内部监听启动方法
        except AttributeError:
            super().ftp_PASV(line) # 兼容性兜底

def main():
    # 实例化权限管理器
    authorizer = DummyAuthorizer()
    # 设置匿名用户路径与全权限 (elradfmw)
    authorizer.add_anonymous("/home/kali/Desktop/tmp/tmp", perm="elradfmw")  # 假设 ftp 主目录在这里

    handler = CpolarFTPHandler
    handler.authorizer = authorizer
    
    # 【关键设置】禁用 IP 来源检查
    # 因为 cpolar 的控制流和数据流 IP 可能被识别为不一致,必须开启此项
    handler.passive_promiscuous = True
    
    # 设置被动模式本地监听端口范围 (对应隧道 B 的本地端)
    handler.passive_ports = range(2120, 2121)

    # 启动服务器,监听本地 2121 (对应隧道 A 的本地端)
    server = FTPServer(('0.0.0.0', 2121), handler)

    print("---------------------------------------")
    print("💎 穿透服务端已就绪 (Python 战神版)")
    print("期待流量路径:外网 11667 -> 隧道 -> 本地 2120")
    print("---------------------------------------")
    server.serve_forever()

if __name__ == "__main__":
    main()

如果复现过程中遇到 2个域名不一样

上面代码请修改为下面的核心逻辑(因复现中又没遇到,故未做调试)

python 复制代码
import socket

# 假设你的两个隧道域名不一样
CONTROL_DOMAIN = "3.tcp.cpolar.top"      # 控制通道域名
DATA_DOMAIN = "4.tcp.vip.cpolar.cn"     # 数据通道域名(假设不同)

def get_ip(domain):
    """自动获取域名的当前公网 IP"""
    return socket.gethostbyname(domain)

class CpolarFTPHandler(FTPHandler):
    def ftp_PASV(self, line):
        # 自动获取数据通道当前的真实公网 IP
        data_ip = get_ip(DATA_DOMAIN)
        # 将 IP 格式化为 FTP 协议需要的逗号分隔形式
        ip_parts = data_ip.replace('.', ',')
        
        # 动态生成响应:使用数据通道的 IP 和 11667 端口 (45*256 + 147)
        # 注意:这里的 P1, P2 需要根据 cpolar 隧道 B 实际分配的外网端口实时计算。
        pasv_resp = f'227 Entering Passive Mode ({ip_parts},45,147).'
        self.respond(pasv_resp)
        self.handle_pasv()

三、访问FTP服务器

  1. windows 直接访问

    随便打开一个文件夹,输入 ftp://内网穿透真实ip:内网穿透控制通道端口 案例中是 ftp://8.148.71.245:11935/

  2. 下图为 FTP 跟目录中的文件,可见和访问结果一模一样。

  3. 但是 cmd 访问会出错

  4. 使用 FileZilla 可以完美解决

    FileZilla 可以读取的原因系:编辑 -> 设置 -> 连接 -> FTP -> 被动模式,已经勾选了使用服务器的外部 IP 地址来代替。

四、 (非必须)客户端:ftp_client.py (Windows 运行)

功能: 强制开启被动模式,避开 Windows 原生 FTP 工具的限制。

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# 运行环境: Windows / Linux / macOS (跨平台通用)
# 依赖说明: 仅使用 Python 标准库,无需额外安装依赖

# --- 配置区:需与 cpolar 公网信息一致 ---
import ftplib
import os

FTP_HOST = "8.148.71.245"  # cpolar 公网 IP/域名
FTP_PORT = 11935           # cpolar 控制端映射的外网端口
FTP_USER = "anonymous"     # 匿名用户
FTP_PASS = ""              # 密码留空

def main():
    try:
        print(f"正在建立隧道连接: {FTP_HOST}:{FTP_PORT}...")
        ftp = ftplib.FTP()
        
        # 1. 建立控制通道连接
        ftp.connect(FTP_HOST, FTP_PORT)
        ftp.login(FTP_USER, FTP_PASS)
        
        # 2. 【核心步骤】强制开启被动模式
        # 解决 Windows CMD 无法列目录的根本手段:让客户端主动连服务器的数据端口
        ftp.set_pasv(True)
        print("✅ 登录成功!数据通道已就绪。")

        while True:
            # 获取用户指令
            cmd = input("\n[远程控制] ls(列表), get(下载), quit(退出): ").strip().split()
            if not cmd: continue
            
            action = cmd[0].lower()

            if action == 'ls':
                print("--- 目录列表 ---")
                ftp.retrlines('LIST')  # 接收数据流并打印
            
            elif action == 'get' and len(cmd) > 1:
                filename = cmd[1]
                print(f"正在通过穿透隧道提取: {filename}...")
                with open(filename, 'wb') as f:
                    # 获取文件二进制流
                    ftp.retrbinary(f'RETR {filename}', f.write)
                print(f"✨ 下载成功!保存在: {os.path.abspath(filename)}")
            
            elif action == 'quit':
                ftp.quit()
                print("👋 连接已关闭。")
                break
    except Exception as e:
        print(f"\n⚠️ 连接中断: {e}")

if __name__ == "__main__":
    main()

五、 快速复现流程总结

1. 隧道配置 (cpolar)

  • 隧道 A (控制):TCP, 本地端口 2121, 获取外网端口(如 11935)。
  • 隧道 B (数据):TCP, 本地端口 2120, 获取外网随机端口(如 11667)。

2. 参数计算如果外网随机端口变更(假设变为 N N N):

  • 计算端口高位: P 1 = N / / 256 P1 = N // 256 P1=N//256
  • 计算端口低位: P 2 = N % 256 P2 = N \% 256 P2=N%256
  • 修改服务端脚本:self.respond('227 ... (IP,P1,P2).')

3. 为什么不再使用 vsftpd?

传统软件很难在不修改源码的情况下实现 "伪造 227 响应" 。使用 Python pyftpdlib 可以精准控制协议握手的每一个字节,是内网穿透环境下的最优解。


  1. cpolar 搭建隧道方法详见 cpolar 官网文档 ↩︎
相关推荐
xianyudx1 小时前
Linux 服务器 DNS 配置指南 (CentOS 7 / 麒麟 V10)
linux·服务器·centos
G皮T1 小时前
【计算机网络】网络时间协议 NTP(二):X-Request-Start
网络·计算机网络·时钟同步·ntp·网络时间协议
文静小土豆2 小时前
CentOS 7 升级 OpenSSL 3.5.4 详细指南
linux·运维·centos·ssl
红中️2 小时前
Nginx
网络
weixin_444579302 小时前
Ubuntu 22.04 服务器安装教程(二)——桌面版系统
linux·服务器·ubuntu
Starry_hello world2 小时前
Linux 网络(8)
linux·运维·网络
monkey011272 小时前
webSocket Demo1
网络·websocket·网络协议
意法半导体STM322 小时前
【官方原创】使用GPDMA进行SPI LCD整屏传输 LAT1435
网络·stm32·单片机·嵌入式硬件·mcu·网络协议·stm32开发
biubiubiu07062 小时前
Certbot 申请SSL证书的三种方式详解(Ubuntu 22.04环境)
网络·网络协议·ssl