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列表中,返回給客戶端。
- 客戶端可以通過返回值判斷接下來是輸入用戶名還是密碼,或是登錄成功或失敗。