LucidChart + JavaScript: Breaking limits with AJAX hijacking.

Update: As it turns out the people at LucidChart work as well as their product and I received some e-mail:

I ran across your blog post this morning and wanted to quickly reach out. Thanks for the compliments on LucidChart — glad to hear you’re enjoying the app! We’d love to hear any feedback you have on how we can continue to improve it going forward. On that note, we have to be able to make money to keep the product available and continue to make it better. The Chrome extension that you’ve written can really hurt us. Would you please take down the blog post and not distribute the Chrome extension? There are certainly a number of safeguards we can put into place to prevent the hack from working but we’d prefer to spend that time making the product better for you and our other customers.

After some back and forth they have closed this hole and sent a kind reminder that for those who are cash-starved students: you can obtain the professional version for free.

With that in mind the techniques present here can still be used to manipulate AJAX requests from any source.

LucidChart is the best online diagramming software I have used hands down. Why? It works. It looks nice. It’s not written in Flash (basically every other charting tool: Gliffy, Creately, LovelyCharts, etc). Granted, it has a ways to go before it becomes competitive with the likes of OmniGraffle, but for a web-based solution it’s still very usable.

Unfortunately you’re stuck with being able to put at most 60 objects into your scene with the free (non-trial) account.

We note there’s a little meter labeled “Complexity” which tracks the total number of objects in the scene divided by the total number of maximum allowed objects. That means each time an object is added it must make a calculation against our limit.

Seems like a good place to start hunting for where the limit ends up being set. Open the Web Inspector for that element.

And we end up with the following result:

With trivial insight, in this case, you can see the width of the element is not a whole number which means in all likelihood their calculation works something like Current Objects / Total Allowed Objects * Complexity Container Width. We know the div has an id of “complexity” so we now have to go find out what references that. Time to take out the scripts.

You can see lots of scripts that this page uses, and it takes a little bit to search through all of them, but “lucid.js” seems like a good place to start. Unfortunately, just looking at the path and the file’s contents it looks like the script was generated using Google’s Closure tools. That makes things overly messy and hard to sort through. Thankfully Chrome can undo some of that mess.

Click on the curly braces and like magic all the code will become formatted with spaces and indenting. Wonderful. Next, we need to search for references to complexity. Simply enter it in the search box.

There are quite a few references, but the one we’re interested in looks something like this:

Looking back at our guessed formula, there seems a reasonable candidate here. “document.lg” is divided by “da.Level.document_objects”. So now we need to figure out where “da” gets set from. Back to the search bar.

We include the “=” sign in the search because we’re really only concerned where “da” gets assigned a value. After going through a few of them you will probably come across this little gem:

So they’re using a standard jQuery AJAX request to fetch session data. Well we can find that using the Network panel in the Web Inspector.

And if we look at the content in that file:

Looks like our cap is coming from this request. So the basic approach is simply going to involve stealing that request before it gets passed to the application and overwriting any values we like. Unfortunately this is harder than it sounds. The approach we’re going to take will involve creating a Chrome extension to do the dirty work. Create a folder and in it place a file named “icon.png” to represent your application’s icon; then create 2 files named “background.js” and “manifest.json”.

The manifest file describes our extension. We can use the following JSON for our purposes:

{
	"name": "Lucid Unlimited",
	"version": "1.0",
	"icons": {
		"48": "icon.png"	
	},
	"description": "Play with Lucid Charts.",
	"content_scripts": [{
		"matches": [ "https://www.lucidchart.com/*" ],
		"js": [ "background.js" ],
		"run_at": "document_start"	
	}],
	"permissions": [
		"https://www.lucidchart.com/*"
	],
	"manifest_version": 2
}

It’s pretty self-explanatory. Content scripts are scripts which get attached to the page (in our case we want the hijack script to run before anything else and only on LucidChart pages). Permissions describe on which pages our extension should be allowed to run.

Fire up developer mode for Chrome extensions and load your unpacked extension.

Hijacking the AJAX request is a tricky feat, mostly because Chrome extensions run in “isolated worlds”. While your extension can access the DOM (i.e. manipulate elements on the page) it CANNOT overwrite other variables set by the page. To do hijacking we unfortunately need to do this. After some thinking you too might come to the elegant conclusion: if we can manipulate the DOM, and the DOM can run “script” elements outside the extension’s isolated world then we might just have something.

function injectScript(source) {
	var elem = document.createElement("script"); //Create a new script element
	elem.type = "text/javascript"; //It's javascript
	elem.innerHTML = source; //Assign the source
	document.documentElement.appendChild(elem); //Inject it into the DOM
}

