DEV Community

Cover image for Accessing local services with SSRF attacks
anes
anes

Posted on

Accessing local services with SSRF attacks

Introduction

So you thought about a great new business idea for your company: Making an URL-Preview service, that stores the URL for a user and then displays it along with an icon inside a list.
That service should then run locally and get forwarded into the internet by a service like ngrok.
So you start coding and come up with a service that looks as follows: A node.js backend (using express.js) that exposes a few API endpoints so the user can interact with the storage:

app.get('/', (req, res) => {
  res.sendFile('index.html', { root: `${__dirname}/public` });
});

app.get('/api/v1/urls', (req, res) => {
  res.send(userUrls);
});

app.post('/api/v1/add-url', async (req, res) => {
  const url = req.body.url;
  console.log(url);
  console.log(req.body);
  if (!url) {
    return res.status(400).send('URL is required');
  }
  userUrls.push(url);
  res.send('URL added successfully');
});


app.post('/api/v1/fetch', async (req, res) => {
  const url = req.body.url;
  if (!url) {
    return res.status(400).send('URL is required');
  }

  try {
    const response = await axios.get(url);
    console.log(response.data);
    res.send(response.data);
  } catch (error) {
    res.status(500).send('Failed to fetch the URL');
  }
});
Enter fullscreen mode Exit fullscreen mode

And some simple HTML and JavaScript to display everything, and allow the user to add new URLs:

Screenshot of the website, displaying a form

The company architecture

Unbeknownst to you, the company has a few other (security critical) services running:

Image of the company architecture showing various internal services

Both other express.js services aren't meant to be connected to the public internet, but rather to an internal network.

Abusing the vulnerability

Because we don't sanitize the URLs given by the user, we can easily abuse security flaws in the code, which we will be doing in this chapter.

Running the architecture

For testing purposes, I have created this repository, which contains whole server architecture. The README.md contains how to run the project and forward the port trough ngrok.
If you did everything correctly, you should be able to go on your ngrok URL and see something like:

Welcome to your URL storage tool!
Enter fullscreen mode Exit fullscreen mode

Understanding the security flaw

One potential security flaw is that the URL storage app runs on the same network as all the other architecture and also allows us to pass in localhost URLs, without sanitizing them.
To verify that, we first have to see how exactly this storage tool works. For that we add some random domain and submit it. After that we refresh the page:

Image of the first added URL

Now we should check the network tab, to see how much we actually get:

Image of the given URL returning the website

Here we can see following things: The server does the request for us, and we get the whole response back.

Trying to exploit it

Our instinct should be to check what other domains we can get, starting from the more used ports:

localhost:80 <- http
localhost:3306 <- mysql
localhost:3000 <- rails/node
localhost:300x <- additional rails/node apps
Enter fullscreen mode Exit fullscreen mode

By adding both localhost:3000 and localhost:3001 we can see, that those have things running:

Image of a running internal service

What we are now essentially doing is creating this red bridge, to access more than we should:

Image visualising how our request went from one service to the next

Inside the HTML we can see that this page calls a route called:

/api/v1/customer-data
Enter fullscreen mode Exit fullscreen mode

If we try to request that one we get what we came for:

Image of customer data

Causing more damage

Let's get back to port 3000 now, where we saw that something is running:

Image of a web-service on localhost:3000

Following what we know from the first request we made, maybe we can deduce that this service also follows the aforementioned pattern of API URLs:

/api/v1/...
Enter fullscreen mode Exit fullscreen mode

Now we just try known endpoints there, until we arrive at this one:

http://localhost:3000/api/v1/docs
Enter fullscreen mode Exit fullscreen mode

Adding that URL and refreshing, nets us this in the network tab:

Image of the api documentation

Which is everything we need. We can see that this is some sort of database proxy, used internally to more easily query the database.
As we now may have complete SQL-Access we can try to see what tables the database has, by sending it show%20tables as the parameter:

Image showing us sending any sort of SQL queries

If we wanted to get crazy, we could try to query a user and all the pills he is prescribed using this query:

SELECT c.firstname, c.lastname, p.name, p.description, p.price
FROM customers c
JOIN customer_pills cp ON c.id = cp.customer_id
JOIN pills p ON cp.pill_id = p.id
WHERE c.id = (SELECT MIN(id) FROM customers);
Enter fullscreen mode Exit fullscreen mode

Converting this query to a URL that we can pass:

http://localhost:3000/api/v1/db-proxy?query=SELECT%20c.firstname,%20c.lastname,%20p.name,%20p.description,%20p.price%20FROM%20customers%20c%20JOIN%20customer_pills%20cp%20ON%20c.id%20=%20cp.customer_id%20JOIN%20pills%20p%20ON%20cp.pill_id%20=%20p.id%20WHERE%20c.id%20=%20(SELECT%20MIN(id)%20FROM%20customers);
Enter fullscreen mode Exit fullscreen mode

Adding this URL and refreshing we can see:

Image of a user and all his pills

And, as they would say in the movies: WE'RE IN!

The consequences of this breach

Getting root access to a database is bad enough. It gets even worse when that database contains sensitive medical information.
There are lots of ways to abuse this security flaw. One of them being a "double-extortion", which we could do as follows:
First, we dump everything from this database that we can dump. And by that I mean query every table we can see and then store everything we got locally.
Then, we can use our privileges to delete the customer data, and hope that they don't have a backup.
Finally we inform the victim that we want money for two separate things: Returning the deleted data AND not leaking this confidential customer data.

Preventing the exploit

One simple way to prevent this security flaw is by sanitizing the given URLs, by for example returning a 400 every time localhost is mentioned:

app.post('/api/v1/add-url', async (req, res) => {
  const url = req.body.url;
  if (url.includes('localhost')) {
    return res.status(400).send('Invalid URL');
  }
  //...
}
Enter fullscreen mode Exit fullscreen mode

Keep in mind: This isn't enough, as URLs that replace the localhost with 127.0.0.1 are valid and still locally. Please sanitize further in this case!

Image showing that 127.0.0.1 can also be used to get local things

A safer fix

A safer way to fix this, is to have the URL app running on a different network and separate it by a firewall

The OWASP definition

Luckily OWASP has a good article about this exploit, how often it happens and how well it is covered.
Please read that one for further information.

Conclusion

While exploiting vulnerabilities is highly illegal it is still important to understand how they work.
This example showed a very real thread of exposing anything on a local network, especially if (unsanitized) user input is run.
Code Responsibly!

Top comments (0)