├── 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 | 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 |
18 |

{{ site.title | default: site.github.repository_name }}

19 |

{{ site.description | default: site.github.project_tagline }}

20 | 21 |

About

22 |

Usage Instructions

23 |

Vulnerabilities

24 |

Publications

25 |

Contact

26 | 27 | {% if site.github.is_user_page %} 28 |

View My GitHub Profile

29 | {% endif %} 30 | 31 | {% if site.show_downloads %} 32 | 37 | {% endif %} 38 |
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 | --------------------------------------------------------------------------------