Unlike CSD, the HIP security checker runs during the connection phase, not during the authentication phase. Therefore we need to build the CSD token (an MD5 digest identifying the client) without relying on the authentication phase having run in the same process. We build it from the cookie containing authentication information, but exclude the volatile field (which changes from session to session) and the preferred-ip field (which may not be present in all cases, or may change from session to session). Signed-off-by: Daniel Lenski <dlenski at gmail.com> --- gpst.c | 203 +++++++++++++++++++++++++++++++++++++++++++++++++- hipreport.sh | 185 +++++++++++++++++++++++++++++++++++++++++++++ www/Makefile.am | 2 +- www/features.xml | 2 +- www/globalprotect.xml | 7 ++ www/hip.xml | 89 ++++++++++++++++++++++ 6 files changed, 485 insertions(+), 3 deletions(-) create mode 100755 hipreport.sh create mode 100644 www/hip.xml diff --git a/gpst.c b/gpst.c index 994f45d..76f66c1 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> @@ -653,6 +654,193 @@ 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; +} + +/* Unlike CSD, the HIP security checker runs during the connection + * phase, not during the authentication phase. + * + * The HIP security checker will (probably) ask us to resubmit the + * HIP report if either of the following changes: + * - Client IP address + * - Client HIP report md5sum + * + * I'm not sure what the md5sum is computed over in the official + * client, but it doesn't really matter. + * + * We just need an identifier for the combination of the local host + * and the VPN gateway which won't change when our IP address + * or authcookie are changed. + */ +static int build_csd_token(struct openconnect_info *vpninfo) +{ + struct oc_text_buf *buf; + unsigned char md5[16]; + int i; + + if (vpninfo->csd_token) + return 0; + + vpninfo->csd_token = malloc(MD5_SIZE * 2 + 1); + if (!vpninfo->csd_token) + return -ENOMEM; + + /* use localname and cookie (excluding volatile authcookie and preferred-ip) to build md5sum */ + buf = buf_alloc(); + append_opt(buf, "computer", vpninfo->localname); + filter_opts(buf, vpninfo->cookie, "authcookie,preferred-ip", 0); + + /* save as csd_token */ + openconnect_md5(md5, buf->data, buf->pos); + for (i=0; i < MD5_SIZE; i++) + sprintf(&vpninfo->csd_token[i*2], "%02x", md5[i]); + + return buf_free(buf); +} + +/* 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; + + /* cookie gives us these fields: authcookie, portal, user, domain, and (maybe the unnecessary) preferred-ip */ + buf_append(request_body, "client-role=global-protect-full&%s", vpninfo->cookie); + append_opt(request_body, "computer", vpninfo->localname); + append_opt(request_body, "client-ip", vpninfo->ip_info.addr); + if (report) { + /* XML report contains many characters requiring URL-encoding (%xx) */ + buf_ensure_space(request_body, strlen(report)*3); + append_opt(request_body, "report", report); + } else { + result = build_csd_token(vpninfo); + if (result) + goto out; + 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); + +out: + 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; @@ -660,7 +848,19 @@ int gpst_setup(struct openconnect_info *vpninfo) /* Get configuration */ ret = gpst_get_config(vpninfo); if (ret) - return ret; + goto out; + + /* 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 @@ -680,6 +880,7 @@ int gpst_setup(struct openconnect_info *vpninfo) vpninfo->ssl_times.last_rekey = 0; } +out: return ret; } diff --git a/hipreport.sh b/hipreport.sh new file mode 100755 index 0000000..a25cf13 --- /dev/null +++ b/hipreport.sh @@ -0,0 +1,185 @@ +#!/bin/sh + +# openconnect will call this script with the follow command-line +# arguments, which are needed to populate the contents of the +# HIP report: +# +# --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. + +# Read command line arguments into variables +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