dlenski / openconnect

OpenConnect client extended to support Palo Alto Networks' GlobalProtect VPN
679 stars 130 forks source link

2FA: Failed to parse server response #109

Closed antt1v closed 6 years ago

antt1v commented 6 years ago

Problem description

I stumbled on a problem with a GlobalProtect VPN server using 2FA where openconnect doesn't understand the challenge response. Produced as follows:

  1. I ran openconnect-gp: ./openconnect --protocol=gp --usergroup=portal vpn.server.com --user user.name --dump -vvv
  2. Authentication is successful and I recieve the 2FA token via SMS.
  3. Openconnect gets confused with the server response and doesn't prompt for the challenge:
POST https://vpn.domain.com/global-protect/getconfig.esp
Attempting to connect to server 
Connected to 
SSL negotiation with vpn.domain.com
Connected to HTTPS on vpn.domain.com
> POST /global-protect/getconfig.esp HTTP/1.1
> Host: vpn.domain.com
> User-Agent: PAN GlobalProtect
> X-Pad: 00000000000000000000000
> Content-Type: application/x-www-form-urlencoded
> Content-Length: 169
> 
> jnlpReady=jnlpReady&ok=Login&direct=yes&clientVer=4100&prot=https:&clientos=linux-64&server=vpn.domain.com&computer=G0015&user=user.name&passwd=password
Got HTTP response: HTTP/1.1 200 OK
Server: 
Date: Tue, 22 May 2018 10:49:38 GMT
Content-Type: application/xml; charset=UTF-8
Content-Length: 168
Connection: keep-alive
ETag: "573dc-5ef-5a0a2e6e"
Pragma: no-cache
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Expires: Thu, 19 Nov 1981 08:52:00 GMT
X-FRAME-OPTIONS: DENY
Set-Cookie: PHPSESSID=bbaaf9b3131a8f943ef4675172f18b9c; secure; HttpOnly
Set-Cookie: PHPSESSID=bbaaf9b3131a8f943ef4675172f18b9c; secure; HttpOnly
HTTP body length:  (168)
<       <challenge>
<       <user>user.name</user>
<       <inputstr>e708</inputstr>
<       <respmsg>  Wait for token to change,  then enter the new tokencode:</respmsg>
<       </challenge>
Failed to parse server response
Response was:       <challenge>
        <user>user.name</user>
        <inputstr>e708</inputstr>
        <respmsg>  Wait for token to change,  then enter the new tokencode:</respmsg>
        </challenge>
Failed to obtain WebVPN cookie

Operating system and openconnect-gp version

openconnect-gp version:

OpenConnect version v7.08-310-gf873d5b
Using GnuTLS. Features present: PKCS#11, HOTP software token, TOTP software token, System keys, DTLS, ESP
Supported protocols: anyconnect (default), nc, gp

operating system:

Linux G0015 4.13.0-41-generic #46~16.04.1-Ubuntu SMP Thu May 3 10:06:43 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

Thanks in advance!

dlenski commented 6 years ago

Thanks, @antt1v. Lord, I hate GlobalProtect.

All the GlobalProtect VPNs I've seen until now deliver their challenge prompts in the form of a JavaScript blob (yes, JavaScript): https://github.com/dlenski/openconnect/blob/HEAD/gpst.c#L129-L200

var respStatus = "Challenge";
var respMsg = "I herd u liek parsing JavaScript so I sent u sum. PLEEZ ENTER UR CHALLENJ CODE";
thisForm.inputStr.value = "TopSecretRandomString";

… but now you're telling me that there are some GlobalProtect VPNs which respond in a (relatively) saner format with XML:

      <challenge>
      <user>user.name</user>
      <inputstr>e708</inputstr>
      <respmsg>  Wait for token to change,  then enter the new tokencode:</respmsg>
      </challenge>

What can you tell me about the GlobalProtect server? What version of GP is the server running?

antt1v commented 6 years ago

