拿到反弹 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。
三种方案对比
| 维度 | rlwrap | script | Python3 + stty |
|---|---|---|---|
| 步骤数 | 1 | 1 | 5~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 更多是锦上添花,而非替代方案。