├── 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 | [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](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('