Thanks for taking the time to look into this! Yeah, GlobalProtect is... quite terrible.

Unfortunately I don't have access to the server itself so I don't know all the details. I assume the server version is one of these two as they're inside the server response after a successful authentication:

<   <portal-config-version>4100</portal-config-version>
<   <version>3.0.3-7</version>

I tried the fix in https://github.com/dlenski/openconnect/commit/077c420a0db8ae96b00888726db1a05901dffa82 and now the challenge response is parsed succesfully and I receive the portal config after enterting the challenge token.

However, after that when trying to contact the gateway, it reports authentication failure and goes right back to the challenge prompt:

HTTP body length:  (132)
< 
< var respStatus = "Error";
< var respMsg = "Authentication failure: Invalid username or password";
< thisForm.inputStr.value = "e8aa";
< 
Unexpected 512 result from server
Invalid username or password.
  Wait for token to change,  then enter the new tokencode:
Challenge: 

Re-entering the token pin (or the password) seems to do nothing.

Here's the full output:

./openconnect --protocol=gp --usergroup=portal vpn.domain.com --dump -vvv
Please enter your username and password
Username: user.name
Password: 
POST https://vpn.domain.com/global-protect/getconfig.esp
Attempting to connect to server ip:port
Connected to ip:port
SSL negotiation with vpn.domain.com
Connected to HTTPS on vpn.domain.com
> POST /global-protect/getconfig.esp HTTP/1.1
> Host: vpn.domain.com
> User-Agent: PAN GlobalProtect
> X-Pad: 00000000000000000000000
> Content-Type: application/x-www-form-urlencoded
> Content-Length: 169
> 
> jnlpReady=jnlpReady&ok=Login&direct=yes&clientVer=4100&prot=https:&clientos=linux-64&server=vpn.domain.com&computer=G0015&user=user.name&passwd=password
Got HTTP response: HTTP/1.1 200 OK
Server: 
Date: Wed, 23 May 2018 07:07:05 GMT
Content-Type: application/xml; charset=UTF-8
Content-Length: 168
Connection: keep-alive
ETag: "573dc-5ef-5a0a2e6e"
Pragma: no-cache
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Expires: Thu, 19 Nov 1981 08:52:00 GMT
X-FRAME-OPTIONS: DENY
Set-Cookie: PHPSESSID=04b1ddc8b0194f2ae293ca2541698931; secure; HttpOnly
Set-Cookie: PHPSESSID=04b1ddc8b0194f2ae293ca2541698931; secure; HttpOnly
HTTP body length:  (168)
<       <challenge>
<       <user>user.name</user>
<       <inputstr>e8aa</inputstr>
<       <respmsg>  Wait for token to change,  then enter the new tokencode:</respmsg>
<       </challenge>
  Wait for token to change,  then enter the new tokencode:
