From 4234492539152887a71e718230bd00f0ce5cfeef Mon Sep 17 00:00:00 2001 From: Daniel Lenski Date: Mon, 22 Feb 2021 17:28:05 -0800 Subject: [PATCH] =?utf8?q?rename=20(resp=5Fbuf,=20form=5Fbuf)=20=E2=86=92?= =?utf8?q?=20(req=5Fbuf,=20resp=5Fbuf)=20in=20f5.c=20and=20fortinet.c?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit The current names were borrowed from Juniper source, and are too confusing. The newly-renamed 'req_buf' now contains data that we send to the servers, and 'resp_buf' contains data that the server sends back to us. Signed-off-by: Daniel Lenski --- auth-html.c | 54 +++++++--- auth-juniper.c | 12 +-- f5.c | 211 ++++++++++++++++++++++++++-------------- fortinet.c | 40 ++++---- openconnect-internal.h | 11 ++- tests/fake-f5-server.py | 20 +++- 6 files changed, 230 insertions(+), 118 deletions(-) diff --git a/auth-html.c b/auth-html.c index 5b7bbcc4..af1373dc 100644 --- a/auth-html.c +++ b/auth-html.c @@ -54,7 +54,7 @@ xmlNodePtr find_form_node(xmlDocPtr doc) } int parse_input_node(struct openconnect_info *vpninfo, struct oc_auth_form *form, - xmlNodePtr node, const char *submit_button, + xmlNodePtr node, const char *submit_button, int flavor, int (*can_gen_tokencode)(struct openconnect_info *vpninfo, struct oc_auth_form *form, struct oc_form_opt *opt)) { char *type = (char *)xmlGetProp(node, (unsigned char *)"type"), *style = (char *)xmlGetProp(node, (unsigned char *)"style"); @@ -93,14 +93,15 @@ int parse_input_node(struct openconnect_info *vpninfo, struct oc_auth_form *form ret = -ENOMEM; goto out; } - if (!strcmp(form->auth_id, "loginForm") && + if (flavor == FORM_FLAVOR_JUNIPER && + !strcmp(form->auth_id, "loginForm") && !strcmp(opt->name, "VerificationCode") && can_gen_tokencode && !can_gen_tokencode(vpninfo, form, opt)) opt->type = OC_FORM_OPT_TOKEN; - } else if (!strcasecmp(type, "submit")) { + } else if (flavor == FORM_FLAVOR_JUNIPER && !strcasecmp(type, "submit")) { + xmlnode_get_prop(node, "name", &opt->name); if (opt->name && submit_button && (!strcmp(opt->name, submit_button) || - /* XX: Juniper-specific */ !strcmp(opt->name, "sn-postauth-proceed") || !strcmp(opt->name, "sn-preauth-proceed") || !strcmp(opt->name, "secidactionEnter"))) { @@ -148,7 +149,7 @@ int parse_input_node(struct openconnect_info *vpninfo, struct oc_auth_form *form } int parse_select_node(struct openconnect_info *vpninfo, struct oc_auth_form *form, - xmlNodePtr node) + xmlNodePtr node, int flavor) { xmlNodePtr child; struct oc_form_opt_select *opt; @@ -161,7 +162,8 @@ int parse_select_node(struct openconnect_info *vpninfo, struct oc_auth_form *for xmlnode_get_prop(node, "name", &opt->form.name); opt->form.label = strdup(opt->form.name); opt->form.type = OC_FORM_OPT_SELECT; - if (!strcmp(opt->form.name, "realm")) /* XX: Juniper-specific */ + if ((flavor == FORM_FLAVOR_JUNIPER && !strcmp(opt->form.name, "realm")) || + (flavor == FORM_FLAVOR_F5 && !strcmp(opt->form.name, "domain"))) form->authgroup_opt = opt; for (child = node->children; child; child = child->next) { @@ -194,7 +196,7 @@ int parse_select_node(struct openconnect_info *vpninfo, struct oc_auth_form *for } struct oc_auth_form *parse_form_node(struct openconnect_info *vpninfo, - xmlNodePtr node, const char *submit_button, + xmlNodePtr node, const char *submit_button, int flavor, int (*can_gen_tokencode)(struct openconnect_info *vpninfo, struct oc_auth_form *form, struct oc_form_opt *opt)) { struct oc_auth_form *form = calloc(1, sizeof(*form)); @@ -205,29 +207,53 @@ struct oc_auth_form *parse_form_node(struct openconnect_info *vpninfo, xmlnode_get_prop(node, "method", &form->method); xmlnode_get_prop(node, "action", &form->action); - if (!form->method || strcasecmp(form->method, "POST") || - !form->action || !form->action[0]) { + if (!form->method || strcasecmp(form->method, "POST")) { vpn_progress(vpninfo, PRG_ERR, _("Cannot handle form method='%s', action='%s'\n"), form->method, form->action); free(form); return NULL; } - xmlnode_get_prop(node, "name", &form->auth_id); - form->banner = strdup(form->auth_id); + + if (flavor == FORM_FLAVOR_JUNIPER) { + xmlnode_get_prop(node, "name", &form->auth_id); + form->banner = strdup(form->auth_id); + } else if (flavor == FORM_FLAVOR_F5) + xmlnode_get_prop(node, "id", &form->auth_id); for (child = htmlnode_dive(node, node); child && child != node; child = htmlnode_dive(node, child)) { if (!child->name) continue; if (!strcasecmp((char *)child->name, "input")) - parse_input_node(vpninfo, form, child, submit_button, can_gen_tokencode); + parse_input_node(vpninfo, form, child, submit_button, flavor, can_gen_tokencode); else if (!strcasecmp((char *)child->name, "select")) { - parse_select_node(vpninfo, form, child); + parse_select_node(vpninfo, form, child, flavor); /* Skip its children */ while (child->children) child = child->last; - } else if (!strcasecmp((char *)child->name, "textarea")) { + } else if (flavor == FORM_FLAVOR_F5 + && !strcasecmp((char *)child->name, "td")) { + + char *id = (char *)xmlGetProp(child, (unsigned char *)"id"); + if (id && !strcmp(id, "credentials_table_header")) { + char *msg = (char *)xmlNodeGetContent(child); + if (msg) { + free(form->banner); + form->banner = msg; + } + } else if (id && !strcmp(id, "credentials_table_postheader")) { + char *msg = (char *)xmlNodeGetContent(child); + if (msg) { + free(form->message); + form->message = msg; + } + } + free(id); + + } else if (flavor == FORM_FLAVOR_JUNIPER && + !strcasecmp((char *)child->name, "textarea")) { + /* display the post sign-in message, if any */ char *fieldname = (char *)xmlGetProp(child, (unsigned char *)"name"); if (fieldname && (!strcasecmp(fieldname, "sn-postauth-text") || /* XX: Juniper-specific */ diff --git a/auth-juniper.c b/auth-juniper.c index 0f180904..d298dc65 100644 --- a/auth-juniper.c +++ b/auth-juniper.c @@ -516,14 +516,14 @@ int oncp_obtain_cookie(struct openconnect_info *vpninfo) _("Encountered form with no 'name' or 'id'\n")); goto dump_form; } else if (form_name && !strcmp(form_name, "frmLogin")) { - form = parse_form_node(vpninfo, node, "btnSubmit", oncp_can_gen_tokencode); + form = parse_form_node(vpninfo, node, "btnSubmit", FORM_FLAVOR_JUNIPER, oncp_can_gen_tokencode); } else if (form_id && !strcmp(form_id, "loginForm")) { - form = parse_form_node(vpninfo, node, "submitButton", oncp_can_gen_tokencode); + form = parse_form_node(vpninfo, node, "submitButton", FORM_FLAVOR_JUNIPER, oncp_can_gen_tokencode); } else if ((form_name && !strcmp(form_name, "frmDefender")) || (form_name && !strcmp(form_name, "frmNextToken"))) { - form = parse_form_node(vpninfo, node, "btnAction", oncp_can_gen_tokencode); + form = parse_form_node(vpninfo, node, "btnAction", FORM_FLAVOR_JUNIPER, oncp_can_gen_tokencode); } else if (form_name && !strcmp(form_name, "frmConfirmation")) { - form = parse_form_node(vpninfo, node, "btnContinue", oncp_can_gen_tokencode); + form = parse_form_node(vpninfo, node, "btnContinue", FORM_FLAVOR_JUNIPER, oncp_can_gen_tokencode); if (!form) { ret = -EINVAL; break; @@ -534,10 +534,10 @@ int oncp_obtain_cookie(struct openconnect_info *vpninfo) form = parse_roles_form_node(node); role_select = 1; } else if (form_name && !strcmp(form_name, "frmTotpToken")) { - form = parse_form_node(vpninfo, node, "totpactionEnter", oncp_can_gen_tokencode); + form = parse_form_node(vpninfo, node, "totpactionEnter", FORM_FLAVOR_JUNIPER, oncp_can_gen_tokencode); } else if ((form_name && !strcmp(form_name, "hiddenform")) || (form_id && !strcmp(form_id, "formSAMLSSO"))) { - form = parse_form_node(vpninfo, node, "submit", oncp_can_gen_tokencode); + form = parse_form_node(vpninfo, node, "submit", FORM_FLAVOR_JUNIPER, oncp_can_gen_tokencode); } else { char *form_action = (char *)xmlGetProp(node, (unsigned char *)"action"); if (form_action && strstr(form_action, "remediate.cgi")) { diff --git a/f5.c b/f5.c index f14236d8..80eedc90 100644 --- a/f5.c +++ b/f5.c @@ -29,99 +29,168 @@ #include #include +#include +#include + #include "openconnect-internal.h" #define XCAST(x) ((const xmlChar *)(x)) +static int check_cookie_success(struct openconnect_info *vpninfo) +{ + struct oc_vpn_option *cookie; + const char *session = NULL, *f5_st = NULL; + + /* XX: if login succeeded worked, we should have a response size of zero, and F5_ST + * and MRHSession cookies in the response. + */ + 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) + return -ENOMEM; + return 0; + } + return -ENOENT; +} + int f5_obtain_cookie(struct openconnect_info *vpninfo) { int ret; - struct oc_text_buf *resp_buf = NULL; - char *form_buf = NULL; + xmlDocPtr doc = NULL; + xmlNode *node; + struct oc_text_buf *req_buf = NULL; struct oc_auth_form *form = NULL; - struct oc_form_opt *opt, *opt2; + char *form_id = NULL; - resp_buf = buf_alloc(); - if ((ret = buf_error(resp_buf))) + req_buf = buf_alloc(); + if ((ret = buf_error(req_buf))) goto out; - /* XX: This initial 'GET /' seems to be necessary to populate LastMRH_Session and - * MRHSession cookies, without which the subsequent 'POST' will fail. - */ - ret = do_https_request(vpninfo, "GET", NULL, NULL, &form_buf, 1); - free(form_buf); - form_buf = NULL; - if (ret < 0) - return ret; + while (1) { + char *resp_buf = NULL; + char *url; - /* XX: build static form (username and password) */ - /* TODO: parse out 'domain' form field from /my.policy as authgroup */ - form = calloc(1, sizeof(*form)); - if (!form) { - nomem: - ret = -ENOMEM; - goto out; - } - 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; + if (req_buf && req_buf->pos) + ret = do_https_request(vpninfo, "POST", + "application/x-www-form-urlencoded", + req_buf, &resp_buf, 2); + else + ret = do_https_request(vpninfo, "GET", NULL, NULL, + &resp_buf, 2); - buf_truncate(resp_buf); - append_form_opts(vpninfo, form, resp_buf); - if (vpninfo->authgroup) - append_opt(resp_buf, "domain", vpninfo->authgroup); + if (ret < 0) + break; - if ((ret = buf_error(resp_buf))) - goto out; - do_https_request(vpninfo, "POST", "application/x-www-form-urlencoded", - resp_buf, &form_buf, 0); + if (!check_cookie_success(vpninfo)) { + free(resp_buf); + ret = 0; + break; + } - /* 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; - } + url = internal_get_url(vpninfo); + if (!url) { + free(resp_buf); + nomem: + ret = -ENOMEM; + break; + } - 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; + doc = htmlReadMemory(resp_buf, ret, url, NULL, + HTML_PARSE_RECOVER|HTML_PARSE_NOERROR|HTML_PARSE_NOWARNING|HTML_PARSE_NONET); + free(url); + free(resp_buf); + if (!doc) { + vpn_progress(vpninfo, PRG_ERR, + _("Failed to parse HTML document\n")); + ret = -EINVAL; + break; + } + + buf_truncate(req_buf); + + node = find_form_node(doc); + if (!node) { + /* XX: some F5 VPNs simply do not have a static HTML form to parse */ + struct oc_form_opt *opt, *opt2; + + vpn_progress(vpninfo, PRG_ERR, + _("WARNING: no HTML login form found; assuming username and password fields\n")); + + 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; + + } else { + if (!xmlnode_get_prop(node, "id", &form_id) && !strcmp(form_id, "auth_form")) + form = parse_form_node(vpninfo, node, NULL, FORM_FLAVOR_F5, NULL); + else { + vpn_progress(vpninfo, PRG_ERR, _("Unknown form ID '%s' (expected 'auth_form')\n"), + form_id); + + fprintf(stderr, _("Dumping unknown HTML form:\n")); + htmlNodeDumpFileFormat(stderr, node->doc, node, NULL, 1); + ret = -EINVAL; + break; } } - if (!form->message) - form->message = strdup(_("Authentication failed. Please retry.")); - nuke_opt_values(form->opts); + + if (!form) { + ret = -EINVAL; + break; + } + + /* XX: do_gen_tokencode would go here, if we knew of any + * token-based 2FA options for F5. + */ + + do { + ret = process_auth_form(vpninfo, form); + } while (ret == OC_FORM_RESULT_NEWGROUP); + if (ret) + goto out; + + append_form_opts(vpninfo, form, req_buf); + if ((ret = buf_error(req_buf))) + goto out; + + if (form->action) { + vpninfo->redirect_url = form->action; + form->action = NULL; + } + free_auth_form(form); + form = NULL; + if (vpninfo->redirect_url) + handle_redirect(vpninfo); + + xmlFreeDoc(doc); + doc = NULL; } out: + if (doc) + xmlFreeDoc(doc); + free(form_id); if (form) free_auth_form(form); - if (resp_buf) buf_free(resp_buf); + if (req_buf) buf_free(req_buf); return ret; } diff --git a/fortinet.c b/fortinet.c index c1a451a0..eab64c37 100644 --- a/fortinet.c +++ b/fortinet.c @@ -90,20 +90,20 @@ static int filter_opts(struct oc_text_buf *buf, const char *query, const char *i int fortinet_obtain_cookie(struct openconnect_info *vpninfo) { int ret; - struct oc_text_buf *resp_buf = NULL; + struct oc_text_buf *req_buf = NULL; struct oc_auth_form *form = NULL; struct oc_form_opt *opt, *opt2; - char *form_buf = NULL, *realm = NULL; + char *resp_buf = NULL, *realm = NULL; - resp_buf = buf_alloc(); - if (buf_error(resp_buf)) { - ret = buf_error(resp_buf); + req_buf = buf_alloc(); + if (buf_error(req_buf)) { + ret = buf_error(req_buf); goto out; } - ret = do_https_request(vpninfo, "GET", NULL, NULL, &form_buf, 1); - free(form_buf); - form_buf = NULL; + ret = do_https_request(vpninfo, "GET", NULL, NULL, &resp_buf, 1); + free(resp_buf); + resp_buf = NULL; if (ret < 0) goto out; @@ -168,24 +168,24 @@ int fortinet_obtain_cookie(struct openconnect_info *vpninfo) goto out; } - buf_truncate(resp_buf); - append_form_opts(vpninfo, form, resp_buf); - buf_append(resp_buf, "&realm=%s", realm ?: ""); /* XX: already URL-escaped */ + buf_truncate(req_buf); + append_form_opts(vpninfo, form, req_buf); + buf_append(req_buf, "&realm=%s", realm ?: ""); /* XX: already URL-escaped */ if (!form->action) { /* "normal" form (fields 'username', 'credential') */ - buf_append(resp_buf, "&ajax=1&just_logged_in=1"); + buf_append(req_buf, "&ajax=1&just_logged_in=1"); } else { /* 2FA form (fields 'username', 'code', and a bunch of values * from the previous response which we mindlessly parrot back) */ - buf_append(resp_buf, "&code2=&%s", form->action); + buf_append(req_buf, "&code2=&%s", form->action); } - if ((ret = buf_error(resp_buf))) + if ((ret = buf_error(req_buf))) goto out; ret = do_https_request(vpninfo, "POST", "application/x-www-form-urlencoded", - resp_buf, &form_buf, 0); + req_buf, &resp_buf, 0); /* XX: if this worked, we should have 200 status */ if (ret >= 0) { @@ -203,7 +203,7 @@ int fortinet_obtain_cookie(struct openconnect_info *vpninfo) } /* XX: We didn't get SVPNCOOKIE. 2FA? */ - if (!strncmp(form_buf, "ret=", 4) && strstr(form_buf, ",tokeninfo=")) { + if (!strncmp(resp_buf, "ret=", 4) && strstr(resp_buf, ",tokeninfo=")) { const char *prompt; struct oc_text_buf *action_buf = buf_alloc(); @@ -222,7 +222,7 @@ int fortinet_obtain_cookie(struct openconnect_info *vpninfo) opt2->type = OC_FORM_OPT_PASSWORD; /* Save a bunch of values to parrot back */ - filter_opts(action_buf, form_buf, "reqid,polid,grp,portal,peer,magic", 1); + filter_opts(action_buf, resp_buf, "reqid,polid,grp,portal,peer,magic", 1); if ((ret = buf_error(action_buf))) goto out; free(form->action); @@ -230,7 +230,7 @@ int fortinet_obtain_cookie(struct openconnect_info *vpninfo) action_buf->data = NULL; buf_free(action_buf); - if ((prompt = strstr(form_buf, ",chal_msg="))) { + if ((prompt = strstr(resp_buf, ",chal_msg="))) { char *end = strchrnul(prompt, ','); prompt += 10; free(form->message); @@ -242,10 +242,10 @@ int fortinet_obtain_cookie(struct openconnect_info *vpninfo) out: free(realm); - free(form_buf); + free(resp_buf); if (form) free_auth_form(form); - buf_free(resp_buf); + buf_free(req_buf); return ret; } diff --git a/openconnect-internal.h b/openconnect-internal.h index 5e92e56e..c7fde10d 100644 --- a/openconnect-internal.h +++ b/openconnect-internal.h @@ -185,6 +185,11 @@ struct pkt { #define DTLS_CONNECTING 4 /* ESP probe received; must tell server */ #define DTLS_CONNECTED 5 /* Server informed and should be sending ESP */ +/* Flavors of HTML forms to screen-scrape */ +#define FORM_FLAVOR_JUNIPER 1 +#define FORM_FLAVOR_F5 2 +#define FORM_FLAVOR_FORTINET 3 + /* All supported PPP packet framings/encapsulations */ #define PPP_ENCAP_RFC1661 1 /* Plain/synchronous/pre-framed PPP (RFC1661) */ #define PPP_ENCAP_RFC1662_HDLC 2 /* PPP with HDLC-like framing (RFC1662) */ @@ -950,12 +955,12 @@ xmlNodePtr htmlnode_next(xmlNodePtr top, xmlNodePtr node); xmlNodePtr htmlnode_dive(xmlNodePtr top, xmlNodePtr node); xmlNodePtr find_form_node(xmlDocPtr doc); int parse_input_node(struct openconnect_info *vpninfo, struct oc_auth_form *form, - xmlNodePtr node, const char *submit_button, + xmlNodePtr node, const char *submit_button, int flavor, int (*can_gen_tokencode)(struct openconnect_info *vpninfo, struct oc_auth_form *form, struct oc_form_opt *opt)); int parse_select_node(struct openconnect_info *vpninfo, struct oc_auth_form *form, - xmlNodePtr node); + xmlNodePtr node, int flavor); struct oc_auth_form *parse_form_node(struct openconnect_info *vpninfo, - xmlNodePtr node, const char *submit_button, + xmlNodePtr node, const char *submit_button, int flavor, int (*can_gen_tokencode)(struct openconnect_info *vpninfo, struct oc_auth_form *form, struct oc_form_opt *opt)); /* auth-juniper.c */ diff --git a/tests/fake-f5-server.py b/tests/fake-f5-server.py index 5981ce75..88a5eb69 100755 --- a/tests/fake-f5-server.py +++ b/tests/fake-f5-server.py @@ -85,26 +85,38 @@ def root(): return redirect(url_for('get_policy')) -# Respond to 'GET /my.policy with a placeholder stub (since OpenConnect doesn't even try to parse the form) +# Respond to 'GET /my.policy with a login form @app.route('/my.policy') def get_policy(): session.update(step='GET-login-form') - return 'login page' + return ''' +
+ + +
''' -# Respond to 'POST /my.policy with an empty response containing MRHSession and F5_ST +# Respond to 'POST /my.policy with a redirect response containing MRHSession and F5_ST # cookies (OpenConnect uses the combination of the two to detect successful authentication) @app.route('/my.policy', methods=['POST']) def post_policy(): session.update(step='POST-login', username=request.form.get('username'), credential=request.form.get('password')) # print(session) - resp = make_response('') + resp = redirect(url_for('webtop')) resp.set_cookie('MRHSession', cookify(dict(session))) resp.set_cookie('F5_ST', '1z1z1z%dz%d' % (time.time(), 3600)) return resp +@app.route('/vdesk/webtop.eui') +def webtop(): + session.update(step='POST-login-webtop') + # print(session) + + return 'some junk HTML webtop' + + # Respond to 'GET /vdesk/vpn/index.php3?outform=xml&client_version=2.0 with an XML config # [Save VPN resource name in the session for verification of client state later] @app.route('/vdesk/vpn/index.php3') -- 2.49.0