From acad194101b4c49a06f1380a46bb951758290b37 Mon Sep 17 00:00:00 2001 From: Daniel Lenski Date: Mon, 8 Feb 2021 12:43:07 -0800 Subject: [PATCH] F5: implement f5_obtain_cookie Like Fortinet, F5's authentication interface is very Javascript-heavy. Unlike with Fortinet, I didn't even both to try to parse and "follow" HTML forms. For now, we just create a static form (with username and password fields), ask the user to fill it out, submit it, and attempt to detect successful login by a length-0 response including F5_ST and MRHSession cookies. The F5_ST cookie appears to be a reliable(?) indicator of successful login. It's a "session timeout" cookie (https://support.f5.com/csp/article/K15387), which looks like '1z1z1z1612808487z604800'. The 4th field appears to be the Unix epoch timestamp of session activation, and the 5th appears to be the duration in seconds until it expires. Signed-off-by: Daniel Lenski --- f5.c | 110 ++++++++++++++++++++++++++++++++++++++++++++++- test-f5-login.py | 12 +++++- 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/f5.c b/f5.c index 213691c7..cf12ffcb 100644 --- a/f5.c +++ b/f5.c @@ -35,7 +35,102 @@ int f5_obtain_cookie(struct openconnect_info *vpninfo) { - return -EINVAL; + int ret; + struct oc_text_buf *resp_buf = NULL; + char *form_buf = NULL; + struct oc_auth_form *form = NULL; + struct oc_form_opt *opt, *opt2; + + resp_buf = buf_alloc(); + if ((ret = buf_error(resp_buf))) + goto out; + + /* XX: Is this initial GET / (to populate LastMRH_Session and MRHSession + * cookies) actually necessary? + */ + ret = do_https_request(vpninfo, "GET", NULL, NULL, &form_buf, 1); + free(form_buf); + form_buf = NULL; + if (ret < 0) + return ret; + + /* XX: Is this second GET /my.policy (to update MRHSession cookie) + * also necessary? + */ + free(vpninfo->urlpath); + if (!(vpninfo->urlpath = strdup("my.policy"))) { + nomem: + ret = -ENOMEM; + goto out; + } + ret = do_https_request(vpninfo, "GET", NULL, NULL, &form_buf, 1); + free(form_buf); + form_buf = NULL; + if (ret < 0) + return ret; + + /* XX: build static form (username and password) */ + form = calloc(1, sizeof(*form)); + if (!form) + goto nomem; + opt = form->opts = calloc(1, sizeof(*opt)); + if (!opt) + goto nomem; + opt->label = strdup("Username: "); + opt->name = strdup("username"); + opt->type = OC_FORM_OPT_TEXT; + + opt2 = opt->next = calloc(1, sizeof(*opt2)); + if (!opt2) + goto nomem; + opt2->label = strdup("Password: "); + opt2->name = strdup("password"); + opt2->type = OC_FORM_OPT_PASSWORD; + + /* XX: submit form repeatedly until success? */ + for (;;) { + ret = process_auth_form(vpninfo, form); + if (ret == OC_FORM_RESULT_CANCELLED || ret < 0) + goto out; + + buf_truncate(resp_buf); + append_form_opts(vpninfo, form, resp_buf); + if ((ret = buf_error(resp_buf))) + goto out; + do_https_request(vpninfo, "POST", "application/x-www-form-urlencoded", + resp_buf, &form_buf, 0); + + /* XX: if this worked, we should have a response size of zero, and F5_ST + * and MRHSession cookies in the response. + */ + if (!form_buf || *form_buf == '\0') { + struct oc_vpn_option *cookie; + const char *session=NULL, *f5_st=NULL; + for (cookie = vpninfo->cookies; cookie; cookie = cookie->next) { + if (!strcmp(cookie->option, "MRHSession")) + session = cookie->value; + else if (!strcmp(cookie->option, "F5_ST")) + f5_st = cookie->value; + } + + if (session && f5_st) { + free(vpninfo->cookie); + if (asprintf(&vpninfo->cookie, "MRHSession=%s; F5_ST=%s", + session, f5_st) <= 0) + goto nomem; + ret = 0; + goto out; + } + } + if (!form->message) + form->message = strdup(_("Authentication failed. Please retry.")); + nuke_opt_values(form->opts); + } + + out: + if (form) free_auth_form(form); + if (resp_buf) buf_free(resp_buf); + return ret; } /* @@ -329,6 +424,7 @@ int f5_connect(struct openconnect_info *vpninfo) { int ret; struct oc_text_buf *reqbuf = NULL; + struct oc_vpn_option *cookie; char *profile_params = NULL; char *sid = NULL, *ur_z = NULL; int ipv4 = -1, ipv6 = -1, hdlc = -1; @@ -345,6 +441,18 @@ int f5_connect(struct openconnect_info *vpninfo) return ret; } + /* XX: parse "session timeout" cookie to get auth expiration */ + for (cookie = vpninfo->cookies; cookie; cookie = cookie->next) { + if (!strcmp(cookie->option, "F5_ST")) { + int junk, start, dur; + char c = 0; + if (sscanf(cookie->value, "%dz%dz%dz%dz%d%c", &junk, &junk, &junk, &start, &dur, &c) >= 5 + && (c == 0 || c == 'z')) + vpninfo->auth_expiration = start + dur; + break; + } + } + free(vpninfo->urlpath); vpninfo->urlpath = strdup("vdesk/vpn/index.php3?outform=xml&client_version=2.0"); ret = do_https_request(vpninfo, "GET", NULL, NULL, &res_buf, 0); diff --git a/test-f5-login.py b/test-f5-login.py index de988aec..aec2701e 100755 --- a/test-f5-login.py +++ b/test-f5-login.py @@ -14,6 +14,7 @@ import sys import requests import argparse import getpass +from datetime import datetime from shlex import quote p = argparse.ArgumentParser() @@ -65,9 +66,18 @@ data=dict(username=args.username, password=args.password, **extra) print("POST /my.policy to submit login credentials...", file=stderr) res = s.post(endpoint._replace(path='/my.policy').geturl(), data=data, headers={'Referer': res.url}) - res.raise_for_status() +st_cookie = next((c for c in s.cookies if c.name=='F5_ST'), None) +if not st_cookie: + print("No F5_ST cookie in response (ended at %s) -- bad credentials?" % res.url, file=stderr) +else: + # Parse "session timeout" cookie (https://support.f5.com/csp/article/K15387) which looks like '1z1z1z1612808487z604800' + fields = st_cookie.value.split('z') + authat, expat = datetime.fromtimestamp(int(fields[3])), datetime.fromtimestamp(int(fields[3])+int(fields[4])) + print("F5_ST cookie %r valid from %s - %s" % (st_cookie.value, authat, expat), file=stderr) + + # Build openconnect --cookie argument from the result: url = urlparse(res.url) if any(c.name=='MRHSession' for c in s.cookies) and url.path.startswith('/vdesk/'): -- 2.49.0