Challenge: 
POST https://vpn.domain.com/global-protect/getconfig.esp
> POST /global-protect/getconfig.esp HTTP/1.1
> Host: vpn.domain.com
> User-Agent: PAN GlobalProtect
> Cookie: PHPSESSID=04b1ddc8b0194f2ae293ca2541698931
> X-Pad: 0000000
> Content-Type: application/x-www-form-urlencoded
> Content-Length: 185
> 
> jnlpReady=jnlpReady&ok=Login&direct=yes&clientVer=4100&prot=https:&clientos=linux-64&server=vpn.domain.com&computer=G0015&inputStr=e8aa&user=user.name&passwd=token_pin
Got HTTP response: HTTP/1.1 200 OK
Server: 
Date: Wed, 23 May 2018 07:07:18 GMT
Content-Type: application/xml; charset=UTF-8
Content-Length: 10486
Connection: keep-alive
ETag: "573dc-5ef-5a0a2e6e"
Pragma: no-cache
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Expires: Thu, 19 Nov 1981 08:52:00 GMT
X-FRAME-OPTIONS: DENY
Set-Cookie: PHPSESSID=04b1ddc8b0194f2ae293ca2541698931; secure; HttpOnly
Set-Cookie: PHPSESSID=04b1ddc8b0194f2ae293ca2541698931; secure; HttpOnly
Set-Cookie: PHPSESSID=04b1ddc8b0194f2ae293ca2541698931; secure; HttpOnly
Set-Cookie: PHPSESSID=04b1ddc8b0194f2ae293ca2541698931; secure; HttpOnly
Set-Cookie: PHPSESSID=04b1ddc8b0194f2ae293ca2541698931; secure; HttpOnly
Set-Cookie: PHPSESSID=04b1ddc8b0194f2ae293ca2541698931; secure; HttpOnly
HTTP body length:  (10486)
< <?xml version="1.0" encoding="UTF-8" ?>
< <policy>
<   <portal-name>VPN</portal-name>
<   <portal-config-version>4100</portal-config-version>
<   <version>3.0.3-7                                                         </version>
<   <client-role>global-protect-full</client-role>
<   <agent-user-override-key>****</agent-user-override-key>
<   <root-ca>
<       <entry name="CA Chain">
<           <cert>
< -----BEGIN CERTIFICATE-----
< ****
< -----END CERTIFICATE-----
< -----BEGIN CERTIFICATE-----
< ****
< -----END CERTIFICATE-----
<           </cert>
<           <install-in-cert-store>no</install-in-cert-store>
<       </entry>
<   </root-ca>
<   <connect-method>on-demand</connect-method>
<   <on-demand>yes</on-demand>
<   <refresh-config>yes</refresh-config>
<   <refresh-config-interval>24</refresh-config-interval>
<   <authentication-modifier>
<       <none/>
<   </authentication-modifier>
<   <authentication-override>
<       <accept-cookie>no</accept-cookie>
<       <generate-cookie>yes</generate-cookie>
<       <cookie-lifetime><lifetime-in-hours>24</lifetime-in-hours></cookie-lifetime>
<       <cookie-encrypt-decrypt-cert>GP-Cookie- certificate</cookie-encrypt-decrypt-cert>
<   </authentication-override>
<   <use-sso>yes</use-sso>
<   <gateways>
<       <cutoff-time>5</cutoff-time>
<       <external>
<           <list>
<               <entry name="gateway1.vpn.domain.com">
<                   <priority>1</priority>
<                   <description>gateway1</description>
<               </entry>
<           </list>
<       </external>
<   </gateways>
<   <agent-ui>
<       <can-save-password>yes</can-save-password>
<       <passcode></passcode>
<       <agent-user-override-timeout>0</agent-user-override-timeout>
<       <max-agent-user-overrides>0</max-agent-user-overrides>
<       <help-page></help-page>
<       <welcome-page>
<           <display>no</display>
<           <page></page>
<       </welcome-page>
< <agent-user-override>with-comment</agent-user-override>
< <enable-advanced-view>yes</enable-advanced-view>
< <enable-do-not-display-this-welcome-page-again>yes</enable-do-not-display-this-welcome-page-again>
< <can-change-portal>yes</can-change-portal>
< <show-agent-icon>yes</show-agent-icon>
< <password-expiry-message></password-expiry-message>
< 
<   </agent-ui>
<   <hip-collection>
<       <hip-report-interval>3600</hip-report-interval>
<       <max-wait-time>20</max-wait-time>
<       <collect-hip-data>yes</collect-hip-data>
<       <default>
<           <category>
<               <member>host-info</member>
<               <member>data-loss-prevention</member>
<               <member>patch-management</member>
<               <member>firewall</member>
<               <member>antivirus</member>
<               <member>anti-spyware</member>
<               <member>disk-backup</member>
<               <member>disk-encryption</member>
<           </category>
<       </default>
<   </hip-collection>
<   <agent-config>
<   <save-user-credentials>1</save-user-credentials>
<   <portal-2fa>no</portal-2fa>
<   <internal-gateway-2fa>no</internal-gateway-2fa>
<   <auto-discovery-external-gateway-2fa>no</auto-discovery-external-gateway-2fa>
<   <manual-only-gateway-2fa>no</manual-only-gateway-2fa>
< <client-upgrade>prompt</client-upgrade>
< <logout-remove-sso>yes</logout-remove-sso>
< <krb-auth-fail-fallback>yes</krb-auth-fail-fallback>
< <retry-tunnel>30</retry-tunnel>
< <retry-timeout>5</retry-timeout>
< <enforce-globalprotect>no</enforce-globalprotect>
< <captive-portal-exception-timeout>0</captive-portal-exception-timeout>
< <traffic-blocking-notification-delay>15</traffic-blocking-notification-delay>
< <display-traffic-blocking-notification-msg>yes</display-traffic-blocking-notification-msg>
< <traffic-blocking-notification-msg>&lt;div style=&quot;font-family:'Helvetica Neue';&quot;&gt;&lt;h1 style=&quot;color:red;text-align:center; margin: 0; font-size: 30px;&quot;&gt;Notice&lt;/h1&gt;&lt;p style=&quot;margin: 0;font-size: 15px; line-height: 1.2em;&quot;&gt;To access the network, you must first connect to GlobalProtect.&lt;/p&gt;&lt;/div&gt;</traffic-blocking-notification-msg>
< <allow-traffic-blocking-notification-dismissal>yes</allow-traffic-blocking-notification-dismissal>
< <display-captive-portal-detection-msg>no</display-captive-portal-detection-msg>
< <captive-portal-detection-msg>&lt;div style=&quot;font-family:'Helvetica Neue';&quot;&gt;&lt;h1 style=&quot;color:red;text-align:center; margin: 0; font-size: 30px;&quot;&gt;Captive Portal Detected&lt;/h1&gt;&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.2em;&quot;&gt;GlobalProtect has temporarily permitted network access for you to connect to the Internet. Follow instructions from your internet provider.&lt;/p&gt;&lt;p style=&quot;margin: 0; font-size: 15px; line-height: 1.2em;&quot;&gt;If you let the connection time out, open GlobalProtect and click Connect to try again.&lt;/p&gt;&lt;/div&gt;</captive-portal-detection-msg>
< <certificate-store-lookup>user-and-machine</certificate-store-lookup>
< <scep-certificate-renewal-period>7</scep-certificate-renewal-period>
< <ext-key-usage-oid-for-client-cert></ext-key-usage-oid-for-client-cert>
< <retain-connection-smartcard-removal>yes</retain-connection-smartcard-removal>
< <rediscover-network>yes</rediscover-network>
< <resubmit-host-info>yes</resubmit-host-info>
< <can-continue-if-portal-cert-invalid>yes</can-continue-if-portal-cert-invalid>
< <user-switch-tunnel-rename-timeout>0</user-switch-tunnel-rename-timeout>
< <pre-logon-tunnel-rename-timeout>-1</pre-logon-tunnel-rename-timeout>
< <show-system-tray-notifications>no</show-system-tray-notifications>
< <max-internal-gateway-connection-attempts>0</max-internal-gateway-connection-attempts>
< <portal-timeout>30</portal-timeout>
< <connect-timeout>60</connect-timeout>
< <receive-timeout>30</receive-timeout>
< <flush-dns>no</flush-dns>
< <proxy-multiple-autodetect>no</proxy-multiple-autodetect>
< <wsc-autodetect>yes</wsc-autodetect>
< <ipv6-preferred>yes</ipv6-preferred>
< 
<   </agent-config>
< <user-email>user.name@domain.com</user-email>
< <portal-userauthcookie>****</portal-userauthcookie>
< <portal-prelogonuserauthcookie>empty</portal-prelogonuserauthcookie>
< <scep-cert-auth-cookie>****</scep-cert-auth-cookie>
< </policy>
1 gateway servers available:
  gateway1(gateway1.vpn.domain.com)
