├── config
├── domain_whitelist
├── docs
├── _config.yml
├── _includes
│ └── youtubePlayer.html
├── 404.html
├── contact.md
├── research.md
├── Gemfile
├── index.md
├── vulnerabilites.md
├── usage.md
├── _layouts
│ └── default.html
└── Gemfile.lock
├── libs
├── jna-4.1.0.jar
├── gson-2.8.0.jar
├── httpcore-4.4.6.jar
├── httpmime-4.5.3.jar
├── fluent-hc-4.5.3.jar
├── httpclient-4.5.3.jar
├── jcommander-1.69.jar
├── commons-codec-1.9.jar
├── commons-logging-1.2.jar
├── jna-platform-4.1.0.jar
├── httpclient-win-4.5.3.jar
└── httpclient-cache-4.5.3.jar
├── Cert.java
├── Config.java
├── Utils.java
├── README.md
├── CheckCertificate.java
├── Launcher.java
├── FakeDNS.java
└── MITM.java
/config:
--------------------------------------------------------------------------------
1 | dns=192.168.0.1
2 |
3 | censysID=
4 | censysSecret=
5 |
--------------------------------------------------------------------------------
/domain_whitelist:
--------------------------------------------------------------------------------
1 | mbanking.meezanbank.com
2 | tunnelbear.com
3 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-minimal
2 | show_downloads: true
3 |
--------------------------------------------------------------------------------
/libs/jna-4.1.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisMcMStone/Spinner/HEAD/libs/jna-4.1.0.jar
--------------------------------------------------------------------------------
/libs/gson-2.8.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisMcMStone/Spinner/HEAD/libs/gson-2.8.0.jar
--------------------------------------------------------------------------------
/libs/httpcore-4.4.6.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisMcMStone/Spinner/HEAD/libs/httpcore-4.4.6.jar
--------------------------------------------------------------------------------
/libs/httpmime-4.5.3.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisMcMStone/Spinner/HEAD/libs/httpmime-4.5.3.jar
--------------------------------------------------------------------------------
/libs/fluent-hc-4.5.3.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisMcMStone/Spinner/HEAD/libs/fluent-hc-4.5.3.jar
--------------------------------------------------------------------------------
/libs/httpclient-4.5.3.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisMcMStone/Spinner/HEAD/libs/httpclient-4.5.3.jar
--------------------------------------------------------------------------------
/libs/jcommander-1.69.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisMcMStone/Spinner/HEAD/libs/jcommander-1.69.jar
--------------------------------------------------------------------------------
/libs/commons-codec-1.9.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisMcMStone/Spinner/HEAD/libs/commons-codec-1.9.jar
--------------------------------------------------------------------------------
/libs/commons-logging-1.2.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisMcMStone/Spinner/HEAD/libs/commons-logging-1.2.jar
--------------------------------------------------------------------------------
/libs/jna-platform-4.1.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisMcMStone/Spinner/HEAD/libs/jna-platform-4.1.0.jar
--------------------------------------------------------------------------------
/libs/httpclient-win-4.5.3.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisMcMStone/Spinner/HEAD/libs/httpclient-win-4.5.3.jar
--------------------------------------------------------------------------------
/libs/httpclient-cache-4.5.3.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChrisMcMStone/Spinner/HEAD/libs/httpclient-cache-4.5.3.jar
--------------------------------------------------------------------------------
/docs/_includes/youtubePlayer.html:
--------------------------------------------------------------------------------
1 |
2 | VIDEO
3 |
4 |
--------------------------------------------------------------------------------
/docs/404.html:
--------------------------------------------------------------------------------
1 | ---
2 | layout: default
3 | ---
4 |
5 |
18 |
19 |
20 |
404
21 |
22 |
Page not found :(
23 |
The requested page could not be found.
24 |
25 |
--------------------------------------------------------------------------------
/Cert.java:
--------------------------------------------------------------------------------
1 | public class Cert{
2 |
3 | private byte[] der;
4 | private String CN;
5 |
6 | public Cert(byte[] der, String CN) {
7 | this.der = der;
8 | this.CN = CN;
9 | }
10 |
11 | public void setDer(byte[] der) {
12 | this.der = der;
13 | }
14 |
15 | public void setCN(String CN){
16 | this.CN = CN;
17 | }
18 |
19 | public String getCN(){
20 | return CN;
21 | }
22 |
23 | public byte[] getDer() {
24 | return der;
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/docs/contact.md:
--------------------------------------------------------------------------------
1 |
2 | # Contact and Additional Information
3 |
4 |
5 | This work was carried out by Chris McMahon Stone, [Tom Chothia](http://www.cs.bham.ac.uk/~tpc/) and [Flavio Garcia](http://www.cs.bham.ac.uk/~garciaf/) at the [University of Birmingham](http://sec.cs.bham.ac.uk/).
6 |
7 | If you have any questions about the operation of the tool, please contact Chris
8 |
9 | Or any questions on the research in general, contact Chris , Tom , or Flavio .
10 |
11 |
--------------------------------------------------------------------------------
/docs/research.md:
--------------------------------------------------------------------------------
1 |
2 | # Publications
3 |
4 | All of details of this work are described in the paper:
5 |
6 | * _Spinner: Semi-Automatic Detection of Pinning without Hostname Verification_
7 | ([paper](https://www.cs.bham.ac.uk/~tpc/Papers/spinner.pdf), [cite](https://dl.acm.org/citation.cfm?id=3134628)) published at [ACSAC 2017](https://www.acsac.org/).
8 |
9 | The paper above built on our previous work on an more general analysis of TLS in UK banking apps. This included various TLS certificate mis-verification vulnerabilites, in addition to phishing attacks. Details of this work can be found here:
10 |
11 | * _Why Banker Bob (Still) Can't Get TLS Right: A Security Analysis of TLS in Leading UK Banking Apps_ ([paper](http://www.cs.bham.ac.uk/~tpc/Papers/BankingApps.pdf), [cite](https://link.springer.com/chapter/10.1007/978-3-319-70972-7_33)) published at [FC 2017](http://fc17.ifca.ai/).
12 |
13 |
14 |
--------------------------------------------------------------------------------
/docs/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | # Hello! This is where you manage which Jekyll version is used to run.
4 | # When you want to use a different version, change it below, save the
5 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
6 | #
7 | # bundle exec jekyll serve
8 | #
9 | # This will help ensure the proper Jekyll version is running.
10 | # Happy Jekylling!
11 | #gem "jekyll", "~> 3.6.2"
12 |
13 | # This is the default theme for new Jekyll sites. You may change this to anything you like.
14 | # gem "github-pages", group: :jekyll_plugins
15 |
16 |
17 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and
18 | # uncomment the line below. To upgrade, run `bundle update github-pages`.
19 | gem "github-pages", group: :jekyll_plugins
20 |
21 | # If you have any plugins, put them here!
22 | group :jekyll_plugins do
23 | gem "jekyll-feed", "~> 0.6"
24 | end
25 |
26 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem
27 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
28 |
29 |
--------------------------------------------------------------------------------
/Config.java:
--------------------------------------------------------------------------------
1 | import java.io.FileInputStream;
2 | import java.io.IOException;
3 | import java.io.BufferedReader;
4 | import java.io.FileReader;
5 | import java.io.InputStream;
6 | import java.util.Properties;
7 | import java.util.ArrayList;
8 | import java.util.List;
9 |
10 | /**
11 | * Configuration class
12 | *
13 | */
14 | public class Config {
15 | String dnsIP;
16 | String censysID;
17 | String censysSecret;
18 | String[] allowList;
19 |
20 | public Config(String configFilename, String whitelistFilename) throws Exception {
21 | Properties properties = new Properties();
22 | InputStream configInput = new FileInputStream(configFilename);
23 | properties.load(configInput);
24 | loadProperties(properties, whitelistFilename);
25 | }
26 |
27 | public void loadProperties(Properties properties, String whitelistFilename) throws Exception {
28 | dnsIP = properties.getProperty("dns");
29 | censysID = properties.getProperty("censysID");
30 | censysSecret = properties.getProperty("censysSecret");
31 | if(whitelistFilename != null) {
32 | BufferedReader in = new BufferedReader(new FileReader(whitelistFilename));
33 | String str;
34 | List list = new ArrayList();
35 | while((str = in.readLine()) != null){
36 | list.add(str);
37 | }
38 | allowList = list.toArray(new String[0]);
39 | } else {
40 | allowList = new String[]{};
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | youtubeId: yUZ1gmhERfs
3 | ---
4 |
5 |
6 | # Spinner: Semi-Automatic Detection of Pinning without Hostname verification
7 |
8 | Tool to enable black box detection of applications that pin to non-leaf TLS certificates and fail to carry out hostname verification. It can also be used to detect apps that have the same issue but do not pin, or indeed apps that will accept any certificate (such as self-signed).
9 |
10 | Spinner analyses the certificate chain of the requested domains and redirects TLS traffic to other sites, which it finds on [censys.io](https://censys.io/), that use the same certificate chain. The handshake is then proxied to determine if encrypted application data is sent by the app to the domain that the app is not expecting.
11 |
12 | For details on the vulnerabilites we found using Spinner, which included apps from some of the world's largest banks, see [here](vulnerabilites.html). Publications related to this work are listed [here](research.md).
13 |
14 | {% include youtubePlayer.html id=page.youtubeId %}
15 |
16 |
17 |
18 | In the video above we demonstrate three different behaviours by testing three apps. The first two are examples of apps that carry out hostname verification correctly. Two different handshake break-downs are shown. The final app, makes insecure connections which do not verify the hostname of the TLS certificate. We demonstrate this by redirecting the TLS traffic to a site which uses the same certificate chain (but with a different leaf certificate).
19 |
20 |
--------------------------------------------------------------------------------
/docs/vulnerabilites.md:
--------------------------------------------------------------------------------
1 |
2 | # Discovered Vulnerabilites
3 |
4 | In [this](https://www.cs.bham.ac.uk/~tpc/Papers/spinner.pdf) paper, we describe our results of testing 400 high security applications using Spinner. These included banking, trading, VPN and cryptocurrency apps. We found 9 apps in total that pinned to a non-leaf TLS certificate but failed to carry out hostname verification. This rendered them vulnerable to Man-in-the-Middle attacks.
5 |
6 | | App name | No. of Downloads | Platform |
7 | |----------|----|----|
8 | | Bank of America Health | 100k - 500k | Android |
9 | | TunnelBear VPN | 1m - 5m | Android |
10 | | Meezan Bank | 10k - 50k | Android |
11 | | Smile Bank | 10k - 50k | Android |
12 | | HSBC | 5m - 10m | iOS |
13 | | HSBC Business | 10k - 50k | iOS |
14 | | HSBC Identity | 10k - 50k | iOS |
15 | | HSBCnet | 10k - 50k | iOS |
16 | | HSBC Private | 10k - 50k | iOS |
17 |
18 |
19 | Of notable impact was HSBC's set of iOS apps. We note that this vulnerability affected their entire global app base, which consists of apps from 30 countries they operate in.
20 |
21 | We also discovered numerous apps that were not pinning but also did not verify certificate hostnames correctly. For a full list of affected apps, see page 8 of our [paper](https://www.cs.bham.ac.uk/~tpc/Papers/spinner.pdf).
22 |
23 | ### Example affected APKs
24 |
25 | Below we link to hosted APKs for apps affected by the pinning without hostname verification vulnerability. These can be used to demonstrate Spinner's detection of vulnerable apps.
26 |
27 | * [TunnelBear VPN v139](https://www.apkmirror.com/apk/tunnelbear-inc/tunnelbear-vpn/tunnelbear-vpn-v139-release/tunnelbear-vpn-v139-android-apk-download/)
28 | * [Meezan Bank v1.3.1](https://meezan-mobile-banking.en.aptoide.com)
29 |
--------------------------------------------------------------------------------
/Utils.java:
--------------------------------------------------------------------------------
1 | import java.security.MessageDigest;
2 |
3 | public class Utils {
4 |
5 | public static String getSha256(byte[] value) {
6 | try{
7 | MessageDigest md = MessageDigest.getInstance("SHA-256");
8 | md.update(value);
9 | return byteArrayToHexString(md.digest());
10 | } catch(Exception ex){
11 | throw new RuntimeException(ex);
12 | }
13 | }
14 |
15 | /**
16 | * @param needle A string to look for
17 | * @param hayStack An array of strings
18 | * @return the index of the first string in hayStack ends with needle, or -1 is no such string exists.
19 | */
20 | public static int stringListMatch(String needle, String[] hayStack) {
21 | for (int i = 0; i < hayStack.length; i++) {
22 | if (needle.endsWith(hayStack[i])) { return i; }
23 | }
24 | return -1;
25 | }
26 |
27 | public static String byteArrayToHexString(byte[] data) {
28 | return byteArrayToHexString(data, 0, data.length);
29 | }
30 |
31 | public static String byteArrayToHexString(byte[] data,int start, int stop) {
32 | StringBuffer buf = new StringBuffer();
33 | for (int i = start; i < stop; i++) {
34 | int halfbyte = (data[i] >>> 4) & 0x0F;
35 | int two_halfs = 0;
36 | do {
37 | if ((0 <= halfbyte) && (halfbyte <= 9))
38 | buf.append((char) ('0' + halfbyte));
39 | else
40 | buf.append((char) ('a' + (halfbyte - 10)));
41 | halfbyte = data[i] & 0x0F;
42 | } while(two_halfs++ < 1);
43 | }
44 | return buf.toString();
45 | }
46 |
47 | // Code from http://javaconversions.blogspot.co.uk
48 | public static byte[] hexStringToByteArray(String s) {
49 | int len = s.length();
50 | byte[] data = new byte[len / 2];
51 | for (int i = 0; i < len; i += 2) {
52 | data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
53 | + Character.digit(s.charAt(i+1), 16));
54 | }
55 | return data;
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spinner: Semi-Automatic Detection of Pinning without Hostname verification
2 |
3 | Tool to enable black box detection of applications that pin to non-leaf TLS certificates and fail to carry out hostname verification. It can also be used to detect apps that have the same issue but do not pin, or indeed apps that will accept any certificate (such as self-signed).
4 |
5 | Spinner analyses the certificate chain of the requested domains and redirects TLS traffic to other sites, which it finds on Censys.io, that use the same certificate chain. The handshake is then proxied to determine if encrypted application data is sent by the app to the domain that the app is not expecting.
6 |
7 | For more details see our [paper](http://www.cs.bham.ac.uk/~garciaf/publications/spinner.pdf)
8 |
9 |
10 | **To compile:**
11 |
12 | On Linux: ```javac -cp .:libs/* *.java```
13 |
14 | On Windows: ```javac -cp ".;libs/*" *.java```
15 |
16 | **Set up:**
17 |
18 | Either:
19 | * Set DNS of mobile device to use IP of machine running Spinner e.g. In android: WiFi -> Modify Network -> Advanced -> IP Settings, Static -> DNS
20 |
21 | or
22 |
23 | * Run Spinner on machine with access point e.g. hostapd. Connect testing device to AP running Spinner.
24 |
25 | **To run:**
26 |
27 | On Linux: ```sudo java -cp .:libs/* Launcher --help```
28 |
29 | On Windows: ```java -cp ".;libs/*" Launcher --help```
30 |
31 |
32 | (Note: root is required as a TLS and DNS server are ran on privileged ports)
33 |
34 | The program requires a config file which contains the IP address of the DNS server on your network, and the credentials to use with Censys.io. You will need to sign up for an account here https://censys.io/register.
35 |
36 |
37 | **Example usage**
38 |
39 | Run the tool with config details specified in the file ```config``` and ignore connections to domains listed in ```whitelist```
40 |
41 | ```sudo java -cp .:libs/* Launcher -c config -w whitelist```
42 |
43 | Run the tool without using Censys by manually specifying a redirect domain.
44 |
45 | ```sudo java -cp .:libs/* Launcher -m google.com```
46 |
47 |
48 | **Disclaimer**: This tool is intended for research use, and is currently undergoing further development. We welcome any feedback which can be provided by raising issues or pull requests.
49 |
50 |
51 |
--------------------------------------------------------------------------------
/docs/usage.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Compilation and Usage Instructions
4 |
5 |
6 | **To compile:**
7 |
8 | On Linux: ```javac -cp .:libs/* *.java```
9 |
10 | On Windows: ```javac -cp ".;libs/*" *.java```
11 |
12 | **Set up:**
13 |
14 | Spinner needs to be able to Man-in-the-Middle DNS requests and TLS traffic. To this end, it sets up a DNS and TLS proxy running on ports 53 and 443 respectively. To direct traffic from your testing device to Spinner:
15 |
16 | * Set DNS of device to use IP of machine running Spinner e.g. In android: WiFi -> Modify Network -> Advanced -> IP Settings, Static -> DNS
17 |
18 | or
19 |
20 | * Set up a Wi-Fi access point on the device running Spinner. In Linux this can be done with [hostapd](https://w1.fi/hostapd/). Connect testing device to the access point running Spinner.
21 |
22 |
23 | **To run:**
24 |
25 | On Linux: ```sudo java -cp .:libs/* Launcher --help```
26 |
27 | On Windows: ```java -cp ".;libs/*" Launcher --help```
28 |
29 |
30 | (Note: root is required as a TLS and DNS server are ran on privileged ports)
31 |
32 | The program requires a config file which contains the IP address of the DNS server on your network, and the credentials to use with Censys.io. You will need to sign up for an account here .
33 |
34 |
35 | **Example usage**
36 |
37 | Run the tool with config details specified in the file ```config``` and ignore connections to domains listed in the file ```domain_whitelist```
38 |
39 | ```sudo java -cp .:libs/* Launcher -c config -w domain_whitelist```
40 |
41 | Run the tool without using Censys by manually specifying a redirect domain.
42 |
43 | ```sudo java -cp .:libs/* Launcher -m google.com```
44 |
45 | If your app is detected as vulnerable. You can narrow down the exact hostname verification vulnerability by using the ```-m``` option to check if:
46 |
47 | * The app accepts self-signed certificates by redirecting the traffic to ```self-signed.badssl.com```
48 |
49 | * The app accepts any valid certificate (but for wrong hostname) by redirecting the traffic to ```wrong.host.badssl.com```
50 |
51 | **Vulnerable apps**
52 |
53 | Example vulnerable APKs are linked to on [this](vulnerabilites.md) page.
54 |
55 |
56 |
57 | **Disclaimer**: This tool is intended for research use, and is currently undergoing further development. We welcome any feedback which can be provided by raising issues or pull requests.
58 |
--------------------------------------------------------------------------------
/docs/_layouts/default.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {% seo %}
8 |
9 |
10 |
11 |
14 |
15 |
16 |
17 |
39 |
40 |
41 | {{ content }}
42 |
43 |
44 |
49 |
50 |
51 |
52 |
53 | {% if site.google_analytics %}
54 |
63 | {% endif %}
64 |
65 |
66 |
--------------------------------------------------------------------------------
/CheckCertificate.java:
--------------------------------------------------------------------------------
1 |
2 | /*
3 | * Handles interation with Censys
4 | *
5 | * Chris McMahon-Stone (c.mcmahon-stone@cs.bham.ac.uk)
6 | */
7 |
8 | import java.io.FileInputStream;
9 | import java.io.IOException;
10 | import java.io.ObjectInputStream;
11 | import java.net.InetSocketAddress;
12 | import java.security.cert.Certificate;
13 | import java.security.cert.X509Certificate;
14 | import java.text.ParseException;
15 | import java.text.SimpleDateFormat;
16 | import java.util.ArrayList;
17 | import java.util.Base64;
18 | import java.util.Date;
19 | import java.util.HashMap;
20 | import java.util.Map;
21 | import java.util.Random;
22 |
23 | import javax.naming.ldap.LdapName;
24 | import javax.naming.ldap.Rdn;
25 | import javax.net.ssl.SSLContext;
26 | import javax.net.ssl.SSLSocket;
27 | import javax.net.ssl.SSLSocketFactory;
28 | import javax.net.ssl.TrustManager;
29 | import javax.net.ssl.X509TrustManager;
30 |
31 | import org.apache.http.HttpResponse;
32 | import org.apache.http.auth.AuthScope;
33 | import org.apache.http.auth.UsernamePasswordCredentials;
34 | import org.apache.http.client.CredentialsProvider;
35 | import org.apache.http.client.methods.HttpGet;
36 | import org.apache.http.client.methods.HttpPost;
37 | import org.apache.http.entity.ContentType;
38 | import org.apache.http.entity.StringEntity;
39 | import org.apache.http.impl.client.BasicCredentialsProvider;
40 | import org.apache.http.impl.client.CloseableHttpClient;
41 | import org.apache.http.impl.client.HttpClients;
42 | import org.apache.http.util.EntityUtils;
43 |
44 | import com.google.gson.JsonArray;
45 | import com.google.gson.JsonObject;
46 | import com.google.gson.JsonParser;
47 |
48 | public class CheckCertificate {
49 | static int portNo = 443;
50 | static boolean verbose = false;
51 |
52 | /**
53 | * Gets the certificates used by the server and prints Issuer details to
54 | * STDOUT
55 | *
56 | * @param host
57 | * Hostname to download certificates for
58 | * @return Issuer details of each certificates concatentated together.
59 | */
60 | public static Cert[] getCertificates(String host) {
61 |
62 | try {
63 | TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
64 | public java.security.cert.X509Certificate[] getAcceptedIssuers() {
65 | return null;
66 | }
67 |
68 | public void checkClientTrusted(X509Certificate[] certs, String authType) {
69 | }
70 |
71 | public void checkServerTrusted(X509Certificate[] certs, String authType) {
72 | }
73 | } };
74 |
75 | // Install the all-trusting trust manager
76 | SSLContext sc = SSLContext.getInstance("TLSv1.2");
77 | sc.init(null, trustAllCerts, new java.security.SecureRandom());
78 |
79 | // Open TLS connection with host
80 | SSLSocket socket = (SSLSocket) sc.getSocketFactory().createSocket();
81 | socket.connect(new InetSocketAddress(host, portNo), 10000);
82 | // Start TLS handshake
83 | socket.startHandshake();
84 | // Get session certificates
85 | javax.security.cert.X509Certificate[] certs = socket.getSession().getPeerCertificateChain();
86 | // Print number of certificates provided by host
87 | StringBuilder result = new StringBuilder();
88 | Cert[] cs = new Cert[certs.length];
89 | for (int i = 0; i < certs.length; i++) {
90 | String dn = certs[i].getSubjectDN().getName();
91 | LdapName ln = new LdapName(dn);
92 | for (Rdn rdn : ln.getRdns()) {
93 | if (rdn.getType().equalsIgnoreCase("CN")) {
94 | cs[i] = new Cert(certs[i].getEncoded(), rdn.getValue().toString());
95 | break;
96 | }
97 | }
98 | }
99 | return cs;
100 | } catch (Exception e) {
101 | System.out.println("Get certificate failed for host: " + host);
102 | return null;
103 | }
104 | }
105 |
106 | /**
107 | * @param host
108 | * hostname to check certificate
109 | * @param mapFile
110 | * the file path of a HashMap> file.
111 | * @return a random host that use the same TLS certificate as the given host
112 | */
113 | @SuppressWarnings("unchecked")
114 | public static String censysLookup(String urlRequested, String certCN, String ID, String secret) {
115 |
116 | ArrayList alternateHosts = new ArrayList<>();
117 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
118 |
119 | try {
120 | CloseableHttpClient httpclient = HttpClients.createDefault();
121 | HttpPost post = new HttpPost("https://www.censys.io/api/v1/search/certificates");
122 | post.setHeader("User-Agent", "python-requests/2.13.0");
123 | String base64 = Base64.getEncoder().encodeToString((ID + ":" + secret).getBytes("utf-8"));
124 | post.setHeader("Authorization", "Basic " + base64);
125 | String jsonQuery = "{" + " \"query\":\"443.https.tls.certificate.parsed.issuer.common_name: " + certCN
126 | + "\"," + " \"page\":1,"
127 | + " \"fields\":[\"parsed.subject.common_name\", \"parsed.validity.end\"]," + " \"flatten\":false"
128 | + "}";
129 | StringEntity requestEntity = new StringEntity(jsonQuery, ContentType.APPLICATION_JSON);
130 | post.setEntity(requestEntity);
131 | HttpResponse response = httpclient.execute(post);
132 | JsonObject json = new JsonParser().parse(EntityUtils.toString(response.getEntity())).getAsJsonObject();
133 | if (json.get("status").getAsString().equals("ok")) {
134 | JsonArray results = json.getAsJsonArray("results");
135 | for (int i = 0; i < results.size(); i++) {
136 | try {
137 | String validity = results.get(i).getAsJsonObject().get("parsed").getAsJsonObject()
138 | .get("validity").getAsJsonObject().get("end").getAsString().replaceAll("Z", "")
139 | .replaceAll("T", " ");
140 | if (sdf.parse(validity).before(new Date()))
141 | continue;
142 | String cn = results.get(i).getAsJsonObject().get("parsed").getAsJsonObject().get("subject")
143 | .getAsJsonObject().get("common_name").getAsJsonArray().get(0).getAsString();
144 | if (cn != null && !cn.contains(urlRequested) && !cn.contains("*."))
145 | alternateHosts.add(cn);
146 | } catch (Exception e) {
147 | continue;
148 | }
149 | }
150 | }
151 | } catch (IOException e) {
152 | e.printStackTrace();
153 | }
154 | if (alternateHosts.size() > 1) {
155 | return alternateHosts.get(new Random().nextInt(alternateHosts.size()));
156 | } else {
157 | return null;
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/Launcher.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Initiates the Man-in-the-middle server and DNS in two seperate threads.
3 | * Takes in 1-3 arguments, verbose option, specify app flag and Certificate-Host map file
4 | *
5 | * Chris McMahon-Stone (c.mcmahon-stone@cs.bham.ac.uk)
6 | */
7 |
8 | import java.io.*;
9 | import java.util.*;
10 | import java.util.stream.*;
11 |
12 | import com.beust.jcommander.JCommander;
13 | import com.beust.jcommander.Parameter;
14 | import com.beust.jcommander.ParameterException;
15 |
16 | import java.text.*;
17 | import java.time.LocalDateTime;
18 |
19 | public class Launcher {
20 |
21 | @Parameter(names={"--verbosity", "-v"}, description = "Verbosity of output", required = false)
22 | int verbose = 2;
23 | @Parameter(names={"--manual", "-m"}, description = "Hostname to redirect traffic too", required = false)
24 | String redirectHost;
25 | @Parameter(names={"--log", "-l"}, description = "Optionally specify logging file", required = false)
26 | String logFile;
27 | @Parameter(names={"-dns"}, description = "DNS only", required = false)
28 | boolean dnsOnly = false;
29 | @Parameter(names={"-h", "--help"}, description = "Show help", required = false)
30 | boolean help = false;
31 | @Parameter(names={"-p", "--passthrough"}, description = "No redirection, just proxy traffic", required = false)
32 | boolean passthrough = false;
33 | @Parameter(names={"--whitelist", "-w"}, description = "New line delimited file of domains to spoof to our TLS proxy.", required = false)
34 | String whiteListFile;
35 | @Parameter(names={"--config", "-c"}, description = "Config file containing required DNS IP and Censys account credentials", required = true)
36 | String configFile;
37 |
38 | public static void main(String[] args) throws Exception {
39 |
40 | Launcher main = new Launcher();
41 | JCommander jc = JCommander.newBuilder().addObject(main).build();
42 | jc.setProgramName(main.getClass().getName());
43 | try{
44 | jc.parse(args);
45 | } catch(ParameterException e) {
46 | if(main.help) {
47 | printIntro();
48 | jc.usage();
49 | return;
50 | } else {
51 | System.out.println("ERROR: " +e.getMessage());
52 | jc.usage();
53 | return;
54 | }
55 | }
56 |
57 | if(main.help) {
58 | printIntro();
59 | jc.usage();
60 | return;
61 | }
62 |
63 | Config config = null;
64 | try {
65 | config = new Config(main.configFile, main.whiteListFile);
66 | } catch (FileNotFoundException e) {
67 | System.out.println("ERROR: " + e.getMessage());
68 | jc.usage();
69 | return;
70 | }
71 |
72 | if(main.logFile == null) {
73 | main.logFile = "log-" + LocalDateTime.now();
74 | System.out.println("Writing log to: " + main.logFile);
75 | }
76 | PrintWriter logOut = new PrintWriter(new BufferedWriter(new FileWriter(main.logFile, true)));
77 | FakeDNS dns;
78 | if(main.dnsOnly) {
79 | dns = new FakeDNS(null, main.verbose, logOut, null, true, false, config);
80 | new Thread(dns).start();
81 | return;
82 | } else {
83 | MITM mitm;
84 | if(main.redirectHost == null) {
85 | mitm = new MITM(main.verbose, logOut, false, main.passthrough);
86 | dns = new FakeDNS(mitm, main.verbose, logOut, null, false, main.passthrough, config);
87 | } else {
88 | mitm = new MITM(main.verbose, logOut, true, main.passthrough);
89 | dns = new FakeDNS(mitm, main.verbose, logOut, main.redirectHost, false, main.passthrough, config);
90 | }
91 | Scanner scan = new Scanner(System.in);
92 | Thread dnsThread = new Thread(dns);
93 | Thread mitmThread = new Thread(mitm);
94 |
95 | dnsThread.start();
96 | mitmThread.start();
97 |
98 | //Ensure log is written to disk when program is closed
99 | Runtime.getRuntime().addShutdownHook(new Thread() {
100 | @Override
101 | public void run() {
102 | logOut.flush();
103 | logOut.close();
104 | }
105 | });
106 | }
107 | }
108 |
109 | private static void printIntro() {
110 |
111 | String message = "-------------------------------------------------------------------------------\n" +
112 | "-------------------------------------------------------------------------------\n" +
113 | " ___________ _____ _ _ _ _ ___________ \n" +
114 | " / ___| ___ \\_ _| \\ | | \\ | | ___| ___ \\\n" +
115 | " \\ `--.| |_/ / | | | \\| | \\| | |__ | |_/ /\n" +
116 | " `--. \\ __/ | | | . ` | . ` | __|| / \n" +
117 | " /\\__/ / | _| |_| |\\ | |\\ | |___| |\\ \\ \n" +
118 | " \\____/\\_| \\___/\\_| \\_|_| \\_|____/\\_| \\_|\n" +
119 | "-------------------------------------------------------------------------------\n" +
120 | "-------------------------------------------------------------------------------\n\n" +
121 | "-------------------------------------------------------------------------------\n" +
122 | " Developed by: Chris McMahon Stone (c.mcmahon-stone@cs.bham.ac.uk)\n" +
123 | "-------------------------------------------------------------------------------\n\n" +
124 | " Tool to enable detection of applications that pin to non-leaf TLS certificates\n " +
125 | "and fail to carry out hostname verification. \n\n" +
126 | " Spinner analyses the certificate chain of the requested domains and redirects \n" +
127 | " TLS traffic to other sites, which it finds on Censys.io, that use the same \n" +
128 | " certificate chain. The handshake is then proxied to determine if encrypted \n" +
129 | " application data is sent by the app to the domain that the app is not expecting.\n\n\n" +
130 | " The target device is required to use Spinner's IP for DNS requests. An \n" +
131 | " account on Censys is also required, credentials should be specified in the \n" +
132 | " config file.\n\n";
133 |
134 |
135 | System.out.println(message);
136 | }
137 |
138 | }
139 |
140 |
--------------------------------------------------------------------------------
/docs/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | activesupport (4.2.9)
5 | i18n (~> 0.7)
6 | minitest (~> 5.1)
7 | thread_safe (~> 0.3, >= 0.3.4)
8 | tzinfo (~> 1.1)
9 | addressable (2.5.2)
10 | public_suffix (>= 2.0.2, < 4.0)
11 | coffee-script (2.4.1)
12 | coffee-script-source
13 | execjs
14 | coffee-script-source (1.11.1)
15 | colorator (1.1.0)
16 | commonmarker (0.17.7.1)
17 | ruby-enum (~> 0.5)
18 | concurrent-ruby (1.0.5)
19 | ethon (0.11.0)
20 | ffi (>= 1.3.0)
21 | execjs (2.7.0)
22 | faraday (0.14.0)
23 | multipart-post (>= 1.2, < 3)
24 | ffi (1.9.21)
25 | forwardable-extended (2.6.0)
26 | gemoji (3.0.0)
27 | github-pages (177)
28 | activesupport (= 4.2.9)
29 | github-pages-health-check (= 1.3.5)
30 | jekyll (= 3.6.2)
31 | jekyll-avatar (= 0.5.0)
32 | jekyll-coffeescript (= 1.0.2)
33 | jekyll-commonmark-ghpages (= 0.1.5)
34 | jekyll-default-layout (= 0.1.4)
35 | jekyll-feed (= 0.9.2)
36 | jekyll-gist (= 1.4.1)
37 | jekyll-github-metadata (= 2.9.3)
38 | jekyll-mentions (= 1.2.0)
39 | jekyll-optional-front-matter (= 0.3.0)
40 | jekyll-paginate (= 1.1.0)
41 | jekyll-readme-index (= 0.2.0)
42 | jekyll-redirect-from (= 0.12.1)
43 | jekyll-relative-links (= 0.5.2)
44 | jekyll-remote-theme (= 0.2.3)
45 | jekyll-sass-converter (= 1.5.0)
46 | jekyll-seo-tag (= 2.3.0)
47 | jekyll-sitemap (= 1.1.1)
48 | jekyll-swiss (= 0.4.0)
49 | jekyll-theme-architect (= 0.1.0)
50 | jekyll-theme-cayman (= 0.1.0)
51 | jekyll-theme-dinky (= 0.1.0)
52 | jekyll-theme-hacker (= 0.1.0)
53 | jekyll-theme-leap-day (= 0.1.0)
54 | jekyll-theme-merlot (= 0.1.0)
55 | jekyll-theme-midnight (= 0.1.0)
56 | jekyll-theme-minimal (= 0.1.0)
57 | jekyll-theme-modernist (= 0.1.0)
58 | jekyll-theme-primer (= 0.5.2)
59 | jekyll-theme-slate (= 0.1.0)
60 | jekyll-theme-tactile (= 0.1.0)
61 | jekyll-theme-time-machine (= 0.1.0)
62 | jekyll-titles-from-headings (= 0.5.0)
63 | jemoji (= 0.8.1)
64 | kramdown (= 1.16.2)
65 | liquid (= 4.0.0)
66 | listen (= 3.0.6)
67 | mercenary (~> 0.3)
68 | minima (= 2.1.1)
69 | nokogiri (>= 1.8.1, < 2.0)
70 | rouge (= 2.2.1)
71 | terminal-table (~> 1.4)
72 | github-pages-health-check (1.3.5)
73 | addressable (~> 2.3)
74 | net-dns (~> 0.8)
75 | octokit (~> 4.0)
76 | public_suffix (~> 2.0)
77 | typhoeus (~> 0.7)
78 | html-pipeline (2.7.1)
79 | activesupport (>= 2)
80 | nokogiri (>= 1.4)
81 | i18n (0.9.4)
82 | concurrent-ruby (~> 1.0)
83 | jekyll (3.6.2)
84 | addressable (~> 2.4)
85 | colorator (~> 1.0)
86 | jekyll-sass-converter (~> 1.0)
87 | jekyll-watch (~> 1.1)
88 | kramdown (~> 1.14)
89 | liquid (~> 4.0)
90 | mercenary (~> 0.3.3)
91 | pathutil (~> 0.9)
92 | rouge (>= 1.7, < 3)
93 | safe_yaml (~> 1.0)
94 | jekyll-avatar (0.5.0)
95 | jekyll (~> 3.0)
96 | jekyll-coffeescript (1.0.2)
97 | coffee-script (~> 2.2)
98 | coffee-script-source (~> 1.11.1)
99 | jekyll-commonmark (1.1.0)
100 | commonmarker (~> 0.14)
101 | jekyll (>= 3.0, < 4.0)
102 | jekyll-commonmark-ghpages (0.1.5)
103 | commonmarker (~> 0.17.6)
104 | jekyll-commonmark (~> 1)
105 | rouge (~> 2)
106 | jekyll-default-layout (0.1.4)
107 | jekyll (~> 3.0)
108 | jekyll-feed (0.9.2)
109 | jekyll (~> 3.3)
110 | jekyll-gist (1.4.1)
111 | octokit (~> 4.2)
112 | jekyll-github-metadata (2.9.3)
113 | jekyll (~> 3.1)
114 | octokit (~> 4.0, != 4.4.0)
115 | jekyll-mentions (1.2.0)
116 | activesupport (~> 4.0)
117 | html-pipeline (~> 2.3)
118 | jekyll (~> 3.0)
119 | jekyll-optional-front-matter (0.3.0)
120 | jekyll (~> 3.0)
121 | jekyll-paginate (1.1.0)
122 | jekyll-readme-index (0.2.0)
123 | jekyll (~> 3.0)
124 | jekyll-redirect-from (0.12.1)
125 | jekyll (~> 3.3)
126 | jekyll-relative-links (0.5.2)
127 | jekyll (~> 3.3)
128 | jekyll-remote-theme (0.2.3)
129 | jekyll (~> 3.5)
130 | rubyzip (>= 1.2.1, < 3.0)
131 | typhoeus (>= 0.7, < 2.0)
132 | jekyll-sass-converter (1.5.0)
133 | sass (~> 3.4)
134 | jekyll-seo-tag (2.3.0)
135 | jekyll (~> 3.3)
136 | jekyll-sitemap (1.1.1)
137 | jekyll (~> 3.3)
138 | jekyll-swiss (0.4.0)
139 | jekyll-theme-architect (0.1.0)
140 | jekyll (~> 3.5)
141 | jekyll-seo-tag (~> 2.0)
142 | jekyll-theme-cayman (0.1.0)
143 | jekyll (~> 3.5)
144 | jekyll-seo-tag (~> 2.0)
145 | jekyll-theme-dinky (0.1.0)
146 | jekyll (~> 3.5)
147 | jekyll-seo-tag (~> 2.0)
148 | jekyll-theme-hacker (0.1.0)
149 | jekyll (~> 3.5)
150 | jekyll-seo-tag (~> 2.0)
151 | jekyll-theme-leap-day (0.1.0)
152 | jekyll (~> 3.5)
153 | jekyll-seo-tag (~> 2.0)
154 | jekyll-theme-merlot (0.1.0)
155 | jekyll (~> 3.5)
156 | jekyll-seo-tag (~> 2.0)
157 | jekyll-theme-midnight (0.1.0)
158 | jekyll (~> 3.5)
159 | jekyll-seo-tag (~> 2.0)
160 | jekyll-theme-minimal (0.1.0)
161 | jekyll (~> 3.5)
162 | jekyll-seo-tag (~> 2.0)
163 | jekyll-theme-modernist (0.1.0)
164 | jekyll (~> 3.5)
165 | jekyll-seo-tag (~> 2.0)
166 | jekyll-theme-primer (0.5.2)
167 | jekyll (~> 3.5)
168 | jekyll-github-metadata (~> 2.9)
169 | jekyll-seo-tag (~> 2.2)
170 | jekyll-theme-slate (0.1.0)
171 | jekyll (~> 3.5)
172 | jekyll-seo-tag (~> 2.0)
173 | jekyll-theme-tactile (0.1.0)
174 | jekyll (~> 3.5)
175 | jekyll-seo-tag (~> 2.0)
176 | jekyll-theme-time-machine (0.1.0)
177 | jekyll (~> 3.5)
178 | jekyll-seo-tag (~> 2.0)
179 | jekyll-titles-from-headings (0.5.0)
180 | jekyll (~> 3.3)
181 | jekyll-watch (1.5.1)
182 | listen (~> 3.0)
183 | jemoji (0.8.1)
184 | activesupport (~> 4.0, >= 4.2.9)
185 | gemoji (~> 3.0)
186 | html-pipeline (~> 2.2)
187 | jekyll (>= 3.0)
188 | kramdown (1.16.2)
189 | liquid (4.0.0)
190 | listen (3.0.6)
191 | rb-fsevent (>= 0.9.3)
192 | rb-inotify (>= 0.9.7)
193 | mercenary (0.3.6)
194 | mini_portile2 (2.3.0)
195 | minima (2.1.1)
196 | jekyll (~> 3.3)
197 | minitest (5.11.3)
198 | multipart-post (2.0.0)
199 | net-dns (0.8.0)
200 | nokogiri (1.8.2)
201 | mini_portile2 (~> 2.3.0)
202 | octokit (4.8.0)
203 | sawyer (~> 0.8.0, >= 0.5.3)
204 | pathutil (0.16.1)
205 | forwardable-extended (~> 2.6)
206 | public_suffix (2.0.5)
207 | rb-fsevent (0.10.2)
208 | rb-inotify (0.9.10)
209 | ffi (>= 0.5.0, < 2)
210 | rouge (2.2.1)
211 | ruby-enum (0.7.1)
212 | i18n
213 | rubyzip (1.2.1)
214 | safe_yaml (1.0.4)
215 | sass (3.5.5)
216 | sass-listen (~> 4.0.0)
217 | sass-listen (4.0.0)
218 | rb-fsevent (~> 0.9, >= 0.9.4)
219 | rb-inotify (~> 0.9, >= 0.9.7)
220 | sawyer (0.8.1)
221 | addressable (>= 2.3.5, < 2.6)
222 | faraday (~> 0.8, < 1.0)
223 | terminal-table (1.8.0)
224 | unicode-display_width (~> 1.1, >= 1.1.1)
225 | thread_safe (0.3.6)
226 | typhoeus (0.8.0)
227 | ethon (>= 0.8.0)
228 | tzinfo (1.2.5)
229 | thread_safe (~> 0.1)
230 | unicode-display_width (1.3.0)
231 |
232 | PLATFORMS
233 | ruby
234 |
235 | DEPENDENCIES
236 | github-pages
237 | jekyll-feed (~> 0.6)
238 | minima (~> 2.0)
239 | tzinfo-data
240 |
241 | BUNDLED WITH
242 | 1.16.1
243 |
--------------------------------------------------------------------------------
/FakeDNS.java:
--------------------------------------------------------------------------------
1 | /**
2 | * A DNS server that serves forged DNS records for spoofing DNS.
3 | * Requests for whitelisted domains are served legitimate response.
4 | *
5 | * Tom Chothia & Chris McMahon Stone (c.mcmahon-stone@cs.bham.ac.uk)
6 | */
7 |
8 | import java.io.*;
9 | import java.math.BigInteger;
10 | import java.net.*;
11 | import java.util.*;
12 | import java.net.DatagramSocket;
13 | import java.net.InetAddress;
14 | import java.net.UnknownHostException;
15 |
16 | public class FakeDNS implements Runnable {
17 |
18 | private int portNo = 53;
19 |
20 | //verbose = 0: print nothing
21 | //verbose = 1: print spoofed requests
22 | //verbose = 2: print spoofed, dropped and allowed requests
23 | //verbose = 3: print spoofed, dropped and allowed requests and DNS details
24 | private int verbose;
25 |
26 | //Realistic looking Flags,# or Qus and RRs info for a DNS response
27 | private byte[] FlagsQusAndRRsInfo = Utils.hexStringToByteArray("81800001000100000000");
28 | //Realistic looking name, type and class info for a DNS response
29 | private byte[] nameTypeClass = Utils.hexStringToByteArray("c00c00010001");
30 | // time to live of 10 seconds
31 | private byte[] ttl = Utils.hexStringToByteArray("0000000a");
32 | // The address of a real DNS server.
33 | private String realDNSserver;
34 |
35 | // allowList will get the real IP address returned from the "realDNSserver".
36 | private String[] allowList;
37 | private String defaultSpoofIP;
38 | private MITM mitm;
39 | private PrintWriter outLog;
40 | private String redirectHost;
41 | private boolean dnsOnly;
42 | private boolean passthrough;
43 | private String censysID;
44 | private String censysSecret;
45 |
46 | public FakeDNS(MITM mitm, int verbose, PrintWriter outLog, String redirectHost, boolean dnsOnly, boolean passthrough, Config config) {
47 | this.mitm = mitm;
48 | this.verbose = verbose;
49 | this.outLog = outLog;
50 | this.redirectHost = redirectHost;
51 | if(!dnsOnly) {
52 | mitm.setForwardingHost(redirectHost);
53 | }
54 | this.dnsOnly = dnsOnly;
55 | this.passthrough = passthrough;
56 | this.realDNSserver=config.dnsIP;
57 | this.allowList=config.allowList;
58 | this.censysID = config.censysID;
59 | this.censysSecret = config.censysSecret;
60 | }
61 |
62 | public void run() {
63 | DatagramSocket sock = null;
64 | try {
65 | //Get IP address of MITM
66 | Enumeration en = NetworkInterface.getNetworkInterfaces();
67 | while(en.hasMoreElements()){
68 | NetworkInterface iface = en.nextElement();
69 | if (iface.isLoopback() || !iface.isUp()) continue;
70 | Enumeration ee = iface.getInetAddresses();
71 | while(ee.hasMoreElements()) {
72 | InetAddress ia= ee.nextElement();
73 | if (ia instanceof Inet6Address) continue;
74 | defaultSpoofIP = ia.getHostAddress();
75 | break;
76 | }
77 | }
78 | //Open a UDP port
79 | sock = new DatagramSocket(portNo);
80 | sock.setSoTimeout(500);
81 | byte[] buffer = new byte[128];
82 | DatagramPacket incoming = new DatagramPacket(buffer, buffer.length);
83 | if (verbose>0) System.out.println("- Listening on UDP port: "+portNo);
84 |
85 | while(true) {
86 | if(Thread.currentThread().isInterrupted()) throw new InterruptedException();
87 | if(dnsOnly || mitm.isConnectionWaiting()) {
88 | // Listen for a request
89 | try { sock.receive(incoming); } catch (SocketTimeoutException e) {continue;}
90 | byte[] origDNSrequest = incoming.getData();
91 |
92 | //Find the port and IP of sender
93 | int portFrom = incoming.getPort();
94 | InetAddress ipAddressFrom = incoming.getAddress();
95 |
96 | //Parse the DNS request
97 | String urlRequested = parseDNSrequest(origDNSrequest);
98 |
99 | int resultInt = Utils.stringListMatch(urlRequested,allowList);
100 | if (resultInt<0) {
101 | if (verbose>1) System.out.println("- Requested URL: "+urlRequested+" on allow list. Returning real DNS response.");
102 | outLog.println("- Requested URL: "+urlRequested+" on allow list. Returning real DNS response.");
103 | // Request real response from the real DNS server
104 | byte[] dnsReply = getRealDNSresponse(origDNSrequest);
105 | // Forward that response back to the original requester.
106 | DatagramPacket reply = new DatagramPacket(dnsReply,dnsReply.length,ipAddressFrom,portFrom);
107 | sock.send(reply);
108 | } else {
109 | if (verbose>0) System.out.println("- Requested URL: "+urlRequested+" default action. Sending default IP: "+defaultSpoofIP);
110 | outLog.println("- Requested URL: "+urlRequested+" default action. Sending default IP: "+defaultSpoofIP);
111 | if(passthrough || alternateHostResponse(urlRequested)) {
112 | //Send a spoofed address back to the original requester.
113 | byte[] response = formDNSresponse(origDNSrequest, urlRequested.length()+1, defaultSpoofIP);
114 | DatagramPacket reply = new DatagramPacket(response,response.length,ipAddressFrom,portFrom);
115 | sock.send(reply);
116 | }
117 | }
118 | }
119 | }
120 | } catch (InterruptedException | IOException e) {
121 | if(e instanceof IOException) {
122 | System.out.println(e.getMessage());
123 | outLog.println(e.getMessage());
124 | }
125 | } finally {
126 | if(sock != null) sock.close();
127 | }
128 | }
129 |
130 | private boolean alternateHostResponse(String urlRequested) {
131 | if(dnsOnly || mitm.getRedirectHosts().containsKey(urlRequested)) return true;
132 | Cert[] certs = CheckCertificate.getCertificates(urlRequested);
133 | if(certs != null && certs.length > 1) {
134 | if(verbose > 1) System.out.println("- CN of Issuer for "+urlRequested + " = " + certs[1].getCN());
135 | outLog.println("- CN of Issuer for "+urlRequested + " = " + certs[1].getCN());
136 | mitm.addRealLeafCert(urlRequested, certs[0].getDer());
137 | mitm.addRealIssuerCert(urlRequested, certs[1].getDer());
138 | if(this.redirectHost != null) {
139 | mitm.addRedirectHost(urlRequested, this.redirectHost);
140 | mitm.setForwardingHost(this.redirectHost);
141 | mitm.setRealHost(urlRequested);
142 | } else {
143 | String alternateHost;
144 | if(!mitm.getCachedAlternateHosts().containsKey(urlRequested)) {
145 | alternateHost = CheckCertificate.censysLookup(urlRequested, certs[1].getCN(), censysID, censysSecret);
146 | if(alternateHost == null) {
147 | System.out.println("- No alternate hosts for given " + urlRequested + ". Dropping request...");
148 | outLog.println("- No alternate hosts for given " + urlRequested + ". Dropping request...");
149 | return false;
150 | }
151 | mitm.addCachedAlternateHost(urlRequested, alternateHost);
152 | } else {
153 | alternateHost = mitm.getCachedAlternateHosts().get(urlRequested);
154 | }
155 | mitm.addRedirectHost(urlRequested, alternateHost);
156 | mitm.setForwardingHost(alternateHost);
157 | mitm.setRealHost(urlRequested);
158 | }
159 | // if (verbose>0) System.out.println("- Forwarding " + urlRequested + " traffic to: " + mitm.getForwardingHost());
160 | // outLog.println("- Forwarding " + urlRequested + " traffic to: " + mitm.getForwardingHost());
161 | return true;
162 | } else {
163 | System.out.println("- Less than two certificates in chain. Dropping request...");
164 | outLog.println("- Less than two certificates in chain. Dropping request...");
165 | }
166 | return false;
167 | }
168 |
169 | /**
170 | * @param data e.g. a DNS request
171 | * @return the response received from sending data over socket
172 | */
173 | // sock is a UDP socket and data is a DNS request.
174 | public byte[] getRealDNSresponse(byte[] data)
175 | throws UnknownHostException, IOException {
176 | DatagramSocket realDNSsocket = new DatagramSocket();
177 | DatagramPacket requestPacket = new DatagramPacket(data,data.length,InetAddress.getByName(realDNSserver),53);
178 | realDNSsocket.send(requestPacket);
179 | byte[] dnsReply = new byte[1024];
180 | DatagramPacket dnsReplyPacket = new DatagramPacket(dnsReply,dnsReply.length);
181 | realDNSsocket.receive(dnsReplyPacket);
182 | return dnsReply;
183 | }
184 |
185 | /**
186 | * @param dnsQuery A DNS query
187 | * @param urlLength The length of the URL in that query
188 | * @param theSpoofIP An IP address
189 | * @return A DNS response that will answer the query with the IP address "theSpoofIP"
190 | */
191 | public byte[] formDNSresponse(byte[] dnsQuery, int urlLength,
192 | String theSpoofIP) {
193 | byte[] response = new byte[urlLength+33];
194 | System.arraycopy(dnsQuery, 0, response, 0, 2); //The Transaction ID:
195 | System.arraycopy(FlagsQusAndRRsInfo, 0, response, 2, 10);
196 | System.arraycopy(dnsQuery, 12, response, 12, urlLength+5); //The query
197 | //The Answer
198 | System.arraycopy(nameTypeClass, 0, response, urlLength+17, 6);
199 | System.arraycopy(ttl, 0, response, urlLength+23, 4);
200 | //Length of IP address is 4 bytes
201 | response[urlLength+27]= 0x00;
202 | response[urlLength+28]= 0x04;
203 | //The Address
204 | String[] IPparts = theSpoofIP.split("\\.");
205 | response[urlLength+29]= (byte)(Integer.parseInt(IPparts[0]));
206 | response[urlLength+30]= (byte)(Integer.parseInt(IPparts[1]));
207 | response[urlLength+31]= (byte)(Integer.parseInt(IPparts[2]));
208 | response[urlLength+32]= (byte)(Integer.parseInt(IPparts[3]));
209 | return response;
210 | }
211 |
212 | /**
213 | * This does not support more than one URL in the query
214 | *
215 | * @param data A DNS query
216 | * @return the URL asked for in the query as a String
217 | */
218 | public String parseDNSrequest(byte[] data) {
219 | if (verbose>2) {
220 | System.out.println(Utils.byteArrayToHexString(data));
221 | System.out.println("- Transaction ID:"+Utils.byteArrayToHexString(data,0,2));
222 | System.out.println("- Flags:"+Utils.byteArrayToHexString(data,2,4));
223 | System.out.println("- Questions:"+Utils.byteArrayToHexString(data,4,6));
224 | System.out.println("- Answers RRs:"+Utils.byteArrayToHexString(data,6,8));
225 | System.out.println("- Authority RRs:"+Utils.byteArrayToHexString(data,8,10));
226 | System.out.println("- Additional RRs:"+Utils.byteArrayToHexString(data,10,12));
227 | }
228 |
229 | //Find the domain name being requested
230 | ArrayList urlList = new ArrayList();
231 | int pos = 12;
232 | Integer length = new Integer(data[pos]);
233 | int totalLength = length.intValue();
234 | while (length!=0) {
235 | String part = new String(data, pos+1,length);
236 | urlList.add(part);
237 | pos=pos+length+1;
238 | length = new Integer(data[pos]);
239 | totalLength = totalLength+length.intValue()+1;
240 | }
241 | String urlString=urlList.get(0);
242 | for (int i =1;i redirectHosts;
22 | //verbose = 0: print nothing
23 | //verbose = 1: print forwarding details
24 | //verbose = 2: print handshake details
25 | private int verbose;
26 | private long connectionTimeout = 5000;
27 | private PrintWriter outLog;
28 | private Map realLeafCerts;
29 | private Map realIssuerCerts;
30 | private volatile boolean connectionWaiting = true;
31 | private boolean manual = false;
32 | private volatile String forwardingHost;
33 | private volatile String realHost;
34 | private boolean passthrough;
35 | private Map cachedAlternateHosts;
36 |
37 | public MITM(int verbose, PrintWriter outLog, boolean manual, boolean passthrough) {
38 | this.verbose = verbose;
39 | this.outLog = outLog;
40 | this.manual = manual;
41 | this.redirectHosts = (new HashMap());
42 | this.realIssuerCerts = (new HashMap());
43 | this.realLeafCerts = (new HashMap());
44 | this.cachedAlternateHosts = (new HashMap<>());
45 | this.passthrough = passthrough;
46 | }
47 |
48 | public void run() {
49 | Thread sessionThread = null;
50 | ServerSocket listener = null;
51 | try {
52 | //Listen for connections
53 | listener = new ServerSocket(clientPortNo);
54 | listener.setSoTimeout(500);
55 | if(verbose > 0) System.out.println("# Listening on TCP port 443");
56 |
57 | while(true) {
58 | if(Thread.currentThread().isInterrupted()) throw new InterruptedException();
59 | Socket connection;
60 | try { connection = listener.accept();} catch (SocketTimeoutException e) {continue;}
61 | connectionWaiting = false;
62 | if(verbose > 0) System.out.println("# Connection with client made");
63 | outLog.println("# Connection with client made");
64 | if((forwardingHost == null || forwardingHost.isEmpty()) && !passthrough) {
65 | if(verbose > 0) System.out.println("WARNING: No redirect host set, dropping connection. Ensure DNS requests are directed to Spinner, or set redirect host manually with -m flag.");
66 | outLog.println("WARNING: No redirect host set, dropping connection. Ensure DNS requests are directed to Spinner, or set redirect host manually with -m flag.");
67 | connectionWaiting = true;
68 | continue;
69 | }
70 | //Spin off new thread & continue listening
71 | sessionThread = new Thread(new SSLSession(connection, realHost, forwardingHost));
72 | sessionThread.start();
73 | sessionThread.join();
74 | connectionWaiting = true;
75 | }
76 | //Deal with exceptions
77 | } catch(InterruptedException e) {
78 | if(sessionThread != null) sessionThread.interrupt();
79 | } catch (IOException e) {
80 | System.out.println(e.getMessage());
81 | outLog.println(e.getMessage());
82 | } finally {
83 | try {
84 | if(listener != null) listener.close();
85 | } catch (IOException e2) {
86 | System.out.println(e2.getMessage());
87 | outLog.println(e2.getMessage());
88 | }
89 | connectionWaiting = true;
90 | }
91 | }
92 |
93 | public String getForwardingHost() {
94 | return forwardingHost;
95 | }
96 |
97 | public void setForwardingHost(String forwardingHost) {
98 | this.forwardingHost = forwardingHost;
99 | }
100 | /**
101 | * Returns whether the
102 | */
103 | public boolean isConnectionWaiting() {
104 | return connectionWaiting;
105 | }
106 |
107 | public Map getRedirectHosts() {
108 | return redirectHosts;
109 | }
110 |
111 | public void addRedirectHost(String from, String to) {
112 | this.redirectHosts.put(from, to);
113 | }
114 |
115 | public Map getRealLeafCerts() {
116 | return realLeafCerts;
117 | }
118 |
119 | public void addRealLeafCert(String host, byte[] leafCert) {
120 | this.realLeafCerts.put(host, leafCert);
121 | }
122 |
123 | public Map getRealIssuerCerts() {
124 | return realIssuerCerts;
125 | }
126 |
127 | public void addRealIssuerCert(String host, byte[] issuerCert) {
128 | this.realIssuerCerts.put(host, issuerCert);
129 | }
130 |
131 | public String getRealHost() {
132 | return realHost;
133 | }
134 |
135 | public void setRealHost(String realHost) {
136 | this.realHost = realHost;
137 | }
138 |
139 | public Map getCachedAlternateHosts() {
140 | return cachedAlternateHosts;
141 | }
142 |
143 | public void addCachedAlternateHost(String urlRequested, String alternateHost) {
144 | this.cachedAlternateHosts.put(urlRequested, alternateHost);
145 | }
146 |
147 | /**
148 | * Handles the SSLSession between a client and server, forwarding
149 | * data between each and printing details to STDOUT.
150 | */
151 | private class SSLSession implements Runnable {
152 |
153 | Socket clientConnection;
154 | Socket serverConnection;
155 | //Map storing alert hex values to messages
156 | Map alertMap;
157 | //Map storing handshake hex values to messages
158 | Map handShakeMap;
159 | OutputStream clientOutStream;
160 | InputStream clientInStream;
161 | OutputStream serverOutStream;
162 | InputStream serverInStream;
163 | String forwardHost;
164 | String realHost;
165 |
166 | public SSLSession(Socket clientConnection, String realHost, String host) {
167 | this.clientConnection = clientConnection;
168 | this.forwardHost = host;
169 | this.realHost = realHost;
170 | alertMap = new HashMap();
171 | handShakeMap = new HashMap();
172 | fillMaps();
173 | }
174 |
175 | public void run() {
176 |
177 | try {
178 | //Get I/O streams for client
179 | clientOutStream = clientConnection.getOutputStream();
180 | clientInStream = clientConnection.getInputStream();
181 |
182 | if(verbose > 1) System.out.println(" STARTED HANDSHAKE");
183 | outLog.println(" STARTED HANDSHAKE");
184 | //Some flags
185 | boolean clientAlert = false;
186 | boolean serverAlert = false;
187 | boolean handShake = false;
188 | boolean failed = false;
189 | boolean finished = false;
190 | boolean serverCCS = false;
191 | boolean clientCCS = false;
192 | boolean timeout = false;
193 | int messageCount = 0;
194 | long timeoutExpiredMs = System.currentTimeMillis() + connectionTimeout;
195 |
196 | while(true) {
197 | if(Thread.currentThread().isInterrupted()) { throw new InterruptedException(); }
198 | //Check if client has sent any data
199 | while(clientInStream.available() > 0) {
200 |
201 | if(!Thread.currentThread().isInterrupted()) {
202 | messageCount++;
203 |
204 | //Read in TLS record header
205 | byte[] header = new byte[5];
206 | clientInStream.read(header);
207 |
208 | //Decode record type
209 | switch(header[0]) {
210 | case 22:
211 | if(clientCCS) {
212 | if(verbose > 1) System.out.println(" " + messageCount + ". Encrypted client Handshake message");
213 | outLog.println(" " + messageCount + ". Encrypted client Handshake message");
214 | } else {
215 | if(verbose > 1) System.out.print(" " + messageCount + ". Client Handshake message: ");
216 | outLog.print(" " + messageCount + ". Client Handshake message: ");
217 | }
218 | handShake = true;
219 | break;
220 | case 23:
221 | if(verbose > 1) System.out.println(" " + messageCount + ". Sending application data to server");
222 | outLog.println(" " + messageCount + ". Sending application data to server");
223 | finished = true;
224 | break;
225 | case 20:
226 | if(verbose > 1) System.out.println(" " + messageCount + ". Client ChangeCipherSpec");
227 | outLog.println(" " + messageCount + ". Client ChangeCipherSpec");
228 | clientCCS = true;
229 | break;
230 | case 21:
231 | if(clientCCS) {
232 | if(verbose > 1) System.out.println(" " + messageCount + ". Client sent an encrypted Alert message");
233 | outLog.println(" " + messageCount + ". Client sent an encrypted Alert message");
234 | failed = true;
235 | } else {
236 | if(verbose > 1) System.out.print(" " + messageCount + ". Client sent an Alert: ");
237 | outLog.print(" " + messageCount + ". Client sent an Alert: ");
238 | clientAlert = true;
239 | }
240 | break;
241 | default:
242 | if(verbose > 1) System.out.println(" " + messageCount + ". Unknown message from client");
243 | outLog.println(" " + messageCount + ". Unknown message from client");
244 | }
245 |
246 | //Caculate length of message excluding header
247 | int length = ((header[3] & 0xff) << 8) | (header[4] & 0xff);
248 | byte[] data = new byte[length];
249 |
250 | //Read in rest of TLS message
251 | for(int i=0; i 1) System.out.print("Fatal ");
259 | outLog.print("Fatal ");
260 | if(verbose > 1) System.out.println(alertMap.get((int)data[1]));
261 | outLog.println(alertMap.get((int)data[1]));
262 | failed = true;
263 | } else if (clientAlert) {
264 | if(verbose > 1) System.out.print("Warning ");
265 | outLog.print("Warning ");
266 | if(verbose > 1) System.out.println(alertMap.get((int)data[1]));
267 | outLog.println(alertMap.get((int)data[1]));
268 | }
269 | }
270 |
271 | //Combine header with rest of message
272 | byte[] forwardMessage = new byte[5 + length];
273 | System.arraycopy(header, 0, forwardMessage, 0, header.length);
274 | System.arraycopy(data, 0, forwardMessage, header.length, data.length);
275 |
276 | //Print handshake message
277 | if(!clientCCS && handShake) {
278 | String messageString = handShakeMap.get((int)forwardMessage[5]);
279 | if(messageString == null) messageString = "UNKNOWN_MESSAGE_TYPE";
280 |
281 | //Log handshake type
282 | if(verbose > 1) System.out.println(messageString);
283 | outLog.println(messageString);
284 |
285 | if(messageString.equals("CLIENT_HELLO")) {
286 | String sni = extractSNI(data);
287 | if(sni != null) {
288 | System.out.println(" > SNI: " + sni);
289 | outLog.println(" > SNI: " + sni);
290 | setForwardHost(redirectHosts.get(sni));
291 | setRealHost(sni);
292 | } else {
293 | System.out.println(" > No SNI, using last DNS lookup");
294 | outLog.println(" > No SNI, using last DNS lookup");
295 | }
296 | if(passthrough) this.forwardHost = sni;
297 | System.out.println(" > Forwarding to: " + this.forwardHost);
298 | outLog.println(" > Forwarding to: " + this.forwardHost);
299 | this.serverConnection = new Socket();
300 | try {
301 | serverConnection.connect(new InetSocketAddress(this.forwardHost, 443), 10000);
302 | } catch (Exception e) {
303 | System.out.println(" > ERROR: Failed to connect to selected redirect host. Restart Spinner to try with different host.");
304 | cachedAlternateHosts.remove(this.realHost);
305 | return;
306 |
307 | }
308 | serverOutStream = serverConnection.getOutputStream();
309 | serverInStream = serverConnection.getInputStream();
310 | }
311 | }
312 |
313 |
314 | //Forward TLS message to server
315 | serverOutStream.write(forwardMessage);
316 | clientAlert = false;
317 | handShake = false;
318 | } else {
319 | break;
320 | }
321 |
322 | }
323 |
324 | //Check if server has sent any data
325 | while(serverInStream != null && serverInStream.available() > 0) {
326 |
327 | if(!Thread.currentThread().isInterrupted()) {
328 | messageCount++;
329 |
330 | //Read in TLS record header
331 | byte[] header = new byte[5];
332 | serverInStream.read(header);
333 |
334 | //Decode record type
335 | switch(header[0]) {
336 | case 22:
337 | if(serverCCS) {
338 | if(verbose > 1) System.out.println(" " + messageCount + ". Encrypted server Handshake message");
339 | outLog.println(" " + messageCount + ". Encrypted server Handshake message");
340 | } else {
341 | if(verbose > 1) System.out.print(" " + messageCount + ". Server Handshake message: ");
342 | outLog.print(" " + messageCount + ". Server Handshake message: ");
343 | }
344 | handShake = true;
345 | break;
346 | case 23:
347 | if(verbose > 1) System.out.println(" " + messageCount + ". Sending application data to client");
348 | outLog.println(" " + messageCount + ". Sending application data to client");
349 | break;
350 | case 20:
351 | if(verbose > 1) System.out.println(" " + messageCount + ". Server ChangeCipherSpec");
352 | outLog.println(" " + messageCount + ". Server ChangeCipherSpec");
353 | serverCCS = true;
354 | break;
355 | case 21:
356 | if(serverCCS) {
357 | if(verbose > 1) System.out.println(" " + messageCount + ". Server sent an encrypted Alert message");
358 | outLog.print(" " + messageCount + ". Server sent an encrypted Alert message");
359 | } else {
360 | if(verbose > 1) System.out.print(" " + messageCount + ". Server sent an Alert: ");
361 | outLog.print(" " + messageCount + ". Server sent an Alert: ");
362 | }
363 | serverAlert = true;
364 | break;
365 | default:
366 | if(verbose > 1) System.out.println(" " + messageCount + ". Unknown message from client");
367 | outLog.println(" " + messageCount + ". Unknown message from client");
368 | }
369 |
370 | //Caculate length of message excluding header
371 | int length = ((header[3] & 0xff) << 8) | (header[4] & 0xff);
372 | byte[] data = new byte[length];
373 |
374 | //Read in rest of TLS message
375 | for(int i=0; i 0) System.out.print("Fatal ");
383 | outLog.print("Fatal ");
384 | if(verbose > 0) System.out.println(alertMap.get((int)data[1]));
385 | outLog.println(alertMap.get((int)data[1]));
386 | failed = true;
387 | } else if (serverAlert) {
388 | if(verbose > 0) System.out.print("Warning ");
389 | outLog.print("Warning ");
390 | if(verbose > 0) System.out.println(alertMap.get((int)data[1]));
391 | outLog.println(alertMap.get((int)data[1]));
392 | }
393 | }
394 |
395 | //Combine header with rest of message
396 | byte[] forwardMessage = new byte[5 + length];
397 | System.arraycopy(header, 0, forwardMessage, 0, header.length);
398 | System.arraycopy(data, 0, forwardMessage, header.length, data.length);
399 |
400 | //Print handshake message
401 | if(!serverCCS && handShake) {
402 | String messageString = handShakeMap.get((int)forwardMessage[5]);
403 | if(messageString == null) messageString = "UNKNOWN_MESSAGE_TYPE";
404 |
405 | //Log handshake type
406 | if(verbose > 1) System.out.println(messageString);
407 | outLog.println(messageString);
408 |
409 | //Check same certificate is not being served, despite being sent to different address
410 |
411 | if(messageString.equals("CERTIFICATE")){
412 | int index = 12;
413 | int leafCertLength = ((forwardMessage[index++] & 0xff) << 16) | ((forwardMessage[index++] & 0xff) << 8) | (forwardMessage[index++] & 0xff);
414 | if(leafCertLength == getRealLeafCerts().get(this.realHost).length) {
415 | //Check if message is server certificate
416 | for(int i=0; i 1) System.out.println("CERT WARNING: Same certificate as legitimate domain detected, possible SNI in use by hosting providers");
420 | outLog.println("CERT WARNING: Same certificate as legitimate domain detected, possible SNI in use by hosting providers");
421 | }
422 | }
423 | }
424 | index += leafCertLength;
425 | if(forwardMessage.length > index) {
426 | int issuerCertLength = ((forwardMessage[index++] & 0xff) << 16) | ((forwardMessage[index++] & 0xff) << 8) | (forwardMessage[index++] & 0xff);
427 | if(issuerCertLength != getRealIssuerCerts().get(this.realHost).length) {
428 | //Check if message is server certificate
429 | for(int i=0; i 1) System.out.println("CERT WARNING: Chosen redirect domain has different issuer cert.");
432 | outLog.println("CERT WARNING: Chosen redirect domain has different issuer cert.");
433 | break;
434 | }
435 | }
436 | }
437 | }
438 | }
439 | }
440 | clientOutStream.write(forwardMessage);
441 | serverAlert = false;
442 | handShake = false;
443 | } else {
444 | break;
445 | }
446 | }
447 |
448 | if (System.currentTimeMillis() >= timeoutExpiredMs) {
449 | timeout = true;
450 | break;
451 | }
452 |
453 | //Stop listening if handshake failure or application data seen
454 | if(finished || failed) break;
455 | if(failed) break;
456 | }
457 |
458 | if(finished) {System.out.println("HANDSHAKE SUCCEEDED - likely app does not check"
459 | + " hostname of pinned certificate");
460 | outLog.println("HANDSHAKE SUCCEEDED - likely app does not check"
461 | + " hostname of pinned certificate");}
462 | if(failed) {System.out.println("HANDSHAKE FAILED - app does not accept alternate certificate from " + this.forwardHost);
463 | outLog.println("HANDSHAKE FAILED - app does not accept alternate certificate " + this.forwardHost);}
464 | if(timeout) {System.out.println("HANDSHAKE TIMEOUT - likely app does not accept certificate from " + this.forwardHost);
465 | outLog.println("HANDSHAKE TIMEOUT - likely app does not accept certificate from " + this.forwardHost);}
466 |
467 |
468 | } catch (InterruptedException | IOException e) {
469 | if(e instanceof IOException) {
470 | System.out.println(e.getMessage());
471 | e.printStackTrace();
472 | outLog.println(e.getMessage());
473 | }
474 | } finally {
475 | try {
476 | //Close all I/O streams and cut connection
477 | clientInStream.close();
478 | clientOutStream.close();
479 | serverInStream.close();
480 | serverOutStream.close();
481 | clientConnection.close();
482 | serverConnection.close();
483 | } catch (Exception e) {
484 | outLog.println("Failed to close sockets");
485 | }
486 | }
487 | }
488 |
489 | public void setForwardHost(String forwardHost) {
490 | this.forwardHost = forwardHost;
491 | }
492 |
493 | public void setRealHost(String realHost) {
494 | this.realHost = realHost;
495 | }
496 |
497 | private String extractSNI(byte[] data) {
498 |
499 | try {
500 | // 1 byte message type
501 | // 3 bytes length
502 | // 2 bytes version
503 | // 32 random value
504 | int index = 38;
505 | // 1 byte len val to skip
506 | int skipLen = data[index++] & 0xff;
507 | index+=skipLen;
508 | // 2 byte len val to skip
509 | skipLen = ((data[index++] & 0xff) << 8) | (data[index++] & 0xff);
510 | index+=skipLen;
511 | // 1 byte len val to skip
512 | skipLen = data[index++] & 0xff;
513 | index+=skipLen;
514 | // extenssions length
515 | int extLen = ((data[index++] & 0xff) << 8) | (data[index++] & 0xff);
516 | while(index < data.length) {
517 | if(data[index++] == 0 && data[index++] == 0) {
518 | //Extract SNI
519 | int totalSNILen = ((data[index++] & 0xff) << 8) | (data[index++] & 0xff);
520 | int firstSNILen = ((data[index++] & 0xff) << 8) | (data[index++] & 0xff);
521 | //skip type
522 | index++;
523 | int sniLen = ((data[index++] & 0xff) << 8) | (data[index++] & 0xff);
524 | return new String(Arrays.copyOfRange(data, index, index+sniLen));
525 | } else {
526 | skipLen = ((data[index++] & 0xff) << 8) | (data[index++] & 0xff);
527 | index+=skipLen;
528 | }
529 | }
530 |
531 | return null;
532 | } catch(Exception e) {
533 | return null;
534 | }
535 | }
536 |
537 | //Fill the maps with alert and handshake messages specified in RFC5246
538 | private void fillMaps() {
539 |
540 | alertMap.put(0, "CLOSE_NOTIFY");
541 | alertMap.put(10, "UNEXPECTED_MESSAGE");
542 | alertMap.put(20, "BAD_RECORD_MAC");
543 | alertMap.put(21, "DECRYPTION_FAILED");
544 | alertMap.put(22, "RECORD_OVERFLOW");
545 | alertMap.put(30, "DECOMPRESSION_FAILURE");
546 | alertMap.put(40, "HANDSHAKE_FAILURE");
547 | alertMap.put(41, "NO_CERTIFICATE");
548 | alertMap.put(42, "BAD_CERTIFICATE");
549 | alertMap.put(43, "UNSUPPORTED_CERTIFICATE");
550 | alertMap.put(44, "CERTIFICATE_REVOKED");
551 | alertMap.put(45, "CERTIFICATE_EXPIRED");
552 | alertMap.put(46, "CERTIFICATE_UNKNOWN");
553 | alertMap.put(47, "ILLEGAL_PARAMETER");
554 | alertMap.put(48, "UNKNOWN_CA");
555 | alertMap.put(49, "ACCESS_DENIED");
556 | alertMap.put(50, "DECODE_ERROR");
557 | alertMap.put(51, "DECRYPT_ERROR");
558 | alertMap.put(60, "EXPORT_RESTRICTION");
559 | alertMap.put(70, "PROTOCOL_VERSION");
560 | alertMap.put(71, "INSUFFICIENT_SECURITY");
561 | alertMap.put(80, "INTERNAL_ERROR");
562 | alertMap.put(90, "USER_CANCELLED");
563 | alertMap.put(100, "NO_RENEGOTIATION");
564 | alertMap.put(110, "UNSUPPORTED_EXTENSION");
565 |
566 | handShakeMap.put(0, "HELLO_REQUEST");
567 | handShakeMap.put(1, "CLIENT_HELLO");
568 | handShakeMap.put(2, "SERVER_HELLO");
569 | handShakeMap.put(4, "NEW_SESSION_TICKET");
570 | handShakeMap.put(11, "CERTIFICATE");
571 | handShakeMap.put(12, "SERVER_KEY_EXCHANGE");
572 | handShakeMap.put(13, "CERTIFICATE_REQUEST");
573 | handShakeMap.put(14, "SERVER_HELLO_DONE");
574 | handShakeMap.put(15, "CERTIFICATE_VERIFY");
575 | handShakeMap.put(16, "CLIENT_KEY_EXCHANGE");
576 | handShakeMap.put(20, "FINISHED");
577 | handShakeMap.put(22, "CERTIFICATE_STATUS");
578 |
579 | }
580 | }
581 | }
582 |
--------------------------------------------------------------------------------