//Test it
injectScript("alert('hello world');");

This creates a bit of an inconvenience though because it means the code we inject has to be a string. It’s far from elegant trying to cram all your patch code into one string, so why not make something that will generate that string for us and let us code like we normally do? We can use the “toString” method of a function to generate all the code for that function.

injectScript("("+(function() {
	alert("Hello world!");
}).toString()+")()");

So now that we’re out of our isolated world, we can really get to work. In order to hijack an AJAX request we need a hook every time it’s created. Overwriting XMLHttpRequest’s “open” method will do the trick.

injectScript("("+(function() {
	//Save the old function
	var proxied = window.XMLHttpRequest.prototype.open;
	//Overwrite with a new one
	window.XMLHttpRequest.prototype.open = function(method, path, async) {
		console.log(arguments); //Log for debugging
		return proxied.apply(this, [].slice.call(arguments)); //Call the old function
	};
}).toString()+")()");

Save the extension and you should see lots of output on the console. You can use this as the basis for intercepting any AJAX request you feel so inclined to.

However, since we only want to mess with the requests we care about, we’re going to have to install a simple check that makes sure we’re getting at the session data.

injectScript("("+(function() {
	//Save the old function
	var proxied = window.XMLHttpRequest.prototype.open;
	//Overwrite with a new one
	window.XMLHttpRequest.prototype.open = function(method, path, async) {
		if (path.match(/\/users\/userSession/)) {
			console.log(arguments); //Log for debugging
		}
		return proxied.apply(this, [].slice.call(arguments)); //Call the old function
	};
}).toString()+")()");

Running it again should result in only the request for the user session being output. Once we have the request we’re interested in, we want to listen for when a response actually comes in. We can accomplish this by adding an event listener. It’s important that useCapture is enabled so that we are always the first person to receive the event due to the way DOM event bubbling works. We listen for when the state of the request changes, and it can change for reasons other than just the content being loaded, so we need to make sure we only do our work when the content is actually ready for us. The state “4″ happens to be the one in which the request has completed.

injectScript("("+(function() {
	//Save the old function
	var proxied = window.XMLHttpRequest.prototype.open;
	//Overwrite with a new one
	window.XMLHttpRequest.prototype.open = function(method, path, async) {
		if (path.match(/\/users\/userSession/)) {
			this.addEventListener('readystatechange', function() {
				if (this.readyState === 4)
					processUserSession(this);
			}, true);
		}
		return proxied.apply(this, [].slice.call(arguments)); //Call the old function
	};
}).toString()+")()");

function processUserSession(request) {
	console.log(request.responseText);
}

Running this, we can see the response is a JSON string. We can use “JSON.parse” to turn it into an object we can work with. And once we can work with it, it means we can set any variable we want, including the one that controls the limit on the number of objects in the scene! Once we’ve done that we just turn our object back into JSON and it’s like nothing ever happened.

function processUserSession(request) {
	var data = JSON.parse(request.responseText);
	data.User.Level.document_objects = 0;
	console.log(JSON.stringify(data));
}

Unfortunately “response” and “responseText” are read-only variables. We can’t assign new values to them. However, there is a neat little trick where we can proxy their true values by defining a getter on top of the original variables. In this case we’re going to override “responseText”.

function bindResponse(request, response) {
	request.__defineGetter__("responseText", function() {
		return response
	})
}

Adding it into our code we get:

function processUserSession(request) {
	var data = JSON.parse(request.responseText);
	data.User.Level.document_objects = 0;
	bindResponse(request, JSON.stringify(data));
}

Interestingly enough the complexity meter disappears for a little, but reappears again. Either we’ve set the wrong variable, or there’s another variable that needs tweaking. If we dig a little deeper into the code we realize there’s also a “chat” command that sets something with an access level type object. We can apply the same trick to the new “chat” request; putting it all together we get this.

function injectScript(source) {
	var elem = document.createElement("script"); //Create a new script element
	elem.type = "text/javascript"; //It's javascript
	elem.innerHTML = source; //Assign the source
	document.documentElement.appendChild(elem); //Inject it into the DOM
}