Please select GlobalProtect gateway.
GATEWAY: [gateway1]:gateway1
POST https://gateway1.vpn.domain.com/ssl-vpn/login.esp
Attempting to connect to server ip:port
Connected to ip:port
SSL negotiation with gateway1.vpn.domain.com
Connected to HTTPS on gateway1.vpn.domain.com
> POST /ssl-vpn/login.esp HTTP/1.1
> Host: gateway1.vpn.domain.com
> User-Agent: PAN GlobalProtect
> X-Pad: 00000000000000000000000000000000000000000000000000000000000
> Content-Type: application/x-www-form-urlencoded
> Content-Length: 197
> 
> jnlpReady=jnlpReady&ok=Login&direct=yes&clientVer=4100&prot=https:&clientos=linux-64&server=gateway1.vpn.domain.com&computer=G0015&inputStr=e8aa&user=user.name&passwd=token_pin
Got HTTP response: HTTP/1.1 512 Custom error
Server: 
Date: Wed, 23 May 2018 07:07:18 GMT
Content-Type: text/html
Content-Length: 132
Connection: keep-alive
ETag: "573f3-1e6f-5a0a2e6e"
Pragma: no-cache
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
x-private-pan-sslvpn: auth-failed
Expires: Thu, 19 Nov 1981 08:52:00 GMT
X-FRAME-OPTIONS: DENY
Set-Cookie: PHPSESSID=d234a7495584b0cc6fef18741041a0c0; secure; HttpOnly
Set-Cookie: PHPSESSID=d234a7495584b0cc6fef18741041a0c0; secure; HttpOnly
Set-Cookie: PHPSESSID=d234a7495584b0cc6fef18741041a0c0; secure; HttpOnly
HTTP body length:  (132)
< 
< var respStatus = "Error";
< var respMsg = "Authentication failure: Invalid username or password";
< thisForm.inputStr.value = "e8aa";
< 
Unexpected 512 result from server
Invalid username or password.
  Wait for token to change,  then enter the new tokencode:
