├── .gitignore ├── CODEOWNERS ├── CONTRIBUTING-ARCHIVED.md ├── LICENSE.txt ├── README.md ├── lists ├── README.md └── osx-nix-ja3.csv ├── python ├── README.rst ├── ja3.py ├── ja3 │ ├── __init__.py │ └── ja3.py ├── ja3s.py ├── requirements.txt └── setup.py ├── zeek ├── README.md ├── __load__.zeek ├── intel_ja3.zeek ├── ja3.zeek └── ja3s.zeek └── zkg.meta /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | *.pyc 3 | # Editor 4 | *\.sw[a-z] 5 | # Setuptools 'develop' target 6 | *.egg-info/ 7 | build/ 8 | dist/ 9 | setuptools-* 10 | venv/ 11 | venv3/ 12 | .pypirc 13 | .notes 14 | *.DS_Store 15 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related gus information. Please be careful while editing. 2 | #ECCN:Open Source 3 | -------------------------------------------------------------------------------- /CONTRIBUTING-ARCHIVED.md: -------------------------------------------------------------------------------- 1 | # ARCHIVED 2 | 3 | This project is `Archived` and is no longer actively maintained; 4 | We are not accepting contributions or Pull Requests. 5 | 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Salesforce.com, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JA3 - A method for profiling SSL/TLS Clients 2 | 3 | JA3 was invented at Salesforce in 2017. However, the project is no longer being actively maintained by Salesforce. Its original creator, John Althouse, maintains the latest in TLS client fingerprinting technology at [FoxIO-LLC](https://github.com/FoxIO-LLC/ja4). 4 | 5 | JA3 is a method for creating SSL/TLS client fingerprints that should be easy to produce on any platform and can be easily shared for threat intelligence. 6 | 7 | Before using, please read this blog post: [TLS Fingerprinting with JA3 and JA3S](https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967) 8 | 9 | This repo includes JA3 and JA3S scripts for [Zeek](https://www.zeekurity.org/) and [Python](https://www.python.org/). You can find a nice Rust implementation of the JA3 algorithm [here](https://github.com/jabedude/ja3-rs) 10 | 11 | JA3 support has also been added to: 12 | [Moloch](http://molo.ch/) 13 | [Trisul NSM](https://github.com/trisulnsm/trisul-scripts/tree/master/lua/frontend_scripts/reassembly/ja3) 14 | [NGiNX](https://github.com/fooinha/nginx-ssl-ja3) 15 | [BFE](https://github.com/bfenetworks/bfe) 16 | [MISP](https://github.com/MISP) 17 | [Darktrace](https://www.darktrace.com/) 18 | [Suricata](https://suricata-ids.org/tag/ja3/) 19 | [Elastic.co Packetbeat](https://www.elastic.co/guide/en/beats/packetbeat/master/exported-fields-tls.html) 20 | [Splunk](https://www.splunk.com/blog/2017/12/18/configuring-ja3-with-bro-for-splunk.html) 21 | [MantisNet](https://www.mantisnet.com/) 22 | [ICEBRG](http://icebrg.io/) 23 | [Redsocks](https://www.redsocks.eu/) 24 | [NetWitness](https://github.com/timetology/nw/tree/master/parsers/ssl_ja3) 25 | [ExtraHop](https://www.extrahop.com/) 26 | [Vectra Cognito Platform](https://vectra.ai/) 27 | [Corvil](https://www.corvil.com/blog/2018/environmentally-conscious-understanding-your-network) 28 | [Java](https://github.com/lafaspot/ja3_4java) 29 | [Go](https://github.com/open-ch/ja3) 30 | [Security Onion](https://securityonion.net/) 31 | [AIEngine](https://bitbucket.org/camp0/aiengine) 32 | [RockNSM](https://rocknsm.io/) 33 | [Corelight](https://www.corelight.com/products/software) 34 | [VirusTotal](https://blog.virustotal.com/2019/10/in-house-dynamic-analysis-virustotal-jujubox.html#ja3) 35 | [SELKS](https://www.stamus-networks.com/selks-6) 36 | [Stamus Networks](https://www.stamus-networks.com/) 37 | [IBM QRadar Network Insights (QNI)](https://community.ibm.com/community/user/security/blogs/tom-obremski1/2020/10/23/qni-ja3-ja3s-for-network-encryption) 38 | [InQuest](https://inquest.net) 39 | [Cloudflare](https://developers.cloudflare.com/bots/concepts/ja3-fingerprint/) 40 | [AWS Network Firewall](https://docs.aws.amazon.com/network-firewall/latest/developerguide/aws-managed-rule-groups-threat-signature.html) 41 | [Azure Firewall](https://learn.microsoft.com/en-us/azure/firewall/idps-signature-categories) 42 | [AWS WAF](https://aws.amazon.com/about-aws/whats-new/2023/09/aws-waf-ja3-fingerprint-match/) 43 | [Google Cloud](https://cloud.google.com/load-balancing/docs/https/custom-headers-global) 44 | and more... 45 | 46 | 47 | ## Examples 48 | 49 | JA3 fingerprint for the standard Tor client: 50 | ``` 51 | e7d705a3286e19ea42f587b344ee6865 52 | ``` 53 | JA3 fingerprint for the Trickbot malware: 54 | ``` 55 | 6734f37431670b3ab4292b8f60f29984 56 | ``` 57 | JA3 fingerprint for the Emotet malware: 58 | ``` 59 | 4d7a28d6f2263ed61de88ca66eb011e3 60 | ``` 61 | 62 | While destination IPs, Ports, and X509 certificates change, the JA3 fingerprint remains constant for the client application in these examples across our sample set. Please be aware that these are just examples, not indicative of all versions ever. 63 | 64 | ## Lists 65 | 66 | Example lists of known JA3's and their associated applications can be found [here](https://github.com/salesforce/ja3/tree/master/lists). 67 | 68 | A more up-to-date crowd sourced method of gathering and reporting on JA3s can be found at [ja3er.com](https://ja3er.com). 69 | 70 | ## How it works 71 | 72 | TLS and it’s predecessor, SSL, I will refer to both as “SSL” for simplicity, are used to encrypt communication for both common applications, to keep your data secure, and malware, so it can hide in the noise. To initiate a SSL session, a client will send a SSL Client Hello packet following the TCP 3-way handshake. This packet and the way in which it is generated is dependant on packages and methods used when building the client application. The server, if accepting SSL connections, will respond with a SSL Server Hello packet that is formulated based on server-side libraries and configurations as well as details in the Client Hello. Because SSL negotiations are transmitted in the clear, it’s possible to fingerprint and identify client applications using the details in the SSL Client Hello packet. 73 | 74 | JA3 is a method of TLS fingerprinting that was inspired by the [research](https://blog.squarelemon.com/tls-fingerprinting/) and works of [Lee Brotherston](https://twitter.com/synackpse) and his TLS Fingerprinting tool: [FingerprinTLS](https://github.com/LeeBrotherston/tls-fingerprinting/tree/master/fingerprintls). 75 | 76 | JA3 gathers the decimal values of the bytes for the following fields in the Client Hello packet; SSL Version, Accepted Ciphers, List of Extensions, Elliptic Curves, and Elliptic Curve Formats. It then concatenates those values together in order, using a "," to delimit each field and a "-" to delimit each value in each field. 77 | 78 | The field order is as follows: 79 | ``` 80 | SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat 81 | ``` 82 | Example: 83 | ``` 84 | 769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0 85 | ``` 86 | If there are no SSL Extensions in the Client Hello, the fields are left empty. 87 | 88 | Example: 89 | ``` 90 | 769,4-5-10-9-100-98-3-6-19-18-99,,, 91 | ``` 92 | These strings are then MD5 hashed to produce an easily consumable and shareable 32 character fingerprint. This is the JA3 SSL Client Fingerprint. 93 | ``` 94 | 769,47-53-5-10-49161-49162-49171-49172-50-56-19-4,0-10-11,23-24-25,0 --> ada70206e40642a3e4461f35503241d5 95 | 769,4-5-10-9-100-98-3-6-19-18-99,,, --> de350869b8c85de67a350c8d186f11e6 96 | ``` 97 | We also needed to introduce some code to account for Google’s GREASE (Generate Random Extensions And Sustain Extensibility) as described [here](https://tools.ietf.org/html/draft-davidben-tls-grease-01). Google uses this as a mechanism to prevent extensibility failures in the TLS ecosystem. JA3 ignores these values completely to ensure that programs utilizing GREASE can still be identified with a single JA3 hash. 98 | 99 | ## JA3S 100 | 101 | JA3S is JA3 for the Server side of the SSL/TLS communication and fingerprints how servers respond to particular clients. 102 | 103 | JA3S uses the following field order: 104 | ``` 105 | SSLVersion,Cipher,SSLExtension 106 | ``` 107 | With JA3S it is possible to fingerprint the entire cryptographic negotiation between client and it's server by combining JA3 + JA3S. That is because servers will respond to different clients differently but will always respond to the same client the same. 108 | 109 | For the Trickbot example: 110 | ``` 111 | JA3 = 6734f37431670b3ab4292b8f60f29984 ( Fingerprint of Trickbot ) 112 | JA3S = 623de93db17d313345d7ea481e7443cf ( Fingerprint of Command and Control Server Response ) 113 | ``` 114 | For the Emotet example: 115 | ``` 116 | JA3 = 4d7a28d6f2263ed61de88ca66eb011e3 ( Fingerprint of Emotet ) 117 | JA3S = 80b3a14bccc8598a1f3bbe83e71f735f ( Fingerprint of Command and Control Server Response ) 118 | ``` 119 | 120 | In these malware examples, the command and control server always responds to the malware client in exactly the same way, it does not deviate. So even though the traffic is encrypted and one may not know the command and control server's IPs or domains as they are constantly changing, we can still identify, with reasonable confidence, the malicious communication by fingerprinting the TLS negotiation between client and server. Again, please be aware that these are examples, not indicative of all versions ever, and are intended to illustrate what is possible. 121 | 122 | ## Intriguing Possibilities 123 | 124 | JA3 is a much more effective way to detect malicious activity over SSL than IP or domain based IOCs. Since JA3 detects the client application, it doesn’t matter if malware uses DGA (Domain Generation Algorithms), or different IPs for each C2 host, or even if the malware uses Twitter for C2, JA3 can detect the malware itself based on how it communicates rather than what it communicates to. 125 | 126 | JA3 is also an excellent detection mechanism in locked-down environments where only a few specific applications are allowed to be installed. In these types of environments one could build a whitelist of expected applications and then alert on any other JA3 hits. 127 | 128 | For more details on what you can see and do with JA3 and JA3S, please see this DerbyCon 2018 talk: https://www.youtube.com/watch?v=NI0Lmp0K1zc or this [blog post.](https://engineering.salesforce.com/tls-fingerprinting-with-ja3-and-ja3s-247362855967) 129 | 130 | Please contact me on twitter @4A4133 or over email, let me know what you find and if you have any feature requests. 131 | 132 | ___ 133 | ### JA3 Created by 134 | 135 | [John Althouse](https://www.linkedin.com/in/johnalthouse/) 136 | [Jeff Atkinson](https://www.linkedin.com/in/annh/) 137 | [Josh Atkins](https://www.linkedin.com/in/joshratkins/) 138 | 139 | Please send questions and comments to **[John Althouse](https://twitter.com/4A4133)**. 140 | 141 | -------------------------------------------------------------------------------- /lists/README.md: -------------------------------------------------------------------------------- 1 | ## Lists 2 | [osx-nix-ja3.csv](https://raw.githubusercontent.com/salesforce/ja3/master/lists/osx-nix-ja3.csv) is a list of JA3 hashes to application name(s) for OSX and Linux. 3 | 4 | This list was generated using an automated process with some manual checking. As applications can vary per environment, please use this list as a best-guess and as an example of JA3's capabilities. Any assistance in fine tuning is appreciated. For example, some of the "Used by many programs" JA3s can likely be linked to specific APIs. 5 | 6 | We're working on a Windows list and getting the automation working there. If you would like to contribute, please make a pull request or contact [John Althouse](mailto:jalthouse@salesforce.com). 7 | 8 | ## 3rd Party Lists 9 | 10 | [JA3er Crowd Sourced DB](https://ja3er.com) JA3er uses a crowd sourced method of building and maintaining an up-to-date database of JA3s and their associated applications. 11 | 12 | [Lee Brotherson's DB](https://github.com/trisulnsm/trisul-scripts/tree/master/lua/frontend_scripts/reassembly/ja3/prints) 13 | Trisul created a script to convert Lee Brotherston's database of 400+ fingerprints to JA3. 14 | -------------------------------------------------------------------------------- /lists/osx-nix-ja3.csv: -------------------------------------------------------------------------------- 1 | "Copyright (c) 2017 salesforce.com inc. 2 | All rights reserved. 3 | Licensed under the BSD 3-Clause license. 4 | For full license text see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause", 5 | 61d50e7771aee7f2f4b89a7200b4d45e,"AcroCEF" 6 | 49a6cf42956937669a01438f26e7c609,"AIM" 7 | 561145462cfc7de1d6a97e93d3264786,"Airmail 3" 8 | f6fd83a21f9f3c5f9ff7b5c63bbc179d,"Alation Compose" 9 | 6003b52942a2e1e1ea72d802d153ec08,"Amazon Music" 10 | eb149984fc9c44d85ed7f12c90d818be,"Amazon Music,Dreamweaver,Spotify" 11 | 8e3f1bf87bc652a20de63bfd4952b16a,"AnypointStudio" 12 | 5507277945374659a5b4572e1b6d9b9f,"apple.geod" 13 | f753495f2eab5155c61b760c838018f8,"apple.geod" 14 | ba40fea2b2638908a3b3b482ac78d729,"apple.geod,parsecd,apple.photomoments" 15 | 474e73aea21d1e0910f25c3e6c178535,"apple.WebKit.Networking" 16 | eeeb5e7485f5e10cbc39db4cfb69b264,"apple.WebKit.Networking" 17 | d4693422c5ce1565377aca25940ad80c,"apple.WebKit.Networking,CalendarAgent,Go for Gmail" 18 | 63de2b6188d5694e79b678f585b13264,"apple.WebKit.Networking,Chatter,FieldServiceApp,socialstudio" 19 | 3e4e87dda5a3162306609b7e330441d2,"apple.WebKit.Networking,itunesstored" 20 | 7b343af1092863fdd822d6f10645abfb,"apple.WebKit.Networking,itunesstored" 21 | a312f9162a08eeedf7feb7a13cd7e9bb,"apple.WebKit.Networking,Spotify,WhatsApp,Skype,iTunes" 22 | c5c11e6105c56fd29cc72c3ac7a2b78b,"AT&T Connect" 23 | fa030dbcb2e3c7141d3c2803780ee8db,"Battle.net,Dropbox" 24 | 0ef9ca1c10d3f186f5786e1ef3461a46,"bitgo,ShapeShift" 25 | cdec81515ccc75a5aa41eb3db22226e6,"BlueJeans,CEPHtmlEngine" 26 | 83e04bc58d402f9633983cbf22724b02,"Charles,Google Play Music Desktop Player,Postman,Slack,and other desktop programs" 27 | 424008725394c634a4616b8b1f2828a5,"Charles,java,eclipse" 28 | be9f1360cf52dc1f61ae025252f192a3,"Chromium" 29 | def8761e4bcaaf91d99801a22ac6f6d4,"Chromium" 30 | fc5cb0985a5f5e295163cc8ffff8a6e1,"Chromium" 31 | e7d46c98b078477c4324031e0d3b22f5,"Cisco AnyConnect Secure Mobility Client" 32 | ed36017db541879619c399c95e22067d,"Cisco AnyConnect Secure Mobility Client" 33 | 5ee1a653fb824db7182714897fd3b5df,"Citrix Viewer" 34 | a9d17f74e55dd53fcf7c234f8a240919,"Covenant Eyes" 35 | c882d9444412c00e71b643f3f54145ff,"Creative Cloud" 36 | bc0608d33dc64506b42f7f5f87958f37,"cscan" 37 | ccaa60f6ccc701bde536ef409be3cf63,"curl no-SNI" 38 | fe048fe8faf797796e278f2b4f1e9c24,"curl SNI" 39 | 4fcd1770545298cc119865aeba81daba,"Deezer" 40 | 4c40bf8baa7c301c5dba8a20bc4119e2,"Dynalist,Postman,Google Chrome,Franz,GOG Galaxy" 41 | 0411bbb5ff27ad46e1874a7a8beedacb,"eclipse" 42 | 4990c9da08f44a01ecd7ddc3837caf25,"eclipse" 43 | fa106fe5beec443af7e211ef8902e7e0,"eclipse" 44 | d74778f454e2b047e030b291b94dd698,"eclipse,java" 45 | 187dfde7edc8ceddccd3deeccc21daeb,"eclipse,java,studio,STS" 46 | 8c5a50f1e833ed581e9cfc690814719a,"eclipse,JavaApplicationStub,idea" 47 | 1fbe5382f9d8430fe921df747c46d95f,"FieldServiceApp,socialstudio" 48 | 0a81538cf247c104edb677bdb8902ed5,"firefox" 49 | 0b6592fd91d4843c823b75e49b43838d,"firefox" 50 | 0ffee3ba8e615ad22535e7f771690a28,"firefox" 51 | 1c15aca4a38bad90f9c40678f6aface9,"firefox" 52 | 5163bc7c08f57077bc652ec370459c2f,"firefox" 53 | a88f1426c4603f2a8cd8bb41e875cb75,"firefox" 54 | b03910cc6de801d2fcfa0c3b9f397df4,"firefox" 55 | bfcc1a3891601edb4f137ab7ab25b840,"firefox" 56 | ce694315cbb81ce95e6ae4ae8cbafde6,"firefox" 57 | f15797a734d0b4f171a86fd35c9a5e43,"firefox" 58 | 07b4162d4db57554961824a21c4a0fde,"firefox,thunderbird" 59 | 61d0d709fe7ac199ef4b2c52bc8cef75,"firefox,thunderbird" 60 | 8498fe4268764dbf926a38283e9d3d8f,"Franz,Google Chrome,Kiwi,Spotify,nwjs,Slack" 61 | 900c1fa84b4ea86537e1d148ee16eae8,"Fuze" 62 | 107144b88827da5da9ed42d8776ccdc5,"geod" 63 | c46941d4de99445aef6b497679474cf4,"geod" 64 | 002205d0f96c37c5e660b9f041363c11,"Google Chrome" 65 | 073eede15b2a5a0302d823ecbd5ad15b,"Google Chrome" 66 | 0b61c673ee71fe9ee725bd687c455809,"Google Chrome" 67 | 6cd1b944f5885e2cfbe98a840b75eeb8,"Google Chrome" 68 | 94c485bca29d5392be53f2b8cf7f4304,"Google Chrome" 69 | b4f4e6164f938870486578536fc1ffce,"Google Chrome" 70 | b8f81673c0e1d29908346f3bab892b9b,"Google Chrome" 71 | baaac9b6bf25ad098115c71c59d29e51,"Google Chrome" 72 | bc6c386f480ee97b9d9e52d472b772d8,"Google Chrome" 73 | da949afd9bd6df820730f8f171584a71,"Google Chrome" 74 | f58966d34ff9488a83797b55c804724d,"Google Chrome" 75 | fd6314b03413399e4f23d1524d206692,"Google Chrome" 76 | 0e46737668fe75092919ee047a0b5945,"Google Chrome Helper" 77 | 39fa85654105398ee7ef6a3a1c81d685,"Google Chrome Helper" 78 | 4ba7b7022f5f5e1e500bb19199d8b1a4,"Google Chrome Helper" 79 | 5498cef2cca704eb01cf2041cc1089c1,"Google Chrome,Slack" 80 | d27fb8deca6e3b9739db3fda2b229fe3,"Google Drive File Stream" 81 | ae340571b4fd0755c4a0821b18d8fa93,"Google Earth" 82 | f059212ce3de94b1e8253a7522cb1b44,"Google Photos Backup" 83 | fd10cc8cce9493a966c57249e074755f,"gramblr" 84 | 3e860202fc555b939e83e7a7ab518c38,"hola_svc" 85 | 56ac3a0bef0824c49e4b569941937088,"hola_svc" 86 | 5c1c89f930122bccc7a97d52f73bea2c,"hola_svc" 87 | 77310efe11f1943306ee317cf02150b7,"hola_svc" 88 | 8bd59c4b7f3193db80fd64318429bcec,"hola_svc" 89 | d1f9f9b224387d2597f02095fcec96d7,"hola_svc" 90 | ff1040ba1e3d235855ef0d7cd9237fdc,"hola_svc" 91 | 5af143afdbf58ec11ab3b3d53dd4e5e3,"IDSyncDaemon" 92 | d06acbe8ac31e753f40600a9d6717cba,"Inbox OSX" 93 | 093081b45872912be9a1f2a8163fe041,"java" 94 | 2080bf56cb87e64303e27fcd781e7efd,"java" 95 | 225a24b45f0f1adbc2e245d4624c6e08,"java" 96 | 3afe1fb5976d0999abe833b14b7d6485,"java" 97 | 3b844830bfbb12eb5d2f8dc281d349a9,"java" 98 | 51a7ad14509fd614c7bb3a50c4982b8c,"java" 99 | 550628650380ff418de25d3d890e836e,"java" 100 | 5b270b309ad8c6478586a15dece20a88,"java" 101 | 5d7abe53ae15b4272a34f10431e06bf3,"java" 102 | 7c7a68b96d2aab15d678497a12119f4f,"java" 103 | 88afa0dea1608e28f50acbad32d7f195,"java" 104 | 8ce6933b8c12ce931ca238e9420cc5dd,"java" 105 | a61299f9b501adcf680b9275d79d4ac6,"java" 106 | a9fead344bf3ac09f62df3cd9b22c268,"java" 107 | 4056657a50a8a4e5cfac40ba48becfa2,"java,eclipse" 108 | f22bdd57e3a52de86cda40da2d84e83b,"java,eclipse,Cyberduck" 109 | 028563cffc7a3a2e32090aee0294d636,"java,eclipse,STS" 110 | 5f9b53f0d39dc9d940a3b5568fe5f0bb,"java,JavaApplicationStub" 111 | 2db6873021f2a95daa7de0d93a1d1bf2,"java,studio,eclipse" 112 | c376061f96329e1020865a1dc726927d,"JavaApplicationStub" 113 | e516ad69a423f8e0407307aa7bfd6344,"Kindle,stack,nextcloud" 114 | 3959d0a1344896e9fb5c0564ca0a2956,"LeagueClientUx" 115 | 0fe51fa93812c2ebb50a655222a57bf2,"LINE Messaging" 116 | 2e094913d88f0ad8dc69447cb7d2ce65,"LINE Messaging" 117 | 193349d34561d1d5d1a270172eb2d97e,"LogMeIn Client" 118 | d732ca39155f38942f90e9fc2b0f97f7,"Maxthon" 119 | c9dbeed362a32f9a50a26f4d9b32bbd8,"Messenger,Jumpshare" 120 | 6acb250ada693067812c3335705dae79,"mono-sgen,Syncplicity,Axure RP 8,Amazon Drive" 121 | 3ee4aaac7147ff2b80ada31686db660c,"node-webkit,Kindle" 122 | 641df9d6dbe7fdb74f70c8ad93def8cc,"node.js" 123 | 9811c1bb9f0f6835d5c13a831cca4173,"node.js" 124 | 106ecbd3d14b4dc6e413494263720afe,"node.js,Postman,WhatsApp" 125 | 49de9b1c7e60bd3b8e1d4f7a49ba362e,"nwjs,Chromium" 126 | 38cbe70b308f42da7c9980c0e1c89656,"p4v,owncloud" 127 | 62448833d8230241227c03b7d441e31b,"parsecd,apple.geod,apple.photomoments,photoanalysisd,FreedomProxy" 128 | e846898acc767ebeb2b4388e58a968d4,"postbox-bin" 129 | a7823092705a5e91ce2b7f561b6e5b98,"Qsync Client" 130 | c048d9f26a79e11ca7276499ef24daf3,"RescueTime,Plantronics Hub" 131 | d219efd07cbb8fbe547e6a5335843f0f,"ruby" 132 | c36fb08942cf19508c08d96af22d4ffc,"Safari" 133 | 844166382cc98d98595e6778c470f5d5,"Salesforce Files" 134 | 49a341a21f4fd4ac63b027ff2b1a331f,"Skype" 135 | a5aa6e939e4770e3b8ac38ce414fd0d5,"Slack" 136 | 116ffc8889873efad60457cd55eaf543,"Spark" 137 | 8db4b0f8e9dd8f2fff38ee7c5a1e4496,"SpotlightNetHelper,Safari" 138 | 39cf5b7a13a764494de562add874f016,"Steam OSX" 139 | 2d3854d1cbcdceece83eabd85bdcc056,"Tableau" 140 | a585c632a2b49be1256881fb0c16c864,"Tableau" 141 | cd7c06b9459c9cfd4af2dba5696ea930,"Tableau" 142 | df65746370dcabc9b4f370c6e14a8156,"True Key" 143 | 84071ea96fc8a60c55fc8a405e214c0f,"Used by many desktop apps,Quip,Spotify,GitHub Desktop" 144 | 40fd0a5e81ebdcf0ec82a4710a12dec1,"Used by many programs on OSX,apple.WebKit.Networking" 145 | 618ee2509ef52bf0b8216e1564eea909,"Used by many programs on OSX,apple.WebKit.Networking" 146 | 799135475da362592a4be9199d258726,"Used by many programs on OSX,apple.WebKit.Networking" 147 | 7b530a25af9016a9d12de5abc54d9e74,"Used by many programs on OSX,apple.WebKit.Networking" 148 | 7e72698146290dd68239f788a452e7d8,"Used by many programs on OSX,apple.WebKit.Networking" 149 | a9aecaa66ad9c6cfe1c361da31768506,"Used by many programs on OSX,apple.WebKit.Networking" 150 | c05de18b01a054f2f6900ffe96b3da7a,"Used by many programs on OSX,apple.WebKit.Networking" 151 | c07cb55f88702033a8f52c046d23e0b2,"Used by many programs on OSX,apple.WebKit.Networking" 152 | e4d448cdfe06dc1243c1eb026c74ac9a,"Used by many programs on OSX,apple.WebKit.Networking" 153 | f1c5cf087b959cec31bd6285407f689a,"Used by many programs on OSX,apple.WebKit.Networking" 154 | 488b6b601cb141b062d4da7f524b4b22,"Used by many programs,Python,PHP,Git,dotnet,Adobe" 155 | f28d34ce9e732f644de2350027d74c3f,"Used by many programs,Quip,Aura,Spotify,Chatty" 156 | 190dfb280fe3b541acc6a2e5f00690e6,"Used by many programs,Quip,Spotify,Dropbox,GitHub Desktop,etc" 157 | 20dd18bdd3209ea718989030a6f93364,"Used by many programs,Slack,Postman,Spotify,Google Chrome" 158 | e0224fc1c33658f2d3d963bfb0a76a85,"Viber" 159 | 01319090aea981dde6fc8d6ae71ead54,"vpnkit" 160 | 84607748f3887541dd60fe974a042c71,"wineserver" 161 | c2b4710c6888a5d47befe865c8e6fb19,"ZwiftApp" 162 | -------------------------------------------------------------------------------- /python/README.rst: -------------------------------------------------------------------------------- 1 | pyJA3 2 | ===== 3 | .. image:: https://readthedocs.org/projects/pyja3/badge/?version=latest 4 | :target: http://pyja3.readthedocs.io/en/latest/?badge=latest 5 | 6 | .. image:: https://badge.fury.io/py/pyja3.svg 7 | :target: https://badge.fury.io/py/pyja3 8 | 9 | 10 | JA3 provides fingerprinting services on SSL packets. This is a python wrapper around JA3 logic in order to produce valid JA3 fingerprints from an input PCAP file. 11 | 12 | 13 | Getting Started 14 | --------------- 15 | 1. Install the pyja3 module: 16 | 17 | ``pip install pyja3`` or ``python setup.py install`` 18 | 19 | 2. Test with a PCAP file or download a sample: 20 | 21 | $(venv) ja3 --json /your/file.pcap 22 | 23 | Example 24 | ------- 25 | Output from sample PCAP:: 26 | 27 | [ 28 | { 29 | "destination_ip": "192.168.1.3", 30 | "destination_port": 443, 31 | "ja3": "769,255-49162-49172-136-135-57-56-49167-49157-132-53-49159-49161-49169-49171-69-68-51-50-49164-49166-49154-49156-150-65-4-5-47-49160-49170-22-19-49165-49155-65279-10,0-10-11-35,23-24-25,0", 32 | "ja3_digest": "2aef69b4ba1938c3a400de4188743185", 33 | "source_ip": "192.168.1.4", 34 | "source_port": 2061, 35 | "timestamp": 1350802591.754299 36 | }, 37 | { 38 | "destination_ip": "192.168.1.3", 39 | "destination_port": 443, 40 | "ja3": "769,255-49162-49172-136-135-57-56-49167-49157-132-53-49159-49161-49169-49171-69-68-51-50-49164-49166-49154-49156-150-65-4-5-47-49160-49170-22-19-49165-49155-65279-10,0-10-11-35,23-24-25,0", 41 | "ja3_digest": "2aef69b4ba1938c3a400de4188743185", 42 | "source_ip": "192.168.1.4", 43 | "source_port": 2068, 44 | "timestamp": 1350802597.517011 45 | } 46 | ] 47 | 48 | Changelog 49 | --------- 50 | 2018-02-05 51 | ~~~~~~~~~~ 52 | * Change: Ported single script to valid Python Package 53 | * Change: Re-factored code to be cleaner and PEP8 compliant 54 | * Change: Supported Python2 and Python3 55 | 56 | -------------------------------------------------------------------------------- /python/ja3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Generate JA3 fingerprints from PCAPs using Python.""" 3 | 4 | import argparse 5 | import dpkt 6 | import json 7 | import socket 8 | import binascii 9 | import struct 10 | import os 11 | from hashlib import md5 12 | 13 | __author__ = "Tommy Stallings" 14 | __copyright__ = "Copyright (c) 2017, salesforce.com, inc." 15 | __credits__ = ["John B. Althouse", "Jeff Atkinson", "Josh Atkins"] 16 | __license__ = "BSD 3-Clause License" 17 | __version__ = "1.0.0" 18 | __maintainer__ = "Tommy Stallings, Brandon Dixon" 19 | __email__ = "tommy.stallings2@gmail.com" 20 | 21 | 22 | GREASE_TABLE = {0x0a0a: True, 0x1a1a: True, 0x2a2a: True, 0x3a3a: True, 23 | 0x4a4a: True, 0x5a5a: True, 0x6a6a: True, 0x7a7a: True, 24 | 0x8a8a: True, 0x9a9a: True, 0xaaaa: True, 0xbaba: True, 25 | 0xcaca: True, 0xdada: True, 0xeaea: True, 0xfafa: True} 26 | # GREASE_TABLE Ref: https://tools.ietf.org/html/draft-davidben-tls-grease-00 27 | SSL_PORT = 443 28 | TLS_HANDSHAKE = 22 29 | 30 | 31 | def convert_ip(value): 32 | """Convert an IP address from binary to text. 33 | 34 | :param value: Raw binary data to convert 35 | :type value: str 36 | :returns: str 37 | """ 38 | try: 39 | return socket.inet_ntop(socket.AF_INET, value) 40 | except ValueError: 41 | return socket.inet_ntop(socket.AF_INET6, value) 42 | 43 | 44 | def parse_variable_array(buf, byte_len): 45 | """Unpack data from buffer of specific length. 46 | 47 | :param buf: Buffer to operate on 48 | :type buf: bytes 49 | :param byte_len: Length to process 50 | :type byte_len: int 51 | :returns: bytes, int 52 | """ 53 | _SIZE_FORMATS = ['!B', '!H', '!I', '!I'] 54 | assert byte_len <= 4 55 | size_format = _SIZE_FORMATS[byte_len - 1] 56 | padding = b'\x00' if byte_len == 3 else b'' 57 | size = struct.unpack(size_format, padding + buf[:byte_len])[0] 58 | data = buf[byte_len:byte_len + size] 59 | 60 | return data, size + byte_len 61 | 62 | 63 | def ntoh(buf): 64 | """Convert to network order. 65 | 66 | :param buf: Bytes to convert 67 | :type buf: bytearray 68 | :returns: int 69 | """ 70 | if len(buf) == 1: 71 | return buf[0] 72 | elif len(buf) == 2: 73 | return struct.unpack('!H', buf)[0] 74 | elif len(buf) == 4: 75 | return struct.unpack('!I', buf)[0] 76 | else: 77 | raise ValueError('Invalid input buffer size for NTOH') 78 | 79 | 80 | def convert_to_ja3_segment(data, element_width): 81 | """Convert a packed array of elements to a JA3 segment. 82 | 83 | :param data: Current PCAP buffer item 84 | :type: str 85 | :param element_width: Byte count to process at a time 86 | :type element_width: int 87 | :returns: str 88 | """ 89 | int_vals = list() 90 | data = bytearray(data) 91 | if len(data) % element_width: 92 | message = '{count} is not a multiple of {width}' 93 | message = message.format(count=len(data), width=element_width) 94 | raise ValueError(message) 95 | 96 | for i in range(0, len(data), element_width): 97 | element = ntoh(data[i: i + element_width]) 98 | if element not in GREASE_TABLE: 99 | int_vals.append(element) 100 | 101 | return "-".join(str(x) for x in int_vals) 102 | 103 | 104 | def process_extensions(client_handshake): 105 | """Process any extra extensions and convert to a JA3 segment. 106 | 107 | :param client_handshake: Handshake data from the packet 108 | :type client_handshake: dpkt.ssl.TLSClientHello 109 | :returns: list 110 | """ 111 | if not hasattr(client_handshake, "extensions"): 112 | # Needed to preserve commas on the join 113 | return ["", "", ""] 114 | 115 | exts = list() 116 | elliptic_curve = "" 117 | elliptic_curve_point_format = "" 118 | for ext_val, ext_data in client_handshake.extensions: 119 | if not GREASE_TABLE.get(ext_val): 120 | exts.append(ext_val) 121 | if ext_val == 0x0a: 122 | a, b = parse_variable_array(ext_data, 2) 123 | # Elliptic curve points (16 bit values) 124 | elliptic_curve = convert_to_ja3_segment(a, 2) 125 | elif ext_val == 0x0b: 126 | a, b = parse_variable_array(ext_data, 1) 127 | # Elliptic curve point formats (8 bit values) 128 | elliptic_curve_point_format = convert_to_ja3_segment(a, 1) 129 | else: 130 | continue 131 | 132 | results = list() 133 | results.append("-".join([str(x) for x in exts])) 134 | results.append(elliptic_curve) 135 | results.append(elliptic_curve_point_format) 136 | return results 137 | 138 | 139 | def process_pcap(pcap, any_port=False): 140 | """Process packets within the PCAP. 141 | 142 | :param pcap: Opened PCAP file to be processed 143 | :type pcap: dpkt.pcap.Reader 144 | :param any_port: Whether or not to search for non-SSL ports 145 | :type any_port: bool 146 | """ 147 | decoder = dpkt.ethernet.Ethernet 148 | linktype = pcap.datalink() 149 | if linktype == dpkt.pcap.DLT_LINUX_SLL: 150 | decoder = dpkt.sll.SLL 151 | elif linktype == dpkt.pcap.DLT_NULL or linktype == dpkt.pcap.DLT_LOOP: 152 | decoder = dpkt.loopback.Loopback 153 | 154 | results = list() 155 | for timestamp, buf in pcap: 156 | try: 157 | eth = decoder(buf) 158 | except Exception: 159 | continue 160 | 161 | if not isinstance(eth.data, (dpkt.ip.IP, dpkt.ip6.IP6)): 162 | # We want an IP packet 163 | continue 164 | if not isinstance(eth.data.data, dpkt.tcp.TCP): 165 | # TCP only 166 | continue 167 | 168 | ip = eth.data 169 | tcp = ip.data 170 | 171 | if not (tcp.dport == SSL_PORT or tcp.sport == SSL_PORT or any_port): 172 | # Doesn't match SSL port or we are picky 173 | continue 174 | if len(tcp.data) <= 0: 175 | continue 176 | 177 | tls_handshake = bytearray(tcp.data) 178 | if tls_handshake[0] != TLS_HANDSHAKE: 179 | continue 180 | 181 | records = list() 182 | 183 | try: 184 | records, bytes_used = dpkt.ssl.tls_multi_factory(tcp.data) 185 | except dpkt.ssl.SSL3Exception: 186 | continue 187 | except dpkt.dpkt.NeedData: 188 | continue 189 | 190 | if len(records) <= 0: 191 | continue 192 | 193 | for record in records: 194 | if record.type != TLS_HANDSHAKE: 195 | continue 196 | if len(record.data) == 0: 197 | continue 198 | client_hello = bytearray(record.data) 199 | if client_hello[0] != 1: 200 | # We only want client HELLO 201 | continue 202 | try: 203 | handshake = dpkt.ssl.TLSHandshake(record.data) 204 | except dpkt.dpkt.NeedData: 205 | # Looking for a handshake here 206 | continue 207 | if not isinstance(handshake.data, dpkt.ssl.TLSClientHello): 208 | # Still not the HELLO 209 | continue 210 | 211 | client_handshake = handshake.data 212 | buf, ptr = parse_variable_array(client_handshake.data, 1) 213 | buf, ptr = parse_variable_array(client_handshake.data[ptr:], 2) 214 | ja3 = [str(client_handshake.version)] 215 | 216 | # Cipher Suites (16 bit values) 217 | ja3.append(convert_to_ja3_segment(buf, 2)) 218 | ja3 += process_extensions(client_handshake) 219 | ja3 = ",".join(ja3) 220 | 221 | record = {"source_ip": convert_ip(ip.src), 222 | "destination_ip": convert_ip(ip.dst), 223 | "source_port": tcp.sport, 224 | "destination_port": tcp.dport, 225 | "ja3": ja3, 226 | "ja3_digest": md5(ja3.encode()).hexdigest(), 227 | "timestamp": timestamp, 228 | "client_hello_pkt": binascii.hexlify(tcp.data).decode('utf-8')} 229 | results.append(record) 230 | 231 | return results 232 | 233 | 234 | def main(): 235 | """Intake arguments from the user and print out JA3 output.""" 236 | desc = "A python script for extracting JA3 fingerprints from PCAP files" 237 | parser = argparse.ArgumentParser(description=(desc)) 238 | parser.add_argument("pcap", help="The pcap file to process") 239 | help_text = "Look for client hellos on any port instead of just 443" 240 | parser.add_argument("-a", "--any_port", required=False, 241 | action="store_true", default=False, 242 | help=help_text) 243 | help_text = "Print out as JSON records for downstream parsing" 244 | parser.add_argument("-j", "--json", required=False, action="store_true", 245 | default=False, help=help_text) 246 | help_text = "Print packet related data for research (json only)" 247 | parser.add_argument("-r", "--research", required=False, action="store_true", 248 | default=False, help=help_text) 249 | args = parser.parse_args() 250 | 251 | # Use an iterator to process each line of the file 252 | output = None 253 | with open(args.pcap, 'rb') as fp: 254 | try: 255 | capture = dpkt.pcap.Reader(fp) 256 | except ValueError as e_pcap: 257 | try: 258 | fp.seek(0, os.SEEK_SET) 259 | capture = dpkt.pcapng.Reader(fp) 260 | except ValueError as e_pcapng: 261 | raise Exception( 262 | "File doesn't appear to be a PCAP or PCAPng: %s, %s" % 263 | (e_pcap, e_pcapng)) 264 | output = process_pcap(capture, any_port=args.any_port) 265 | 266 | if args.json: 267 | if not args.research: 268 | def remove_items(x): 269 | del x['client_hello_pkt'] 270 | list(map(remove_items,output)) 271 | output = json.dumps(output, indent=4, sort_keys=True) 272 | print(output) 273 | else: 274 | for record in output: 275 | tmp = '[{dest}:{port}] JA3: {segment} --> {digest}' 276 | tmp = tmp.format(dest=record['destination_ip'], 277 | port=record['destination_port'], 278 | segment=record['ja3'], 279 | digest=record['ja3_digest']) 280 | print(tmp) 281 | 282 | 283 | if __name__ == "__main__": 284 | main() 285 | -------------------------------------------------------------------------------- /python/ja3/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/ja3/502cc6395811c54743b0561419d61900a6df3ff7/python/ja3/__init__.py -------------------------------------------------------------------------------- /python/ja3/ja3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Generate JA3 fingerprints from PCAPs using Python.""" 3 | 4 | import argparse 5 | import dpkt 6 | import json 7 | import socket 8 | import struct 9 | import os 10 | from hashlib import md5 11 | 12 | __author__ = "Tommy Stallings" 13 | __copyright__ = "Copyright (c) 2017, salesforce.com, inc." 14 | __credits__ = ["John B. Althouse", "Jeff Atkinson", "Josh Atkins"] 15 | __license__ = "BSD 3-Clause License" 16 | __version__ = "1.0.0" 17 | __maintainer__ = "Tommy Stallings, Brandon Dixon" 18 | __email__ = "tommy.stallings@salesforce.com" 19 | 20 | 21 | GREASE_TABLE = {0x0a0a: True, 0x1a1a: True, 0x2a2a: True, 0x3a3a: True, 22 | 0x4a4a: True, 0x5a5a: True, 0x6a6a: True, 0x7a7a: True, 23 | 0x8a8a: True, 0x9a9a: True, 0xaaaa: True, 0xbaba: True, 24 | 0xcaca: True, 0xdada: True, 0xeaea: True, 0xfafa: True} 25 | # GREASE_TABLE Ref: https://tools.ietf.org/html/draft-davidben-tls-grease-00 26 | SSL_PORT = 443 27 | TLS_HANDSHAKE = 22 28 | 29 | 30 | def convert_ip(value): 31 | """Convert an IP address from binary to text. 32 | 33 | :param value: Raw binary data to convert 34 | :type value: str 35 | :returns: str 36 | """ 37 | try: 38 | return socket.inet_ntop(socket.AF_INET, value) 39 | except ValueError: 40 | return socket.inet_ntop(socket.AF_INET6, value) 41 | 42 | 43 | def parse_variable_array(buf, byte_len): 44 | """Unpack data from buffer of specific length. 45 | 46 | :param buf: Buffer to operate on 47 | :type buf: bytes 48 | :param byte_len: Length to process 49 | :type byte_len: int 50 | :returns: bytes, int 51 | """ 52 | _SIZE_FORMATS = ['!B', '!H', '!I', '!I'] 53 | assert byte_len <= 4 54 | size_format = _SIZE_FORMATS[byte_len - 1] 55 | padding = b'\x00' if byte_len == 3 else b'' 56 | size = struct.unpack(size_format, padding + buf[:byte_len])[0] 57 | data = buf[byte_len:byte_len + size] 58 | 59 | return data, size + byte_len 60 | 61 | 62 | def ntoh(buf): 63 | """Convert to network order. 64 | 65 | :param buf: Bytes to convert 66 | :type buf: bytearray 67 | :returns: int 68 | """ 69 | if len(buf) == 1: 70 | return buf[0] 71 | elif len(buf) == 2: 72 | return struct.unpack('!H', buf)[0] 73 | elif len(buf) == 4: 74 | return struct.unpack('!I', buf)[0] 75 | else: 76 | raise ValueError('Invalid input buffer size for NTOH') 77 | 78 | 79 | def convert_to_ja3_segment(data, element_width): 80 | """Convert a packed array of elements to a JA3 segment. 81 | 82 | :param data: Current PCAP buffer item 83 | :type: str 84 | :param element_width: Byte count to process at a time 85 | :type element_width: int 86 | :returns: str 87 | """ 88 | int_vals = list() 89 | data = bytearray(data) 90 | if len(data) % element_width: 91 | message = '{count} is not a multiple of {width}' 92 | message = message.format(count=len(data), width=element_width) 93 | raise ValueError(message) 94 | 95 | for i in range(0, len(data), element_width): 96 | element = ntoh(data[i: i + element_width]) 97 | if element not in GREASE_TABLE: 98 | int_vals.append(element) 99 | 100 | return "-".join(str(x) for x in int_vals) 101 | 102 | 103 | def process_extensions(client_handshake): 104 | """Process any extra extensions and convert to a JA3 segment. 105 | 106 | :param client_handshake: Handshake data from the packet 107 | :type client_handshake: dpkt.ssl.TLSClientHello 108 | :returns: list 109 | """ 110 | if not hasattr(client_handshake, "extensions"): 111 | # Needed to preserve commas on the join 112 | return ["", "", ""] 113 | 114 | exts = list() 115 | elliptic_curve = "" 116 | elliptic_curve_point_format = "" 117 | for ext_val, ext_data in client_handshake.extensions: 118 | if not GREASE_TABLE.get(ext_val): 119 | exts.append(ext_val) 120 | if ext_val == 0x0a: 121 | a, b = parse_variable_array(ext_data, 2) 122 | # Elliptic curve points (16 bit values) 123 | elliptic_curve = convert_to_ja3_segment(a, 2) 124 | elif ext_val == 0x0b: 125 | a, b = parse_variable_array(ext_data, 1) 126 | # Elliptic curve point formats (8 bit values) 127 | elliptic_curve_point_format = convert_to_ja3_segment(a, 1) 128 | else: 129 | continue 130 | 131 | results = list() 132 | results.append("-".join([str(x) for x in exts])) 133 | results.append(elliptic_curve) 134 | results.append(elliptic_curve_point_format) 135 | return results 136 | 137 | 138 | def process_pcap(pcap, any_port=False): 139 | """Process packets within the PCAP. 140 | 141 | :param pcap: Opened PCAP file to be processed 142 | :type pcap: dpkt.pcap.Reader 143 | :param any_port: Whether or not to search for non-SSL ports 144 | :type any_port: bool 145 | """ 146 | decoder = dpkt.ethernet.Ethernet 147 | linktype = pcap.datalink() 148 | if linktype == dpkt.pcap.DLT_LINUX_SLL: 149 | decoder = dpkt.sll.SLL 150 | elif linktype == dpkt.pcap.DLT_NULL or linktype == dpkt.pcap.DLT_LOOP: 151 | decoder = dpkt.loopback.Loopback 152 | 153 | results = list() 154 | for timestamp, buf in pcap: 155 | try: 156 | eth = decoder(buf) 157 | except Exception: 158 | continue 159 | 160 | if not isinstance(eth.data, (dpkt.ip.IP, dpkt.ip6.IP6)): 161 | # We want an IP packet 162 | continue 163 | if not isinstance(eth.data.data, dpkt.tcp.TCP): 164 | # TCP only 165 | continue 166 | 167 | ip = eth.data 168 | tcp = ip.data 169 | 170 | if not (tcp.dport == SSL_PORT or tcp.sport == SSL_PORT or any_port): 171 | # Doesn't match SSL port or we are picky 172 | continue 173 | if len(tcp.data) <= 0: 174 | continue 175 | 176 | tls_handshake = bytearray(tcp.data) 177 | if tls_handshake[0] != TLS_HANDSHAKE: 178 | continue 179 | 180 | records = list() 181 | 182 | try: 183 | records, bytes_used = dpkt.ssl.tls_multi_factory(tcp.data) 184 | except dpkt.ssl.SSL3Exception: 185 | continue 186 | except dpkt.dpkt.NeedData: 187 | continue 188 | 189 | if len(records) <= 0: 190 | continue 191 | 192 | for record in records: 193 | if record.type != TLS_HANDSHAKE: 194 | continue 195 | if len(record.data) == 0: 196 | continue 197 | client_hello = bytearray(record.data) 198 | if client_hello[0] != 1: 199 | # We only want client HELLO 200 | continue 201 | try: 202 | handshake = dpkt.ssl.TLSHandshake(record.data) 203 | except dpkt.dpkt.NeedData: 204 | # Looking for a handshake here 205 | continue 206 | if not isinstance(handshake.data, dpkt.ssl.TLSClientHello): 207 | # Still not the HELLO 208 | continue 209 | 210 | client_handshake = handshake.data 211 | buf, ptr = parse_variable_array(client_handshake.data, 1) 212 | buf, ptr = parse_variable_array(client_handshake.data[ptr:], 2) 213 | ja3 = [str(client_handshake.version)] 214 | 215 | # Cipher Suites (16 bit values) 216 | ja3.append(convert_to_ja3_segment(buf, 2)) 217 | ja3 += process_extensions(client_handshake) 218 | ja3 = ",".join(ja3) 219 | 220 | record = {"source_ip": convert_ip(ip.src), 221 | "destination_ip": convert_ip(ip.dst), 222 | "source_port": tcp.sport, 223 | "destination_port": tcp.dport, 224 | "ja3": ja3, 225 | "ja3_digest": md5(ja3.encode()).hexdigest(), 226 | "timestamp": timestamp} 227 | results.append(record) 228 | 229 | return results 230 | 231 | 232 | def main(): 233 | """Intake arguments from the user and print out JA3 output.""" 234 | desc = "A python script for extracting JA3 fingerprints from PCAP files" 235 | parser = argparse.ArgumentParser(description=(desc)) 236 | parser.add_argument("pcap", help="The pcap file to process") 237 | help_text = "Look for client hellos on any port instead of just 443" 238 | parser.add_argument("-a", "--any_port", required=False, 239 | action="store_true", default=False, 240 | help=help_text) 241 | help_text = "Print out as JSON records for downstream parsing" 242 | parser.add_argument("-j", "--json", required=False, action="store_true", 243 | default=True, help=help_text) 244 | args = parser.parse_args() 245 | 246 | # Use an iterator to process each line of the file 247 | output = None 248 | with open(args.pcap, 'rb') as fp: 249 | try: 250 | capture = dpkt.pcap.Reader(fp) 251 | except ValueError as e_pcap: 252 | try: 253 | fp.seek(0, os.SEEK_SET) 254 | capture = dpkt.pcapng.Reader(fp) 255 | except ValueError as e_pcapng: 256 | raise Exception( 257 | "File doesn't appear to be a PCAP or PCAPng: %s, %s" % 258 | (e_pcap, e_pcapng)) 259 | output = process_pcap(capture, any_port=args.any_port) 260 | 261 | if args.json: 262 | output = json.dumps(output, indent=4, sort_keys=True) 263 | print(output) 264 | else: 265 | for record in output: 266 | tmp = '[{dest}:{port}] JA3: {segment} --> {digest}' 267 | tmp = tmp.format(dest=record['destination_ip'], 268 | port=record['destination_port'], 269 | segment=record['ja3'], 270 | digest=record['ja3_digest']) 271 | print(tmp) 272 | 273 | 274 | if __name__ == "__main__": 275 | main() 276 | -------------------------------------------------------------------------------- /python/ja3s.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Generate JA3 fingerprints from PCAPs using Python.""" 3 | 4 | import argparse 5 | import dpkt 6 | import json 7 | import socket 8 | import struct 9 | import os 10 | from hashlib import md5 11 | from distutils.version import LooseVersion 12 | 13 | __author__ = "Tommy Stallings" 14 | __copyright__ = "Copyright (c) 2017, salesforce.com, inc." 15 | __credits__ = ["John B. Althouse", "Jeff Atkinson", "Josh Atkins"] 16 | __license__ = "BSD 3-Clause License" 17 | __version__ = "1.0.1" 18 | __maintainer__ = "Tommy Stallings, Brandon Dixon" 19 | __email__ = "tommy.stallings2@gmail.com" 20 | 21 | 22 | SSL_PORT = 443 23 | TLS_HANDSHAKE = 22 24 | 25 | 26 | def convert_ip(value): 27 | """Convert an IP address from binary to text. 28 | 29 | :param value: Raw binary data to convert 30 | :type value: str 31 | :returns: str 32 | """ 33 | try: 34 | return socket.inet_ntop(socket.AF_INET, value) 35 | except ValueError: 36 | return socket.inet_ntop(socket.AF_INET6, value) 37 | 38 | 39 | def process_extensions(server_handshake): 40 | """Process any extra extensions and convert to a JA3 segment. 41 | 42 | :param client_handshake: Handshake data from the packet 43 | :type client_handshake: dpkt.ssl.TLSClientHello 44 | :returns: list 45 | """ 46 | if not hasattr(server_handshake, "extensions"): 47 | # Needed to preserve commas on the join 48 | return [""] 49 | 50 | exts = list() 51 | for ext_val, ext_data in server_handshake.extensions: 52 | exts.append(ext_val) 53 | 54 | results = list() 55 | results.append("-".join([str(x) for x in exts])) 56 | return results 57 | 58 | 59 | def process_pcap(pcap, any_port=False): 60 | """Process packets within the PCAP. 61 | 62 | :param pcap: Opened PCAP file to be processed 63 | :type pcap: dpkt.pcap.Reader 64 | :param any_port: Whether or not to search for non-SSL ports 65 | :type any_port: bool 66 | """ 67 | decoder = dpkt.ethernet.Ethernet 68 | linktype = pcap.datalink() 69 | if linktype == dpkt.pcap.DLT_LINUX_SLL: 70 | decoder = dpkt.sll.SLL 71 | elif linktype == dpkt.pcap.DLT_NULL or linktype == dpkt.pcap.DLT_LOOP: 72 | decoder = dpkt.loopback.Loopback 73 | 74 | results = list() 75 | for timestamp, buf in pcap: 76 | try: 77 | eth = decoder(buf) 78 | except Exception: 79 | continue 80 | 81 | if not isinstance(eth.data, (dpkt.ip.IP, dpkt.ip6.IP6)): 82 | # We want an IP packet 83 | continue 84 | if not isinstance(eth.data.data, dpkt.tcp.TCP): 85 | # TCP only 86 | continue 87 | 88 | ip = eth.data 89 | tcp = ip.data 90 | 91 | if not (tcp.dport == SSL_PORT or tcp.sport == SSL_PORT or any_port): 92 | # Doesn't match SSL port or we are picky 93 | continue 94 | if len(tcp.data) <= 0: 95 | continue 96 | 97 | tls_handshake = bytearray(tcp.data) 98 | if tls_handshake[0] != TLS_HANDSHAKE: 99 | continue 100 | 101 | records = list() 102 | 103 | try: 104 | records, bytes_used = dpkt.ssl.tls_multi_factory(tcp.data) 105 | except dpkt.ssl.SSL3Exception: 106 | continue 107 | except dpkt.dpkt.NeedData: 108 | continue 109 | 110 | if len(records) <= 0: 111 | continue 112 | 113 | for record in records: 114 | if record.type != TLS_HANDSHAKE: 115 | continue 116 | if len(record.data) == 0: 117 | continue 118 | server_hello = bytearray(record.data) 119 | if server_hello[0] != 2: 120 | # We only want server HELLO 121 | continue 122 | try: 123 | handshake = dpkt.ssl.TLSHandshake(record.data) 124 | except dpkt.dpkt.NeedData: 125 | # Looking for a handshake here 126 | continue 127 | if not isinstance(handshake.data, dpkt.ssl.TLSServerHello): 128 | # Still not the HELLO 129 | continue 130 | 131 | server_handshake = handshake.data 132 | ja3 = [str(server_handshake.version)] 133 | 134 | # Cipher Suites (16 bit values) 135 | if LooseVersion(dpkt.__version__) <= LooseVersion('1.9.1'): 136 | ja3.append(str(server_handshake.cipher_suite)) 137 | else: 138 | ja3.append(str(server_handshake.ciphersuite.code)) 139 | ja3 += process_extensions(server_handshake) 140 | ja3 = ",".join(ja3) 141 | 142 | record = {"source_ip": convert_ip(ip.src), 143 | "destination_ip": convert_ip(ip.dst), 144 | "source_port": tcp.sport, 145 | "destination_port": tcp.dport, 146 | "ja3": ja3, 147 | "ja3_digest": md5(ja3.encode()).hexdigest(), 148 | "timestamp": timestamp} 149 | results.append(record) 150 | 151 | return results 152 | 153 | 154 | def main(): 155 | """Intake arguments from the user and print out JA3 output.""" 156 | desc = "A python script for extracting JA3 fingerprints from PCAP files" 157 | parser = argparse.ArgumentParser(description=(desc)) 158 | parser.add_argument("pcap", help="The pcap file to process") 159 | help_text = "Look for client hellos on any port instead of just 443" 160 | parser.add_argument("-a", "--any_port", required=False, 161 | action="store_true", default=False, 162 | help=help_text) 163 | help_text = "Print out as JSON records for downstream parsing" 164 | parser.add_argument("-j", "--json", required=False, action="store_true", 165 | default=False, help=help_text) 166 | args = parser.parse_args() 167 | 168 | # Use an iterator to process each line of the file 169 | output = None 170 | with open(args.pcap, 'rb') as fp: 171 | try: 172 | capture = dpkt.pcap.Reader(fp) 173 | except ValueError as e_pcap: 174 | try: 175 | fp.seek(0, os.SEEK_SET) 176 | capture = dpkt.pcapng.Reader(fp) 177 | except ValueError as e_pcapng: 178 | raise Exception( 179 | "File doesn't appear to be a PCAP or PCAPng: %s, %s" % 180 | (e_pcap, e_pcapng)) 181 | output = process_pcap(capture, any_port=args.any_port) 182 | 183 | if args.json: 184 | output = json.dumps(output, indent=4, sort_keys=True) 185 | print(output) 186 | else: 187 | for record in output: 188 | tmp = '[{dest}:{port}] JA3S: {segment} --> {digest}' 189 | tmp = tmp.format(dest=record['destination_ip'], 190 | port=record['destination_port'], 191 | segment=record['ja3'], 192 | digest=record['ja3_digest']) 193 | print(tmp) 194 | 195 | 196 | if __name__ == "__main__": 197 | main() 198 | -------------------------------------------------------------------------------- /python/requirements.txt: -------------------------------------------------------------------------------- 1 | dpkt==1.9.1 2 | -------------------------------------------------------------------------------- /python/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | from setuptools import setup, find_packages 4 | 5 | 6 | def read(fname): 7 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 8 | 9 | setup( 10 | name='pyja3', 11 | version='1.1.0', 12 | description='Generate JA3 fingerprints from PCAPs using Python.', 13 | url="https://github.com/salesforce/ja3", 14 | author="Tommy Stallings", 15 | author_email="tommy.stallings2@gmail.com", 16 | maintainer = "John B. Althouse", 17 | maintainer_email = "jalthouse@salesforce.com", 18 | license="BSD", 19 | packages=find_packages(), 20 | install_requires=['dpkt'], 21 | long_description=read('README.rst'), 22 | classifiers=[ 23 | 'Development Status :: 4 - Beta', 24 | 'Intended Audience :: End Users/Desktop', 25 | 'License :: OSI Approved :: BSD License', 26 | 'Natural Language :: English', 27 | 'Programming Language :: Python', 28 | 'Topic :: Software Development :: Libraries' 29 | ], 30 | package_data={ 31 | 'pyja3': [], 32 | }, 33 | entry_points={ 34 | 'console_scripts': [ 35 | 'ja3 = ja3.ja3:main' 36 | ] 37 | }, 38 | keywords=['ja3', 'fingerprints', 'defender', 'ssl', 'packets'] 39 | ) 40 | -------------------------------------------------------------------------------- /zeek/README.md: -------------------------------------------------------------------------------- 1 | ## Features 2 | - **ja3.zeek** will add the field "ja3" to ssl.log. 3 | - It can also append fields used by JA3 to ssl.log 4 | 5 | - **intel_ja3.zeek** will add INTEL::JA3 to the Zeek Intel Framwork 6 | - This will allow you to import JA3 fingerprints directly into your intel feed. 7 | 8 | - **ja3s.zeek** will add the field "ja3s" to ssl.log, JA3 for the server hello. 9 | - It can also append fields used by JA3S to ssl.log. 10 | 11 | - Tested on Zeek 3.0.0 12 | 13 | ## Installation 14 | - If you're running Zeek >= 3.0.0 or a Zeek product like Corelight, you can install by using the Zeek Package Manager and this one simple command: 15 | ```bash 16 | zkg install ja3 17 | ``` 18 | 19 | - For everyone else, download the files to zeek/share/zeek/site/ja3 and add this line to your local.zeek script: 20 | ```bash 21 | @load ./ja3 22 | ``` 23 | 24 | ## Configuration 25 | 26 | By default ja3.zeek will only append ja3 to the ssl.log. However, if you would like to log all aspects of the SSL Client Hello Packet, uncomment the following lines in ja3.zeek 27 | ```bash 28 | # ja3_version: string &optional &log; 29 | # ja3_ciphers: string &optional &log; 30 | # ja3_extensions: string &optional &log; 31 | # ja3_ec: string &optional &log; 32 | # ja3_ec_fmt: string &optional &log; 33 | ``` 34 | ... 35 | ```bash 36 | #c$ssl$ja3_version = cat(c$tlsfp$client_version); 37 | #c$ssl$ja3_ciphers = c$tlsfp$client_ciphers; 38 | #c$ssl$ja3_extensions = c$tlsfp$extensions; 39 | #c$ssl$ja3_ec = c$tlsfp$e_curves; 40 | #c$ssl$ja3_ec_fmt = c$tlsfp$ec_point_fmt; 41 | ``` 42 | The same changes can be made in ja3s.zeek as well. 43 | 44 | ___ 45 | ### JA3 Created by 46 | 47 | [John B. Althouse](mailto:jalthouse@salesforce.com) 48 | [Jeff Atkinson](mailto:jatkinson@salesforce.com) 49 | [Josh Atkins](mailto:j.atkins@salesforce.com) 50 | 51 | Please send questions and comments to **[John B. Althouse](mailto:jalthouse@salesforce.com)**. 52 | 53 | -------------------------------------------------------------------------------- /zeek/__load__.zeek: -------------------------------------------------------------------------------- 1 | @load ./ja3.zeek 2 | @load ./intel_ja3.zeek 3 | @load ./ja3s.zeek 4 | -------------------------------------------------------------------------------- /zeek/intel_ja3.zeek: -------------------------------------------------------------------------------- 1 | # This Zeek script adds JA3 to the Zeek Intel Framework as Intel::JA3 2 | # 3 | # Author: John B. Althouse (jalthouse@salesforce.com) 4 | # 5 | # Copyright (c) 2017, salesforce.com, inc. 6 | # All rights reserved. 7 | # Licensed under the BSD 3-Clause license. 8 | # For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 9 | 10 | module Intel; 11 | 12 | export { 13 | redef enum Intel::Type += { Intel::JA3 }; 14 | } 15 | 16 | export { 17 | redef enum Intel::Where += { SSL::IN_JA3 }; 18 | } 19 | 20 | @if ( Version::at_least("2.6") || ( Version::number == 20500 && Version::info$commit >= 944 ) ) 21 | event ssl_client_hello(c: connection, version: count, record_version: count, possible_ts: time, client_random: string, session_id: string, ciphers: index_vec, comp_methods: index_vec) 22 | @else 23 | event ssl_client_hello(c: connection, version: count, possible_ts: time, client_random: string, session_id: string, ciphers: index_vec) 24 | @endif 25 | { 26 | if ( c$ssl?$ja3 ) 27 | Intel::seen([$indicator=c$ssl$ja3, $indicator_type=Intel::JA3, $conn=c, $where=SSL::IN_JA3]); 28 | } 29 | -------------------------------------------------------------------------------- /zeek/ja3.zeek: -------------------------------------------------------------------------------- 1 | # This Zeek script appends JA3 to ssl.log 2 | # Version 1.4 (January 2020) 3 | # 4 | # Authors: John B. Althouse (jalthouse@salesforce.com) & Jeff Atkinson (jatkinson@salesforce.com) 5 | # 6 | # Copyright (c) 2017, salesforce.com, inc. 7 | # All rights reserved. 8 | # Licensed under the BSD 3-Clause license. 9 | # For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 10 | 11 | module JA3; 12 | 13 | export { 14 | redef enum Log::ID += { LOG }; 15 | } 16 | 17 | type TLSFPStorage: record { 18 | client_version: count &default=0 &log; 19 | client_ciphers: string &default="" &log; 20 | extensions: string &default="" &log; 21 | e_curves: string &default="" &log; 22 | ec_point_fmt: string &default="" &log; 23 | }; 24 | 25 | redef record connection += { 26 | tlsfp: TLSFPStorage &optional; 27 | }; 28 | 29 | redef record SSL::Info += { 30 | ja3: string &optional &log; 31 | # LOG FIELD VALUES ## 32 | # ja3_version: string &optional &log; 33 | # ja3_ciphers: string &optional &log; 34 | # ja3_extensions: string &optional &log; 35 | # ja3_ec: string &optional &log; 36 | # ja3_ec_fmt: string &optional &log; 37 | }; 38 | 39 | # Google. https://tools.ietf.org/html/draft-davidben-tls-grease-01 40 | const grease: set[int] = { 41 | 2570, 42 | 6682, 43 | 10794, 44 | 14906, 45 | 19018, 46 | 23130, 47 | 27242, 48 | 31354, 49 | 35466, 50 | 39578, 51 | 43690, 52 | 47802, 53 | 51914, 54 | 56026, 55 | 60138, 56 | 64250 57 | }; 58 | const sep = "-"; 59 | event zeek_init() { 60 | Log::create_stream(JA3::LOG,[$columns=TLSFPStorage, $path="tlsfp"]); 61 | } 62 | 63 | event ssl_extension(c: connection, is_orig: bool, code: count, val: string) 64 | { 65 | if ( is_orig == T ) { 66 | if ( code in grease ) { 67 | return; 68 | } 69 | if ( ! c?$tlsfp ){ 70 | c$tlsfp=TLSFPStorage(); 71 | } 72 | if ( c$tlsfp$extensions == "" ) { 73 | c$tlsfp$extensions = cat(code); 74 | } 75 | else { 76 | c$tlsfp$extensions = string_cat(c$tlsfp$extensions, sep,cat(code)); 77 | } 78 | } 79 | } 80 | 81 | 82 | event ssl_extension_ec_point_formats(c: connection, is_orig: bool, point_formats: index_vec) 83 | { 84 | if ( is_orig == T ) { 85 | if ( !c?$tlsfp ) 86 | c$tlsfp=TLSFPStorage(); 87 | for ( i in point_formats ) { 88 | if ( point_formats[i] in grease ) { 89 | next; 90 | } 91 | if ( c$tlsfp$ec_point_fmt == "" ) { 92 | c$tlsfp$ec_point_fmt += cat(point_formats[i]); 93 | } 94 | else { 95 | c$tlsfp$ec_point_fmt += string_cat(sep,cat(point_formats[i])); 96 | } 97 | } 98 | } 99 | } 100 | 101 | event ssl_extension_elliptic_curves(c: connection, is_orig: bool, curves: index_vec) 102 | { 103 | if ( !c?$tlsfp ) 104 | c$tlsfp=TLSFPStorage(); 105 | if ( is_orig == T ) { 106 | for ( i in curves ) { 107 | if ( curves[i] in grease ) { 108 | next; 109 | } 110 | if ( c$tlsfp$e_curves == "" ) { 111 | c$tlsfp$e_curves += cat(curves[i]); 112 | } 113 | else { 114 | c$tlsfp$e_curves += string_cat(sep,cat(curves[i])); 115 | } 116 | } 117 | } 118 | } 119 | 120 | @if ( ( Version::number >= 20600 ) || ( Version::number == 20500 && Version::info$commit >= 944 ) ) 121 | event ssl_client_hello(c: connection, version: count, record_version: count, possible_ts: time, client_random: string, session_id: string, ciphers: index_vec, comp_methods: index_vec) &priority=1 122 | @else 123 | event ssl_client_hello(c: connection, version: count, possible_ts: time, client_random: string, session_id: string, ciphers: index_vec) &priority=1 124 | @endif 125 | { 126 | if ( !c?$tlsfp ) 127 | c$tlsfp=TLSFPStorage(); 128 | c$tlsfp$client_version = version; 129 | for ( i in ciphers ) { 130 | if ( ciphers[i] in grease ) { 131 | next; 132 | } 133 | if ( c$tlsfp$client_ciphers == "" ) { 134 | c$tlsfp$client_ciphers += cat(ciphers[i]); 135 | } 136 | else { 137 | c$tlsfp$client_ciphers += string_cat(sep,cat(ciphers[i])); 138 | } 139 | } 140 | local sep2 = ","; 141 | local ja3_string = string_cat(cat(c$tlsfp$client_version),sep2,c$tlsfp$client_ciphers,sep2,c$tlsfp$extensions,sep2,c$tlsfp$e_curves,sep2,c$tlsfp$ec_point_fmt); 142 | local tlsfp_1 = md5_hash(ja3_string); 143 | c$ssl$ja3 = tlsfp_1; 144 | 145 | # LOG FIELD VALUES ## 146 | #c$ssl$ja3_version = cat(c$tlsfp$client_version); 147 | #c$ssl$ja3_ciphers = c$tlsfp$client_ciphers; 148 | #c$ssl$ja3_extensions = c$tlsfp$extensions; 149 | #c$ssl$ja3_ec = c$tlsfp$e_curves; 150 | #c$ssl$ja3_ec_fmt = c$tlsfp$ec_point_fmt; 151 | # 152 | # FOR DEBUGGING ## 153 | #print "JA3: "+tlsfp_1+" Fingerprint String: "+ja3_string; 154 | 155 | } 156 | -------------------------------------------------------------------------------- /zeek/ja3s.zeek: -------------------------------------------------------------------------------- 1 | # This Zeek script appends JA3S (JA3 Server) to ssl.log 2 | # Version 1.1 (January 2020) 3 | # This builds a fingerprint for the SSL Server Hello packet based on SSL/TLS version, cipher picked, and extensions used. 4 | # Designed to be used in conjunction with JA3 to fingerprint SSL communication between clients and servers. 5 | # 6 | # Authors: John B. Althouse (jalthouse@salesforce.com) Jeff Atkinson (jatkinson@salesforce.com) 7 | # Copyright (c) 2018, salesforce.com, inc. 8 | # All rights reserved. 9 | # Licensed under the BSD 3-Clause license. 10 | # For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 11 | # 12 | 13 | 14 | 15 | module JA3_Server; 16 | 17 | export { 18 | redef enum Log::ID += { LOG }; 19 | } 20 | 21 | type JA3Sstorage: record { 22 | server_version: count &default=0 &log; 23 | server_cipher: count &default=0 &log; 24 | server_extensions: string &default="" &log; 25 | }; 26 | 27 | redef record connection += { 28 | ja3sfp: JA3Sstorage &optional; 29 | }; 30 | 31 | redef record SSL::Info += { 32 | ja3s: string &optional &log; 33 | # LOG FIELD VALUES # 34 | # ja3s_version: string &optional &log; 35 | # ja3s_cipher: string &optional &log; 36 | # ja3s_extensions: string &optional &log; 37 | }; 38 | 39 | 40 | const sep = "-"; 41 | event zeek_init() { 42 | Log::create_stream(JA3_Server::LOG,[$columns=JA3Sstorage, $path="ja3sfp"]); 43 | } 44 | 45 | event ssl_extension(c: connection, is_orig: bool, code: count, val: string) 46 | { 47 | if ( ! c?$ja3sfp ) 48 | c$ja3sfp=JA3Sstorage(); 49 | if ( is_orig == F ) { 50 | if ( c$ja3sfp$server_extensions == "" ) { 51 | c$ja3sfp$server_extensions = cat(code); 52 | } 53 | else { 54 | c$ja3sfp$server_extensions = string_cat(c$ja3sfp$server_extensions, sep,cat(code)); 55 | } 56 | } 57 | } 58 | 59 | @if ( ( Version::number >= 20600 ) || ( Version::number == 20500 && Version::info$commit >= 944 ) ) 60 | event ssl_server_hello(c: connection, version: count, record_version: count, possible_ts: time, server_random: string, session_id: string, cipher: count, comp_method: count) &priority=1 61 | @else 62 | event ssl_server_hello(c: connection, version: count, possible_ts: time, server_random: string, session_id: string, cipher: count, comp_method: count) &priority=1 63 | @endif 64 | { 65 | if ( !c?$ja3sfp ) 66 | c$ja3sfp=JA3Sstorage(); 67 | c$ja3sfp$server_version = version; 68 | c$ja3sfp$server_cipher = cipher; 69 | local sep2 = ","; 70 | local ja3s_string = string_cat(cat(c$ja3sfp$server_version),sep2,cat(c$ja3sfp$server_cipher),sep2,c$ja3sfp$server_extensions); 71 | local ja3sfp_1 = md5_hash(ja3s_string); 72 | c$ssl$ja3s = ja3sfp_1; 73 | 74 | # LOG FIELD VALUES # 75 | #c$ssl$ja3s_version = cat(c$ja3sfp$server_version); 76 | #c$ssl$ja3s_cipher = cat(c$ja3sfp$server_cipher); 77 | #c$ssl$ja3s_extensions = c$ja3sfp$server_extensions; 78 | # 79 | # FOR DEBUGGING # 80 | #print "JA3S: "+ja3sfp_1+" Fingerprint String: "+ja3s_string; 81 | 82 | } 83 | -------------------------------------------------------------------------------- /zkg.meta: -------------------------------------------------------------------------------- 1 | [package] 2 | script_dir = zeek 3 | description = JA3 creates 32 character SSL client fingerprints and logs them as a field in ssl.log. These fingerprints can easily be shared as threat intelligence or used as correlation items for enhanced alerting and analysis. This package also adds JA3 to the Zeek Intel Framework. 4 | https://github.com/salesforce/ja3 5 | tags = intel, ssl, logging 6 | version = 1.2.0 7 | --------------------------------------------------------------------------------