injectScript("("+(function() {

	function bindResponse(request, response) {
		request.__defineGetter__("responseText", function() {
			return response;
		})
	}

	function processLevel(level) {
		level.document_objects = 0;
		level.watermark = 0;
	}

	function processUserSession(request) {
		var session = JSON.parse(request.responseText);
		processLevel(session.User.Level);
		bindResponse(request, JSON.stringify(session));
	}

	function processChat(request) {
		var chat = JSON.parse(request.responseText);
		processLevel(chat.level);
		bindResponse(request, JSON.stringify(chat));
	}

	var proxied = window.XMLHttpRequest.prototype.open;
	window.XMLHttpRequest.prototype.open = function(method, path, async) {
		if (path.match(/\/users\/userSession/)) {
			this.addEventListener('readystatechange', function() {
				if (this.readyState === 4)
					processUserSession(this);
			}, true);
			
		} else if (path.match(/\/chats\//)) {
			this.addEventListener('readystatechange', function() {
				if (this.readyState === 4)
					processChat(this);
			}, true);
		}

		return proxied.apply(this, [].slice.call(arguments));
	};
}).toString()+")()");

Now there is no more complexity bar and you can use as many objects as you like.

You can use the extension developer tools “Pack extension” tool to create a proper extension.

About these ads

About izaakschroeder
I sell cheese and cheese accessories.

4 Responses to LucidChart + JavaScript: Breaking limits with AJAX hijacking.

  1. A capable piece of reverse engineering, but spoilt by 2 things, 1) The gloating picture at the end and 2) If you’d simply kept quiet you might have been able to use Lucid without limitation for longer. It certainly is a better tool than it’s flash competitors, but as they say themselves, it would be better for them to focus their time on improving it rather than fixing this (which, in fairness, you have helped them locate). Wouldn’t it make more sense to find the holes in software such as this and offer to charge them in return for the details and possible fixes (but try not to sound like a blackmailer ^^)? You could then publish the details after the product has been patched.

    • Thanks for the feedback!

      I find it interesting you consider the picture one of gloating; to me it is merely one of success. As defined by the internet “This image is typically used to express a feeling of accomplishment or success in various situations”: http://knowyourmeme.com/memes/aww-yea-guy.

      I don’t think the article is spoiled by the fact the hole has been patched (a technique for generic AJAX request manipulation remains broadly useful). In terms of my using it without limitation longer, the owner of the site was kind enough to get me an educational account (I go to Simon Fraser University at present) which I could use to finish the diagram required for one of my classes.

      Which leads to your last point: I have, at present, little interest in charging people for things I discover though in general I’d be happy to help them patch whatever hole happens to exist. I write for personal entertainment and educational purposes mostly (though this article came out of a requirement for one of my writing courses).

      Ideally what you suggest is what I should do (contact author, publish exploit after, etc.) and that is general practice in the security community; one day that will very likely wind up happening.

      Judging by your username your either an author of or member of another HTML-based diagramming program. It’s not bad, but does not yet have the same polished feel as Lucid. Nice to hear from people other than just those wanting free things.

      Cheers,

  2. Fair enough, the picture is probably lost in translation to anyone in Europe, smiling causes us pain over here.

    No, I agree, re-reading you’re clearly skilled in the process of reverse engineering and I can’t pretend I haven’t done similar things in my past. Just saying you’d be surprised how crappy people who claim to be experts on this subject claim to be and how much your skills are probably actually worth.

    I thinking more in terms of this blog being something like a resume of your skills if you decided to follow this as a career path and given that they’re the ones who’ll pay and need to trust you, blah blah blah *insert something about trust and love here*. The funny thing about companies, as Lucidchart showed, they tend to be really quite professional and polite, whereas the free users write you message in caps and text speak asking you to assist in wiping their bottoms, I know who I prefer.

    Yes, “we” are that tool. No, I doubt we’ll ever be particularly polished, it’s just a pet project of a couple of us. I would post personally, but none of the logins support anything I have a personal account in (https://plus.google.com/106160348960403302854 is me).

    Best of luck with your studies.

    • Poor Europeans; some of them even have to drive on the wrong side of the road.

      I don’t know if I consider myself necessarily skilled in this trade, but I feel at least competent that I’m not your generic skid. People like Rutkowska, however, dwarf my meager tinkering abilities. I’m glad you think my talent is worth something though and perhaps, like you suggest, I will keep this blog around for noting my capabilities in this field. I think it’s good to be well rounded.

      I’m not sure if I would consider security analysis as a career, though it certainly is interesting. I have also found most professional individuals (that is to say those from companies) tend to be, well, professional about it. I think there is a lot open-minded, motivated individuals can learn from each other. Sadly I have met few, locally, in my field.

      Best of luck with your app!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: