This is part of a series of posts about setting up Play WS as a TLS client for a "secure by default" setup and configuration through text files, along with the research and thinking behind the setup. I recommend The Most Dangerous Code in the World for more background. And thanks to Jon for the shoutout in Techcrunch.
Previous posts are:
- Fixing The Most Dangerous Code In The World (MITM, Protocols, Cipher Suites, Cert Stores)
- Fixing X.509 Certificates (General PKI, Weak Signature and Key Algorithms)
- Fixing Certificate Revocation (CRL, OCSP)
- Fixing Hostname Verification (HTTPS server identity checks)
The last talked about implementing hostname verification, which was a particular concern in TMDCitW. This post shows how you can test that your TLS client implements hostname verification correctly, by staging an attack. We're going to use dnschef, a DNS proxy server, to confuse the client into talking to the wrong server.
To keep things simple, I'm going to assume you're on Mac OS X Mavericks at this point. (If you're on Linux, this is old hat. If you're on Windows, it's probably easier to use a VM like Virtualbox to set up a Linux environment.)
The first step to installing dnschef is to install a decent Python. The Python Guide suggests Homebrew, and Homebrew requires XCode be installed, so let's start there.
Install XCode
Install XCode from the App Store and also install the command line tools:
xcode-select --install
Install Homebrew itself:
ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)"
Homebrew has some notes about Python, so we set up the command line environment:
export ARCHFLAGS="-arch x86_64"
export PATH=/usr/local/bin:/usr/local/sbin:~/bin:$PATH
Now (if you already have homebrew installed):
brew update
brew install openssl
brew install python --with-brewed-openssl --framework
You should see:
$ python --version
Python 2.7.6
$ which python
/usr/local/bin/python
If you run into trouble, then brew doctor
or brew link --overwrite python
should sort things out.
Now upgrade the various package tools for Python:
pip install --upgrade setuptools
pip install --upgrade pip
Now that we've got Python installed, we can install dnschef:
wget https://thesprawl.org/media/projects/dnschef-0.2.1.tar.gz
tar xvzf dnschef-0.2.1.tar.gz
cd dnschef-0.2.1
Then, we need to use dnschef as a nameserver. An attacker would use rogue DHCP or ARP spoofing to fool your computer into accepting this, but we can just add it directly:
OS X - Open System Preferences and click on the Network icon.
Select the active interface and fill in the DNS Server field. If you are using Airport then you will have to click on Advanced… button and edit DNS servers from there.
Don't forget to click "Apply" after making the changes!
Now, we're going to use DNS to redirect https://www.howsmyssl.com to https://playframework.com.
$ host playframework.com
playframework.com has address 54.243.50.169
We need to specify the IP address 54.243.50.169
as the fakeip argument.
$ sudo /usr/local/bin/python ./dnschef.py --fakedomains www.howsmyssl.com --fakeip 54.243.50.169
_ _ __
| | version 0.2 | | / _|
__| |_ __ ___ ___| |__ ___| |_
/ _` | '_ \/ __|/ __| '_ \ / _ \ _|
| (_| | | | \__ \ (__| | | | __/ |
\__,_|_| |_|___/\___|_| |_|\___|_|
[email protected]
[*] DNSChef started on interface: 127.0.0.1
[*] Using the following nameservers: 8.8.8.8
[*] Cooking A replies to point to 54.243.50.169 matching: www.howsmyssl.com
Now that we've got dnschef working as a proxy, we can see whether various TLS clients notice that www.howsmyssl.com has started returning an X.509 certificate that says it came from "playframework.com":
$ curl https://www.howsmyssl.com/
curl: (60) SSL certificate problem: Invalid certificate chain
More details here: http://curl.haxx.se/docs/sslcerts.html
Curl is not fooled. It knows the subjectAltName.dnsName
is different.
Let's try Play WS:
[ssltest] $ testOnly HowsMySSLSpec
[info] Compiling 1 Scala source to /Users/wsargent/work/ssltest/target/scala-2.10/test-classes...
Mar 31, 2014 6:11:08 PM org.jboss.netty.channel.DefaultChannelFuture
WARNING: An exception was thrown by ChannelFutureListener.
java.net.ConnectException: HostnameVerifier exception.
at com.ning.http.client.providers.netty.NettyConnectListener.operationComplete(NettyConnectListener.java:81)
at org.jboss.netty.channel.DefaultChannelFuture.notifyListener(DefaultChannelFuture.java:427)
at org.jboss.netty.channel.DefaultChannelFuture.notifyListeners(DefaultChannelFuture.java:413)
at org.jboss.netty.channel.DefaultChannelFuture.setSuccess(DefaultChannelFuture.java:362)
at org.jboss.netty.handler.ssl.SslHandler.setHandshakeSuccess(SslHandler.java:1383)
at org.jboss.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1252)
at org.jboss.netty.handler.ssl.SslHandler.decode(SslHandler.java:913)
at org.jboss.netty.handler.codec.frame.FrameDecoder.callDecode(FrameDecoder.java:425)
at org.jboss.netty.handler.codec.frame.FrameDecoder.messageReceived(FrameDecoder.java:303)
at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:268)
at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:255)
at org.jboss.netty.channel.socket.nio.NioWorker.read(NioWorker.java:88)
at org.jboss.netty.channel.socket.nio.AbstractNioWorker.process(AbstractNioWorker.java:109)
at org.jboss.netty.channel.socket.nio.AbstractNioSelector.run(AbstractNioSelector.java:312)
at org.jboss.netty.channel.socket.nio.AbstractNioWorker.run(AbstractNioWorker.java:90)
at org.jboss.netty.channel.socket.nio.NioWorker.run(NioWorker.java:178)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:744)
[info] HowsMySSLSpec
[info]
[info] WS should
[info] + NOT be fooled by dnschef
[info]
[info] Total for specification HowsMySSLSpec
[info] Finished in 21 seconds, 162 ms
[info] 1 example, 0 failure, 0 error
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 25 s, completed Mar 31, 2014 6:11:26 PM]
Yep, it throws an exception.
Now let's try it with hostname verification off by setting the 'loose' option on the client:
class HowsMySSLSpec extends PlaySpecification with ClientMethods {
val timeout: Timeout = 20.seconds
"WS" should {
"be fooled by dnschef" in {
val rawConfig = play.api.Configuration(ConfigFactory.parseString(
"""
|ws.ssl.loose.disableHostnameVerification=true
""".stripMargin))
val client = createClient(rawConfig)
val response = await(client.url("https://www.howsmyssl.com").get())(timeout)
response.status must be_==(200)
response.body must contain("Play Framework")
}
}
}
Run the test:
[ssltest] $ testOnly HowsMySSLSpec
[info] HowsMySSLSpec
[info]
[info] WS should
[info] + be fooled by dnschef
[info]
[info] Total for specification HowsMySSLSpec
[info] Finished in 9 seconds, 675 ms
[info] 1 example, 0 failure, 0 error
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 12 s, completed Mar 31, 2014 6:08:50 PM
It works! We have fooled WS into setting up a TLS connection with a different host, one that we have control over. If we were evil, we could then proxy https://playframework.com to the intended URL, and save off all the content or inject fake data.
Let's try Apache HttpClient 3.x:
name := "httpclienttest"
version := "1.0-SNAPSHOT"
libraryDependencies ++= Seq(
"commons-httpclient" % "commons-httpclient" % "3.1",
"org.specs2" %% "specs2" % "2.3.10" % "test"
)
scalacOptions in Test ++= Seq("-Yrangepos")
resolvers ++= Seq("snapshots", "releases").map(Resolver.sonatypeRepo)
import org.apache.commons.httpclient.HttpClient
import org.apache.commons.httpclient.methods.GetMethod
import org.specs2.mutable.Specification
class HttpClientSpec extends Specification {
"HTTPClient" should {
"do something" in {
val httpclient = new HttpClient()
val httpget = new GetMethod("https://www.howsmyssl.com/")
try {
httpclient.executeMethod(httpget)
//val line = httpget.getResponseBodyAsString
//line must not contain("Play Framework")
httpget.getStatusCode must not be_==(200)
} finally {
httpget.releaseConnection()
}
}
}
}
Running this gives:
[info] HttpClientSpec
[info]
[info] HTTPClient should
[info] x do something
[error] '200' is equal to '200' (HttpClientSpec.scala:14)
[info]
[info]
[info] Total for specification HttpClientSpec
[info] Finished in 18 ms
[info] 1 example, 1 failure, 0 error
[error] Failed: Total 1, Failed 1, Errors 0, Passed 0
[error] Failed tests:
[error] HttpClientSpec
[error] (test:test) sbt.TestsFailedException: Tests unsuccessful
[error] Total time: 4 s, completed Mar 31, 2014 8:26:46 PM
Nope. HttpClient 3.x was retired in 2007, but any code that's still using it under the hood is vulnerable to this attack.
Try this on your own code and see what it does. I'll bet it'll be interesting.
Next
Odds and ends I couldn't cover elsewhere. And then best practices, and summing things up.
Comments