├── applicationHost.xdt ├── LICENSE ├── README.md ├── amazon.profile └── cs2webconfig.py /applicationHost.xdt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2021, Jesse 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Automatically Generate Rulesets for IIS for Intelligent HTTP/S C2 Redirection 2 | This project converts a Cobalt Strike profile to a functional web.config file to support HTTP/S reverse proxy redirection from IIS to a Cobalt Strike teamserver. 3 | 4 | This is a spiritual counterpart to [cs2modrewrite](https://github.com/threatexpress/cs2modrewrite). 5 | 6 | --------------------------------------------------- 7 | #### cs2webconfig.py 8 | Script to generate web.config files for IIS servers based on Cobalt Strike malleable profiles. 9 | 10 | **Usage:** 11 | 12 | `python cs2webconfig.py -t -p -r -o ` 13 | 14 | 15 | --------------------------------------------------- 16 | #### applicationHost.xdt 17 | Template file needed by IIS servers to enable proxying similar to apache2 mod_proxy. Upload to the `site` parent folder of the IIS server, then restart the IIS service. 18 | 19 | 20 | --------------------------------------------------- 21 | #### Final Thoughts 22 | 23 | Once redirection is configured and functioning, ensure your C2 servers only allow ingress from the redirector and your trusted IPs (VPN, office ranges, etc). 24 | 25 | For a quick walkthrough on how to use this with Azure Application Services, check out the [wiki](https://github.com/bashexplode/cs2webconfig/wiki/Azure-Web-Application-Service-Usage)! 26 | 27 | IIS servers require the teamserver has a valid SSL certificate from a trusted provider. Let's Encrypt is a valid option. 28 | -------------------------------------------------------------------------------- /amazon.profile: -------------------------------------------------------------------------------- 1 | # 2 | # Amazon browsing traffic profile 3 | # 4 | # Author: @harmj0y 5 | # 6 | 7 | set sleeptime "5000"; 8 | set jitter "0"; 9 | set maxdns "255"; 10 | set useragent "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"; 11 | 12 | http-get { 13 | 14 | set uri "/s/ref=nb_sb_noss_1/167-3294888-0262949/field-keywords=books"; 15 | 16 | client { 17 | 18 | header "Accept" "*/*"; 19 | header "Host" "www.amazon.com"; 20 | 21 | metadata { 22 | base64; 23 | prepend "session-token="; 24 | prepend "skin=noskin;"; 25 | append "csm-hit=s-24KU11BB82RZSYGJ3BDK|1419899012996"; 26 | header "Cookie"; 27 | } 28 | } 29 | 30 | server { 31 | 32 | header "Server" "Server"; 33 | header "x-amz-id-1" "THKUYEZKCKPGY5T42PZT"; 34 | header "x-amz-id-2" "a21yZ2xrNDNtdGRsa212bGV3YW85amZuZW9ydG5rZmRuZ2tmZGl4aHRvNDVpbgo="; 35 | header "X-Frame-Options" "SAMEORIGIN"; 36 | header "Content-Encoding" "gzip"; 37 | 38 | output { 39 | print; 40 | } 41 | } 42 | } 43 | 44 | http-post { 45 | 46 | set uri "/N4215/adj/amzn.us.sr.aps"; 47 | 48 | client { 49 | 50 | header "Accept" "*/*"; 51 | header "Content-Type" "text/xml"; 52 | header "X-Requested-With" "XMLHttpRequest"; 53 | header "Host" "www.amazon.com"; 54 | 55 | parameter "sz" "160x600"; 56 | parameter "oe" "oe=ISO-8859-1;"; 57 | 58 | id { 59 | parameter "sn"; 60 | } 61 | 62 | parameter "s" "3717"; 63 | parameter "dc_ref" "http%3A%2F%2Fwww.amazon.com"; 64 | 65 | output { 66 | base64; 67 | print; 68 | } 69 | } 70 | 71 | server { 72 | 73 | header "Server" "Server"; 74 | header "x-amz-id-1" "THK9YEZJCKPGY5T42OZT"; 75 | header "x-amz-id-2" "a21JZ1xrNDNtdGRsa219bGV3YW85amZuZW9zdG5rZmRuZ2tmZGl4aHRvNDVpbgo="; 76 | header "X-Frame-Options" "SAMEORIGIN"; 77 | header "x-ua-compatible" "IE=edge"; 78 | 79 | output { 80 | print; 81 | } 82 | } 83 | 84 | http-stager { 85 | set uri_x86 "/robots2.txt"; 86 | set uri_x64 "/robots.txt"; 87 | 88 | client { 89 | header "Accept" "text/html,*/*"; 90 | header "Accept-Language" "en-US,en;q=0.5"; 91 | } 92 | 93 | server { 94 | header "Content-Type" "text/html"; 95 | header "Connection" "close"; 96 | output { 97 | prepend "User-agent: *\r\nDisallow: /\r\n# Ref ID: "; 98 | print; 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /cs2webconfig.py: -------------------------------------------------------------------------------- 1 | # script by Jesse Nebling (@bashexplode) 2 | # idea based off of cs2modrewrite by ThreatExpress (github.com/threatexpress/cs2modrewrite) 3 | 4 | import argparse 5 | import sys 6 | import re 7 | import os 8 | 9 | 10 | class CS2WebConfigRewrite: 11 | def __init__(self, teamserver, redirector, profile): 12 | self.c2server = teamserver 13 | self.redirect = redirector 14 | self.c2profile = profile 15 | 16 | self.webconfig_template = ''' 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ''' 62 | 63 | def webconfigparse(self): 64 | regex = re.compile( 65 | r'^(?:http|ftp)s?://' # http:// or https:// 66 | r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... 67 | r'localhost|' # localhost... 68 | r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip 69 | r'(?::\d+)?' # optional port 70 | r'(?:/?|[/?]\S+)$', re.IGNORECASE) 71 | 72 | if re.match(regex, self.c2server) is None: 73 | print("[!] c2server is malformed. Are you sure {} is a valid URL?".format(self.c2server)) 74 | sys.exit(1) 75 | 76 | profile = open(self.c2profile, "r") 77 | contents = profile.read() 78 | 79 | # Strip all single line comments (#COMMENT\n) from profile before searching so it doens't break our crappy parsing 80 | contents = re.sub(re.compile("#.*?\n"), "", contents) 81 | 82 | # Search Strings 83 | ua_string = "set useragent" 84 | http_get = "http-get" 85 | http_post = "http-post" 86 | set_uri = "set uri " 87 | 88 | http_stager = "http-stager" 89 | set_uri_86 = "set uri_x86" 90 | set_uri_64 = "set uri_x64" 91 | 92 | # Errors 93 | errorfound = False 94 | errors = "\n##########\n[!] ERRORS\n" 95 | 96 | # Get UserAgent 97 | if contents.find(ua_string) == -1: 98 | ua = "" 99 | errors += "[!] User-Agent Not Found\n" 100 | errorfound = True 101 | else: 102 | ua_start = contents.find(ua_string) + len(ua_string) 103 | ua_end = contents.find("\n", ua_start) 104 | ua = contents[ua_start:ua_end].strip()[1:-2] 105 | 106 | # Get HTTP GET URIs 107 | http_get_start = contents.find(http_get) 108 | if contents.find(set_uri) == -1: 109 | get_uri = "" 110 | errors += "[!] GET URIs Not Found\n" 111 | errorfound = True 112 | else: 113 | get_uri_start = contents.find(set_uri, http_get_start) + len(set_uri) 114 | get_uri_end = contents.find("\n", get_uri_start) 115 | get_uri = contents[get_uri_start:get_uri_end].strip()[1:-2] 116 | 117 | # Get HTTP POST URIs 118 | http_post_start = contents.find(http_post) 119 | if contents.find(set_uri) == -1: 120 | post_uri = "" 121 | errors += "[!] POST URIs Not Found\n" 122 | errorfound = True 123 | else: 124 | post_uri_start = contents.find(set_uri, http_post_start) + len(set_uri) 125 | post_uri_end = contents.find("\n", post_uri_start) 126 | post_uri = contents[post_uri_start:post_uri_end].strip()[1:-2] 127 | 128 | # Get HTTP Stager URIs x86 129 | http_stager_start = contents.find(http_stager) 130 | if contents.find(set_uri_86) == -1: 131 | stager_uri_86 = "" 132 | errors += "[!] x86 Stager URIs Not Found\n" 133 | errorfound = True 134 | else: 135 | stager_uri_start = contents.find(set_uri_86, http_stager_start) + len(set_uri_86) 136 | stager_uri_end = contents.find("\n", stager_uri_start) 137 | stager_uri_86 = contents[stager_uri_start:stager_uri_end].strip()[1:-2] 138 | 139 | # Get HTTP Stager URIs x64 140 | http_stager_start = contents.find(http_stager) 141 | if contents.find(set_uri_64) == -1: 142 | stager_uri_64 = "" 143 | errors += "[!] x64 Stager URIs Not Found\n" 144 | errorfound = True 145 | else: 146 | stager_uri_start = contents.find(set_uri_64, http_stager_start) + len(set_uri_64) 147 | stager_uri_end = contents.find("\n", stager_uri_start) 148 | stager_uri_64 = contents[stager_uri_start:stager_uri_end].strip()[1:-2] 149 | 150 | # Create URIs list - workaround only accepts 1 uri right now 151 | get_uris = get_uri.split()[0] 152 | post_uris = post_uri.split()[0] 153 | stager86_uris = stager_uri_86.split()[0] 154 | stager64_uris = stager_uri_64.split()[0] 155 | 156 | # Create UA in web.config rewrite syntax. No regex needed in UA string matching, but (). characters must be escaped 157 | ua_string = ua.replace('(', '\(').replace(')', '\)').replace('.', '\.') 158 | 159 | print("#### Save the following as web.config in the root web directory") 160 | print(self.webconfig_template.format(uri1=get_uris[1:], uri2=post_uris[1:], uri3=stager86_uris[1:], uri4=stager64_uris[1:], ua=ua_string, redirect=self.c2server)) 161 | return self.webconfig_template.format(uri1=get_uris[1:], uri2=post_uris[1:], uri3=stager86_uris[1:], uri4=stager64_uris[1:], ua=ua_string, redirect=self.c2server) 162 | 163 | 164 | class webconfigWriter(): 165 | def __init__(self, outputfile): 166 | if outputfile: 167 | self.outputfile = outputfile 168 | else: 169 | self.outputfile = "web.config" 170 | 171 | def writefile(self, redirector, webconf): 172 | if not self.outputfile: 173 | ofile = redirector + ".web.config" 174 | else: 175 | ofile = self.outputfile 176 | firstline = open(ofile, 'w') 177 | firstline.close() 178 | with open(ofile, 'w') as f: 179 | f.write(webconf) 180 | print("[+] %s written to current directory" % ofile) 181 | 182 | 183 | class Main: 184 | def __init__(self): 185 | parser = argparse.ArgumentParser( 186 | description='Uses the inventory.yaml file and C2 profile to generate web.config files per C2 grouping') 187 | parser.add_argument('-t', '--teamserver', default=False, 188 | help='Cobalt Strike team server IP address or domain name') 189 | parser.add_argument('-r', '--redirector', default=False, help='Redirector IP address or domain name') 190 | parser.add_argument('-p', '--profile', default=False, help='C2 malleable profile (i.e. sick.profile)') 191 | parser.add_argument('-o', '--outputfile', default=False, 192 | help='Output file name (i.e. web.config) - omitting will only output to standard out') 193 | 194 | args = parser.parse_args() 195 | 196 | self.teamserver = args.teamserver 197 | self.redirector = args.redirector 198 | self.profile = args.profile 199 | if args.outputfile: 200 | self.outputfile = args.outputfile 201 | else: 202 | self.outputfile = None 203 | 204 | self.go() 205 | 206 | def go(self): 207 | if os.path.isfile(self.profile): 208 | if os.stat(self.profile).st_size != 0: 209 | teamserver = self.teamserver 210 | redirector = self.redirector 211 | 212 | webconfparser = CS2WebConfigRewrite("https://" + teamserver, "https://" + redirector, self.profile) 213 | webconf = webconfparser.webconfigparse() 214 | 215 | if self.outputfile: 216 | writer = webconfigWriter(self.outputfile) 217 | writer.writefile(redirector, webconf) 218 | else: 219 | print("[!] %s is empty!" % self.profile) 220 | else: 221 | print("[!] %s does not exist!" % self.profile) 222 | 223 | 224 | if __name__ == "__main__": 225 | try: 226 | Main() 227 | except KeyboardInterrupt: 228 | print("You killed it.") 229 | sys.exit() 230 | --------------------------------------------------------------------------------