Challenge: 
dlenski commented 6 years ago

However, after that when trying to contact the gateway, it reports authentication failure and goes right back to the challenge prompt:

When logging in via the portal interface, the current behavior is (a) do the portal login and (b) if the portal login succeeds, reuse the same credentials from the portal form to attempt to login to the gateway.

The problem here, I think, is that the credentials being reused are those of the 2FA challenge login (username, token code, inputStr) rather than the "original" login (username, password).

Is that a correct statement?

What happens if you skip the portal entirely, and just connect to https://gateway1.vpn.domain.com/gateway? There are very few cases I've seen where the portal login is actually necessary or desirable… and they mainly involve weird external web-based authentication mechanisms.

Re-entering the token pin (or the password) seems to do nothing.

Yeah …

dlenski commented 6 years ago

27c5568 should fix this, though it will require you to re-enter the original password again for the gateway.

I really need to clean up the GP form handling, but the problem is that I don't know all the possible weird things that can happen, such as this one :(

antt1v commented 6 years ago

The problem here, I think, is that the credentials being reused are those of the 2FA challenge login (username, token code, inputStr) rather than the "original" login (username, password). Is that a correct statement?

Ah, that makes perfect sense. You are correct.

What happens if you skip the portal entirely, and just connect to https://gateway1.vpn.domain.com/gateway?

Great idea, that worked perfectly! I have some trouble with the DNS server behind the VPN not being used, but I'm guessing that's more likely due to my local setup rather than skipping the portal. I'll work it out. Other than that, no issues.

For the record, the challenge prompt from the gateway was in JavaScript, not XML like from the portal.

https://github.com/dlenski/openconnect/commit/27c556829a2c0d21d2ee5035b56365056feef96a should fix this, though it will require you to re-enter the original password again for the gateway.

