diff options
Diffstat (limited to 'fun')
-rw-r--r-- | fun/watchblob/README.md | 35 | ||||
-rw-r--r-- | fun/watchblob/main.go | 108 | ||||
-rw-r--r-- | fun/watchblob/main_test.go | 96 | ||||
-rw-r--r-- | fun/watchblob/urls.go | 37 |
4 files changed, 276 insertions, 0 deletions
diff --git a/fun/watchblob/README.md b/fun/watchblob/README.md new file mode 100644 index 000000000000..712c96cd95d4 --- /dev/null +++ b/fun/watchblob/README.md @@ -0,0 +1,35 @@ +Watchblob - WatchGuard VPN on Linux +=================================== + +This tiny helper tool makes it possible to use WatchGuard / Firebox / <<whatever +they are actually called>> VPNs that use multi-factor authentication on Linux. + +Rather than using OpenVPN's built-in dynamic challenge/response protocol, WatchGuard +has opted for a separate implementation negotiating credentials outside of the +OpenVPN protocol, which makes it impossible to start those connections solely by +using the `openvpn` CLI and configuration files. + +What this application does has been reverse-engineered from the "WatchGuard Mobile VPN +with SSL" application on OS X. + +I've published a [blog post](https://www.tazj.in/en/1486830338) describing the process +and what is actually going on in this protocol. + +## Installation + +Make sure you have Go installed and `GOPATH` configured, then simply +`go get github.com/tazjin/watchblob/...`. + +## Usage + +Right now the usage is very simple. Make sure you have the correct OpenVPN client +config ready (this is normally supplied by the WatchGuard UI) simply run: + +``` +watchblob vpnserver.somedomain.org username p4ssw0rd +``` + +The server responds with a challenge which is displayed to the user, wait until you +receive the SMS code or whatever and enter it. `watchblob` then completes the +credential negotiation and you may proceed to log in with OpenVPN using your username +and *the OTP token* (**not** your password) as credentials. diff --git a/fun/watchblob/main.go b/fun/watchblob/main.go new file mode 100644 index 000000000000..a7ab65d3d196 --- /dev/null +++ b/fun/watchblob/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "bufio" + "encoding/xml" + "fmt" + "golang.org/x/crypto/ssh/terminal" + "net/http" + "os" + "strings" + "syscall" +) + +// The XML response returned by the WatchGuard server +type Resp struct { + Action string `xml:"action"` + LogonStatus int `xml:"logon_status"` + LogonId int `xml:"logon_id"` + Error string `xml:"errStr"` + Challenge string `xml:"chaStr"` +} + +func main() { + args := os.Args[1:] + + if len(args) != 1 { + fmt.Fprintln(os.Stderr, "Usage: watchblob <vpn-host>") + os.Exit(1) + } + + host := args[0] + + username, password, err := readCredentials() + if err != nil { + fmt.Fprintf(os.Stderr, "Could not read credentials: %v\n", err) + } + + fmt.Printf("Requesting challenge from %s as user %s\n", host, username) + challenge, err := triggerChallengeResponse(&host, &username, &password) + + if err != nil || challenge.LogonStatus != 4 { + fmt.Fprintln(os.Stderr, "Did not receive challenge from server") + fmt.Fprintf(os.Stderr, "Response: %v\nError: %v\n", challenge, err) + os.Exit(1) + } + + token := getToken(&challenge) + err = logon(&host, &challenge, &token) + + if err != nil { + fmt.Fprintf(os.Stderr, "Logon failed: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Login succeeded, you may now (quickly) authenticate OpenVPN with %s as your password\n", token) +} + +func readCredentials() (string, string, error) { + fmt.Printf("Username: ") + reader := bufio.NewReader(os.Stdin) + username, err := reader.ReadString('\n') + + fmt.Printf("Password: ") + password, err := terminal.ReadPassword(syscall.Stdin) + fmt.Println() + + // If an error occured, I don't care about which one it is. + return strings.TrimSpace(username), strings.TrimSpace(string(password)), err +} + +func triggerChallengeResponse(host *string, username *string, password *string) (r Resp, err error) { + return request(templateUrl(host, templateChallengeTriggerUri(username, password))) +} + +func getToken(challenge *Resp) string { + fmt.Println(challenge.Challenge) + + reader := bufio.NewReader(os.Stdin) + token, _ := reader.ReadString('\n') + + return strings.TrimSpace(token) +} + +func logon(host *string, challenge *Resp, token *string) (err error) { + resp, err := request(templateUrl(host, templateResponseUri(challenge.LogonId, token))) + if err != nil { + return + } + + if resp.LogonStatus != 1 { + err = fmt.Errorf("Challenge/response authentication failed: %v", resp) + } + + return +} + +func request(url string) (r Resp, err error) { + resp, err := http.Get(url) + if err != nil { + return + } + + defer resp.Body.Close() + decoder := xml.NewDecoder(resp.Body) + + err = decoder.Decode(&r) + return +} diff --git a/fun/watchblob/main_test.go b/fun/watchblob/main_test.go new file mode 100644 index 000000000000..1af52d0cd4eb --- /dev/null +++ b/fun/watchblob/main_test.go @@ -0,0 +1,96 @@ +package main + +import ( + "encoding/xml" + "reflect" + "testing" +) + +func TestUnmarshalChallengeRespones(t *testing.T) { + var testXml string = ` +<?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>` + + var r Resp + xml.Unmarshal([]byte(testXml), &r) + + expected := Resp{ + Action: "sslvpn_logon", + LogonStatus: 4, + LogonId: 441, + Challenge: "Enter Your 6 Digit Passcode ", + } + + assertEqual(t, expected, r) +} + +func TestUnmarshalLoginError(t *testing.T) { + var testXml string = ` +<?xml version="1.0" encoding="UTF-8"?> +<resp> + <action>sslvpn_logon</action> + <logon_status>2</logon_status> + <auth-domain-list> + <auth-domain> + <name>RADIUS</name> + </auth-domain> + </auth-domain-list> + <errStr>501</errStr> +</resp>` + + var r Resp + xml.Unmarshal([]byte(testXml), &r) + + expected := Resp{ + Action: "sslvpn_logon", + LogonStatus: 2, + Error: "501", + } + + assertEqual(t, expected, r) +} + +func TestUnmarshalLoginSuccess(t *testing.T) { + var testXml string = ` +<?xml version="1.0" encoding="UTF-8"?> +<resp> + <action>sslvpn_logon</action> + <logon_status>1</logon_status> + <auth-domain-list> + <auth-domain> + <name>RADIUS</name> + </auth-domain> + </auth-domain-list> +</resp> +` + var r Resp + xml.Unmarshal([]byte(testXml), &r) + + expected := Resp{ + Action: "sslvpn_logon", + LogonStatus: 1, + } + + assertEqual(t, expected, r) +} + +func assertEqual(t *testing.T, expected interface{}, result interface{}) { + if !reflect.DeepEqual(expected, result) { + t.Errorf( + "Unmarshaled values did not match.\nExpected: %v\nResult: %v\n", + expected, result, + ) + + t.Fail() + } +} diff --git a/fun/watchblob/urls.go b/fun/watchblob/urls.go new file mode 100644 index 000000000000..37f65e0faef4 --- /dev/null +++ b/fun/watchblob/urls.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + "net/url" + "strconv" +) + +const urlFormat string = "https://%s%s" +const uriFormat = "/?%s" + +func templateChallengeTriggerUri(username *string, password *string) string { + v := url.Values{} + v.Set("action", "sslvpn_logon") + v.Set("style", "fw_logon_progress.xsl") + v.Set("fw_logon_type", "logon") + v.Set("fw_domain", "Firebox-DB") + v.Set("fw_username", *username) + v.Set("fw_password", *password) + + return fmt.Sprintf(uriFormat, v.Encode()) +} + +func templateResponseUri(logonId int, token *string) string { + v := url.Values{} + v.Set("action", "sslvpn_logon") + v.Set("style", "fw_logon_progress.xsl") + v.Set("fw_logon_type", "response") + v.Set("response", *token) + v.Set("fw_logon_id", strconv.Itoa(logonId)) + + return fmt.Sprintf(uriFormat, v.Encode()) +} + +func templateUrl(baseUrl *string, uri string) string { + return fmt.Sprintf(urlFormat, *baseUrl, uri) +} |