-- cgit 1.4.1 From 98e81c2c0edd3d9bb483000d598e07e6dd9da6b0 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 11 Feb 2017 12:27:12 +0100 Subject: feat: Initial working implementation --- main.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ main_test.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ urls.go | 19 ++++++++++++ 3 files changed, 205 insertions(+) create mode 100644 main.go create mode 100644 main_test.go create mode 100644 urls.go diff --git a/main.go b/main.go new file mode 100644 index 0000000000..11f27260e3 --- /dev/null +++ b/main.go @@ -0,0 +1,90 @@ +package main + +import ( + "bufio" + "encoding/xml" + "fmt" + "net/http" + "os" + "strings" +) + +// 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) != 3 { + fmt.Fprintf(os.Stderr, "Usage: watchblob \n") + os.Exit(1) + } + + host := args[0] + username := args[1] + password := args[2] + + 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.Println("Login succeeded, you may now (quickly) authenticate OpenVPN with %s as your password", token) +} + +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) { + fmt.Println(url) + 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/main_test.go b/main_test.go new file mode 100644 index 0000000000..5c171c4041 --- /dev/null +++ b/main_test.go @@ -0,0 +1,96 @@ +package main + +import ( + "encoding/xml" + "reflect" + "testing" +) + +func TestUnmarhshalChallengeRespones(t *testing.T) { + var testXml string = ` + + + sslvpn_logon + 4 + + + RADIUS + + + 441 + Enter Your 6 Digit Passcode +` + + 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 = ` + + + sslvpn_logon + 2 + + + RADIUS + + + 501 +` + + 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 = ` + + + sslvpn_logon + 1 + + + RADIUS + + + +` + 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/urls.go b/urls.go new file mode 100644 index 0000000000..a1fd825f57 --- /dev/null +++ b/urls.go @@ -0,0 +1,19 @@ +package main + +import "fmt" + +const urlFormat string = "https://%s%s" +const triggerChallengeUri = "/?action=sslvpn_logon&fw_username=%s&fw_password=%s&style=fw_logon_progress.xsl&fw_logon_type=logon&fw_domain=Firebox-DB" +const responseUri = "/?action=sslvpn_logon&style=fw_logon_progress.xsl&fw_logon_type=response&response=%s&fw_logon_id=%d" + +func templateChallengeTriggerUri(username *string, password *string) string { + return fmt.Sprintf(triggerChallengeUri, *username, *password) +} + +func templateResponseUri(logonId int, token *string) string { + return fmt.Sprintf(responseUri, *token, logonId) +} + +func templateUrl(baseUrl *string, uri string) string { + return fmt.Sprintf("https://%s%s", *baseUrl, uri) +} -- cgit 1.4.1 From 01ad38d5320e0b6d2f27d6f0c7b44f82be1887d6 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 11 Feb 2017 13:17:55 +0100 Subject: docs: Add README --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000000..95e53115dc --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +Watchblob - WatchGuard VPN on Linux +=================================== + +This tiny helper tool makes it possible to use WatchGuard / Firebox / <> 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. A writeup of the protocol and the security implications +will be linked here in the future. + +## 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. -- cgit 1.4.1 From e6c3212018b6bcbe4a83c3e7c557fc43b0e7571d Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 11 Feb 2017 16:19:24 +0100 Subject: chore: Don't print URLs --- .gitignore | 1 + main.go | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..62c893550a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ \ No newline at end of file diff --git a/main.go b/main.go index 11f27260e3..b556d606dd 100644 --- a/main.go +++ b/main.go @@ -46,7 +46,7 @@ func main() { os.Exit(1) } - fmt.Println("Login succeeded, you may now (quickly) authenticate OpenVPN with %s as your password", token) + fmt.Printf("Login succeeded, you may now (quickly) authenticate OpenVPN with %d as your password\n", token) } func triggerChallengeResponse(host *string, username *string, password *string) (r Resp, err error) { @@ -76,7 +76,6 @@ func logon(host *string, challenge *Resp, token *string) (err error) { } func request(url string) (r Resp, err error) { - fmt.Println(url) resp, err := http.Get(url) if err != nil { return -- cgit 1.4.1 From 393cff48479e3cb065c00fc542c353c5ac29f0af Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 11 Feb 2017 16:30:23 +0100 Subject: feat: Don't echo password while inputting --- README.md | 2 +- main.go | 28 +++++++++++++++++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 95e53115dc..1846a8beab 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ will be linked here in the future. ## Installation Make sure you have Go installed and `GOPATH` configured, then simply -`go get github.com/tazjin/watchblob`. +`go get github.com/tazjin/watchblob/...`. ## Usage diff --git a/main.go b/main.go index b556d606dd..93a85324f1 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "net/http" "os" "strings" + "golang.org/x/crypto/ssh/terminal" ) // The XML response returned by the WatchGuard server @@ -21,16 +22,20 @@ type Resp struct { func main() { args := os.Args[1:] - if len(args) != 3 { - fmt.Fprintf(os.Stderr, "Usage: watchblob \n") + if len(args) != 1 { + fmt.Fprintln(os.Stderr, "Usage: watchblob ") os.Exit(1) } host := args[0] - username := args[1] - password := args[2] - challenge, err := triggerChallengeResponse(&host, &username, &password) + username, password, err := readCredentials() + if err != nil { + fmt.Fprintf(os.Stderr, "Could not read credentials: %v\n", err) + } + + fmt.Println("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") @@ -49,6 +54,19 @@ func main() { fmt.Printf("Login succeeded, you may now (quickly) authenticate OpenVPN with %d 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: ") + passwordBytes, err := terminal.ReadPassword(1) + password := string(passwordBytes) + + // If an error occured, I don't care about which one it is. + return &username, &password, err +} + func triggerChallengeResponse(host *string, username *string, password *string) (r Resp, err error) { return request(templateUrl(host, templateChallengeTriggerUri(username, password))) } -- cgit 1.4.1 From 4a85116b4a04765e13a0aa7c8b8a37778ef8dcbd Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 11 Feb 2017 16:32:36 +0100 Subject: fix: Remove trailing newlines from input --- main.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 93a85324f1..9ac5598b17 100644 --- a/main.go +++ b/main.go @@ -34,8 +34,8 @@ func main() { fmt.Fprintf(os.Stderr, "Could not read credentials: %v\n", err) } - fmt.Println("Requesting challenge from %s as user %s\n", host, *username) - challenge, err := triggerChallengeResponse(&host, username, password) + fmt.Println("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") @@ -54,17 +54,16 @@ func main() { fmt.Printf("Login succeeded, you may now (quickly) authenticate OpenVPN with %d as your password\n", token) } -func readCredentials() (*string, *string, error) { +func readCredentials() (string, string, error) { fmt.Printf("Username: ") reader := bufio.NewReader(os.Stdin) username, err := reader.ReadString('\n') fmt.Printf("Password: ") - passwordBytes, err := terminal.ReadPassword(1) - password := string(passwordBytes) + password, err := terminal.ReadPassword(1) // If an error occured, I don't care about which one it is. - return &username, &password, err + return strings.TrimSpace(username), strings.TrimSpace(string(password)), err } func triggerChallengeResponse(host *string, username *string, password *string) (r Resp, err error) { -- cgit 1.4.1 From e4ee5a452622de946e4b9a62aeedb6eb3a6851dd Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 11 Feb 2017 16:33:16 +0100 Subject: fix: Portability of stdin --- main.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 9ac5598b17..ec0f55f429 100644 --- a/main.go +++ b/main.go @@ -4,10 +4,11 @@ import ( "bufio" "encoding/xml" "fmt" + "golang.org/x/crypto/ssh/terminal" "net/http" "os" "strings" - "golang.org/x/crypto/ssh/terminal" + "syscall" ) // The XML response returned by the WatchGuard server @@ -60,7 +61,7 @@ func readCredentials() (string, string, error) { username, err := reader.ReadString('\n') fmt.Printf("Password: ") - password, err := terminal.ReadPassword(1) + password, err := terminal.ReadPassword(syscall.Stdin) // If an error occured, I don't care about which one it is. return strings.TrimSpace(username), strings.TrimSpace(string(password)), err -- cgit 1.4.1 From dd1e6c3b36d4b055af879fc1a0f6febecae35591 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 11 Feb 2017 16:38:23 +0100 Subject: fix: Two minor, silly fixes --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index ec0f55f429..fd74e8b457 100644 --- a/main.go +++ b/main.go @@ -35,7 +35,7 @@ func main() { fmt.Fprintf(os.Stderr, "Could not read credentials: %v\n", err) } - fmt.Println("Requesting challenge from %s as user %s\n", host, username) + fmt.Printf("Requesting challenge from %s as user %s\n", host, username) challenge, err := triggerChallengeResponse(&host, &username, &password) if err != nil || challenge.LogonStatus != 4 { @@ -52,7 +52,7 @@ func main() { os.Exit(1) } - fmt.Printf("Login succeeded, you may now (quickly) authenticate OpenVPN with %d as your password\n", token) + fmt.Printf("Login succeeded, you may now (quickly) authenticate OpenVPN with %s as your password\n", token) } func readCredentials() (string, string, error) { -- cgit 1.4.1 From 7824e0e7e34a4b5245f817fb70da53d4bcd707c7 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 11 Feb 2017 17:34:09 +0100 Subject: docs: Add blog post to README --- README.md | 6 ++++-- main_test.go | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1846a8beab..712c96cd95 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,10 @@ 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. A writeup of the protocol and the security implications -will be linked here in the future. +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 diff --git a/main_test.go b/main_test.go index 5c171c4041..1af52d0cd4 100644 --- a/main_test.go +++ b/main_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -func TestUnmarhshalChallengeRespones(t *testing.T) { +func TestUnmarshalChallengeRespones(t *testing.T) { var testXml string = ` -- cgit 1.4.1 From 6dcb0f4b2bddc583212995397d6c6e1ec14adc58 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Mon, 13 Feb 2017 09:55:24 +0100 Subject: fix urls: Escape values in URLs For usernames and passwords containing special characters the URL parameters must be escaped. Because the entire URI is just query parameters I've opted for using net/url.Values for the entire URI. Fixes #1 --- main.go | 1 + urls.go | 30 ++++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index fd74e8b457..a7ab65d3d1 100644 --- a/main.go +++ b/main.go @@ -62,6 +62,7 @@ func readCredentials() (string, string, error) { 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 diff --git a/urls.go b/urls.go index a1fd825f57..37f65e0fae 100644 --- a/urls.go +++ b/urls.go @@ -1,19 +1,37 @@ package main -import "fmt" +import ( + "fmt" + "net/url" + "strconv" +) const urlFormat string = "https://%s%s" -const triggerChallengeUri = "/?action=sslvpn_logon&fw_username=%s&fw_password=%s&style=fw_logon_progress.xsl&fw_logon_type=logon&fw_domain=Firebox-DB" -const responseUri = "/?action=sslvpn_logon&style=fw_logon_progress.xsl&fw_logon_type=response&response=%s&fw_logon_id=%d" +const uriFormat = "/?%s" func templateChallengeTriggerUri(username *string, password *string) string { - return fmt.Sprintf(triggerChallengeUri, *username, *password) + 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 { - return fmt.Sprintf(responseUri, *token, logonId) + 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("https://%s%s", *baseUrl, uri) + return fmt.Sprintf(urlFormat, *baseUrl, uri) } -- cgit 1.4.1 From 24b075bdeba5457a96965ceb705ffc8d57f03388 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sat, 21 Dec 2019 01:11:29 +0000 Subject: chore(watchblob): Prepare for depot merge --- .gitignore | 1 - README.md | 35 --------------- fun/watchblob/README.md | 35 +++++++++++++++ fun/watchblob/main.go | 108 +++++++++++++++++++++++++++++++++++++++++++++ fun/watchblob/main_test.go | 96 ++++++++++++++++++++++++++++++++++++++++ fun/watchblob/urls.go | 37 ++++++++++++++++ main.go | 108 --------------------------------------------- main_test.go | 96 ---------------------------------------- urls.go | 37 ---------------- 9 files changed, 276 insertions(+), 277 deletions(-) delete mode 100644 .gitignore delete mode 100644 README.md create mode 100644 fun/watchblob/README.md create mode 100644 fun/watchblob/main.go create mode 100644 fun/watchblob/main_test.go create mode 100644 fun/watchblob/urls.go delete mode 100644 main.go delete mode 100644 main_test.go delete mode 100644 urls.go diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 62c893550a..0000000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.idea/ \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 712c96cd95..0000000000 --- a/README.md +++ /dev/null @@ -1,35 +0,0 @@ -Watchblob - WatchGuard VPN on Linux -=================================== - -This tiny helper tool makes it possible to use WatchGuard / Firebox / <> 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/README.md b/fun/watchblob/README.md new file mode 100644 index 0000000000..712c96cd95 --- /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 / <> 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 0000000000..a7ab65d3d1 --- /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 ") + 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 0000000000..1af52d0cd4 --- /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 = ` + + + sslvpn_logon + 4 + + + RADIUS + + + 441 + Enter Your 6 Digit Passcode +` + + 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 = ` + + + sslvpn_logon + 2 + + + RADIUS + + + 501 +` + + 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 = ` + + + sslvpn_logon + 1 + + + RADIUS + + + +` + 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 0000000000..37f65e0fae --- /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) +} diff --git a/main.go b/main.go deleted file mode 100644 index a7ab65d3d1..0000000000 --- a/main.go +++ /dev/null @@ -1,108 +0,0 @@ -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 ") - 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/main_test.go b/main_test.go deleted file mode 100644 index 1af52d0cd4..0000000000 --- a/main_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package main - -import ( - "encoding/xml" - "reflect" - "testing" -) - -func TestUnmarshalChallengeRespones(t *testing.T) { - var testXml string = ` - - - sslvpn_logon - 4 - - - RADIUS - - - 441 - Enter Your 6 Digit Passcode -` - - 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 = ` - - - sslvpn_logon - 2 - - - RADIUS - - - 501 -` - - 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 = ` - - - sslvpn_logon - 1 - - - RADIUS - - - -` - 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/urls.go b/urls.go deleted file mode 100644 index 37f65e0fae..0000000000 --- a/urls.go +++ /dev/null @@ -1,37 +0,0 @@ -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) -} -- cgit 1.4.1