Signed-off-by: Daniel Lenski <dlenski at gmail.com> --- auth-globalprotect.c | 29 +++++--- gpst.c | 158 +++++++++++++++++++++++++++++++++++++++++++ hipreport.sh | 182 ++++++++++++++++++++++++++++++++++++++++++++++++++ www/Makefile.am | 2 +- www/features.xml | 2 +- www/globalprotect.xml | 7 ++ www/hip.xml | 89 ++++++++++++++++++++++++ 7 files changed, 459 insertions(+), 10 deletions(-) create mode 100755 hipreport.sh create mode 100644 www/hip.xml diff --git a/auth-globalprotect.c b/auth-globalprotect.c index 1e7c854..8e27cfb 100644 --- a/auth-globalprotect.c +++ b/auth-globalprotect.c @@ -83,7 +83,8 @@ static struct oc_auth_form *auth_form(struct openconnect_info *vpninfo, */ struct gp_login_arg { const char *opt; - unsigned save:1; + unsigned save_cookie:1; + unsigned save_token:1; unsigned show:1; unsigned warn_missing:1; unsigned err_missing:1; @@ -91,13 +92,13 @@ struct gp_login_arg { }; static const struct gp_login_arg gp_login_args[] = { { .opt="unknown-arg0", .show=1 }, - { .opt="authcookie", .save=1, .err_missing=1 }, + { .opt="authcookie", .save_cookie=1, .err_missing=1 }, { .opt="persistent-cookie", .warn_missing=1 }, /* 40 hex digits; persists across sessions */ - { .opt="portal", .save=1, .warn_missing=1 }, - { .opt="user", .save=1, .err_missing=1 }, + { .opt="portal", .save_cookie=1, .save_token=1, .warn_missing=1 }, + { .opt="user", .save_cookie=1, .save_token=1, .err_missing=1 }, { .opt="authentication-source", .show=1 }, /* LDAP-auth, AUTH-RADIUS_RSA_OTP, etc. */ { .opt="configuration", .warn_missing=1 }, /* usually vsys1 (sometimes vsys2, etc.) */ - { .opt="domain", .save=1, .warn_missing=1 }, + { .opt="domain", .save_cookie=1, .save_token=1, .warn_missing=1 }, { .opt="unknown-arg8", .show=1 }, { .opt="unknown-arg9", .show=1 }, { .opt="unknown-arg10", .show=1 }, @@ -105,15 +106,17 @@ static const struct gp_login_arg gp_login_args[] = { { .opt="connection-type", .err_missing=1, .check="tunnel" }, { .opt="password-expiration-days", .show=1 }, /* days until password expires, if not -1 */ { .opt="clientVer", .err_missing=1, .check="4100" }, - { .opt="preferred-ip", .save=1 }, + { .opt="preferred-ip", .save_cookie=1 }, { .opt=NULL }, }; static int parse_login_xml(struct openconnect_info *vpninfo, xmlNode *xml_node) { - struct oc_text_buf *cookie = buf_alloc(); + struct oc_text_buf *cookie = buf_alloc(), *token = buf_alloc(); char *value = NULL; const struct gp_login_arg *arg; + unsigned char md5[16]; + int i; if (!xmlnode_is_named(xml_node, "jnlp")) goto err_out; @@ -154,20 +157,30 @@ static int parse_login_xml(struct openconnect_info *vpninfo, xmlNode *xml_node) arg->opt, value); } - if (value && arg->save) + if (value && arg->save_cookie) append_opt(cookie, arg->opt, value); + if (value && arg->save_token) + append_opt(token, arg->opt, value); free(value); value = NULL; } vpninfo->cookie = cookie->data; cookie->data = NULL; + + openconnect_md5(md5, token->data, token->pos); + vpninfo->csd_token = malloc(MD5_SIZE * 2 + 1); + for (i=0; i < MD5_SIZE; i++) + sprintf(&vpninfo->csd_token[i*2], "%02x", md5[i]); + buf_free(cookie); + buf_free(token); return 0; err_out: free(value); buf_free(cookie); + buf_free(token); return -EINVAL; } diff --git a/gpst.c b/gpst.c index a4ec789..1262612 100644 --- a/gpst.c +++ b/gpst.c @@ -26,6 +26,7 @@ #include <stdlib.h> #include <stdio.h> #include <sys/types.h> +#include <sys/wait.h> #include <stdarg.h> #ifdef HAVE_LZ4 #include <lz4.h> @@ -631,6 +632,150 @@ static int gpst_connect(struct openconnect_info *vpninfo) return ret; } +static int parse_hip_report_check(struct openconnect_info *vpninfo, xmlNode *xml_node) +{ + char *s; + int result = -EINVAL; + + if (!xml_node || !xmlnode_is_named(xml_node, "response")) + goto out; + + for (xml_node = xml_node->children; xml_node; xml_node=xml_node->next) { + if (!xmlnode_get_text(xml_node, "hip-report-needed", &s)) { + if (!strcmp(s, "no")) + result = 0; + else if (!strcmp(s, "yes")) + result = -EAGAIN; + else + result = -EINVAL; + free(s); + goto out; + } + } + +out: + return result; +} + +/* check if HIP report is needed (to ssl-vpn/hipreportcheck.esp) or submit HIP report contents (to ssl-vpn/hipreport.esp) */ +static int check_or_submit_hip_report(struct openconnect_info *vpninfo, const char *report) +{ + int result; + + struct oc_text_buf *request_body = buf_alloc(); + const char *request_body_type = "application/x-www-form-urlencoded"; + const char *method = "POST"; + char *xml_buf=NULL, *orig_path; + + buf_truncate(request_body); + + /* cookie gives us these fields: authcookie, portal, user, domain, and (maybe the unnecessary) preferred-ip */ + buf_truncate(request_body); + buf_append(request_body, "%s", vpninfo->cookie); + append_opt(request_body, "computer", vpninfo->localname); + append_opt(request_body, "client-ip", vpninfo->ip_info.addr); + append_opt(request_body, "client-role", "global-protect-full"); + if (report) { + buf_ensure_space(request_body, strlen(report)*5); + append_opt(request_body, "report", report); + } else + append_opt(request_body, "md5", vpninfo->csd_token); + + orig_path = vpninfo->urlpath; + vpninfo->urlpath = strdup(report ? "ssl-vpn/hipreport.esp" : "ssl-vpn/hipreportcheck.esp"); + result = do_https_request(vpninfo, method, request_body_type, request_body, + &xml_buf, 0); + free(vpninfo->urlpath); + vpninfo->urlpath = orig_path; + + result = gpst_xml_or_error(vpninfo, result, xml_buf, report ? NULL : parse_hip_report_check, NULL, NULL); + + buf_free(request_body); + free(xml_buf); + return result; +} + +static int run_hip_script(struct openconnect_info *vpninfo) +{ +#if defined(_WIN32) || defined(__native_client__) + vpn_progress(vpninfo, PRG_ERR, + _("Error: Running the 'HIP Report' script on this platform is not yet implemented.\n")); + return -EPERM; +#else + int pipefd[2]; + int ret; + pid_t child; + + if (!vpninfo->csd_wrapper) { + vpn_progress(vpninfo, PRG_ERR, + _("WARNING: Server asked us to submit HIP report with md5sum %s.\n" + "VPN connectivity may be disabled or limited without HIP report submission.\n" + "You need to provide a --csd-wrapper argument with the HIP report submission script.\n"), + vpninfo->csd_token); + /* XXX: Many GlobalProtect VPNs work fine despite allegedly requiring HIP report submission */ + return 0; + } + + if (pipe(pipefd) == -1) + goto out; + child = fork(); + if (child == -1) { + goto out; + } else if (child > 0) { + /* in parent: read report from child */ + struct oc_text_buf *report_buf = buf_alloc(); + char b[256]; + int i, status; + close(pipefd[1]); + + buf_truncate(report_buf); + while ((i = read(pipefd[0], b, sizeof(b))) > 0) + buf_append_bytes(report_buf, b, i); + + waitpid(child, &status, 0); + if (status != 0) { + vpn_progress(vpninfo, PRG_ERR, + _("HIP script returned non-zero status: %d\n"), status); + ret = -EINVAL; + } else { + ret = check_or_submit_hip_report(vpninfo, report_buf->data); + if (ret < 0) + vpn_progress(vpninfo, PRG_ERR, _("HIP report submission failed.\n")); + else { + vpn_progress(vpninfo, PRG_INFO, _("HIP report submitted successfully.\n")); + ret = 0; + } + } + buf_free(report_buf); + return ret; + } else { + /* in child: run HIP script */ + char *hip_argv[32]; + int i = 0; + close(pipefd[0]); + dup2(pipefd[1], 1); + + hip_argv[i++] = openconnect_utf8_to_legacy(vpninfo, vpninfo->csd_wrapper); + hip_argv[i++] = (char *)"--cookie"; + hip_argv[i++] = vpninfo->cookie; + hip_argv[i++] = (char *)"--computer"; + hip_argv[i++] = vpninfo->localname; + hip_argv[i++] = (char *)"--client-ip"; + hip_argv[i++] = (char *)vpninfo->ip_info.addr; + hip_argv[i++] = (char *)"--md5"; + hip_argv[i++] = vpninfo->csd_token; + hip_argv[i++] = NULL; + execv(hip_argv[0], hip_argv); + + out: + vpn_progress(vpninfo, PRG_ERR, + _("Failed to exec HIP script %s\n"), hip_argv[0]); + exit(1); + } + +#endif /* !_WIN32 && !__native_client__ */ +} + int gpst_setup(struct openconnect_info *vpninfo) { int ret; @@ -640,6 +785,18 @@ int gpst_setup(struct openconnect_info *vpninfo) if (ret) return ret; + /* Check HIP */ + ret = check_or_submit_hip_report(vpninfo, NULL); + if (ret == -EAGAIN) { + vpn_progress(vpninfo, PRG_DEBUG, + _("Gateway says HIP report submission is needed.\n")); + ret = run_hip_script(vpninfo); + if (ret != 0) + goto out; + } else if (ret == 0) + vpn_progress(vpninfo, PRG_DEBUG, + _("Gateway says no HIP report submission is needed.\n")); + /* We do NOT actually start the HTTPS tunnel yet if we want to * use ESP, because the ESP tunnel won't work if the HTTPS tunnel * is connected! >:-( @@ -647,6 +804,7 @@ int gpst_setup(struct openconnect_info *vpninfo) if (vpninfo->dtls_state == DTLS_DISABLED || vpninfo->dtls_state == DTLS_NOSECRET) ret = gpst_connect(vpninfo); +out: return ret; } diff --git a/hipreport.sh b/hipreport.sh new file mode 100755 index 0000000..afc3067 --- /dev/null +++ b/hipreport.sh @@ -0,0 +1,182 @@ +#!/bin/sh + +# Read required variables from command line: +# +# --cookie: a URL-encoded string, as output by openconnect +# --authenticate --protocol=gp, which includes parameters +# --from the /ssl-vpn/login.esp response +# +# --computer: local hostname, which can be overriden with +# --openconnect local-hostname=HOSTNAME +# +# --client-ip: IPv4 address allocated by the GlobalProtect VPN for +# this client (included in /ssl-vpn/getconfig.esp +# response) +# +# --md5: The md5 digest to encode into this HIP report. I'm not sure +# exactly what this is the md5 digest *of*, but all that +# really matters is that the value in the HIP report +# submission should match the value in the HIP report check. + +COOKIE= +COMPUTER= +IP= +MD5= + +while [ "$1" ]; do + if [ "$1" = "--cookie" ]; then shift; COOKIE="$1"; fi + if [ "$1" = "--computer" ]; then shift; COMPUTER="$1"; fi + if [ "$1" = "--client-ip" ]; then shift; IP="$1"; fi + if [ "$1" = "--md5" ]; then shift; MD5="$1"; fi + shift +done + +if [ -z "$COOKIE" -o -z "$COMPUTER" -o -z "$IP" -o -z "$MD5" ]; then + echo "Parameters --cookie, --computer, --client-ip, and --md5 are required" >&2 + exit 1; +fi + +# Extract username and domain from cookie +USER=$(echo "$COOKIE" | sed -rn 's/(.+&|^)user=([^&]+)(&.+|$)/\2/p') +DOMAIN=$(echo "$COOKIE" | sed -rn 's/(.+&|^)domain=([^&]+)(&.+|$)/\2/p') + +# Timestamp in the format expected by GlobalProtect server +NOW=$(date +'%m/%d/%Y %H:%M:%S') + +# This value may need to be extracted from the official HIP report, if a made-up value is not accepted. +HOSTID="deadbeef-dead-beef-dead-beefdeadbeef" + +cat <<EOF +<hip-report name="hip-report"> + <md5-sum>$MD5</md5-sum> + <user-name>$USER</user-name> + <domain>$DOMAIN</domain> + <host-name>$COMPUTER</host-name> + <host-id>$HOSTID</host-id> + <ip-address>$IP</ip-address> + <ipv6-address></ipv6-address> + <generate-time>$NOW</generate-time> + <categories> + <entry name="host-info"> + <client-version>4.0.2-19</client-version> + <os>Microsoft Windows 10 Pro , 64-bit</os> + <os-vendor>Microsoft</os-vendor> + <domain>$DOMAIN.internal</domain> + <host-name>$COMPUTER</host-name> + <host-id>$HOSTID</host-id> + <network-interface> + <entry name="{DEADBEEF-DEAD-BEEF-DEAD-BEEFDEADBEEF}"> + <description>PANGP Virtual Ethernet Adapter #2</description> + <mac-address>01-02-03-00-00-01</mac-address> + <ip-address> + <entry name="$IP"/> + </ip-address> + <ipv6-address> + <entry name="dead::beef:dead:beef:dead"/> + </ipv6-address> + </entry> + </network-interface> + </entry> + <entry name="antivirus"> + <list> + <entry> + <ProductInfo> + <Prod name="McAfee VirusScan Enterprise" version="8.8.0.1804" defver="8682.0" prodType="1" engver="5900.7806" osType="1" vendor="McAfee, Inc." dateday="12" dateyear="2017" datemon="10"> + </Prod> + <real-time-protection>yes</real-time-protection> + <last-full-scan-time>10/11/2017 15:23:41</last-full-scan-time> + </ProductInfo> + </entry> + <entry> + <ProductInfo> + <Prod name="Windows Defender" version="4.11.15063.332" defver="1.245.683.0" prodType="1" engver="1.1.13804.0" osType="1" vendor="Microsoft Corp." dateday="8" dateyear="2017" datemon="6"> + </Prod> + <real-time-protection>no</real-time-protection> + <last-full-scan-time>n/a</last-full-scan-time> + </ProductInfo> + </entry> + </list> + </entry> + <entry name="anti-spyware"> + <list> + <entry> + <ProductInfo> + <Prod name="McAfee VirusScan Enterprise" version="8.8.0.1804" defver="8682.0" prodType="2" engver="5900.7806" osType="1" vendor="McAfee, Inc." dateday="12" dateyear="2017" datemon="10"> + </Prod> + <real-time-protection>yes</real-time-protection> + <last-full-scan-time>10/11/2017 15:23:41</last-full-scan-time> + </ProductInfo> + </entry> + <entry> + <ProductInfo> + <Prod name="Windows Defender" version="4.11.15063.332" defver="1.245.683.0" prodType="2" engver="1.1.13804.0" osType="1" vendor="Microsoft Corp." dateday="8" dateyear="2017" datemon="6"> + </Prod> + <real-time-protection>no</real-time-protection> + <last-full-scan-time>n/a</last-full-scan-time> + </ProductInfo> + </entry> + </list> + </entry> + <entry name="disk-backup"> + <list> + <entry> + <ProductInfo> + <Prod name="Windows Backup and Restore" version="10.0.15063.0" vendor="Microsoft Corp."> + </Prod> + <last-backup-time>n/a</last-backup-time> + </ProductInfo> + </entry> + </list> + </entry> + <entry name="disk-encryption"> + <list> + <entry> + <ProductInfo> + <Prod name="Windows Drive Encryption" version="10.0.15063.0" vendor="Microsoft Corp."> + </Prod> + <drives> + <entry> + <drive-name>C:</drive-name> + <enc-state>full</enc-state> + </entry> + </drives> + </ProductInfo> + </entry> + </list> + </entry> + <entry name="firewall"> + <list> + <entry> + <ProductInfo> + <Prod name="Microsoft Windows Firewall" version="10.0" vendor="Microsoft Corp."> + </Prod> + <is-enabled>yes</is-enabled> + </ProductInfo> + </entry> + </list> + </entry> + <entry name="patch-management"> + <list> + <entry> + <ProductInfo> + <Prod name="McAfee ePolicy Orchestrator Agent" version="5.0.5.658" vendor="McAfee, Inc."> + </Prod> + <is-enabled>yes</is-enabled> + </ProductInfo> + </entry> + <entry> + <ProductInfo> + <Prod name="Microsoft Windows Update Agent" version="10.0.15063.0" vendor="Microsoft Corp."> + </Prod> + <is-enabled>yes</is-enabled> + </ProductInfo> + </entry> + </list> + <missing-patches/> + </entry> + <entry name="data-loss-prevention"> + <list/> + </entry> + </categories> +</hip-report> +EOF diff --git a/www/Makefile.am b/www/Makefile.am index f791a00..a6de6ac 100644 --- a/www/Makefile.am +++ b/www/Makefile.am @@ -3,7 +3,7 @@ SUBDIRS = styles inc images CONV = "$(srcdir)/html.py" -FTR_PAGES = csd.html charset.html token.html pkcs11.html tpm.html features.html gui.html nonroot.html +FTR_PAGES = csd.html charset.html token.html pkcs11.html tpm.html features.html gui.html nonroot.html hip.html START_PAGES = building.html connecting.html manual.html vpnc-script.html INDEX_PAGES = changelog.html download.html index.html packages.html platforms.html PROTO_PAGES = anyconnect.html juniper.html globalprotect.html diff --git a/www/features.xml b/www/features.xml index 92457dc..cbe9144 100644 --- a/www/features.xml +++ b/www/features.xml @@ -24,7 +24,7 @@ <li>Automatic update of VPN server list / configuration.</li> <li>Roaming support, allowing reconnection when the local IP address changes.</li> <li>Run without root privileges <i>(see <a href="nonroot.html">here</a>)</i>.</li> - <li>"Cisco Secure Desktop" support <i>(see <a href="csd.html">here</a>)</i>.</li> + <li>Support for "Cisco Secure Desktop" <i>(see <a href="csd.html">here</a>)</i> and "GlobalProtect HIP report" <i>(see <a href="hip.html">here</a>)</i>.</li> <li>Graphical connection tools for various environments <i>(see <a href="gui.html">here</a>)</i>.</li> </ul> diff --git a/www/globalprotect.xml b/www/globalprotect.xml index ee45819..655db9a 100644 --- a/www/globalprotect.xml +++ b/www/globalprotect.xml @@ -16,15 +16,22 @@ href="https://tools.ietf.org/html/rfc3948">ESP</a>, with routing and configuration information distributed in XML format.</p> +<h3>Authentication</h3> + <p>To authenticate, you connect to the secure web server (<tt>POST /ssl-vpn/login.esp</tt>), provide a username, password, and (optionally) a certificate, and receive an authcookie. The username, authcookie, and a couple other bits of information obtained at login are combined into the OpenConnect cookie.</p> +<h3>Tunnel configuration</h3> + <p>To connect to the secure tunnel, the cookie is used to read routing and tunnel configuration information (<tt>POST /ssl-vpn/getconfig.esp</tt>).</p> +<p>Next, a <a href="hip.html">HIP report</a> (security scanner report) is +generated by the client and submitted to the server, if required.</p> + <p>Finally, either an HTTPS-based or ESP-based tunnel is setup:</p> <ol> diff --git a/www/hip.xml b/www/hip.xml new file mode 100644 index 0000000..0009c32 --- /dev/null +++ b/www/hip.xml @@ -0,0 +1,89 @@ +<PAGE> + <INCLUDE file="inc/header.tmpl" /> + + <VAR match="VAR_SEL_FEATURES" replace="selected" /> + <VAR match="VAR_SEL_FEATURE_CSD" replace="selected" /> + <PARSE file="menu1.xml" /> + <PARSE file="menu2-features.xml" /> + + <INCLUDE file="inc/content.tmpl" /> + +<h1>PAN GlobalProtect HIP</h1> + +<p>The HIP ('Host Integrity Protection') mechanism is a security +scanner for the <a href="globalprotect.html">PAN GlobalProtect</a> +VPNs, in the same vein as <a href="csd.html">Cisco's CSD</a> and <a +href="juniper.html">Juniper's Host Checker (tncc.jar)</a>.</p> + +<h2>How it works</h2> + +<p>It is somewhat <i>less</i> intrusive than CSD or TNCC, because it +does not appear to work by downloading a trojan binary from the VPN +server. Instead, it runs a HIP report generator (built-in as part of +the official GlobalProtect VPN client software), which generates an +"HIP report" XML file.</p> + +<p>HIP flow used in the official clients:</p> +<ol> + <li>Client authenticates and fetches the tunnel configuration from the GlobalProtect gateway.</li> + <li>Client runs HIP report generator and computes MD5 digest of report.</li> + <li>Client checks whether a HIP report is required (<code>/ssl-vpn/hipreportcheck.esp</code>), including its MD5 digest and gateway-assigned IP address in the report.</li> + <li>Gateway responds whether or not a HIP report is required (normally, it doesn't require a new one if a report with the same MD5 digest and same IP address have been submitted recently).</li> + <li>Client uploads the complete HIP report to (<code>/ssl-vpn/hipreport.esp</code>).</li> + <li>Server confirms acceptance of HIP report with a success message.</li> +</ol> + +<p>If all goes well, the client should have the expected level of +access to resources on the network after these steps are +complete. However, two things can go wrong:</p> + +<ul> + <li>Many GlobalProtect servers report that they require HIP reports + (#3 above), but don't actually enforce this requirement. (For this + reason, OpenConnect does not currently fail if a HIP report is + required but no HIP report script is provided.)</li> + <li>Many GlobalProtect servers will claim that the HIP report was + accepted successfully (#6 above) but silently fail to enable the + expected network access, presumably because some aspect of the + HIP report contents were not approved.</li> +</ul> + +<h2>HIP support in openconnect</h2> + +<p>OpenConnect supports HIP report generation and submission by passing the <code>--csd-wrapper=SCRIPT</code> argument with a shell script to generate a HIP report in the format expected by the +server. This shell script must output the HIP report to standard output and exit successfully (status code 0). The HIP script is called with the following command-line arguments:</p> + +<pre> + --cookie: a URL-encoded string, as output by openconnect + --authenticate --protocol=gp, which includes parameters + --from the /ssl-vpn/login.esp response + + --computer: local hostname, which can be overriden with + --openconnect local-hostname=HOSTNAME + + --client-ip: IPv4 address allocated by the GlobalProtect VPN for + this client (included in /ssl-vpn/getconfig.esp + response) + + --md5: The md5 digest to encode into this HIP report. All that + really matters is that the value in the HIP report + submission should match the value in the HIP report check. +</pre> + +<h2>Generating/spoofing a HIP report</h2> + +<p>An example <code>hipreport.sh</code> script is included in the +openconnect distribution.</p> + +<p>Depending on how picky your GlobalProtect +VPN is, it may be necessary to spoof or alter some of the parameters +of the HIP report to match the output of one of the official +clients. In order to capture the contents of the official Windows +client's HIP reports, enable the highest logging level for the "PanGPS +Service", and then sift through the giant <code>PanGPS.log</code> file +(which should be in the same directory as the executables, normally +<code>c:\Program Files\PaloAlto Networks\GlobalProtect</code>) to find +the HIP report submission.</p> + +<INCLUDE file="inc/footer.tmpl" /> +</PAGE> -- 2.7.4