Accessing local services with SSRF attacks
Tags: security, webdev
Published: 2024-03-11 16:06:45

## 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:
```javascript
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');
  }
});
```

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

![Screenshot of the website, displaying a form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8k7kwtgzr235b6ica36d.png)

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hdgrdgndfywpqfr5yb0n.png)

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](https://github.com/aneshodza/ssrf-demo), which contains whole server architecture. The [README.md](https://github.com/aneshodza/ssrf-demo/blob/main/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!
```

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jn5g54ay9szqqa4vdmy0.png)

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

![Image of the given URL returning the website](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xxwsvcf4rmilikb4pdoa.png)

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
```
By adding both `localhost:3000` and `localhost:3001` we can see, that those have things running:

![Image of a running internal service](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h0142p93lgzudm19gzjy.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zm650l1t29fbtbv6cv0q.png)

Inside the HTML we can see that this page calls a route called:
```
/api/v1/customer-data
```
If we try to request that one we get what we came for:

![Image of customer data](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/846kx2k5hn3xi4wn02f7.png)

### 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/herhbjwd1vdany2ycarb.png)

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/...
```
Now we just try known endpoints there, until we arrive at this one:
```
http://localhost:3000/api/v1/docs
```
Adding that URL and refreshing, nets us this in the network tab:

![Image of the api documentation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y0d9gripldk55jmzl90v.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ufcptfaeq2dtbz5zal6r.png)

If we wanted to get crazy, we could try to query a user and all the pills he is prescribed using this query:
```sql
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);
```
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);
```

Adding this URL and refreshing we can see:

![Image of a user and all his pills](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c7bdtls48fzwfkr41lmv.png)

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:
```javascript
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');
  }
  //...
}
```
**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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a76p67hw32ct26d4trtz.png)

### 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](https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/) 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!