├── 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 |
--------------------------------------------------------------------------------