基于service的远程主机os识别之抄个痛快

2021-10-14 12:16:10 浏览数 (1)

这半个月我主要都在狂抄nessus/openvas/xxxx的os_finger/detect脚本,明天去叙利亚打暑假工了,做个最后总结。

0. Overview

Y老师之前说,对于开源扫描引擎的os探测脚本,应该重点关注五大协议“SSH,TELNET,SNMP,RDP,FTP”。其实这五个协议在实践中可以分三类处理

  1. SSH/Telnet/FTP。这三个协议在tcp三次握手以后就可以收到response,并且banner就在response中。所以不需要主动发送任何数据。
  2. SNMP。首先这是个UDP协议,其次我们需要发送特定的udp数据包(即一个request,或者叫它probe)才能在response中找到banner。万幸,snmp也比较简单,我们可以参考"snmpwalk"的工作来得到指纹。
  3. RDP。基于TLS,想想就麻烦。Openvas根本没有含RDP的脚本,Nessus藏藏掖掖给了个bin文件没开源,只好自己想想办法。

1. SSH/Telnet/FTP

如前文所说,TCP三次握手后,Server会直接把banner传给Client,所以使用nc连接端口即可简单测试。这里给出三个典型例子。

代码语言:javascript复制
nc -v <targetIP> <targetPort>

得到回显: SSH: 得到是Ubuntu xx.xx

代码语言:javascript复制
[root@VM-0-14-centos ~]# nc -v xxxxxxxxxx 22
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Connected to xxxxxxxxxxx:22.
SSH-2.0-OpenSSH_7.6p1 Ubuntu-4ubuntu0.5

Telnet: 得到(可能)是Cisco Router

代码语言:javascript复制
[root@VM-0-14-centos ~]# nc -v xxxxxxxxxx 23
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Connected to xxxxxxxxxxxxx:23.
                        Cesar Castillo Inc.
---------------------------------------------------------------------------------

Do not attempt logon if you are not authorized. 
This Router easts hackers for lunch!
User Access Verification
Username:

FTP: 得到是威联通家用NAS

代码语言:javascript复制
[root@VM-0-14-centos ~]# nc -v xxxxxxxxx 21
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Connected to xxxxxxxxxxx:21.
220 NASFTPD Turbo station 1.3.5a Server (ProFTPD)

当然,并不是每个回显都有特别鲜明的版本信息。比如大部分国产OS的如上三种服务的Banner,就难以和Linux区别开来。 此外,在信息收集时也发现,有些OS可以由这些banner间接确认,比如FTP服务中,Filezilla服务端几乎就只存在于Windows系统中,所以由Filezilla字段就可以高确信度推导出host采用了Win系统。

2. SNMP

实践中,对于开启SNMP服务的主机,我们只需要发送snmpwalk -c public -v 1 〈ip〉或者snmpwalk -c public -v 2c 〈ip〉 的UDP包中的Payload(可以轻松通过wireshark抓取到),就足以获得该的banner。 而倘若其SNMP的版本为v3,由于v3考虑了安全性,此种方法将失效。当然你也可以考虑snmpwalk -v 3 -u 弱口令用户名 -l authPriv -a sha -A 弱口令 -x aes -X 弱口令 <ip> ".1.3.6.1.2.1",你看,要猜的地方太多了。不过并不排除某种设备有通用的默认设置。

代码语言:javascript复制
[root@VM-0-14-centos ~]# snmpwalk -v 1 -c public xxxxxxxxxxxxxx
SNMPv2-MIB::sysDescr.0 = STRING: Linux vn10441.dns-vinodes.com 4.18.0-147.8.1.el7h.lve.1.x86_64 #1 SMP Mon Jun 29 09:05:02 EDT 2020 x86_64
SNMPv2-MIB::sysObjectID.0 = OID: NET-SNMP-MIB::netSnmpAgentOIDs.10
DISMAN-EVENT-MIB::sysUpTimeInstance = Timeticks: (59825982) 6 days, 22:10:59.82
SNMPv2-MIB::sysContact.0 = STRING: Virtara Group Bili..im Teknolojileri <hostmaster@virtaragroup.com.tr>
SNMPv2-MIB::sysName.0 = STRING: vn10441.dns-vinodes.com
SNMPv2-MIB::sysLocation.0 = STRING: Teknotel / ..stanbul
^C

得到是Linux XX.xx

