Telnet协议是TCP/IP协议族中应用最广泛的协议。它允许用户(Telnet客户端)通过一个协商过程来与一个远程设备进行通信。Telnet协议是基于网络虚拟终端NVT(Network Virtual Termina1)的实现,NVT是虚拟设备,连接双方(客户机和服务器)都必须把它们的物理终端和NVT进行相互转换。
NVT需要顾及到所有的各种型号的机器,所以他定义的操作十分有限。为了能够兼容所有的可能性,Telnet协议使用"用于扩展基本NVT功能的协议,提供了选项协商的机制"来解决兼容性问题。
也就是说,服务端与客户端发送请求开启某选项或拒绝启动某选项的这种交互模式,实现通信。
以下是各常见指令所表示的含义。
最常用指令 十进制 十六进制 字符信息 描述信息
255 0xff IAC 命令解释符 -- 每条指令的前缀都必须是它
251 0xfb WILL 同意启动(enable)选项 -- 发送方本身将激活选项
252 0xfc WONT 拒绝启动选项 -- 发送方本身想禁止选项 表示拒绝执行或继续执行指示的选项。
253 0xfd DO 认可选项请求 -- 发送方想叫接受端激活选项 表示另一方执行的请求,或确认您希望另一方执行,指示的选项。
254 0xfe DONT 拒绝选项请求 -- 发送方想让接受端去禁止选项 十进制 十六进制 字符信息 描述信息
236 0xec EOF 文件结束符
237 0xed SUSP 挂起当前进程(作业控制)
238 0xee ABORT 异常中止进程
239 0xef EOR 记录结束符
240 0xf0 SE 子选项结束 -- 表示子选项协商结束。
241 0xf1 NOP 无操作
242 0xf2 DM 数据标记
243 0xf3 BRK 中断
244 0xf4 IP 中断进程
245 0xf5 AO 异常中止输出
246 0xf6 AYT 对方是否还在运行?
247 0xf7 EC 转义字符
248 0xf8 EL 删除行
249 0xf9 GA 继续进行
250 0xfa SB 子选项开始 -- 表示接下来是协商子选项。
子选项 十进制 十六进制 字符信息 描述信息
0 0x00 二进制传输
1 0x01 回显
3 0x03 继续抑制
5 0x05 状态
6 0x06 计时标记
10 0x0a \n
13 0x0d \r
18 0x12 退出登录
19 0x13 字节宏
23 0x17 发送位置
24 0x18 终端类型
25 0x19 记录结束
26 0x20 数据输入终端
31 0x1F 窗口大小
32 0x20 空格 终端速度
33 0x21 远程流量控制
34 0x22 运行方式
35 0x23 # X 显示位置
36 0x24 (老版本)环境变量
37 0x25 认证
39 0x27 ' 输出标记
38 0x26 TACACS 用户识别
51 0x39 (新版本)环境变量
其中不同的操作系统,所返回的指令集是不同的,也可以通过对应的指令集,也可以称为banner信息,使用FOFA等工具搜集相关信息进行登录尝试。
CentOS 7.6
b"\xff\xfd\x18 \xff\xfd \xff\xfd#\xff\xfd'"
b"\xff\xfc\x18\xff\xfc \xff\xfc#\xff\xfc'"
b'\xff\xfb\x03\xff\xfd\x01\xff\xfd\x1f\xff\xfb\x05\xff\xfd!' b'\xff\xfd\x03\xff\xfb\x01\xff\xfc\x1f\xff\xfe\x05\xff\xfc!'
b'\xff\xfe\x01\r\nKernel 3.10.0-1160.42.2.el7.x86_64 on an x86_64
Windows Server 2003
b"\xff\xfd%\xff\xfb\x01\xff\xfb\x03\xff\xfd'\xff\xfd\x1f\xff\xfd\x00\xff\xfb\x00" b"\xff\xfc%\xff\xfd\x01\xff\xfd\x03\xff\xfc'\xff\xfc\x1f"
b'Welcome to Microsoft Telnet Service '
Routerbo
b"\xff\xfd\x18\xff\xfd \xff\xfd#\xff\xfd'"
b"\xff\xfb\x18\xff\xfc \xff\xfc#\xff\xfb'"
b"\xff\xfa'\x01\xff\xf0\xff\xfa\x18\x01\xff\xf0" ff fa 27 01 ff f0 ff fa 18 01 ff f0
根据上面的指令集对照表,可以“翻译”一条指令集做个样例,其他的也可以对照着“密码表”进行翻译,大概就知道是怎么回事了。
# 注意这里的空格和`#`、`'`符号,也对应着十六进制数。
b"\xff\xfd\x18 \xff\xfd \xff\xfd#\xff\xfd'"
# 下面是将空格等字符补全的结果。
b"\xff\xfd\x18\0x20\xff\xfd\0x20\xff\xfd\0x23\xff\xfd\0x27"
# 开始翻译
# DO 认可选项请求 -- 发送方想叫接受端激活选项 表示另一方执行的请求,或确认您希望另一方执行,指示的选项。
DO 终端类型 终端速度 DO 终端速度 X 显示位置 DO 输出标记
针对于CentOS和Windows,不需要返回指定内容即可看到提示登录的信息。而针对Routerbo等机器,需要发送相关指令与之交互,才能看到提示登录的消息。
因此,我们可以通过进一步分析,来捕获登录特征,利用socket实现通用的登录接口,而实现我们最终的服务爆破的目的。
查看telnet源码进行分析
def process_rawq(self):
"""Transfer from raw queue to cooked queue.
Set self.eof when connection is closed. Don't block unless in
the midst of an IAC sequence.
"""
buf = [b'', b'']
try:
# self.rawq初始值是空字节,首次执行的时候会跳过然后执行到socket.recv的时候赋值,就进入到while循环中直到取完所有的值。
while self.rawq:
# 每次读一个字节self.irawq自动加1
# self.rawq_getchar()每次是取一个十六进制数
c = self.rawq_getchar()
# self.iacseq初始值是空字节
# 当self.iacseq不为空的时候
if not self.iacseq:
if c == theNULL:
continue
if c == b"\021":
continue
# IAC就是0xff,不是0xff,就拼接到buf列表中的第一个元素中
if c != IAC:
# 如果是login等字符,就会拼接到buf[self.sb]中
# 当self.irawq+=1后大于等于self.rawq后,说明self.rawq已经被读完了
# 最后将buf[self.sb]中的值返回给客户端,也就是拼接到的比如login:返回给客户端,客户端就可以根据返回的值进行判断,是login还是username或者登录失败等。
# self.sb 初始值是0
buf[self.sb] = buf[self.sb] + c
continue
else:
# 拼接指令集中0xff的指令
self.iacseq += c
# 当self.iacseq长度为1时,说明self.iacseq是0xff,
elif len(self.iacseq) == 1:
# 判断取到的变量为c的指令在不在DO, DONT, WILL, WONT中。
if c in (DO, DONT, WILL, WONT):
# 如果在其中,那么拼接成`0xff`+(DO, DONT, WILL, WONT)中的一个,比如`0xff0xfd`并继续读取下一个。
self.iacseq += c
continue
# 否则初始化self.iacseq为空字节
self.iacseq = b''
if c == IAC:
# 如果取出的指令是`0xff`,拼接到buf[self.sb]中,self.sb为0,正常情况不太可能走到这里,除非发生粘包等问题,导致接收到连续两个0xff。
buf[self.sb] = buf[self.sb] + c
else:
# 如果取出的指令为SB,子选项协商开始,把self.sb设为1。
if c == SB: # SB ... SE start.
self.sb = 1
self.sbdataq = b''
# 子选项协商结束
elif c == SE:
self.sb = 0
self.sbdataq = self.sbdataq + buf[1]
buf[1] = b''
if self.option_callback:
# Callback is supposed to look into
# the sbdataq
self.option_callback(self.sock, c, NOOPT)
else:
self.msg('IAC %d not recognized' % ord(c))
# 当self.iacseq长度为2时,说明self.iacseq是\xff\xfd或\fb等四个中的一个,
elif len(self.iacseq) == 2:
# 而cmd就是0xfb 0xfd等四个中的一个
cmd = self.iacseq[1:2]
# 这里就将0xff0xf(a, b)等设为空字节了,用于进行下一次循环判断
self.iacseq = b''
# 这里的c就是新取出的值,不在0xff 0xfb 0xfd等四个中的一个,而是一个子选项
opt = c
# ##############################################################################
# 重点来了,当cmd是(DO, DONT)中的一个,回复0xff + 0xfe + opt(子选项),通杀所有系统!!!
# ##############################################################################
if cmd in (DO, DONT):
self.msg('IAC %s %d',
cmd == DO and 'DO' or 'DONT', ord(opt))
if self.option_callback:
self.option_callback(self.sock, cmd, opt)
else:
self.sock.sendall(IAC + WONT + opt)
# ##############################################################################
# 重点来了,当cmd是(WILL, WONT)中的一个,回复0xff + 0xfc + opt(子选项),通杀所有系统!!!
# ##############################################################################
elif cmd in (WILL, WONT):
self.msg('IAC %s %d',
cmd == WILL and 'WILL' or 'WONT', ord(opt))
if self.option_callback:
self.option_callback(self.sock, cmd, opt)
else:
self.sock.sendall(IAC + DONT + opt)
except EOFError: # raised by self.rawq_getchar()
self.iacseq = b'' # Reset on EOF
self.sb = 0
pass
self.cookedq = self.cookedq + buf[0]
self.sbdataq = self.sbdataq + buf[1]
核心总结下来就是:
- 每次读取判断字符是0xff还是0xfb 0xfc 0xfd等,拼接成长度为2的一个字符串。
- 当长度为2的时候,判断执行的是DO, DONT还是WILL, WONT
- DO, DONT 是拼接 0xfb 0xfe + 子选项发送给服务端。
- WILL, WONT是拼接 0xfb 0xfc + 子选项发送给服务端。
- 最后将除了0xff的字符串拼接到buf列表中,返回给客户端。
- 客户端可以通过返回值判断接下来是输入用户名还是密码,或是登录成功或失败。