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');
}
});
And some simple HTML and JavaScript to display everything, and allow the user to add new URLs:
The company architecture
Unbeknownst to you, the company has a few other (security critical) services running:
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!
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:
Now we should check the network tab, to see how much we actually get:
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:
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:
Causing more damage
Let’s get back to port 3000 now, where we saw that something is running:
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:
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:
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);
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:
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');
}
//...
}
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!
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!