Telnet协议底层研究及python中telnetlib核心源码分析

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] 

核心总结下来就是:

  1. 每次读取判断字符是0xff还是0xfb 0xfc 0xfd等,拼接成长度为2的一个字符串。
  2. 当长度为2的时候,判断执行的是DO, DONT还是WILL, WONT
    1. DO, DONT 是拼接 0xfb 0xfe + 子选项发送给服务端。
    2. WILL, WONT是拼接 0xfb 0xfc + 子选项发送给服务端。
  3. 最后将除了0xff的字符串拼接到buf列表中,返回给客户端。
  4. 客户端可以通过返回值判断接下来是输入用户名还是密码,或是登录成功或失败。
原文地址:https://www.cnblogs.com/liuhuan086/p/15673999.html