about summary refs log tree commit diff
path: root/users/tazjin/blog/posts/reversing-watchguard-vpn.md
blob: 8968dc8645902ea3d992955b5588ad07c3f862dd (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
TIP: WatchGuard has
[responded](https://www.reddit.com/r/netsec/comments/5tg0f9/reverseengineering_watchguard_mobile_vpn/dds6knx/)
to this post on Reddit. If you haven\'t read the post yet I\'d recommend
doing that first before reading the response to have the proper context.

------------------------------------------------------------------------

One of my current clients makes use of
[WatchGuard](http://www.watchguard.com/help/docs/fireware/11/en-US/Content/en-US/mvpn/ssl/mvpn_ssl_client-install_c.html)
Mobile VPN software to provide access to the internal network.

Currently WatchGuard only provides clients for OS X and Windows, neither
of which I am very fond of. In addition an OpenVPN configuration file is
provided, but it quickly turned out that this was only a piece of the
puzzle.

The problem is that this VPN setup is secured using 2-factor
authentication (good!), but it does not use OpenVPN's default
[challenge/response](https://openvpn.net/index.php/open-source/documentation/miscellaneous/79-management-interface.html)
functionality to negotiate the credentials.

Connecting with the OpenVPN config that the website supplied caused the
VPN server to send me a token to my phone, but I simply couldn't figure
out how to supply it back to the server. In a normal challenge/response
setting the token would be supplied as the password on the second
authentication round, but the VPN server kept rejecting that.

Other possibilities were various combinations of username&password
(I've seen a lot of those around) so I tried a whole bunch, for example
`$password:$token` or even a `sha1(password, token)` - to no avail.

At this point it was time to crank out
[Hopper](https://www.hopperapp.com/) and see what's actually going on
in the official OS X client - which uses OpenVPN under the hood!

Diving into the client
----------------------

The first surprise came up right after opening the executable: It had
debug symbols in it - and was written in Objective-C!

![Debug symbols](/static/img/watchblob_1.webp)

A good first step when looking at an application binary is going through
the strings that are included in it, and the WatchGuard client had a lot
to offer. Among the most interesting were a bunch of URIs that looked
important:

![Some URIs](/static/img/watchblob_2.webp)

I started with the first one

    %@?action=sslvpn_download&filename=%@&fw_password=%@&fw_username=%@

and just curled it on the VPN host, replacing the username and
password fields with bogus data and the filename field with
`client.wgssl` - another string in the executable that looked like a
filename.

To my surprise this endpoint immediately responded with a GZIPed file
containing the OpenVPN config, CA certificate, and the client
*certificate and key*, which I previously thought was only accessible
after logging in to the web UI - oh well.

The next endpoint I tried ended up being a bit more interesting still:

    /?action=sslvpn_logon&fw_username=%@&fw_password=%@&style=fw_logon_progress.xsl&fw_logon_type=logon&fw_domain=Firebox-DB

Inserting the correct username and password into the query parameters
actually triggered the process that sent a token to my phone. The
response was a simple XML blob:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<resp>
  <action>sslvpn_logon</action>
  <logon_status>4</logon_status>
  <auth-domain-list>
    <auth-domain>
      <name>RADIUS</name>
    </auth-domain>
  </auth-domain-list>
  <logon_id>441</logon_id>
  <chaStr>Enter Your 6 Digit Passcode </chaStr>
</resp>
```

Somewhat unsurprisingly that `chaStr` field is actually the challenge
string displayed in the client when logging in.

This was obviously going in the right direction so I proceeded to the
procedures making use of this string. The first step was a relatively
uninteresting function called `-[VPNController sslvpnLogon]` which
formatted the URL, opened it and checked whether the `logon_status` was
`4` before proceeding with the `logon_id` and `chaStr` contained in the
response.

*(Code snippets from here on are Hopper's pseudo-Objective-C)*

![sslvpnLogon](/static/img/watchblob_3.webp)

It proceeded to the function `-[VPNController processTokenPrompt]` which
showed the dialog window into which the user enters the token, sent it
off to the next URL and checked the `logon_status` again:

(`r12` is the reference to the `VPNController` instance, i.e. `self`).

![processTokenPrompt](/static/img/watchblob_4.webp)

If the `logon_status` was `1` (apparently \"success\" here) it proceeded
to do something quite interesting:

![processTokenPrompt2](/static/img/watchblob_5.webp)

The user's password was overwritten with the (verified) OTP token -
before OpenVPN had even been started!

Reading a bit more of the code in the subsequent
`-[VPNController doLogin]` method revealed that it shelled out to
`openvpn` and enabled the management socket, which makes it possible to
remotely control an `openvpn` process by sending it commands over TCP.

It then simply sent the username and the OTP token as the credentials
after configuring OpenVPN with the correct config file:

![doLogin](/static/img/watchblob_6.webp)

... and the OpenVPN connection then succeeds.

TL;DR
-----

Rather than using OpenVPN's built-in challenge/response mechanism, the
WatchGuard client validates user credentials *outside* of the VPN
connection protocol and then passes on the OTP token, which seems to be
temporarily in a 'blessed' state after verification, as the user's
password.

I didn't check to see how much verification of this token is performed
(does it check the source IP against the IP that performed the challenge
validation?), but this certainly seems like a bit of a security issue -
considering that an attacker on the same network would, if they time the
attack right, only need your username and 6-digit OTP token to
authenticate.

Don't roll your own security, folks!

Bonus
-----

The whole reason why I set out to do this is so I could connect to this
VPN from Linux, so this blog post wouldn't be complete without a
solution for that.

To make this process really easy I've written a [little
tool](https://github.com/tazjin/watchblob) that performs the steps
mentioned above from the CLI and lets users know when they can
authenticate using their OTP token.