Discovering and Exploiting API Attack Surface Using Client-Side Javascript

Graph-X
10 min readJul 5, 2019

--

Have you ever been on an engagement where the client, while providing you with the information you needed to start, didn’t give you the whole picture? You know those bodies in the digital backyard they hoped you don’t find, but somehow always seem to turn up. That happened to me recently on an web app test. We were provided a short engagement window and a lot of application to grey box test. As part of SOP we asked for any API swagger/WSDL information that would help us test and confirm access controls. We got information on a bunch of third party APIs, but the customer said they weren’t using any they had developed. OK, you ask for nothing what do you get?

As it turned out, once I began crawling the site, I noticed all of these calls to a REST API that we were told didn’t exist. I knew what it was instantly because of the URL https://example.com/index.php/foo/rest/bar.

What have we here? Is this a custom API?

I started watching for that particular URL as I moved around the site. I saw a few more endpoints pop up, and I watched the data being sent/received to get a better handle on this thing. I didn’t know it at the time but I would later discover that it was the gears of the entire application. Maybe something that should have been disclosed at the beginning. Usually when someone omits something as big as this, they are already well aware of the issues that plague the undisclosed API. It’s also a big indicator of what to focus on. I figured that I could map this sucker out no problem. Well, a day went by and I got a whopping five very uninteresting endpoints. This API couldn’t have only five endpoints. Then I realize maybe this particular (read-only access) user role only has (legitimate) access to those five endpoints. Great… Now what?! First I needed to figure out how the access controls were implemented.

On one page the application presented dead buttons that teased me with the hope of adding various content to the site, but when I clicked, a tooltip opened telling me that I needed to contact someone to request more access. The culprit was some Javascript calling preventDefault() and then showing a modal instead. I cracked open the dev tools, inspected the button element and chased down the the piece of Javascript that was attached to the event listener for that button. It looked as though the button’s click event listener was determined by the CSS style class it was given on load.

read-only style class dictates button behavior

Once I removed it, the button would preform its default action… Which was nothing. Yeah, not the action I was hoping for, but still getting warmer. I could try one of two things here. I could hunt down a more appropriate class to assign the button by looking in the stylesheet files (there were 8 stylesheets not counting inline and third party), but ain’t nobody got time for that.

Scraping Javascript

At this point, I knew that at least some of the access controls were dictated by the client-side CSS and Javascript. My hope was that it was all they were doing to prevent less privileged users from preforming actions they shouldn’t be able to. One by one, I started to take a look at the Javascript files that got served up with this page. At first it appeared to be served dynamically. Luckily it wasn’t too dynamic. I zeroed in on one file in particular. This was larger than the others, and seemed to be the guts of a form creation tool. Of course it was minified, but that can easily be pasted into Notepad++ and parsed to be more pleasing to the eye. I prefer using the JSTool plugin for Notepad++.

Using the JSTool plugin to pretty print a Javascript file

Dev tools in the browser can also pretty print the Javascript, but the search capabilities are a bit more robust in Notepad++. Next I searched for the keyword “index.php/foo/rest”. I got a hit… Then another, and another. It appeared that all endpoints for creating, saving, deleting and modifying a form within the application were defined within this one file. That explained why it’s so large even though the user I’m rocking is a read-only user.

saveData function syntax, endpoint and expected parameter information

Now that I knew what to look for, I could go through all the Javascript looking for the same “index.php/foo/rest/” string. Within 20 minutes, I had acquired not only all API endpoints, but I also knew what parameters and request methods each of them accepted. I laid them all out in front of me and noticed something odd. One of the endpoints I had first mapped had additional functionality. The endpoint in question was at “/rest/userAdmin/setUserData”. Originally, I mapped this endpoint as part of the user profile section of the app. I had been able to use it to change my name, email, password, etc. Pretty boring.

Post request sent to the setUserData endpoint changing the information for the current user

Abusing Permissive API Access Controls

However, it looked as though the endpoint accepted additional parameters in both the URL and the POST body. I already knew that I had access to that endpoint via my own user profile page, but I questioned whether I could affect change on other users or affect change on my user with those additional known parameters? This also seemed a ripe target for access control configuration issues.

HTML source providing parameters for successful setUserData

One of the pages had a listing of all the admin users. Information listed for each user included: name, email, access role, phone number and some other application specific attributes that I didn’t really care about. I took a look at my user information in this area. I could see my role, adminId, email, etc. I already knew that there were a couple more parameters that could be passed to this endpoint from changing my user information in the settings page, “password” and “password2”. Those attributes didn’t seem to be available on this page though. While I was poking around at the source, I also took a look at what the other roles were called. I was informed that there were three access roles in this app during the scoping phase: view only, editor, and admin. I was not surprised to find that the application contained a fourth role called “super user”. If that role was something that was omitted, then I wanna be that role. That’s got to have all the cool stuff this app can do available to it.

