反弹 Shell 后的 TTY 升级 — 三种方案与原理 的文章封面
返回渗透 Wiki
后渗透技术栈 渗透 Wiki

反弹 Shell 后的 TTY 升级 — 三种方案与原理

拿到反弹 Shell 后第一步做什么?TTY 升级。本文拆解 rlwrap、script、Python3+stty 三种方案的原理、操作和选型逻辑。

拿到反弹 Shell 之后

无论用 bash TCP、nc -e 还是 Meterpreter,第一次连上目标时拿到的 Shell 几乎都是”哑壳”:

$ █

没有 Tab 补全。不能按 ↑ 回看历史。按 Ctrl+C 会直接断连而不是中断程序。sudo、su、vim 全都报错或表现异常。

这不是 Shell 本身的问题,而是它背后没有挂一个真正的终端设备。本文拆解三种主流的 TTY 升级方案,讲清楚每条命令在做什么,以及什么场景选哪一种。

先确认 Shell 的当前状态

执行一下就知道当前是不是哑壳:

tty
# 哑壳 → "not a tty"
# 完整 TTY → "/dev/pts/0"

echo $TERM
# 哑壳 → "" 或 "dumb"
# 完整 TTY → "xterm-256color" 或 "xterm"

如果输出 “not a tty”,那说明数据流直接走 socket,缺少终端控制层。下面三种方案都能解决这个问题,只是程度不一样。


方案一:rlwrap — 攻击机端救急

一句话: 在攻击机本地包装 nc,让它支持 readline。

# 攻击机(Kali)
sudo apt install rlwrap -y
rlwrap nc -lvnp 4444

原理

rlwrap 是一个 readline wrapper。它拦截 nc 的 stdio,在外面包一层行编辑能力。Tab 补全和 ↑↓ 历史是在攻击机本地完成的,目标机那边仍然是哑壳。

效果

  • Tab 补全 ✅
  • ↑↓ 历史 ✅
  • sudo / su ❌
  • vim / nano ❌
  • Ctrl+C → 会退出 rlwrap,不是中断目标进程

用在哪

当目标机环境极简、不想写磁盘、或者只是快速执行几条命令搜集信息时,rlwrap 足够。你在 Kali 端多开一个终端窗口跑着就行,完全不需要碰目标。

但注意——这不是真正的 TTY 升级。sudo 和 su 仍然会因为 “not a tty” 而拒绝工作。


方案二:script 命令 — 一行,通用性最强

一句话: 在目标反弹 Shell 里执行这一行即可。

script /dev/null -c /bin/bash

原理

script 是一个几乎所有 Linux 发行版都预装的工具,原本用于录制终端会话。但它内部做了一件事:打开一对 PTY master/slave,然后把 Shell 的 stdio 挂上去。

所以当你执行 script /dev/null -c /bin/bash

  • script 创建了一个伪终端
  • 在这个伪终端里启动 /bin/bash
  • 原本哑壳的输入输出被桥接到这个伪终端上
  • Shell 立刻认为自己连接到了一个真正的终端设备

/dev/null 只是告诉它不要写日志文件,我们不需要录制内容。

效果

  • Tab 补全 ✅
  • ↑↓ 历史 ✅
  • sudo / su ✅
  • 信号传递 ⚠️(有限,不如方案三完整)
  • 终端行列数 ⚠️(需要额外 stty 修正)

用在哪

这是三种方案里性价比最高的一个:一行命令,零准备,几乎所有目标都预装。不需要关心目标有没有 Python、有没有 Perl、有没有 socat。

如果你只能记住一种 TTY 升级方法,我建议你记这个。


方案三:Python3 + stty — 最完整

这是社区流传最广的经典流程,步骤多一点,但做完之后终端功能完美。

完整流程

# Step 1 — 目标机:用 Python 生成 PTY
python3 -c 'import pty; pty.spawn("/bin/bash")'

# Step 2 — 目标机:设置终端类型
export TERM=xterm-256color

# Step 3 — 按 Ctrl+Z 把 nc 会话挂起到后台
# (此时回到攻击机的本地终端提示符)

# Step 4 — 攻击机:传输终端设置
stty raw -echo; fg

# 按两次 Enter

# Step 5 — 目标机:修正
reset
export SHELL=bash
stty rows 43 columns 155   # 数字从攻击机 stty size 获得

每一步在做什么

步骤作用
pty.spawn(“/bin/bash”)Python 的 pty 模块创建一个新的 PTY,将 bash 连接上去
TERM=xterm-256color告诉 Shell 终端支持 256 色,vim 配色不崩
Ctrl+Z把 nc 进程挂起到后台,回到攻击机本地终端
stty raw -echo; fg将攻击机终端设为 raw 模式(关闭回显和行缓冲),然后恢复 nc 到前台
reset重置终端状态
stty rows/columns告诉 Shell 攻击终端窗口的实际大小,使 vim/less/man 的排版正确

stty raw -echo 是这套流程的关键一步。如果你不把攻击机的终端改成 raw 模式,数据在传输过程中会被攻击机器的终端驱动额外处理(加回显、做行缓冲),最终破坏目标 Shell 的行为。

效果

  • Tab 补全 ✅
  • ↑↓ 历史 ✅
  • 信号传递(Ctrl+C/Ctrl+Z)✅
  • sudo / su ✅
  • vim / nano ✅
  • 行列精确 ✅

用在哪

当你要在目标机上做精细化操作——编辑提权脚本、执行 sudo 提权、用 vim 修改配置文件、进行 SSH 横向移动——选择这个方案。它的步骤最多,但终端体验最接近本地 Shell。


三种方案对比

维度rlwrapscriptPython3 + stty
步骤数115~7
目标依赖script(几乎预装)python3 + pty
攻击依赖rlwrap 包stty(预装)
Tab 补全
sudo / su✅ 完美
vim⚠️ 基本可用
Ctrl+C 信号⚠️ 有限✅ 完整
适用场景快速查看通用首选完整交互

实用经验

没有 Python 怎么办

# 尝试 system python
python -c 'import pty; pty.spawn("/bin/bash")'

# 或者直接走 script 方案——它通常可用
script /dev/null -c /bin/bash

# 装了 socat 的话也可以
socat exec:'bash -li',pty,stderr,setsid,sigint,sane

Ctrl+Z 后 fg 失败

# 检查作业列表
jobs
# 输出: [1]+  Stopped  nc -lvnp 4444

# 恢复前台
fg %1

# 如果还是不行
stty sane; fg

为什么 sudo 说 “no tty present”

因为 sudo 默认配置了 requiretty,要求在一个真正的终端上运行。在已经升级好的 TTY 中不会遇到这个问题。

怎么知道当前行列值

# 在攻击机本地终端执行
stty size
# 输出类似: 43 155

然后把这两个数字填到目标机的 stty rows/columns 里。


怎么选

我自己的判断标准:

  • 如果只是跑自动化脚本、收集信息 → rlwrap 就够了。
  • 如果需要正常执行命令、查看文件 → script /dev/null -c /bin/bash。
  • 如果需要在目标机上提权、编辑、SSH 横向 → 花一分钟走完 Python3 + stty 流程,不要省。

从实战频率来说,script 方案是日常主力——一行、快、零准备、够用。Python3 方案留给需要完整交互的时候。rlwrap 更多是锦上添花,而非替代方案。