3. RDP(3389)

3.1 看看同行们做的怎么样

  1. nmap -sV 非常垃圾,因为只能匹配不基于SSL/TLS的明文数据。
代码语言:javascript复制
nmap -sV 116.62.138.140 -p 3389 -Pn
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times will be slower.
Starting Nmap 7.91 ( https://nmap.org ) at 2021-08-27 10:17 ?D1ú±ê×?ê±??
Nmap scan report for 116.62.138.140
Host is up (0.023s latency).
PORT     STATE SERVICE            VERSION
3389/tcp open  ssl/ms-wbt-server?
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 19.29 seconds
  1. zoomeye zoomeye也非常。。。 banner是一串人类看不懂的16进制,缺乏更进一步的解析
  1. 360quake/fofa 有点东西

看到这个OS Version 我眼冒绿光

3.2 同行是怎么做到的

感谢前辈的开源精神网络空间测绘技术之:协议识别(RDP篇)

https://zhuanlan.zhihu.com/p/336936793

一语道破天机——

其实还有一个知识点大部分人没有掌握,而很早在nmap中就进行了实现:就是在进行tls连接后会进行ntlmssp的挑战响应,能够非常准确的提取出来主机名和操作系统的版本。

没有更多线索了,github上也没有特别好的实现,顺着这个思路,发现了nmap开源的一个脚本rdp-ntlm-info

3.2.1 ntlm是什么

看雪这篇不错:

https://bbs.pediy.com/thread-248128.html

  1. Type1 消息: Negotiate 协商消息。 客户端在发起认证时,是首先向服务器发送协商消息,协商需要认证的服务类型从数据包中UUID为IOXIDResolver。
  2. Type2 消息: Challenge 挑战消息。 服务器在收到客户端的协商消息之后,在Negotiate Flags写入出自己所能接受的加密等级,并生成一个随机数challenge返回给客户端。这个challenge实际上也可以被重放,由接受另一个Authenticate来认证,实现身份窃取。
  3. Type3 消息: Authenticate激活消息。

3.3 开干

3.3.1 破除知见障

我们进行的其实是tls下ntlmssp的challenge-response,其实和RDP协议的client本身没有什么关系了,所以虽然golang没有一个很好的rdp的库,对我们也没有任何影响,因为根本咩有rdp。(我一度想用cgo调freerdp 那就完了)

更重要的,这意味着对于"MS-RPC"等协议,或许也可以通过这种方法来完成os探测*

3.3.2 tls 发包

包的二进制在nmap脚本里有,直接tls.dial,con.Write/Read,得到的二进制转字符串: 我的结果是

代码语言:javascript复制
l1q@QundeAir cgoRDP % sudo go run main.go
0�����0��0�������NTLMSSP85���,Y�i���V�%WIN-L1JUUFJNJL0WIN-L1JUUFJNJL0WIN-L1JUUFJNJL0WIN-L1JUUFJNJL0WIN-L1JUUFJNJL����

nmap -p 3389 --script rdp-ntlm-info <target>的结果是

代码语言:javascript复制
|   Target_Name: WIN-L1JUUFJNJL0
|   NetBIOS_Domain_Name: WIN-L1JUUFJNJL0
|   NetBIOS_Computer_Name: WIN-L1JUUFJNJL0
|   DNS_Domain_Name: WIN-L1JUUFJNJL0
|   DNS_Computer_Name: WIN-L1JUUFJNJL0
|   Product_Version: 6.3.9600
|_  System_Time: 2021-08-05T03:31:28 00:00

看到我的结果中的这个“WIN-L1JUUFJNJL0WIN”也出现在了nmap的结果中,感觉就很靠谱。

3.3.3 精确解析ntlm包

看看官方文档官方文档,发现回包基本上是以"NTLM"作为开头的,这说明conn.Read()收到的东西里可能把下层协议的头部之类的东西也包含进去了。尝试buf中截取以“NTLM”开头的内容,再将其传入ntlmparser(这也是在网上找到的一个npm的包),发现终于可以解析了:

代码语言:javascript复制

l1q@QundeAir ~ % ntlm-parser -x 4e544c4d53535000020000001e001e003800000035828ae20e8f346bda4e294300000000000000009800980056000000060380250000000f570049004e002d004c0031004a005500550046004a004e004a004c00300002001e00570049004e002d004c0031004a005500550046004a004e004a004c00300001001e00570049004e002d004c0031004a005500550046004a004e004a004c00300004001e00570049004e002d004c0031004a005500550046004a004e004a004c00300003001e00570049004e002d004c0031004a005500550046004a004e004a004c00300007000800b4e53e9cb489d70100000000

object:  {
  messageType: 'CHALLENGE_MESSAGE (type 2)',
  targetNameSecBuf: { length: 30, allocated: 30, offset: 56 },
  flags: 'UNICODE NTLMSSP_REQUEST_TARGET SIGN SEAL NTLM ALWAYS_SIGN NTLMSSP_TARGET_TYPE_SERVER EXTENDED_SESSIONSECURITY TARGET_INFO VERSION 128 KEY_EXCH 56',
  challenge: '0e8f346bda4e2943',
  targetNameData: 'WIN-L1JUUFJNJL0',
  context: '0000000000000000',
  targetInfoSecBuf: { length: 152, allocated: 152, offset: 86 },
  targetInfoData: [
    { type: 2, length: 30, content: 'WIN-L1JUUFJNJL0' },
    { type: 1, length: 30, content: 'WIN-L1JUUFJNJL0' },
    { type: 4, length: 30, content: 'WIN-L1JUUFJNJL0' },
    { type: 3, length: 30, content: 'WIN-L1JUUFJNJL0' },
    { type: 7, length: 8, content: '2021-08-05T04:44:43.920Z' },
    { type: 0, length: 0, content: '' }
  ],
  osVersionStructure: { majorVersion: 6, minorVersion: 3, buildNumber: 9600, unknown: 15 }
}

看起来再抄一个 ntlm-parser 就成了

3.3.4 通过major/minor OSversion获得相应的人类熟悉的os版本

成了。具体见代码附录。

4. 由RDP拓展开来

事实证明,基于ntlmssp,我们也可以在MS-RPC/Https/NetBIOS等协议(服务)中获得相应的Major-Minor-Build版本号。版本号与OS具体版本的对应关系有待进一步探索。如同前辈在另一篇文章DCERPC中说的,有ntlmssp的地方,就会有丰富的版本信息。

附录 代码草稿

代码语言:javascript复制

package main
import (
	"bytes"
	"crypto/tls"
	"encoding/binary"
	"fmt"
	"log"
	"strings"
)

type ntlmChallengeInfo struct {
	signature string
	messageType int
	nameFieldsBuf []byte
	nameLen     int
	nameMaxLen int
	nameOffset  int
	negotiateFlag int

	majorVersion int
	minorVersion int
	buildNumber int
	unknown int
}
func main() {
	conf := &tls.Config{
		InsecureSkipVerify: true,
	}
	conn, err := tls.Dial("tcp", "74.15.171.191:3389", conf)
	if err != nil {
		log.Println(err)
		return
	}
	defer conn.Close()
	/*
	    -- NTLMSSP Negotiate request mimicking a Windows 10 client
	    local NTLM_NEGOTIATE_BLOB = stdnse.fromhex(
	      "30 37 A0 03 02 01 60 A1 30 30 2E 30 2C A0 2A 04 28" ..
	      "4e 54 4c 4d 53 53 50 00" .. -- Identifier - NTLMSSP
	      "01 00 00 00" ..  -- Type: NTLMSSP Negotiate - 01
	      "B7 82 08 E2 " .. -- Flags (NEGOTIATE_SIGN_ALWAYS | NEGOTIATE_NTLM | NEGOTIATE_SIGN | REQUEST_TARGET | NEGOTIATE_UNICODE)
	      "00 00 " ..       -- DomainNameLen
	      "00 00" ..        -- DomainNameMaxLen
	      "00 00 00 00" ..  -- DomainNameBufferOffset
	      "00 00 " ..       -- WorkstationLen
	      "00 00" ..        -- WorkstationMaxLen
	      "00 00 00 00" ..  -- WorkstationBufferOffset
	      "0A" ..           -- ProductMajorVersion = 10
	      "00 " ..          -- ProductMinorVersion = 0
	      "63 45 " ..       -- ProductBuild = 0x4563 = 17763
	      "00 00 00" ..     -- Reserved
	      "0F"              -- NTLMRevision = 5 = NTLMSSP_REVISION_W2K3
	*/
	ntlmBin :=[]byte{0x30,0x37,0xA0,0x03,0x02,0x01,0x60,0xA1,0x30,0x30,0x2E,0x30,0x2C,0xA0,0x2A,0x04,0x28,0x4e,0x54,0x4c,0x4d,0x53,0x53,0x50,0x00,0x01,0x00,0x00,0x00,0xB7,0x82,0x08,0xE2,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x0A,0x00,0x63,0x45,0x00,0x00,0x00,0x0F}
	n, err := conn.Write(ntlmBin)
	if err != nil {
		log.Println(n, err)
		return
	}

	buf := make([]byte, 2048)
	n, err = conn.Read(buf)
	if err != nil {
		log.Println(n, err)
		return
	}

	StrWithPrefix:=string(buf[:n])
	index:=strings.Index(StrWithPrefix,"NTLM")
	if len(StrWithPrefix[index:])<150{
		log.Println("Seems not long enough")
		return
	}
	bufChallenge:=buf[index:n]
	strChallenge:=StrWithPrefix[index:]
	//这一步应该是把包裹在ntlm外层的那些东西去掉了,下面开始解析ntlmssp的challenge

	cInfo :=&ntlmChallengeInfo{}
	cInfo.signature=string(bufChallenge[:7])
	cInfo.messageType,err=bytesToIntU(bufChallenge[8:12]) //LittleEndian
	cInfo.nameFieldsBuf=bufChallenge[12:20]
	cInfo.nameLen,err=bytesToIntU(cInfo.nameFieldsBuf[:2])
	cInfo.nameMaxLen,err=bytesToIntU(cInfo.nameFieldsBuf[2:4])
	cInfo.nameOffset,err=bytesToIntU(cInfo.nameFieldsBuf[4:8])

	cInfo.negotiateFlag,err=bytesToIntU(bufChallenge[20:24])//todo:int to Flag:MsvAvEOL             = 0x0000。。。
	//serverChallenge:=bufChallenge[24:32]
	//reserved := bufChallenge[32:40]
	//太多了 暂时先不写解析了 直捣黄龙——major-minor-build
	versionfields:=bufChallenge[48:56]
    cInfo.majorVersion=int(versionfields[0])
    cInfo.minorVersion=int(versionfields[1])
    cInfo.buildNumber,err=bytesToIntU(versionfields[2:4])


	if err!=nil{
		log.Println("bytes2int Error in messageType")
		return
	}
	//println(hex.EncodeToString(bufChallenge))
	println(strChallenge)
    println(cInfo.ParseOSVersion())
	return
}

func (c *ntlmChallengeInfo) ParseOSVersion() string {
	if c.majorVersion == 5 && c.minorVersion==1{
		return "Windows XP (SP2)"
	} else if c.majorVersion==5 && c.minorVersion==2{
		return "Windows Server 2003"
	} else if c.majorVersion==6 && c.minorVersion==0{
		return "Windows Server 2008 / Windows Vista"
	}else if c.majorVersion==6 && c.minorVersion==1{
		return "Windows Server 2008 R2 / Windows 7"
	}else if c.majorVersion==6 && c.minorVersion==2{
		return "Windows Server 2012 / Windows 8"
	}else if c.majorVersion==6 && c.minorVersion==3{
		return "Windows Server 2012 R2 / Windows 8.1"
	}else if c.majorVersion==10 && c.minorVersion==0{
		return "Windows Server 2016 or 2019 / Windows 10"
	}else{
		return "Windows"
	}
}

func bytesToIntU(b []byte) (int, error) {
	if len(b) == 3 {
		b = append([]byte{0},b...)
	}
	bytesBuffer := bytes.NewBuffer(b)
	switch len(b) {
	case 1:
		var tmp uint8
		err := binary.Read(bytesBuffer, binary.LittleEndian, &tmp)
		return int(tmp), err
	case 2:
		var tmp uint16
		err := binary.Read(bytesBuffer, binary.LittleEndian, &tmp)
		return int(tmp), err
	case 4:
		var tmp uint32
		err := binary.Read(bytesBuffer, binary.LittleEndian, &tmp)
		return int(tmp), err
	default:
		return 0,fmt.Errorf("%s", "BytesToInt bytes lenth is invaild!")
	}
}

参考

https://zhuanlan.zhihu.com/p/336936793 白帽汇--赵武

https://zhuanlan.zhihu.com/p/359608347 白帽汇--赵武

https://bbs.pediy.com/thread-248128.html 看雪

0 人点赞