I gave this a try but no luck. The inputStr got screwed up and it goes back to the challenge prompt. Here's the result:

1 gateway servers available:
  gateway1 (gateway1.vpn.domain.com)
Please select GlobalProtect gateway.
GATEWAY: [gateway1]:gateway1
0L�
Challenge: 
POST https://gateway1.vpn.domain.com/ssl-vpn/login.esp
Attempting to connect to server ip:port
Connected to ip:port
SSL negotiation with gateway1.vpn.domain.com
Connected to HTTPS on gateway1.vpn.domain.com
> POST /ssl-vpn/login.esp HTTP/1.1
> Host: gateway1.vpn.domain.com
> User-Agent: PAN GlobalProtect
> X-Pad: 0000000000000000000000000000000000000000000000000000000
> Content-Type: application/x-www-form-urlencoded
> Content-Length: 201
> 
> jnlpReady=jnlpReady&ok=Login&direct=yes&clientVer=4100&prot=https:&clientos=linux-64&server=gateway1.vpn.domain.com&computer=G0015&inputStr=p%c3%9a%01&user=user.name&passwd=password
Got HTTP response: HTTP/1.1 512 Custom error
Server: 
Date: Wed, 23 May 2018 09:37:29 GMT
Content-Type: text/html
Content-Length: 139
Connection: keep-alive
ETag: "573f3-1e6f-5a0a2e6e"
Pragma: no-cache
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
x-private-pan-sslvpn: auth-failed
Expires: Thu, 19 Nov 1981 08:52:00 GMT
X-FRAME-OPTIONS: DENY
Set-Cookie: PHPSESSID=e12dc946f6174c97661bb3ecc4e61748; secure; HttpOnly
Set-Cookie: PHPSESSID=e12dc946f6174c97661bb3ecc4e61748; secure; HttpOnly
Set-Cookie: PHPSESSID=e12dc946f6174c97661bb3ecc4e61748; secure; HttpOnly
HTTP body length:  (139)
< 
< var respStatus = "Error";
< var respMsg = "Authentication failure: Invalid username or password";
< thisForm.inputStr.value = "p&Atilde;�";
< 
Unexpected 512 result from server
Invalid username or password.

Challenge: 

In any case I'm personally fine skipping the portal and using the gateway. Thanks for the help! :)

If you do choose to continue improving the form handling I'd be happy to test any changes.

dlenski commented 6 years ago

For the record, the challenge prompt from the gateway was in JavaScript, not XML like from the portal.

:man_facepalming:

Internal consistency is not one of the strong points of the GP authentication flow. And that's the nicest possible thing I could say about it.

If you do choose to continue improving the form handling I'd be happy to test any changes.

I see what I screwed up in my haste. Will fix tomorrow.

dlenski commented 6 years ago

@antt1v, please let me know if 49a7074 fixed the problem with the portal. It'll be good to know that this case works, even if it's not necessary for you to use it.

antt1v commented 6 years ago

I can confirm that the portal authentication now works at least for me. Basically everything is done twice, first with the portal and then with the gateway. The authentication prompts are:

  1. Portal username
  2. Portal password
  3. Portal token challenge
  4. GW username
  5. GW password
  6. GW token challenge (a new one)

Not the prettiest, but it works! Thanks! :)

dlenski commented 6 years ago

Great, thanks for filling me in!

I can confirm that the portal authentication now works at least for me. Basically everything is done twice, first with the portal and then with the gateway.

Yeah, that's what I expected :slightly_frowning_face:. I assume that when you do this with the official client, the portal passes off some kind of "portal authentication cookie" to prevent the gateway from having to redo the login and challenge.

Except for (4): it re-prompts you for the username? That's not supposed to happen. I wrote a quick-and-dirty GlobalProtect server "simulator" and used it to test this scenario, and at least with the simulator it does not reprompt for username…

screenshot

antt1v commented 6 years ago

Ah, my bad! Step 4 doesn't actually happen, just the passwords.