UNPKG

cdk-interactsh-instance

Version:

This construct implements an easy-to-use, cheap-to-run Interact.sh instance. It is a semi-highly available implementation where only a single server is running at a time, but an AutoScaling Group is used to achieve high availability: In case the instance

299 lines (261 loc) 11.2 kB
import { RemovalPolicy, Size, Stack, Tags } from 'aws-cdk-lib'; import { AutoScalingGroup, IAutoScalingGroup } from 'aws-cdk-lib/aws-autoscaling'; import { AmazonLinuxCpuType, AmazonLinuxEdition, AmazonLinuxGeneration, AmazonLinuxImage, BlockDeviceVolume, CfnEIP, EbsDeviceVolumeType, IKeyPair, ILaunchTemplate, ISecurityGroup, IVolume, IVpc, InstanceType, LaunchTemplate, LaunchTemplateHttpTokens, Peer, Port, SecurityGroup, SubnetType, UserData, Volume, } from 'aws-cdk-lib/aws-ec2'; import { Effect, IInstanceProfile, IRole, InstanceProfile, ManagedPolicy, PolicyStatement, Role, ServicePrincipal, } from 'aws-cdk-lib/aws-iam'; import { Construct } from 'constructs'; export interface InteractshInstanceProps { domains: string | string[]; // Domain or list of domains to listen for eip: CfnEIP; // Elastic IP address the instance associates with itself image?: string; // Docker image, defaults to projectdiscovery/interactsh-server:latest keyPair: IKeyPair; // SSH keypair used to connect to the instance token: string; // Authentication token volumeSize?: number; // Size of persistent storage volume vpc: IVpc; // VPC in which to place the instance (public subnet in first availability zone is used) } interface UserDataProps { domains: string; eipAllocationId: string; image: string; region: string; token: string; volume: string; } const DefaultImage = 'projectdiscovery/interactsh-server:latest'; const DefaultVolumeSize = 10; const createUserData = (props: UserDataProps) => `#!/bin/bash set -e # Acquire the instance id token=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 3600") instance_id=$(curl -s -H "X-aws-ec2-metadata-token: \${token}" "http://169.254.169.254/latest/meta-data/instance-id") # TODO: Should we force detach the volume if its attached already? # Attach persistent storage volume aws ec2 attach-volume --device xvdb --instance-id \${instance_id} --volume-id ${props.volume} # Update packages yum update yum upgrade -y # Install and enable Docker yum install -y docker systemctl enable docker systemctl start docker # Pull the image docker pull "${props.image}" # Wait for the volume attachment to finalize before moving on aws ec2 wait volume-in-use --volume-ids ${props.volume} # Create a filesystem on the volume (if one does not exist) and persist the mount and mount it xfs_info /dev/xvdb > /dev/null || mkfs.xfs /dev/xvdb echo "UUID=$(blkid -s UUID -o value /dev/xvdb) /var/lib/interactsh-server xfs defaults,noatime 1 2" >> /etc/fstab mkdir -p /var/lib/interactsh-server mount /var/lib/interactsh-server # Start Interact.sh mkdir -p /var/lib/interactsh-server/{data,www,ftp} docker run --name interactsh-server --detach --restart always \ -p 21:21/tcp -p 25:25/tcp -p 53:53/tcp -p 53:53/udp -p 80:80/tcp -p 389:389/tcp \ -p 443:443/tcp -p 465:465/tcp -p 587:587/tcp \ -v /var/lib/interactsh-server/data:/data \ -v /var/lib/interactsh-server/www:/www \ -v /var/lib/interactsh-server/ftp:/ftp \ "${props.image}" \ -domain ${props.domains} -token ${props.token} \ -ldap -smb -ftp -wildcard \ -disk -disk-path /data \ -http-directory /www \ -ftp-dir /ftp \ -disable-version -disable-update-check # We're ready to accept connections, grab the public IP address aws ec2 associate-address \ --instance-id="\${instance_id}" \ --allocation-id="${props.eipAllocationId}" \ --region="${props.region}" `; export class InteractshInstance extends Construct { constructor(scope: Construct, id: string, props: InteractshInstanceProps) { super(scope, id); // Create role and instance profile const role = new Role(this, `${id}-role`, { assumedBy: new ServicePrincipal('ec2.amazonaws.com'), description: 'Role for Interact.sh instance to use.', }); const instanceProfile = new InstanceProfile(this, `${id}-instance-profile`, { role: role, }); // Select the first AZ and create a volume for persistent data in it const availabilityZone = props.vpc.availabilityZones[0]; const volume = new Volume(this, `${id}-volume`, { availabilityZone: availabilityZone, encrypted: true, iops: 3000, removalPolicy: RemovalPolicy.SNAPSHOT, size: Size.gibibytes(props.volumeSize ? props.volumeSize : DefaultVolumeSize), throughput: 125, volumeType: EbsDeviceVolumeType.GP3, }); Tags.of(volume).add('Name', 'Interactsh-persistent-volume'); // Create the managed policy and assign it to the role this.createManagedPolicy(`${id}-role-policy`, role, props.eip, volume); // Create user data object const userData = UserData.custom( createUserData({ domains: Array.isArray(props.domains) ? props.domains.join(',') : props.domains, eipAllocationId: props.eip.attrAllocationId, image: DefaultImage, region: Stack.of(this).region, token: props.token, volume: volume.volumeId, }) ); // Create a security group const securityGroup = this.createSecurityGroup(`${id}-sg`, props.vpc); // Create a launch template const launchTemplate = this.createLaunchTemplate( `${id}-launch-template`, userData, instanceProfile, securityGroup, props.keyPair ); // And finally create the auto scaling group this.createAutoScalingGroup(`${id}-asg`, launchTemplate, props.vpc, availabilityZone); } private createSecurityGroup(id: string, vpc: IVpc): ISecurityGroup { const securityGroup = new SecurityGroup(this, id, { allowAllIpv6Outbound: true, allowAllOutbound: true, vpc: vpc, }); const ports: { port: Port; description: string }[] = [ { port: Port.tcp(21), description: 'Interact.sh server: Permit FTP TCP' }, { port: Port.tcp(25), description: 'Interact.sh server: Permit SMTP TCP' }, { port: Port.tcp(53), description: 'Interact.sh server: Permit DNS TCP' }, { port: Port.tcp(80), description: 'Interact.sh server: Permit HTTP TCP' }, { port: Port.tcp(389), description: 'Interact.sh server: Permit LDAP TCP' }, { port: Port.tcp(443), description: 'Interact.sh server: Permit HTTPS TCP' }, // { port: Port.tcp(445), description: 'Interact.sh server: Permit SMB TCP' }, { port: Port.tcp(465), description: 'Interact.sh server: Permit SMTP AUTOTLS TCP' }, { port: Port.tcp(587), description: 'Interact.sh server: Permit SMTPS TCP' }, { port: Port.udp(53), description: 'Interact.sh server: Permit DNS UDP' }, ]; // Allow both IPv4 and IPv6 ports.forEach(({ port, description }) => { securityGroup.addIngressRule(Peer.anyIpv4(), port, `${description} IPv4`); securityGroup.addIngressRule(Peer.anyIpv6(), port, `${description} IPv6`); }); return securityGroup; } private createLaunchTemplate( id: string, userData: UserData, instanceProfile: IInstanceProfile, securityGroup: ISecurityGroup, keyPair: IKeyPair ): ILaunchTemplate { // Create an encrypted volume (deleted on termination) for the instance const volume = BlockDeviceVolume.ebs(10, { deleteOnTermination: true, encrypted: true, volumeType: EbsDeviceVolumeType.GP3, }); const launchTemplate = new LaunchTemplate(this, id, { associatePublicIpAddress: true, // Temporary public ip address is needed so we can call AWS APIs blockDevices: [{ deviceName: '/dev/xvda', volume: volume }], detailedMonitoring: false, ebsOptimized: true, httpEndpoint: true, httpTokens: LaunchTemplateHttpTokens.REQUIRED, instanceProfile: instanceProfile, instanceType: new InstanceType('t4g.nano'), keyPair: keyPair, machineImage: new AmazonLinuxImage({ cachedInContext: false, cpuType: AmazonLinuxCpuType.ARM_64, edition: AmazonLinuxEdition.STANDARD, generation: AmazonLinuxGeneration.AMAZON_LINUX_2023, }), requireImdsv2: true, userData: userData, securityGroup: securityGroup, }); return launchTemplate; } private createAutoScalingGroup( id: string, launchTemplate: ILaunchTemplate, vpc: IVpc, availabilityZone: string ): IAutoScalingGroup { const autoScalingGroup = new AutoScalingGroup(this, id, { launchTemplate: launchTemplate, maxCapacity: 1, minCapacity: 1, vpc: vpc, vpcSubnets: { availabilityZones: [availabilityZone] }, }); // Apply a tag to the instances so we can use it as a condition in IAM policies Tags.of(autoScalingGroup).add('Interactsh', 'server', { applyToLaunchedInstances: true }); return autoScalingGroup; } private createManagedPolicy(id: string, role: IRole, eip: CfnEIP, volume: IVolume): void { const account = Stack.of(this).account; const partition = Stack.of(this).partition; const region = Stack.of(this).region; const policy = new ManagedPolicy(this, id, { statements: [ new PolicyStatement({ actions: ['ec2:AssociateAddress'], effect: Effect.ALLOW, resources: [`arn:${partition}:ec2:${region}:${account}:elastic-ip/${eip.attrAllocationId}`], }), new PolicyStatement({ actions: ['ec2:AttachVolume'], effect: Effect.ALLOW, resources: [`arn:${partition}:ec2:${region}:${account}:volume/${volume.volumeId}`], }), new PolicyStatement({ actions: ['ec2:DescribeVolumes'], effect: Effect.ALLOW, resources: ['*'], }), new PolicyStatement({ actions: ['ec2:AssociateAddress', 'ec2:AttachVolume', 'ec2:DetachVolume'], conditions: { StringEquals: { 'aws:ResourceTag/Interactsh': 'server' }, }, effect: Effect.ALLOW, resources: [`arn:${partition}:ec2:${region}:${account}:instance/*`], }), ], }); role.addManagedPolicy(policy); role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')); } }