这半个月我主要都在狂抄nessus/openvas/xxxx的os_finger/detect脚本,明天去叙利亚打暑假工了,做个最后总结。
0. Overview
Y老师之前说,对于开源扫描引擎的os探测脚本,应该重点关注五大协议“SSH,TELNET,SNMP,RDP,FTP”。其实这五个协议在实践中可以分三类处理
- SSH/Telnet/FTP。这三个协议在tcp三次握手以后就可以收到response,并且banner就在response中。所以不需要主动发送任何数据。
- SNMP。首先这是个UDP协议,其次我们需要发送特定的udp数据包(即一个request,或者叫它probe)才能在response中找到banner。万幸,snmp也比较简单,我们可以参考"snmpwalk"的工作来得到指纹。
- 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"
,你看,要猜的地方太多了。不过并不排除某种设备有通用的默认设置。
[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 看看同行们做的怎么样
- nmap -sV 非常垃圾,因为只能匹配不基于SSL/TLS的明文数据。
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
- zoomeye zoomeye也非常。。。 banner是一串人类看不懂的16进制,缺乏更进一步的解析
- 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
- Type1 消息: Negotiate 协商消息。 客户端在发起认证时,是首先向服务器发送协商消息,协商需要认证的服务类型从数据包中UUID为IOXIDResolver。
- Type2 消息: Challenge 挑战消息。 服务器在收到客户端的协商消息之后,在Negotiate Flags写入出自己所能接受的加密等级,并生成一个随机数challenge返回给客户端。这个challenge实际上也可以被重放,由接受另一个Authenticate来认证,实现身份窃取。
- 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>
的结果是
| 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 看雪