• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# SPDX-License-Identifier: GPL-2.0-only
2# This file is part of Scapy
3# See https://scapy.net/ for more information
4# Copyright (C) Philippe Biondi <phil@secdev.org>
5
6"""Clone of Nmap's first generation OS fingerprinting.
7
8This code works with the first-generation OS detection and
9nmap-os-fingerprints, which has been removed from Nmap on November 3,
102007 (https://github.com/nmap/nmap/commit/50c49819), which means it is
11outdated.
12
13To get the last published version of this outdated fingerprint
14database, you can fetch it from
15<https://raw.githubusercontent.com/nmap/nmap/9efe1892/nmap-os-fingerprints>.
16
17"""
18
19import os
20import re
21
22from scapy.data import KnowledgeBase
23from scapy.config import conf
24from scapy.arch import WINDOWS
25from scapy.error import warning
26from scapy.layers.inet import IP, TCP, UDP, ICMP, UDPerror, IPerror
27from scapy.packet import NoPayload, Packet
28from scapy.sendrecv import sr
29from scapy.compat import plain_str, raw
30from scapy.plist import SndRcvList, PacketList
31
32# Typing imports
33from typing import (
34    Dict,
35    List,
36    Tuple,
37    Optional,
38    cast,
39    Union,
40)
41
42if WINDOWS:
43    conf.nmap_base = os.environ["ProgramFiles"] + "\\nmap\\nmap-os-fingerprints"  # noqa: E501
44else:
45    conf.nmap_base = "/usr/share/nmap/nmap-os-fingerprints"
46
47
48######################
49#  nmap OS fp stuff  #
50######################
51
52
53_NMAP_LINE = re.compile('^([^\\(]*)\\(([^\\)]*)\\)$')
54
55
56class NmapKnowledgeBase(KnowledgeBase):
57    """A KnowledgeBase specialized in Nmap first-generation OS
58fingerprints database. Loads from conf.nmap_base when self.filename is
59None.
60
61    """
62
63    def lazy_init(self):
64        # type: () -> None
65        try:
66            fdesc = open(conf.nmap_base
67                         if self.filename is None else
68                         self.filename, "rb")
69        except (IOError, TypeError):
70            warning("Cannot open nmap database [%s]", self.filename)
71            self.filename = None
72            return
73
74        self.base = []
75        self.base = cast(List[Tuple[str, Dict[str, Dict[str, str]]]], self.base)
76        name = None
77        sig = {}  # type: Dict[str,Dict[str,str]]
78        for line in fdesc:
79            str_line = plain_str(line)
80            str_line = str_line.split('#', 1)[0].strip()
81            if not str_line:
82                continue
83            if str_line.startswith("Fingerprint "):
84                if name is not None:
85                    self.base.append((name, sig))
86                name = str_line[12:].strip()
87                sig = {}
88                continue
89            if str_line.startswith("Class "):
90                continue
91            match_line = _NMAP_LINE.search(str_line)
92            if match_line is None:
93                continue
94            test, values = match_line.groups()
95            sig[test] = dict(val.split('=', 1) for val in
96                             (values.split('%') if values else []))
97        if name is not None:
98            self.base.append((name, sig))
99        fdesc.close()
100
101    def get_base(self):
102        # type: () -> List[Tuple[str, Dict]]
103        return cast(List[Tuple[str, Dict]], super(NmapKnowledgeBase, self).get_base())
104
105
106conf.nmap_kdb = NmapKnowledgeBase(None)
107conf.nmap_kdb = cast(NmapKnowledgeBase, conf.nmap_kdb)
108
109
110def nmap_tcppacket_sig(pkt):
111    # type: (Optional[Packet]) -> Dict
112    res = {}
113    if pkt is not None:
114        res["DF"] = "Y" if pkt.flags.DF else "N"
115        res["W"] = "%X" % pkt.window
116        res["ACK"] = "S++" if pkt.ack == 2 else "S" if pkt.ack == 1 else "O"
117        res["Flags"] = str(pkt[TCP].flags)[::-1]
118        res["Ops"] = "".join(x[0][0] for x in pkt[TCP].options)
119    else:
120        res["Resp"] = "N"
121    return res
122
123
124def nmap_udppacket_sig(snd, rcv):
125    # type: (SndRcvList, PacketList) -> Dict
126    res = {}
127    if rcv is None:
128        res["Resp"] = "N"
129    else:
130        res["DF"] = "Y" if rcv.flags.DF else "N"
131        res["TOS"] = "%X" % rcv.tos
132        res["IPLEN"] = "%X" % rcv.len
133        res["RIPTL"] = "%X" % rcv.payload.payload.len
134        res["RID"] = "E" if snd.id == rcv[IPerror].id else "F"
135        res["RIPCK"] = "E" if snd.chksum == rcv[IPerror].chksum else (
136            "0" if rcv[IPerror].chksum == 0 else "F"
137        )
138        res["UCK"] = "E" if snd.payload.chksum == rcv[UDPerror].chksum else (
139            "0" if rcv[UDPerror].chksum == 0 else "F"
140        )
141        res["ULEN"] = "%X" % rcv[UDPerror].len
142        res["DAT"] = "E" if (
143            isinstance(rcv[UDPerror].payload, NoPayload) or
144            raw(rcv[UDPerror].payload) == raw(snd[UDP].payload)
145        ) else "F"
146    return res
147
148
149def nmap_match_one_sig(seen, ref):
150    # type: (Dict, Dict) -> float
151    cnt = sum(val in ref.get(key, "").split("|") for key, val in seen.items())
152    if cnt == 0 and seen.get("Resp") == "N":
153        return 0.7
154    return float(cnt) / len(seen)
155
156
157def nmap_sig(target, oport=80, cport=81, ucport=1):
158    # type: (str, int, int, int) -> Dict
159    res = {}
160
161    tcpopt = [("WScale", 10),
162              ("NOP", None),
163              ("MSS", 256),
164              ("Timestamp", (123, 0))]
165    tests = [
166        IP(dst=target, id=1) /
167        TCP(seq=1, sport=5001 + i, dport=oport if i < 4 else cport,
168            options=tcpopt, flags=flags)
169        for i, flags in enumerate(["CS", "", "SFUP", "A", "S", "A", "FPU"])
170    ]
171    tests.append(IP(dst=target) / UDP(sport=5008, dport=ucport) / (300 * "i"))
172
173    ans, unans = sr(tests, timeout=2)
174    ans.extend((x, None) for x in unans)
175
176    for snd, rcv in ans:
177        if snd.sport == 5008:
178            res["PU"] = (snd, rcv)
179        else:
180            test = "T%i" % (snd.sport - 5000)
181            if rcv is not None and ICMP in rcv:
182                warning("Test %s answered by an ICMP", test)
183                rcv = None  # type: ignore
184            res[test] = rcv
185
186    return nmap_probes2sig(res)
187
188
189def nmap_probes2sig(tests):
190    # type: (Dict) -> Dict
191    tests = tests.copy()
192    res = {}
193    if "PU" in tests:
194        res["PU"] = nmap_udppacket_sig(*tests["PU"])
195        del tests["PU"]
196    for k in tests:
197        res[k] = nmap_tcppacket_sig(tests[k])
198    return res
199
200
201def nmap_search(sigs):
202    # type: (Dict) -> Tuple[Union[int, float], List]
203    guess = 0, []  # type: Tuple[Union[int, float], List]
204    conf.nmap_kdb = cast(NmapKnowledgeBase, conf.nmap_kdb)
205    for osval, fprint in conf.nmap_kdb.get_base():
206        score = 0.0
207        for test, values in fprint.items():
208            if test in sigs:
209                score += nmap_match_one_sig(sigs[test], values)
210        score /= len(sigs)
211        if score > guess[0]:
212            guess = score, [osval]
213        elif score == guess[0]:
214            guess[1].append(osval)
215    return guess
216
217
218@conf.commands.register
219def nmap_fp(target, oport=80, cport=81):
220    # type: (str, int, int) -> Tuple[Union[int, float], List]
221    """nmap fingerprinting
222nmap_fp(target, [oport=80,] [cport=81,]) -> list of best guesses with accuracy
223"""
224    sigs = nmap_sig(target, oport, cport)
225    return nmap_search(sigs)
226
227
228@conf.commands.register
229def nmap_sig2txt(sig):
230    # type: (Dict) -> str
231    torder = ["TSeq", "T1", "T2", "T3", "T4", "T5", "T6", "T7", "PU"]
232    korder = ["Class", "gcd", "SI", "IPID", "TS",
233              "Resp", "DF", "W", "ACK", "Flags", "Ops",
234              "TOS", "IPLEN", "RIPTL", "RID", "RIPCK", "UCK", "ULEN", "DAT"]
235    txt = []
236    for i in sig:
237        if i not in torder:
238            torder.append(i)
239    for test in torder:
240        testsig = sig.get(test)
241        if testsig is None:
242            continue
243        txt.append("%s(%s)" % (test, "%".join(
244            "%s=%s" % (key, testsig[key]) for key in korder if key in testsig
245        )))
246    return "\n".join(txt)
247