I sent one of the API requests in burp to repeater and took a look at the structure. It was a POST request to the endpoint “index.php/foo/rest/userAdmin/setUserData”. The parameters in the request were limited to only the boring things. Being the same endpoint used in both pages, I wondered what happened if I added in those additional attributes that were available in the admin user listing. things like role, or job title. Job title didn’t seem to affect access so role was the parameter to play with. At first I left role blank. Response 200. JSON in the body {‘data’:[]}. I guess that meant it was accepted. I then reviewed the page that lists all the users and lo and behold my assigned roles had become “unknown”. Not a step in the right direction access wise, but it was still a step forward in getting myself the desired access. Next I tried admin. Again the role I had listed had changed. The user account had been assigned an admin role and all the modals were gone when I would click on buttons. Finally I wanted to try “super user”. 500 Server Error. Huh, maybe it doesn’t like the space. I tried to encode that space a bunch of different ways. I even used an underscore to denote the space and also just remove the space. No joy. It wasn’t liking anything I was feeding it. Most of the time I got an unknown role assignment. When I had a space I get a 500 error. It was time to try a different approach. Just for giggles I tried a number for the role. I start with 0. Unknown role. 1, Viewer. 2, Editor. 3, Admin. 4, super user. Eureka! I was now a super user.

Abusing API to elevate user privileges

So this was neat and all, but if I did bad things as this user, the logs would point to me being the mischief maker. I needed to be able to be someone else. Now, I remembered seeing in the Javascript the ability to call the setUserData endpoint and include adminId= in the url. Taking a closer look, this endpoint was only called in this fashion when one admin was changing the information of another admin. However, it looked like the API endpoint did not use things like password and password2. At least according to the Javascript, you weren’t allowed to change the password there, but you could send a password reset request. This presented us with two paths to account hijacking. One was that we (as the view-only user) could change the email address of the target account and send a password reset. The second option was to ignore what we were told by the Javascript, and to try changing the password of the account using the API even though the Javascript doesn’t specifically include that functionality.

Just because it’s not used, doesn’t mean it’s not an acceptable and valid parameter to send with the request. I spent a solid 10–15 minutes playing with the API, I discovered that any parameter that is a valid name, was accepted by the server. That meant that if I wanted to change the password for an account that wasn’t mine, I just needed to fill out the information like I was changing my account settings, but when sending the request to include the “?adminId=” parameter in the url prior to sending. Testing the theory, I instinctively used adminId=1 since the first user is usually the most permissive simply by virtue of being the first and sometimes only user in a database.

Replay of setUserData endpoint from account profile page but adminID=1 added to end of URL

The response appeared to indicate a successful request. Now the only thing left to do was login as the admin to verify that the information was changed. Sure enough, when entering in the new credentials I had just set for that user, I was able to login as admin 1, which was coincidentally the name of the user that the tooltip had earlier told me to contact for specific access rights. That was not needed now though.

Conclusions

Let’s take a look all the way back to the beginning of this scenario. We’ve come a long way from being a lowly view-only user to becoming an admin user then hijacking the account of a legitimate admin user. Let’s take a look at the how and what could be done to prevent this in the future.

  • Remove unused Javascript functions from js files. — Disclosure of unauthorized API endpoints
    I’m not sure how many people actually take the time to sift through them on an engagement, but I’ve found huge amounts of data and hidden functionality within these files. This may be a little bit of work up front, but in the long run, if you dynamically load Javascript based on permitted access, you won’t have as much potential for abuse from unused, unauthorized or obsolete functions There will also be less code to load which will allow the application to load faster.
  • Do not rely on client-side mechanisms for access controls. — Improper access controls via client-side scripts
    It may look slick and have the appearance of smooth load time, but if you are placing all the access control decision making on your client-side Javascript or CSS, it will eventually fail you. Everything on the client can be modified by the user and therefore cannot be implicitly trusted by the application. Verify all requests server-side as well or you’re going to have a bad time.
  • API endpoints should have a single purpose. — Complicated API endpoints murky the Access Control waters
    In this instance, we were able to use the setUserData endpoint to not only change our own account information, but to also change other user account information. While this is technically the same function of changing user profile information, it could be argued that changing your settings on your profile is a different function than changing the information of another user. In an ideal situation the personal profile setting changes should be handled by a different endpoint with only the ability to change the information of the user that is currently logged in. As well, the ability to change ones own access role should be removed from this endpoint. It is likely the individual who put this together was attempting to keep a DRY (Don’t Repeat Yourself) format. Which would make sense to keep it all in one endpoint since they do have similar functions. However, without the additional access controls, we see that the endpoint is exposed to abuse potential.
  • Don’t try to hide things from the people doing the testing.
    You can try, but it usually doesn’t do any good. What many don’t realize is that when I’m testing an application, I’m spending maybe 10–15% of my time actually looking at the browser. The rest of the time I’m watching all those XMLHttp Requests, every single request to every single page, image and 3rd party script source. If it’s used by the application, we will eventually see it. The problem here is that instead of having all the information up front on the mechanics of an application, I’m spending time that could have been saved on mapping out the functionality that was omitted, hidden, or forgotten about. Also, when I see a REST API that I was told didn’t exist, that is where I’m going to focus my attention. If I wasn’t supposed to find this thing, then there’s likely a reason for it.

--

--

Graph-X

I’m the last person you should be taking advice from