diff --git a/gpst.c b/gpst.c index 0097c955..bd6af62d 100644 --- a/gpst.c +++ b/gpst.c @@ -26,6 +26,9 @@ #include #include #include +#ifndef _WIN32 +#include +#endif #include #ifdef HAVE_LZ4 #include @@ -717,6 +720,200 @@ 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); + if (buf_error(buf)) + goto out; + + /* 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]); + +out: + 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); + } + if ((result = buf_error(request_body))) + goto out; + + 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__) + int pipefd[2]; + int ret; + pid_t child; +#endif + + 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 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 + 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; @@ -728,7 +925,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 @@ -737,6 +946,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 00000000..a25cf139 --- /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 < + $MD5 + $USER + $DOMAIN + $COMPUTER + $HOSTID + $IP + + $NOW + + + 4.0.2-19 + Microsoft Windows 10 Pro , 64-bit + Microsoft + $DOMAIN.internal + $COMPUTER + $HOSTID + + + PANGP Virtual Ethernet Adapter #2 + 01-02-03-00-00-01 + + + + + + + + + + + + + + + + yes + 10/11/2017 15:23:41 + + + + + + + no + n/a + + + + + + + + + + + yes + 10/11/2017 15:23:41 + + + + + + + no + n/a + + + + + + + + + + + n/a + + + + + + + + + + + + + C: + full + + + + + + + + + + + + + yes + + + + + + + + + + + yes + + + + + + + yes + + + + + + + + + + +EOF diff --git a/www/Makefile.am b/www/Makefile.am index f791a004..a6de6ac2 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 92457dc4..cbe91447 100644 --- a/www/features.xml +++ b/www/features.xml @@ -24,7 +24,7 @@
  • Automatic update of VPN server list / configuration.
  • Roaming support, allowing reconnection when the local IP address changes.
  • Run without root privileges (see here).
  • -
  • "Cisco Secure Desktop" support (see here).
  • +
  • Support for "Cisco Secure Desktop" (see here) and "GlobalProtect HIP report" (see here).
  • Graphical connection tools for various environments (see here).
  • diff --git a/www/globalprotect.xml b/www/globalprotect.xml index ee458199..655db9a2 100644 --- a/www/globalprotect.xml +++ b/www/globalprotect.xml @@ -16,15 +16,22 @@ href="https://tools.ietf.org/html/rfc3948">ESP, with routing and configuration information distributed in XML format.

    +

    Authentication

    +

    To authenticate, you connect to the secure web server (POST /ssl-vpn/login.esp), 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.

    +

    Tunnel configuration

    +

    To connect to the secure tunnel, the cookie is used to read routing and tunnel configuration information (POST /ssl-vpn/getconfig.esp).

    +

    Next, a HIP report (security scanner report) is +generated by the client and submitted to the server, if required.

    +

    Finally, either an HTTPS-based or ESP-based tunnel is setup:

      diff --git a/www/hip.xml b/www/hip.xml new file mode 100644 index 00000000..0009c320 --- /dev/null +++ b/www/hip.xml @@ -0,0 +1,89 @@ + + + + + + + + + + +

      PAN GlobalProtect HIP

      + +

      The HIP ('Host Integrity Protection') mechanism is a security +scanner for the PAN GlobalProtect +VPNs, in the same vein as Cisco's CSD and Juniper's Host Checker (tncc.jar).

      + +

      How it works

      + +

      It is somewhat less 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.

      + +

      HIP flow used in the official clients:

      +
        +
      1. Client authenticates and fetches the tunnel configuration from the GlobalProtect gateway.
      2. +
      3. Client runs HIP report generator and computes MD5 digest of report.
      4. +
      5. Client checks whether a HIP report is required (/ssl-vpn/hipreportcheck.esp), including its MD5 digest and gateway-assigned IP address in the report.
      6. +
      7. 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).
      8. +
      9. Client uploads the complete HIP report to (/ssl-vpn/hipreport.esp).
      10. +
      11. Server confirms acceptance of HIP report with a success message.
      12. +
      + +

      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:

      + +
        +
      • 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.)
      • +
      • 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.
      • +
      + +

      HIP support in openconnect

      + +

      OpenConnect supports HIP report generation and submission by passing the --csd-wrapper=SCRIPT 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:

      + +
      +   --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.
      +
      + +

      Generating/spoofing a HIP report

      + +

      An example hipreport.sh script is included in the +openconnect distribution.

      + +

      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 PanGPS.log file +(which should be in the same directory as the executables, normally +c:\Program Files\PaloAlto Networks\GlobalProtect) to find +the HIP report submission.

      + + +