Thursday, 25 February 2016

Reverse-engineering the OpenVPN AS login process

When logging into OpenVPN AS via it's web based frontend it automatically connects by instructing the VPN client to do so. I wondered how exactly it was doing this - so running Fiddler I observed it was using some kind of RPC call over HTTPS (on port 946) to a server named openvpn-client.openvpn.yourdomain.com - which funnily enough is a lookback address (172.27.232.2) on the local machine.

It appears that when installing the OpenVPN client on a users computer it add the host to the hosts file.

So me being me - I was not paticulary happy with the OpenVPN AS web-based interface and it's somewhat lack of aesthetic appeal I decided to implement by own version - this post briefly describes how the login process works so others can build their own versions if desired.

So on the OpenVPN AS web login - after the user has entered thier credentials - the browser sends an RPC call as follows to intruct the client to connect to the VPN server in non-interacrtive mode:

HTTP POST to https://openvpn-client.openvpn.yourdomain.com:946/RPC2

NOTE: The 'X-OpenVPN' header MUST be present in all RPC requests and must equal to '1'.

Headers:

Host: openvpn-client.openvpn.yourdomain.com:946
Connection: keep-alive
Content-Length: 784
X-OpenVPN: 1
Origin: https://openvpn-client.openvpn.yourdomain.com:946
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.116 Safari/537.36
Content-Type: text/xml
Accept: */*
DNT: 1
Referer: https://openvpn-client.openvpn.yourdomain.com:946/?_ts=1456406537206
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.8
Cookie: __utma=158273187.1431551106.1455640284.1456310756.1456331497.4; __utmc=158273187; __utmz=158273187.1456310756.3.2.utmcsr=google|utmccn=(organic)|utmcmd=organic|utmctr=(not%20provided)


<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
   <methodName>Connect</methodName>
   <params>
      <param>
         <value>
            <struct>
               <member>
                  <name>type</name>
                  <value>
                     <string>dynamic</string>
                  </value>
               </member>
               <member>
                  <name>profile_id</name>
                  <value>
                     <string>openvpn_yourdomain_com_dynamic_p3151</string>
                  </value>
               </member>
               <member>
                  <name>non_interactive</name>
                  <value>
                     <boolean>0</boolean>
                  </value>
               </member>
            </struct>
         </value>
      </param>
      <param>
         <value>
            <array>
               <data>
                  <string>STATE</string>
                  <string>PASSWORD</string>
                  <string>ACTIVE</string>
                  <string>CERT_APPROVAL</string>
                  <string>INFO</string>
                  <string>CONNECTED_USER</string>
                  <string>BYTECOUNT</string>
                  <string>FATAL</string>
                  <string>SCRIPT</string>
                  <string>CHALLENGE</string>
                  <string>DELETE_PENDING</string>
               </data>
            </array>
         </value>
      </param>
      <param>
         <value>
            <struct />
         </value>
      </param>
   </params>
</methodCall>

We can replay this with something like Advanced Rest Client addon for chrome.

Response:

<?xml version='1.0'?>
<methodResponse>
<params>
<param>
<value><string>sess_openvpn_yourdomain_com_dynamic_p3151_vmXGe0u27f6ebAGD_1</string></value>
</param>
</params>
</methodResponse>

I beleive this is then returning a session ID that we can then use a reference point for any further RPC's we do.

It then sends a poll with our session ID:

<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
   <methodName>Poll</methodName>
   <params>
      <param>
         <value>
            <string>sess_openvpn_yourdomain_com_dynamic_p3151_vmXGe0u27f6ebAGD_1</string>
         </value>
      </param>
      <param>
         <value>
            <int>10</int>
         </value>
      </param>
   </params>
</methodCall>

We then get a reponse asking us for the credentials:

<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
   <params>
      <param>
         <value>
            <array>
               <data>
                  <value>
                     <struct>
                        <member>
                           <name>status</name>
                           <value>
                              <string>need</string>
                           </value>
                        </member>
                        <member>
                           <name>timestamp</name>
                           <value>
                              <int>1456409871</int>
                           </value>
                        </member>
                        <member>
                           <name>need</name>
                           <value>
                              <array>
                                 <data>
                                    <value>
                                       <string>username</string>
                                    </value>
                                    <value>
                                       <string>password</string>
                                    </value>
                                 </data>
                              </array>
                           </value>
                        </member>
                        <member>
                           <name>type</name>
                           <value>
                              <string>PASSWORD</string>
                           </value>
                        </member>
                        <member>
                           <name>auth_type</name>
                           <value>
                              <string>Dynamic</string>
                           </value>
                        </member>
                     </struct>
                  </value>
               </data>
            </array>
         </value>
      </param>
   </params>
</methodResponse>

So we send another RPC this time sending the login details:

<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
   <methodName>SubmitCreds</methodName>
   <params>
      <param>
         <value>
            <string>sess_openvpn_mydomain_com_dynamic_p3151_vmXGe0u27f6ebAGD_1</string>
         </value>
      </param>
      <param>
         <value>
            <struct>
               <member>
                  <name>username</name>
                  <value>
                     <string>myuser</string>
                  </value>
               </member>
               <member>
                  <name>password</name>
                  <value>
                     <string>SESS_ID_fWrz/SDV511111111ZDA==</string>
                  </value>
               </member>
            </struct>
         </value>
      </param>
      <param>
         <value>
            <string>Dynamic</string>
         </value>
      </param>
      <param>
         <value>
            <boolean>1</boolean>
         </value>
      </param>
   </params>
</methodCall>

We then need to keep POST'ing the 'POLL' method until we get an XML node == '<string>CONNECTED</string>':

We need to look out (apply error handling) for '<string>pyovpn.client.asxmlcli.AuthError</string>' - which indicates that there is an authentication problem and as a result the VPN will drop!

We should also look out for '<string>twisted.internet.defer.TimeoutError</string>' which indicates a connection problem e.g. timeout, dns lookup problems and so on.

<?xml version="1.0" encoding="UTF-8"?>
<methodCall>
   <methodName>Poll</methodName>
   <params>
      <param>
         <value>
            <string>sess_openvpn_yourdomain_com_dynamic_p3151_vmXGe0u27f6ebAGD_1</string>
         </value>
      </param>
      <param>
         <value>
            <int>10</int>
         </value>
      </param>
   </params>
</methodCall>

0 comments:

Post a Comment