前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于service的远程主机os识别之抄个痛快

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

作者头像
黑伞安全
发布2021-10-14 12:16:10
8060
发布2021-10-14 12:16:10
举报
文章被收录于专栏:黑伞安全黑伞安全

这半个月我主要都在狂抄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 看雪

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-09-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 黑伞攻防实验室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0. Overview
  • 1. SSH/Telnet/FTP
  • 2. SNMP
  • 3. RDP(3389)
    • 3.1 看看同行们做的怎么样
      • 3.2 同行是怎么做到的
        • 3.3 开干
        • 4. 由RDP拓展开来
        • 附录 代码草稿
        • 参考
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档