├── LICENSE
├── Pipfile
├── Pipfile.lock
├── README.md
├── docs
└── fatt.png
├── fatt.py
└── fatt.service
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2019, Adel "0x4d31" Karimi
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/Pipfile:
--------------------------------------------------------------------------------
1 | [[source]]
2 | url = "https://pypi.org/simple"
3 | verify_ssl = true
4 | name = "pypi"
5 |
6 | [packages]
7 | pyshark = "==0.4.2.2"
8 |
9 | [dev-packages]
10 |
11 | [requires]
12 | python_version = "3.7"
13 |
--------------------------------------------------------------------------------
/Pipfile.lock:
--------------------------------------------------------------------------------
1 | {
2 | "_meta": {
3 | "hash": {
4 | "sha256": "f5798e94cc6549ed791c07af1c8f47ecbdde003202488057efdd6e2ed7c3e2a4"
5 | },
6 | "pipfile-spec": 6,
7 | "requires": {
8 | "python_version": "3.7"
9 | },
10 | "sources": [
11 | {
12 | "name": "pypi",
13 | "url": "https://pypi.org/simple",
14 | "verify_ssl": true
15 | }
16 | ]
17 | },
18 | "default": {
19 | "logbook": {
20 | "hashes": [
21 | "sha256:0cf2cdbfb65a03b5987d19109dacad13417809dcf697f66e1a7084fb21744ea9",
22 | "sha256:2dc85f1510533fddb481e97677bb7bca913560862734c0b3b289bfed04f78c92",
23 | "sha256:56ee54c11df3377314cedcd6507638f015b4b88c0238c2e01b5eb44fd3a6ad1b",
24 | "sha256:66f454ada0f56eae43066f604a222b09893f98c1adc18df169710761b8f32fe8",
25 | "sha256:7c533eb728b3d220b1b5414ba4635292d149d79f74f6973b4aa744c850ca944a",
26 | "sha256:8f76a2e7b1f72595f753228732f81ce342caf03babc3fed6bbdcf366f2f20f18",
27 | "sha256:94e2e11ff3c2304b0d09a36c6208e5ae756eb948b210e5cbd63cd8d27f911542",
28 | "sha256:97fee1bd9605f76335b169430ed65e15e457a844b2121bd1d90a08cf7e30aba0",
29 | "sha256:e18f7422214b1cf0240c56f884fd9c9b4ff9d0da2eabca9abccba56df7222f66"
30 | ],
31 | "version": "==1.5.3"
32 | },
33 | "lxml": {
34 | "hashes": [
35 | "sha256:11ae552a78612620afd15625be9f1b82e3cc2e634f90d6b11709b10a100cba59",
36 | "sha256:121fc6f71c692b49af6c963b84ab7084402624ffbe605287da362f8af0668ea3",
37 | "sha256:124f09614f999551ac65e5b9875981ce4b66ac4b8e2ba9284572f741935df3d9",
38 | "sha256:12ae2339d32a2b15010972e1e2467345b7bf962e155671239fba74c229564b7f",
39 | "sha256:12d8d6fe3ddef629ac1349fa89a638b296a34b6529573f5055d1cb4e5245f73b",
40 | "sha256:1a2a7659b8eb93c6daee350a0d844994d49245a0f6c05c747f619386fb90ba04",
41 | "sha256:1ccbfe5d17835db906f2bab6f15b34194db1a5b07929cba3cf45a96dbfbfefc0",
42 | "sha256:2f77556266a8fe5428b8759fbfc4bd70be1d1d9c9b25d2a414f6a0c0b0f09120",
43 | "sha256:3534d7c468c044f6aef3c0aff541db2826986a29ea73f2ca831f5d5284d9b570",
44 | "sha256:3884476a90d415be79adfa4e0e393048630d0d5bcd5757c4c07d8b4b00a1096b",
45 | "sha256:3b95fb7e6f9c2f53db88f4642231fc2b8907d854e614710996a96f1f32018d5c",
46 | "sha256:46515773570a33eae13e451c8fcf440222ef24bd3b26f40774dd0bd8b6db15b2",
47 | "sha256:46f21f2600d001af10e847df9eb3b832e8a439f696c04891bcb8a8cedd859af9",
48 | "sha256:473701599665d874919d05bb33b56180447b3a9da8d52d6d9799f381ce23f95c",
49 | "sha256:4b9390bf973e3907d967b75be199cf1978ca8443183cf1e78ad80ad8be9cf242",
50 | "sha256:4f415624cf8b065796649a5e4621773dc5c9ea574a944c76a7f8a6d3d2906b41",
51 | "sha256:534032a5ceb34bba1da193b7d386ac575127cc39338379f39a164b10d97ade89",
52 | "sha256:558485218ee06458643b929765ac1eb04519ca3d1e2dcc288517de864c747c33",
53 | "sha256:57cf05466917e08f90e323f025b96f493f92c0344694f5702579ab4b7e2eb10d",
54 | "sha256:59d77bfa3bea13caee95bc0d3f1c518b15049b97dd61ea8b3d71ce677a67f808",
55 | "sha256:5d5254c815c186744c8f922e2ce861a2bdeabc06520b4b30b2f7d9767791ce6e",
56 | "sha256:5ea121cb66d7e5cb396b4c3ca90471252b94e01809805cfe3e4e44be2db3a99c",
57 | "sha256:60aeb14ff9022d2687ef98ce55f6342944c40d00916452bb90899a191802137a",
58 | "sha256:642eb4cabd997c9b949a994f9643cd8ae00cf4ca8c5cd9c273962296fadf1c44",
59 | "sha256:6548fc551de15f310dd0564751d9dc3d405278d45ea9b2b369ed1eccf142e1f5",
60 | "sha256:68a851176c931e2b3de6214347b767451243eeed3bea34c172127bbb5bf6c210",
61 | "sha256:6e84edecc3a82f90d44ddee2ee2a2630d4994b8471816e226d2b771cda7ac4ca",
62 | "sha256:73e8614258404b2689a26cb5d002512b8bc4dfa18aca86382f68f959aee9b0c8",
63 | "sha256:7679bb6e4d9a3978a46ab19a3560e8d2b7265ef3c88152e7fdc130d649789887",
64 | "sha256:76b6c296e4f7a1a8a128aec42d128646897f9ae9a700ef6839cdc9b3900db9b5",
65 | "sha256:7f00cc64b49d2ef19ddae898a3def9dd8fda9c3d27c8a174c2889ee757918e71",
66 | "sha256:8021eeff7fabde21b9858ed058a8250ad230cede91764d598c2466b0ba70db8b",
67 | "sha256:87f8f7df70b90fbe7b49969f07b347e3f978f8bd1046bb8ecae659921869202b",
68 | "sha256:916d457ad84e05b7db52700bad0a15c56e0c3000dcaf1263b2fb7a56fe148996",
69 | "sha256:925174cafb0f1179a7fd38da90302555d7445e34c9ece68019e53c946be7f542",
70 | "sha256:9801bcd52ac9c795a7d81ea67471a42cffe532e46cfb750cd5713befc5c019c0",
71 | "sha256:99cf827f5a783038eb313beee6533dddb8bdb086d7269c5c144c1c952d142ace",
72 | "sha256:a21b78af7e2e13bec6bea12fc33bc05730197674f3e5402ce214d07026ccfebd",
73 | "sha256:a52e8f317336a44836475e9c802f51c2dc38d612eaa76532cb1d17690338b63b",
74 | "sha256:a702005e447d712375433ed0499cb6e1503fadd6c96a47f51d707b4d37b76d3c",
75 | "sha256:a708c291900c40a7ecf23f1d2384ed0bc0604e24094dd13417c7e7f8f7a50d93",
76 | "sha256:a7790a273225b0c46e5f859c1327f0f659896cc72eaa537d23aa3ad9ff2a1cc1",
77 | "sha256:abcf7daa5ebcc89328326254f6dd6d566adb483d4d00178892afd386ab389de2",
78 | "sha256:add017c5bd6b9ec3a5f09248396b6ee2ce61c5621f087eb2269c813cd8813808",
79 | "sha256:af4139172ff0263d269abdcc641e944c9de4b5d660894a3ec7e9f9db63b56ac9",
80 | "sha256:b4015baed99d046c760f09a4c59d234d8f398a454380c3cf0b859aba97136090",
81 | "sha256:ba0006799f21d83c3717fe20e2707a10bbc296475155aadf4f5850f6659b96b9",
82 | "sha256:bdb98f4c9e8a1735efddfaa995b0c96559792da15d56b76428bdfc29f77c4cdb",
83 | "sha256:c34234a1bc9e466c104372af74d11a9f98338a3f72fae22b80485171a64e0144",
84 | "sha256:c580c2a61d8297a6e47f4d01f066517dbb019be98032880d19ece7f337a9401d",
85 | "sha256:ca9a40497f7e97a2a961c04fa8a6f23d790b0521350a8b455759d786b0bcb203",
86 | "sha256:cab343b265e38d4e00649cbbad9278b734c5715f9bcbb72c85a1f99b1a58e19a",
87 | "sha256:ce52aad32ec6e46d1a91ff8b8014a91538800dd533914bfc4a82f5018d971408",
88 | "sha256:da07c7e7fc9a3f40446b78c54dbba8bfd5c9100dfecb21b65bfe3f57844f5e71",
89 | "sha256:dc8a0dbb2a10ae8bb609584f5c504789f0f3d0d81840da4849102ec84289f952",
90 | "sha256:e5b4b0d9440046ead3bd425eb2b852499241ee0cef1ae151038e4f87ede888c4",
91 | "sha256:f33d8efb42e4fc2b31b3b4527940b25cdebb3026fb56a80c1c1c11a4271d2352",
92 | "sha256:f6befb83bca720b71d6bd6326a3b26e9496ae6649e26585de024890fe50f49b8",
93 | "sha256:fcc849b28f584ed1dbf277291ded5c32bb3476a37032df4a1d523b55faa5f944",
94 | "sha256:ff44de36772b05c2eb74f2b4b6d1ae29b8f41ed5506310ce1258d44826ee38c1"
95 | ],
96 | "index": "pypi",
97 | "version": "==4.6.5"
98 | },
99 | "py": {
100 | "hashes": [
101 | "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
102 | "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
103 | ],
104 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
105 | "version": "==1.11.0"
106 | },
107 | "pyshark": {
108 | "hashes": [
109 | "sha256:d46c7470561243c02abb32c31eff8817a76dcc4fa29df976378e172429639d8b"
110 | ],
111 | "index": "pypi",
112 | "version": "==0.4.2.2"
113 | }
114 | },
115 | "develop": {}
116 | }
117 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | 66 61 74 74 2e
3 |
4 | fingerprint all the things!
5 |
6 | [](https://www.gnu.org/licenses/gpl-3.0)
7 |
8 | > More info about the fingerprinting methods, sample use-cases and research results will be added to the repo soon. Stay tuned!
9 |
10 | A script for extracting network metadata and fingerprints such as [JA3](https://github.com/salesforce/ja3) and [HASSH](https://github.com/salesforce/hassh) from packet capture files (pcap) or live network traffic. The main use-case is for monitoring honeypots, but you can also use it for other use cases such as network forensic analysis. fatt works on Linux, macOS and Windows.
11 |
12 | Note that fatt uses pyshark (a python wrapper for tshark) and therefore the performance is not great! But that's not a big issue as obviously this is not a tool you use in production. You can use other network analysis tools such as [Bro/Zeek](https://github.com/bro/bro), [Suricata](https://github.com/OISF/suricata) or [Netcap](https://github.com/dreadl0ck/netcap) for more serious use cases. [Joy](https://github.com/cisco/joy) is another great tool you can use for capturing and analyzing network flow data.
13 |
14 | Other than that, I'm working on a go based version of fatt which is faster, and you can use its libraries in your gopacket based tools such as packetbeat. I released the initial version of its gQUIC library ([QUICk](https://github.com/0x4D31/quick)).
15 |
16 |
17 | ### Features
18 |
19 | - Protocol support: SSL/TLS, SSH, RDP, HTTP, gQUIC.
20 | - To be added soon: IETF QUIC, MySQL, MSSQL, etc.
21 | - Fingerprinting
22 | - JA3: TLS client/server fingerprint
23 | - HASSH: SSH client/server fingerprint
24 | - RDFP: my experimental RDP fingerprint for standard RDP security protocol (note that other RDP security modes use TLS and can be fingerprinted with JA3)
25 | - HTTP header fingerprint
26 | - gQUIC/iQUIC fingerprint will be added soon
27 | - JSON output
28 |
29 | ## Getting Started
30 |
31 | 1. Install tshark
32 |
33 | You need to first install [tshark](https://github.com/wireshark/wireshark). Make sure you have the version v2.9.0 or later. Tshark/Wireshak renamed 'ssl' to 'tls' from version v2.9.0, and fatt is written based on the new version of tshark.
34 |
35 | If you have an old version of tshark (< v2.9.0), you can use the fatt script from ["old-tshark" branch](https://github.com/0x4D31/fatt/tree/old-tshark).
36 |
37 | 2. Install dependencies
38 |
39 | ```buildoutcfg
40 | cd fatt/
41 | pip3 install pipenv
42 | pipenv install
43 | ```
44 |
45 | OR just install pyshark if you don't want to use a virtual environment:
46 |
47 | ```buildoutcfg
48 | pip3 install pyshark==0.4.2.2
49 | ```
50 |
51 | To activate the virtualenv, run pipenv shell:
52 | ```buildoutcfg
53 | $ pipenv shell
54 | Launching subshell in virtual environment…
55 | bash-3.2$ . /Users/adel/.local/share/virtualenvs/fatt-ucJHMzzt/bin/activate
56 | (fatt-ucJHMzzt) bash-3.2$ python3 fatt.py -h
57 | ```
58 |
59 | Alternatively, run the command inside the virtualenv with `pipenv run`:
60 |
61 | ```buildoutcfg
62 | $ pipenv run python3 fatt.py -h
63 | ```
64 |
65 | Output:
66 |
67 | ```buildoutcfg
68 | usage: fatt.py [-h] [-r READ_FILE] [-d READ_DIRECTORY] [-i INTERFACE]
69 | [-fp [{tls,ssh,rdp,http,gquic} [{tls,ssh,rdp,http,gquic} ...]]]
70 | [-da DECODE_AS] [-f BPF_FILTER] [-j] [-o OUTPUT_FILE]
71 | [-w WRITE_PCAP] [-p]
72 |
73 | A python script for extracting network fingerprints
74 |
75 | optional arguments:
76 | -h, --help show this help message and exit
77 | -r READ_FILE, --read_file READ_FILE
78 | pcap file to process
79 | -d READ_DIRECTORY, --read_directory READ_DIRECTORY
80 | directory of pcap files to process
81 | -i INTERFACE, --interface INTERFACE
82 | listen on interface
83 | -fp [{tls,ssh,rdp,http,gquic} [{tls,ssh,rdp,http,gquic} ...]], --fingerprint [{tls,ssh,rdp,http,gquic} [{tls,ssh,rdp,http,gquic} ...]]
84 | protocols to fingerprint. Default: all
85 | -da DECODE_AS, --decode_as DECODE_AS
86 | a dictionary of {decode_criterion_string:
87 | decode_as_protocol} that is used to tell tshark to
88 | decode protocols in situations it wouldn't usually.
89 | -f BPF_FILTER, --bpf_filter BPF_FILTER
90 | BPF capture filter to use (for live capture only).'
91 | -j, --json_logging log the output in json format
92 | -o OUTPUT_FILE, --output_file OUTPUT_FILE
93 | specify the output log file. Default: fatt.log
94 | -w WRITE_PCAP, --write_pcap WRITE_PCAP
95 | save the live captured packets to this file
96 | -p, --print_output print the output
97 | ```
98 |
99 | ## Usage
100 |
101 | #### Live network traffic capture:
102 |
103 | ```buildoutcfg
104 | $ python3 fatt.py -i en0 --print_output --json_logging
105 | 192.168.1.10:59565 -> 192.168.1.3:80 [HTTP] hash=598c34a2838e82f9ec3175305f233b89 userAgent="Spotify/109600181 OSX/0 (MacBookPro14,3)"
106 | 192.168.1.10:59566 -> 13.237.44.5:22 [SSH] hassh=ec7378c1a92f5a8dde7e8b7a1ddf33d1 client=SSH-2.0-OpenSSH_7.9
107 | 13.237.44.5:22 -> 192.168.1.10:59566 [SSH] hasshS=3f0099d323fed5119bbfcca064478207 server=SSH-2.0-babeld-80573d3e
108 | 192.168.1.10:59584 -> 93.184.216.34:443 [TLS] ja3=e6573e91e6eb777c0933c5b8f97f10cd serverName=example.com
109 | 93.184.216.34:443 -> 192.168.1.10:59584 [TLS] ja3s=ae53107a2e47ea20c72ac44821a728bf
110 | 192.168.1.10:59588 -> 192.168.1.3:80 [HTTP] hash=598c34a2838e82f9ec3175305f233b89 userAgent="Spotify/109600181 OSX/0 (MacBookPro14,3)"
111 | 192.168.1.10:59601 -> 216.58.196.142:80 [HTTP] hash=d6662c018cd4169689ddf7c6c0f8ca1b userAgent="curl/7.54.0"
112 | 216.58.196.142:80 -> 192.168.1.10:59601 [HTTP] hash=c5241aca9a7c86f06f476592f5dda9a1 server=gws
113 | 192.168.1.10:54387 -> 216.58.203.99:443 [QUIC] UAID="Chrome/74.0.3729.169 Intel Mac OS X 10_14_5" SNI=clientservices.googleapis.com AEAD=AESG KEXS=C255
114 | ```
115 |
116 | JSON output:
117 |
118 | ```buildoutcfg
119 | $ cat fatt.log
120 | {"timestamp": "2019-05-28T03:41:25.415086", "sourceIp": "192.168.1.10", "destinationIp": "192.168.1.3", "sourcePort": "59565", "destinationPort": "80", "protocol": "http", "http": {"requestURI": "/DIAL/apps/com.spotify.Spotify.TVv2", "requestFullURI": "http://192.168.1.3/DIAL/apps/com.spotify.Spotify.TVv2", "requestVersion": "HTTP/1.1", "requestMethod": "GET", "userAgent": "Spotify/109600181 OSX/0 (MacBookPro14,3)", "clientHeaderOrder": "connection,accept_encoding,host,user_agent", "clientHeaderHash": "598c34a2838e82f9ec3175305f233b89"}}
121 | {"timestamp": "2019-05-28T03:41:26.099574", "sourceIp": "13.237.44.5", "destinationIp": "192.168.1.10", "sourcePort": "22", "destinationPort": "59566", "protocol": "ssh", "ssh": {"server": "SSH-2.0-babeld-80573d3e", "hasshServer": "3f0099d323fed5119bbfcca064478207", "hasshServerAlgorithms": "curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256;chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr,aes256-cbc,aes192-cbc,aes128-cbc;hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1;none,zlib,zlib@openssh.com", "hasshVersion": "1.0", "skex": "curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256", "seastc": "chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr,aes256-cbc,aes192-cbc,aes128-cbc", "smastc": "hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1", "scastc": "none,zlib,zlib@openssh.com", "slcts": "[Empty]", "slstc": "[Empty]", "seacts": "chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr,aes256-cbc,aes192-cbc,aes128-cbc", "smacts": "hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1", "scacts": "none,zlib,zlib@openssh.com", "sshka": "ssh-dss,rsa-sha2-512,rsa-sha2-256,ssh-rsa"}}
122 | {"timestamp": "2019-05-28T03:41:26.106737", "sourceIp": "192.168.1.10", "destinationIp": "13.237.44.5", "sourcePort": "59566", "destinationPort": "22", "protocol": "ssh", "ssh": {"client": "SSH-2.0-OpenSSH_7.9", "hassh": "ec7378c1a92f5a8dde7e8b7a1ddf33d1", "hasshAlgorithms": "curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,ext-info-c;chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com;umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1;none,zlib@openssh.com,zlib", "hasshVersion": "1.0", "ckex": "curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,ext-info-c", "ceacts": "chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com", "cmacts": "umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1", "ccacts": "none,zlib@openssh.com,zlib", "clcts": "[Empty]", "clstc": "[Empty]", "ceastc": "chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com", "cmastc": "umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1", "ccastc": "none,zlib@openssh.com,zlib", "cshka": "rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256,ssh-rsa,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519"}}
123 | {"timestamp": "2019-05-28T03:41:36.762811", "sourceIp": "192.168.1.10", "destinationIp": "93.184.216.34", "sourcePort": "59584", "destinationPort": "443", "protocol": "tls", "tls": {"serverName": "example.com", "ja3": "e6573e91e6eb777c0933c5b8f97f10cd", "ja3Algorithms": "771,49200-49196-49192-49188-49172-49162-159-107-57-52393-52392-52394-65413-196-136-129-157-61-53-192-132-49199-49195-49191-49187-49171-49161-158-103-51-190-69-156-60-47-186-65-49170-49160-22-10-255,0-11-10-13-16,29-23-24,0", "ja3Version": "771", "ja3Ciphers": "49200-49196-49192-49188-49172-49162-159-107-57-52393-52392-52394-65413-196-136-129-157-61-53-192-132-49199-49195-49191-49187-49171-49161-158-103-51-190-69-156-60-47-186-65-49170-49160-22-10-255", "ja3Extensions": "0-11-10-13-16", "ja3Ec": "29-23-24", "ja3EcFmt": "0"}}
124 | {"timestamp": "2019-05-28T03:41:36.920935", "sourceIp": "93.184.216.34", "destinationIp": "192.168.1.10", "sourcePort": "443", "destinationPort": "59584", "protocol": "tls", "tls": {"ja3s": "ae53107a2e47ea20c72ac44821a728bf", "ja3sAlgorithms": "771,49199,65281-0-11-16", "ja3sVersion": "771", "ja3sCiphers": "49199", "ja3sExtensions": "65281-0-11-16"}}
125 | {"timestamp": "2019-05-28T03:41:37.487609", "sourceIp": "192.168.1.10", "destinationIp": "192.168.1.3", "sourcePort": "59588", "destinationPort": "80", "protocol": "http", "http": {"requestURI": "/DIAL/apps/com.spotify.Spotify.TVv2", "requestFullURI": "http://192.168.1.3/DIAL/apps/com.spotify.Spotify.TVv2", "requestVersion": "HTTP/1.1", "requestMethod": "GET", "userAgent": "Spotify/109600181 OSX/0 (MacBookPro14,3)", "clientHeaderOrder": "connection,accept_encoding,host,user_agent", "clientHeaderHash": "598c34a2838e82f9ec3175305f233b89"}}
126 | {"timestamp": "2019-05-28T03:41:48.700730", "sourceIp": "192.168.1.10", "destinationIp": "216.58.196.142", "sourcePort": "59601", "destinationPort": "80", "protocol": "http", "http": {"requestURI": "/", "requestFullURI": "http://google.com/", "requestVersion": "HTTP/1.1", "requestMethod": "GET", "userAgent": "curl/7.54.0", "clientHeaderOrder": "host,user_agent,accept", "clientHeaderHash": "d6662c018cd4169689ddf7c6c0f8ca1b"}}
127 | {"timestamp": "2019-05-28T03:41:48.805393", "sourceIp": "216.58.196.142", "destinationIp": "192.168.1.10", "sourcePort": "80", "destinationPort": "59601", "protocol": "http", "http": {"server": "gws", "serverHeaderOrder": "location,content_type,date,cache_control,server,content_length", "serverHeaderHash": "c5241aca9a7c86f06f476592f5dda9a1"}}
128 | {"timestamp": "2019-05-28T03:41:58.038530", "sourceIp": "192.168.1.10", "destinationIp": "216.58.203.99", "sourcePort": "54387", "destinationPort": "443", "protocol": "gquic", "gquic": {"tagNumber": "25", "sni": "clientservices.googleapis.com", "uaid": "Chrome/74.0.3729.169 Intel Mac OS X 10_14_5", "ver": "Q043", "aead": "AESG", "smhl": "1", "mids": "100", "kexs": "C255", "xlct": "cd9baccc808a6d3b", "copt": "NSTP", "ccrt": "cd9baccc808a6d3b67f8adc58015e3ff", "stk": "d6a64aeb563a19fe091bc34e8c038b0a3a884c5db7caae071180c5b739bca3dd7c42e861386718982fbe6db9d1cb136f799e8d10fd5a", "pdmd": "X509", "ccs": "01e8816092921ae8", "scid": "376976b980c73b669fea57104fb725c6"}}
129 | ```
130 |
131 | #### Packet capture file (pcap):
132 |
133 | Let's have a look at the captured traffic of Metasploit auxiliary scanner for the recent CVE-2019-0708 RDP vulnerability (BlueKeep).
134 |
135 | ```
136 | $ python3 fatt.py -r RDP/cve-2019-0708_metasploit_aux.pcap -p -j; cat fatt.log | python -m json.tool
137 | 192.168.1.10:39079 -> 192.168.1.20:3389 [RDP] rdfp=3ba3d115055e593e3550575a36e68153 cookie="mstshash=user0" req_protocols=0x00000000
138 |
139 | {
140 | "destinationIp": "192.168.1.20",
141 | "destinationPort": "3389",
142 | "protocol": "rdp",
143 | "rdp": {
144 | "channelDefArray": {
145 | "0": {
146 | "name": "cliprdr",
147 | "options": "c0a00000"
148 | },
149 | "1": {
150 | "name": "MS_T120",
151 | "options": "80800000"
152 | },
153 | "2": {
154 | "name": "rdpsnd",
155 | "options": "c0000000"
156 | },
157 | "3": {
158 | "name": "snddbg",
159 | "options": "c0000000"
160 | },
161 | "4": {
162 | "name": "rdpdr",
163 | "options": "80800000"
164 | }
165 | },
166 | "clientBuild": "2600",
167 | "clientDigProductId": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
168 | "clientName": "x1810",
169 | "clientProductId": "1",
170 | "clusterFlags": "09000000",
171 | "colorDepth": "0x0000ca01",
172 | "connectionType": "0",
173 | "cookie": "mstshash=user0",
174 | "desktopHeight": "600",
175 | "desktopWidth": "800",
176 | "earlyCapabilityFlags": "1",
177 | "encryptionMethods": "03000000",
178 | "extEncMethods": "00000000",
179 | "highColorDepth": "0x00000018",
180 | "keyboardFuncKey": "12",
181 | "keyboardLayout": "1033",
182 | "keyboardSubtype": "0",
183 | "keyboardType": "4",
184 | "pad1Octet": "00",
185 | "postbeta2ColorDepth": "0x0000ca01",
186 | "rdfp": "3ba3d115055e593e3550575a36e68153",
187 | "rdfpAlgorithms": "4,8,09000000,03000000,00000000,cliprdr:c0a00000-MS_T120:80800000-rdpsnd:c0000000-snddbg:c0000000-rdpdr:80800000",
188 | "rdfpVersion": "0.3",
189 | "requestedProtocols": "0x00000000",
190 | "sasSequence": "43523",
191 | "serialNumber": "0",
192 | "supportedColorDepths": "0x00000007",
193 | "verMajor": "4",
194 | "verMinor": "8"
195 | },
196 | "sourceIp": "192.168.1.10",
197 | "sourcePort": "39079",
198 | "timestamp": "2019-05-23T03:51:25.438445"
199 | }
200 | ```
201 |
202 | Let's test it with another CVE-2019-0708 PoC:
203 |
204 | ```buildoutcfg
205 | $ python3 fatt.py -r RDP/cve-2019-0708_poc.pcap -p -j; cat fatt.log | python -m json.tool
206 | 192.168.1.10:54303 -> 192.168.1.20:3389 [RDP] req_protocols=0x00000001
207 |
208 | {
209 | "destinationIp": "192.168.1.20",
210 | "destinationPort": "3389",
211 | "protocol": "rdp",
212 | "rdp": {
213 | "requestedProtocols": "0x00000001"
214 | },
215 | "sourceIp": "192.168.1.10",
216 | "sourcePort": "54303",
217 | "timestamp": "2019-05-23T18:41:42.572758"
218 | }
219 | ```
220 |
221 | This time we don't see the RDP ClientInfo message because the PoC uses TLS (not the standard RDP security protocol). So we can just see the `Negotiation Request` messages, but if you decode the packet as TLS, you can see the TLS clientHello and JA3 fingerprint. Here's how you can decode a specific port as another protocol:
222 |
223 | ```buildoutcfg
224 | $ python3 fatt.py -r RDP//cve-2019-0708_poc.pcap -p -j --decode_as '{"tcp.port==3389": "tls"}'
225 | 192.168.1.10:50026 -> 192.168.1.20:3389 [TLS] ja3=67e3d18fd9dddbbc8eca65f7dedac674 serverName=192.168.1.20
226 | 192.168.1.20:3389 -> 192.168.1.10:50026 [TLS] ja3s=649d6810e8392f63dc311eecb6b7098b
227 |
228 | $ cat fatt.log
229 | {"timestamp": "2019-05-23T17:21:56.056200", "sourceIp": "192.168.1.10", "destinationIp": "192.168.1.20", "sourcePort": "50026", "destinationPort": "3389", "protocol": "tls", "tls": {"serverName": "192.168.1.20", "ja3": "67e3d18fd9dddbbc8eca65f7dedac674", "ja3Algorithms": "771,49196-49195-49200-49199-159-158-49188-49187-49192-49191-49162-49161-49172-49171-57-51-157-156-61-60-53-47-10-106-64-56-50-19-5-4,0-5-10-11-13-35-23-65281,29-23-24,0", "ja3Version": "771", "ja3Ciphers": "49196-49195-49200-49199-159-158-49188-49187-49192-49191-49162-49161-49172-49171-57-51-157-156-61-60-53-47-10-106-64-56-50-19-5-4", "ja3Extensions": "0-5-10-11-13-35-23-65281", "ja3Ec": "29-23-24", "ja3EcFmt": "0"}}
230 | {"timestamp": "2019-05-23T17:21:56.059333", "sourceIp": "192.168.1.20", "destinationIp": "192.168.1.10", "sourcePort": "3389", "destinationPort": "50026", "protocol": "tls", "tls": {"ja3s": "649d6810e8392f63dc311eecb6b7098b", "ja3sAlgorithms": "771,49192,23-65281", "ja3sVersion": "771", "ja3sCiphers": "49192", "ja3sExtensions": "23-65281"}}
231 | ```
232 |
233 | ## TODO:
234 |
235 | - https://github.com/0x4D31/fatt/wiki/TODO
236 |
--------------------------------------------------------------------------------
/docs/fatt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/0x4D31/fatt/c29e553514281e50781f86932b82337a5ada5640/docs/fatt.png
--------------------------------------------------------------------------------
/fatt.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # Copyright (c) 2019, Adel "0x4d31" Karimi.
3 | # All rights reserved.
4 | # Licensed under the BSD 3-Clause license.
5 | # For full license text, see the LICENSE file in the repo root
6 | # or https://opensource.org/licenses/BSD-3-Clause
7 |
8 | # fatt. Fingerprint All The Things
9 | # Supported protocols: SSL/TLS, SSH, RDP, HTTP, gQUIC
10 |
11 | import argparse
12 | import pyshark
13 | import os
14 | import json
15 | import logging
16 | import struct
17 | from hashlib import md5
18 | from collections import defaultdict
19 |
20 | __author__ = "Adel '0x4D31' Karimi"
21 | __version__ = "1.0"
22 |
23 |
24 | CAP_BPF_FILTER = (
25 | 'tcp port 22 or tcp port 2222 or tcp port 3389 or '
26 | 'tcp port 443 or tcp port 993 or tcp port 995 or '
27 | 'tcp port 636 or tcp port 990 or tcp port 992 or '
28 | 'tcp port 989 or tcp port 563 or tcp port 614 or '
29 | 'tcp port 3306 or tcp port 80 or udp port 80 or '
30 | 'udp port 443')
31 | DISPLAY_FILTER = (
32 | 'tls.handshake.type == 1 || tls.handshake.type == 2 ||'
33 | 'ssh.message_code == 20 || ssh.protocol || rdp ||'
34 | 'gquic.tag == "CHLO" || http.request.method || data-text-lines'
35 | )
36 | DECODE_AS = {
37 | 'tcp.port==2222': 'ssh', 'tcp.port==3389': 'tpkt',
38 | 'tcp.port==993': 'tls', 'tcp.port==995': 'tls',
39 | 'tcp.port==990': 'tls', 'tcp.port==992': 'tls',
40 | 'tcp.port==989': 'tls', 'tcp.port==563': 'tls',
41 | 'tcp.port==614': 'tls', 'tcp.port==636': 'tls'}
42 | HASSH_VERSION = '1.0'
43 | RDFP_VERSION = '0.3'
44 |
45 |
46 | class ProcessPackets:
47 |
48 | def __init__(self, fingerprint, jlog, pout):
49 | self.logger = logging.getLogger()
50 | self.fingerprint = fingerprint
51 | self.jlog = jlog
52 | self.pout = pout
53 | self.protocol_dict = {}
54 | self.rdp_dict = defaultdict(dict)
55 |
56 | def process(self, packet):
57 | record = None
58 | proto = packet.highest_layer
59 | sourceIp = packet.ipv6.src if 'ipv6' in packet else packet.ip.src
60 | destinationIp = packet.ipv6.dst if 'ipv6' in packet else packet.ip.dst
61 |
62 | # Clear the dictionary used for extracting ssh protocol strings
63 | # and rdp cookies/negotiateRequests
64 | if len(self.protocol_dict) > 100 and proto != 'SSH':
65 | self.protocol_dict.clear()
66 | if len(self.rdp_dict) > 100 and proto != 'RDP':
67 | self.rdp_dict.clear()
68 |
69 | # [ SSH ]
70 | if proto == 'SSH' and ('ssh' in self.fingerprint or
71 | self.fingerprint == 'all'):
72 | # Extract SSH identification string and correlate with KEXINIT msg
73 | if 'protocol' in packet.ssh.field_names:
74 | key = '{}:{}_{}:{}'.format(
75 | sourceIp,
76 | packet.tcp.srcport,
77 | destinationIp,
78 | packet.tcp.dstport)
79 | self.protocol_dict[key] = packet.ssh.protocol
80 | if 'message_code' not in packet.ssh.field_names:
81 | return
82 | if packet.ssh.message_code != '20':
83 | return
84 | # log the anomalous / retransmission packets
85 | if ("analysis_retransmission" in packet.tcp.field_names or
86 | "analysis_spurious_retransmission" in packet.tcp.field_names):
87 | event = event_log(packet, event="retransmission")
88 | if record and self.jlog:
89 | self.logger.info(json.dumps(event))
90 | return
91 | # Client HASSH
92 | if int(packet.tcp.srcport) > int(packet.tcp.dstport):
93 | record = self.client_hassh(packet)
94 | # Print the result
95 | if self.pout:
96 | tmp = ('{sip}:{sp} -> {dip}:{dp} [SSH] hassh={hassh} client={client}')
97 | tmp = tmp.format(
98 | sip=record['sourceIp'],
99 | sp=record['sourcePort'],
100 | dip=record['destinationIp'],
101 | dp=record['destinationPort'],
102 | client=record['ssh']['client'],
103 | hassh=record['ssh']['hassh'],
104 | )
105 | print(tmp)
106 | # Server HASSH
107 | elif int(packet.tcp.srcport) < int(packet.tcp.dstport):
108 | record = self.server_hassh(packet)
109 | # Print the result
110 | if self.pout:
111 | tmp = ('{sip}:{sp} -> {dip}:{dp} [SSH] hasshS={hasshs} server={server}')
112 | tmp = tmp.format(
113 | sip=record['sourceIp'],
114 | sp=record['sourcePort'],
115 | dip=record['destinationIp'],
116 | dp=record['destinationPort'],
117 | server=record['ssh']['server'],
118 | hasshs=record['ssh']['hasshServer'],
119 | )
120 | print(tmp)
121 | if record and self.jlog:
122 | self.logger.info(json.dumps(record))
123 | return
124 |
125 | # [ TLS ]
126 | # TODO: extract tls certificates
127 | elif proto == 'TLS' and ('tls' in self.fingerprint or
128 | self.fingerprint == 'all'):
129 | if 'record_content_type' not in packet.tls.field_names:
130 | return
131 | # Content Type: Handshake (22)
132 | if packet.tls.record_content_type != '22':
133 | return
134 | # Handshake Type: Client Hello (1) / Server Hello (2)
135 | if 'handshake_type' not in packet.tls.field_names:
136 | return
137 | htype = packet.tls.handshake_type
138 | if not (htype == '1' or htype == '2'):
139 | return
140 | # log the anomalous / retransmission packets
141 | if ("analysis_retransmission" in packet.tcp.field_names or
142 | "analysis_spurious_retransmission" in packet.tcp.field_names):
143 | event = event_log(packet, event="retransmission")
144 | # JA3
145 | if htype == '1':
146 | record = self.client_ja3(packet)
147 | # Print the result
148 | if self.pout:
149 | tmp = ('{sip}:{sp} -> {dip}:{dp} [TLS] ja3={ja3} serverName={sname}')
150 | tmp = tmp.format(
151 | sip=record['sourceIp'],
152 | sp=record['sourcePort'],
153 | dip=record['destinationIp'],
154 | dp=record['destinationPort'],
155 | sname=record['tls']['serverName'],
156 | ja3=record['tls']['ja3'],
157 | )
158 | print(tmp)
159 | elif htype == '2':
160 | record = self.server_ja3(packet)
161 | # Print the result
162 | if self.pout:
163 | tmp = (
164 | '{sip}:{sp} -> {dip}:{dp} [TLS] ja3s={ja3s}')
165 | tmp = tmp.format(
166 | sip=record['sourceIp'],
167 | sp=record['sourcePort'],
168 | dip=record['destinationIp'],
169 | dp=record['destinationPort'],
170 | ja3s=record['tls']['ja3s'],
171 | )
172 | print(tmp)
173 | if record and self.jlog:
174 | self.logger.info(json.dumps(record))
175 | return
176 |
177 | # [ RDP ]
178 | elif proto == 'RDP' and ('rdp' in self.fingerprint or
179 | self.fingerprint == 'all'):
180 | # Extract RDP cookie & negotiate request and correlate with ClientData msg
181 | key = None
182 | if 'rt_cookie' or 'negreq_requestedprotocols':
183 | key = '{}:{}_{}:{}'.format(
184 | sourceIp,
185 | packet.tcp.srcport,
186 | destinationIp,
187 | packet.tcp.dstport)
188 | if 'rt_cookie' in packet.rdp.field_names:
189 | cookie = packet.rdp.rt_cookie.replace('Cookie: ', '')
190 | self.rdp_dict[key]["cookie"] = cookie
191 | if 'negreq_requestedprotocols' in packet.rdp.field_names:
192 | req_protos = packet.rdp.negreq_requestedprotocols
193 | self.rdp_dict[key]["req_protos"] = req_protos
194 | # TLS/CredSSP (not standard RDP security protocols)
195 | if req_protos != "0x00000000":
196 | record = {
197 | "timestamp": packet.sniff_time.isoformat(),
198 | "sourceIp": sourceIp,
199 | "destinationIp": destinationIp,
200 | "sourcePort": packet.tcp.srcport,
201 | "destinationPort": packet.tcp.dstport,
202 | "protocol": "rdp",
203 | "rdp": {
204 | "requestedProtocols": req_protos
205 | }
206 | }
207 | if self.pout:
208 | tmp = (
209 | '{sip}:{sp} -> {dip}:{dp} [RDP] req_protocols={proto}')
210 | tmp = tmp.format(
211 | sip=record['sourceIp'],
212 | sp=record['sourcePort'],
213 | dip=record['destinationIp'],
214 | dp=record['destinationPort'],
215 | proto=record['rdp']['requestedProtocols']
216 | )
217 | print(tmp)
218 | if self.jlog:
219 | self.logger.info(json.dumps(record))
220 | if 'clientdata' not in packet.rdp.field_names:
221 | return
222 | if ("analysis_retransmission" in packet.tcp.field_names or
223 | "analysis_spurious_retransmission" in packet.tcp.field_names):
224 | event = event_log(packet, event="retransmission")
225 | if self.jlog:
226 | self.logger.info(json.dumps(event))
227 | return
228 | # Client RDFP
229 | record = self.client_rdfp(packet)
230 | # Print the result
231 | if self.pout:
232 | tmp = ('{sip}:{sp} -> {dip}:{dp} [RDP] rdfp={rdfp} cookie="{cookie}" req_protocols={proto}')
233 | tmp = tmp.format(
234 | sip=record['sourceIp'],
235 | sp=record['sourcePort'],
236 | dip=record['destinationIp'],
237 | dp=record['destinationPort'],
238 | rdfp=record['rdp']['rdfp'],
239 | cookie=record['rdp']['cookie'],
240 | proto=record['rdp']['requestedProtocols']
241 | )
242 | print(tmp)
243 | if record and self.jlog:
244 | self.logger.info(json.dumps(record))
245 | return
246 |
247 | # [ HTTP ]
248 | elif (proto == 'HTTP' or proto == 'DATA-TEXT-LINES') and \
249 | ('http' in self.fingerprint or self.fingerprint == 'all'):
250 | if 'request' in packet.http.field_names:
251 | record = self.client_http(packet)
252 | # Print the result
253 | if self.pout:
254 | tmp = ('{sip}:{sp} -> {dip}:{dp} [HTTP] hash={hash} userAgent="{ua}"')
255 | tmp = tmp.format(
256 | sip=record['sourceIp'],
257 | sp=record['sourcePort'],
258 | dip=record['destinationIp'],
259 | dp=record['destinationPort'],
260 | hash=record['http']['clientHeaderHash'],
261 | ua=record['http']['userAgent'],
262 | )
263 | print(tmp)
264 | elif 'response' in packet.http.field_names:
265 | record = self.server_http(packet)
266 | # Print the result
267 | if self.pout:
268 | tmp = ('{sip}:{sp} -> {dip}:{dp} [HTTP] hash={hash} server={server}')
269 | tmp = tmp.format(
270 | sip=record['sourceIp'],
271 | sp=record['sourcePort'],
272 | dip=record['destinationIp'],
273 | dp=record['destinationPort'],
274 | hash=record['http']['serverHeaderHash'],
275 | server=record['http']['server'],
276 | )
277 | print(tmp)
278 | if record and self.jlog:
279 | self.logger.info(json.dumps(record))
280 | return
281 |
282 | # [ QUIC ]
283 | elif (proto == 'GQUIC' or proto == 'QUIC') and\
284 | ('quic' in self.fingerprint or self.fingerprint == 'all'):
285 | if 'tag' in packet.gquic.field_names:
286 | if packet.gquic.tag == 'CHLO':
287 | record = self.client_gquic(packet)
288 | # Print the result
289 | if self.pout:
290 | tmp = ('{sip}:{sp} -> {dip}:{dp} [QUIC] UAID="{ua}" SNI={sn} AEAD={ea} KEXS={kex}')
291 | tmp = tmp.format(
292 | sip=record['sourceIp'],
293 | sp=record['sourcePort'],
294 | dip=record['destinationIp'],
295 | dp=record['destinationPort'],
296 | ua=record['gquic']['uaid'],
297 | sn=record['gquic']['sni'],
298 | ea=record['gquic']['aead'],
299 | kex=record['gquic']['kexs']
300 | )
301 | print(tmp)
302 | if record and self.jlog:
303 | self.logger.info(json.dumps(record))
304 | return
305 | return
306 |
307 | def client_hassh(self, packet):
308 | """returns HASSH (i.e. SSH Client Fingerprint)
309 | HASSH = md5(KEX;EACTS;MACTS;CACTS)
310 | """
311 | protocol = None
312 | sourceIp = packet.ipv6.src if 'ipv6' in packet else packet.ip.src
313 | destinationIp = packet.ipv6.dst if 'ipv6' in packet else packet.ip.dst
314 | key = '{}:{}_{}:{}'.format(
315 | sourceIp,
316 | packet.tcp.srcport,
317 | destinationIp,
318 | packet.tcp.dstport)
319 | if key in self.protocol_dict:
320 | protocol = self.protocol_dict[key]
321 | # hassh fields
322 | ckex = ceacts = cmacts = ccacts = ""
323 | if 'kex_algorithms' in packet.ssh.field_names:
324 | ckex = packet.ssh.kex_algorithms
325 | if 'encryption_algorithms_client_to_server' in packet.ssh.field_names:
326 | ceacts = packet.ssh.encryption_algorithms_client_to_server
327 | if 'mac_algorithms_client_to_server' in packet.ssh.field_names:
328 | cmacts = packet.ssh.mac_algorithms_client_to_server
329 | if 'compression_algorithms_client_to_server' in packet.ssh.field_names:
330 | ccacts = packet.ssh.compression_algorithms_client_to_server
331 | # Log other kexinit fields (only in JSON)
332 | clcts = clstc = ceastc = cmastc = ccastc = cshka = ""
333 | if 'languages_client_to_server' in packet.ssh.field_names:
334 | clcts = packet.ssh.languages_client_to_server
335 | if 'languages_server_to_client' in packet.ssh.field_names:
336 | clstc = packet.ssh.languages_server_to_client
337 | if 'encryption_algorithms_server_to_client' in packet.ssh.field_names:
338 | ceastc = packet.ssh.encryption_algorithms_server_to_client
339 | if 'mac_algorithms_server_to_client' in packet.ssh.field_names:
340 | cmastc = packet.ssh.mac_algorithms_server_to_client
341 | if 'compression_algorithms_server_to_client' in packet.ssh.field_names:
342 | ccastc = packet.ssh.compression_algorithms_server_to_client
343 | if 'server_host_key_algorithms' in packet.ssh.field_names:
344 | cshka = packet.ssh.server_host_key_algorithms
345 | # Create hassh
346 | hassh_str = ';'.join([ckex, ceacts, cmacts, ccacts])
347 | hassh = md5(hassh_str.encode()).hexdigest()
348 | record = {
349 | "timestamp": packet.sniff_time.isoformat(),
350 | "sourceIp": sourceIp,
351 | "destinationIp": destinationIp,
352 | "sourcePort": packet.tcp.srcport,
353 | "destinationPort": packet.tcp.dstport,
354 | "protocol": 'ssh',
355 | "ssh": {
356 | "client": protocol,
357 | "hassh": hassh,
358 | "hasshAlgorithms": hassh_str,
359 | "hasshVersion": HASSH_VERSION,
360 | "ckex": ckex,
361 | "ceacts": ceacts,
362 | "cmacts": cmacts,
363 | "ccacts": ccacts,
364 | "clcts": clcts,
365 | "clstc": clstc,
366 | "ceastc": ceastc,
367 | "cmastc": cmastc,
368 | "ccastc": ccastc,
369 | "cshka": cshka
370 | }
371 | }
372 | return record
373 |
374 | def server_hassh(self, packet):
375 | """returns HASSHServer (i.e. SSH Server Fingerprint)
376 | HASSHServer = md5(KEX;EASTC;MASTC;CASTC)
377 | """
378 | protocol = None
379 | sourceIp = packet.ipv6.src if 'ipv6' in packet else packet.ip.src
380 | destinationIp = packet.ipv6.dst if 'ipv6' in packet else packet.ip.dst
381 | key = '{}:{}_{}:{}'.format(
382 | sourceIp,
383 | packet.tcp.srcport,
384 | destinationIp,
385 | packet.tcp.dstport)
386 | if key in self.protocol_dict:
387 | protocol = self.protocol_dict[key]
388 | # hasshServer fields
389 | skex = seastc = smastc = scastc = ""
390 | if 'kex_algorithms' in packet.ssh.field_names:
391 | skex = packet.ssh.kex_algorithms
392 | if 'encryption_algorithms_server_to_client' in packet.ssh.field_names:
393 | seastc = packet.ssh.encryption_algorithms_server_to_client
394 | if 'mac_algorithms_server_to_client' in packet.ssh.field_names:
395 | smastc = packet.ssh.mac_algorithms_server_to_client
396 | if 'compression_algorithms_server_to_client' in packet.ssh.field_names:
397 | scastc = packet.ssh.compression_algorithms_server_to_client
398 | # Log other kexinit fields (only in JSON)
399 | slcts = slstc = seacts = smacts = scacts = sshka = ""
400 | if 'languages_client_to_server' in packet.ssh.field_names:
401 | slcts = packet.ssh.languages_client_to_server
402 | if 'languages_server_to_client' in packet.ssh.field_names:
403 | slstc = packet.ssh.languages_server_to_client
404 | if 'encryption_algorithms_client_to_server' in packet.ssh.field_names:
405 | seacts = packet.ssh.encryption_algorithms_client_to_server
406 | if 'mac_algorithms_client_to_server' in packet.ssh.field_names:
407 | smacts = packet.ssh.mac_algorithms_client_to_server
408 | if 'compression_algorithms_client_to_server' in packet.ssh.field_names:
409 | scacts = packet.ssh.compression_algorithms_client_to_server
410 | if 'server_host_key_algorithms' in packet.ssh.field_names:
411 | sshka = packet.ssh.server_host_key_algorithms
412 | # Create hasshServer
413 | hasshs_str = ';'.join([skex, seastc, smastc, scastc])
414 | hasshs = md5(hasshs_str.encode()).hexdigest()
415 | record = {
416 | "timestamp": packet.sniff_time.isoformat(),
417 | "sourceIp": sourceIp,
418 | "destinationIp": destinationIp,
419 | "sourcePort": packet.tcp.srcport,
420 | "destinationPort": packet.tcp.dstport,
421 | "protocol": 'ssh',
422 | "ssh": {
423 | "server": protocol,
424 | "hasshServer": hasshs,
425 | "hasshServerAlgorithms": hasshs_str,
426 | "hasshVersion": HASSH_VERSION,
427 | "skex": skex,
428 | "seastc": seastc,
429 | "smastc": smastc,
430 | "scastc": scastc,
431 | "slcts": slcts,
432 | "slstc": slstc,
433 | "seacts": seacts,
434 | "smacts": smacts,
435 | "scacts": scacts,
436 | "sshka": sshka
437 | }
438 | }
439 | return record
440 |
441 | def client_ja3(self, packet):
442 | # GREASE_TABLE Ref: https://tools.ietf.org/html/draft-davidben-tls-grease-00
443 | GREASE_TABLE = ['2570', '6682', '10794', '14906', '19018', '23130',
444 | '27242', '31354', '35466', '39578', '43690', '47802',
445 | '51914', '56026', '60138', '64250']
446 | # ja3 fields
447 | tls_version = ciphers = extensions = elliptic_curve = ec_pointformat = ""
448 | if 'handshake_version' in packet.tls.field_names:
449 | tls_version = int(packet.tls.handshake_version, 16)
450 | tls_version = str(tls_version)
451 | if 'handshake_ciphersuite' in packet.tls.field_names:
452 | cipher_list = [
453 | c.show for c in packet.tls.handshake_ciphersuite.fields
454 | if c.show not in GREASE_TABLE]
455 | ciphers = '-'.join(cipher_list)
456 | if 'handshake_extension_type' in packet.tls.field_names:
457 | extension_list = [
458 | e.show for e in packet.tls.handshake_extension_type.fields
459 | if e.show not in GREASE_TABLE]
460 | extensions = '-'.join(extension_list)
461 | if 'handshake_extensions_supported_group' in packet.tls.field_names:
462 | ec_list = [str(int(ec.show, 16)) for ec in
463 | packet.tls.handshake_extensions_supported_group.fields
464 | if str(int(ec.show, 16)) not in GREASE_TABLE]
465 | elliptic_curve = '-'.join(ec_list)
466 | if 'handshake_extensions_ec_point_format' in packet.tls.field_names:
467 | ecpf_list = [ecpf.show for ecpf in
468 | packet.tls.handshake_extensions_ec_point_format.fields
469 | if ecpf.show not in GREASE_TABLE]
470 | ec_pointformat = '-'.join(ecpf_list)
471 | # TODO: log other non-ja3 fields
472 | server_name = ""
473 | if 'handshake_extensions_server_name' in packet.tls.field_names:
474 | server_name = packet.tls.handshake_extensions_server_name
475 | # Create ja3
476 | ja3_string = ','.join([
477 | tls_version, ciphers, extensions, elliptic_curve, ec_pointformat])
478 | ja3 = md5(ja3_string.encode()).hexdigest()
479 | sourceIp = packet.ipv6.src if 'ipv6' in packet else packet.ip.src
480 | destinationIp = packet.ipv6.dst if 'ipv6' in packet else packet.ip.dst
481 | record = {
482 | "timestamp": packet.sniff_time.isoformat(),
483 | "sourceIp": sourceIp,
484 | "destinationIp": destinationIp,
485 | "sourcePort": packet.tcp.srcport,
486 | "destinationPort": packet.tcp.dstport,
487 | "protocol": "tls",
488 | "tls": {
489 | "serverName": server_name,
490 | "ja3": ja3,
491 | "ja3Algorithms": ja3_string,
492 | "ja3Version": tls_version,
493 | "ja3Ciphers": ciphers,
494 | "ja3Extensions": extensions,
495 | "ja3Ec": elliptic_curve,
496 | "ja3EcFmt": ec_pointformat
497 | }
498 | }
499 | return record
500 |
501 | def server_ja3(self, packet):
502 | # GREASE_TABLE Ref: https://tools.ietf.org/html/draft-davidben-tls-grease-00
503 | GREASE_TABLE = ['2570', '6682', '10794', '14906', '19018', '23130',
504 | '27242', '31354', '35466', '39578', '43690', '47802',
505 | '51914', '56026', '60138', '64250']
506 | # ja3s fields
507 | tls_version = ciphers = extensions = ""
508 | if 'handshake_version' in packet.tls.field_names:
509 | tls_version = int(packet.tls.handshake_version, 16)
510 | tls_version = str(tls_version)
511 | if 'handshake_ciphersuite' in packet.tls.field_names:
512 | cipher_list = [
513 | c.show for c in packet.tls.handshake_ciphersuite.fields
514 | if c.show not in GREASE_TABLE]
515 | ciphers = '-'.join(cipher_list)
516 | if 'handshake_extension_type' in packet.tls.field_names:
517 | extension_list = [
518 | e.show for e in packet.tls.handshake_extension_type.fields
519 | if e.show not in GREASE_TABLE]
520 | extensions = '-'.join(extension_list)
521 | # TODO: log other non-ja3s fields
522 | # Create ja3s
523 | ja3s_string = ','.join([
524 | tls_version, ciphers, extensions])
525 | ja3s = md5(ja3s_string.encode()).hexdigest()
526 | sourceIp = packet.ipv6.src if 'ipv6' in packet else packet.ip.src
527 | destinationIp = packet.ipv6.dst if 'ipv6' in packet else packet.ip.dst
528 | record = {
529 | "timestamp": packet.sniff_time.isoformat(),
530 | "sourceIp": sourceIp,
531 | "destinationIp": destinationIp,
532 | "sourcePort": packet.tcp.srcport,
533 | "destinationPort": packet.tcp.dstport,
534 | "protocol": "tls",
535 | "tls": {
536 | "ja3s": ja3s,
537 | "ja3sAlgorithms": ja3s_string,
538 | "ja3sVersion": tls_version,
539 | "ja3sCiphers": ciphers,
540 | "ja3sExtensions": extensions
541 | }
542 | }
543 | return record
544 |
545 | def client_rdfp(self, packet):
546 | """returns ClientData message fields and RDFP (experimental fingerprint)
547 | RDFP = md5(verMajor,verMinor,clusterFlags,encryptionMethods,extEncMethods,channelDef)
548 | """
549 | # RDP fields
550 | verMajor = verMinor = desktopWidth = desktopHeight = colorDepth = \
551 | sasSequence = keyboardLayout = clientBuild = clientName = \
552 | keyboardSubtype = keyboardType = keyboardFuncKey = postbeta2ColorDepth \
553 | = clientProductId = serialNumber = highColorDepth = \
554 | supportedColorDepths = earlyCapabilityFlags = clientDigProductId = \
555 | connectionType = pad1Octet = clusterFlags = encryptionMethods = \
556 | extEncMethods = channelCount = channelDef = cookie = req_protos = ""
557 |
558 | sourceIp = packet.ipv6.src if 'ipv6' in packet else packet.ip.src
559 | destinationIp = packet.ipv6.dst if 'ipv6' in packet else packet.ip.dst
560 | key = '{}:{}_{}:{}'.format(
561 | sourceIp,
562 | packet.tcp.srcport,
563 | destinationIp,
564 | packet.tcp.dstport)
565 | if key in self.rdp_dict and "cookie" in self.rdp_dict[key]:
566 | cookie = self.rdp_dict[key]["cookie"]
567 | if key in self.rdp_dict and "req_protos" in self.rdp_dict[key]:
568 | req_protos = self.rdp_dict[key]["req_protos"]
569 |
570 | # Client Core Data
571 | # https://msdn.microsoft.com/en-us/library/cc240510.aspx
572 | if 'version_major' in packet.rdp.field_names:
573 | verMajor = packet.rdp.version_major
574 | if 'version_minor' in packet.rdp.field_names:
575 | verMinor = packet.rdp.version_minor
576 | if 'desktop_width' in packet.rdp.field_names:
577 | desktopWidth = packet.rdp.desktop_width
578 | if 'desktop_height' in packet.rdp.field_names:
579 | desktopHeight = packet.rdp.desktop_height
580 | if 'colordepth' in packet.rdp.field_names:
581 | colorDepth = packet.rdp.colordepth
582 | if 'sassequence' in packet.rdp.field_names:
583 | sasSequence = packet.rdp.sassequence
584 | if 'keyboardlayout' in packet.rdp.field_names:
585 | keyboardLayout = packet.rdp.keyboardlayout
586 | if 'client_build' in packet.rdp.field_names:
587 | clientBuild = packet.rdp.client_build
588 | if 'client_name' in packet.rdp.field_names:
589 | clientName = packet.rdp.client_name
590 | if 'keyboard_subtype' in packet.rdp.field_names:
591 | keyboardSubtype = packet.rdp.keyboard_subtype
592 | if 'keyboard_type' in packet.rdp.field_names:
593 | keyboardType = packet.rdp.keyboard_type
594 | if 'keyboard_functionkey' in packet.rdp.field_names:
595 | keyboardFuncKey = packet.rdp.keyboard_functionkey
596 | if 'postbeta2colordepth' in packet.rdp.field_names:
597 | postbeta2ColorDepth = packet.rdp.postbeta2colordepth
598 | if 'client_productid' in packet.rdp.field_names:
599 | clientProductId = packet.rdp.client_productid
600 | if 'serialnumber' in packet.rdp.field_names:
601 | serialNumber = packet.rdp.serialnumber
602 | if 'highcolordepth' in packet.rdp.field_names:
603 | highColorDepth = packet.rdp.highcolordepth
604 | if 'supportedcolordepths' in packet.rdp.field_names:
605 | supportedColorDepths = packet.rdp.supportedcolordepths
606 | if 'earlycapabilityflags' in packet.rdp.field_names:
607 | earlyCapabilityFlags = packet.rdp.earlycapabilityflags
608 | if 'client_digproductid' in packet.rdp.field_names:
609 | clientDigProductId = packet.rdp.client_digproductid
610 | if 'connectiontype' in packet.rdp.field_names:
611 | connectionType = packet.rdp.connectiontype
612 | if 'pad1octet' in packet.rdp.field_names:
613 | pad1Octet = packet.rdp.pad1octet.raw_value
614 |
615 | # Client Cluster Data
616 | # https://msdn.microsoft.com/en-us/library/cc240514.aspx
617 | if 'clusterflags' in packet.rdp.field_names:
618 | clusterFlags_raw = packet.rdp.clusterflags.raw_value
619 | # convert to little-endian
620 | clusterFlags = struct.pack('