Skip to content

Commit 5bfa9fa

Browse files
authored
Typed config (BC-SECURITY#308)
* make config a typed object to reduce the pain of far-nested dicts and do some validation on-load * cleanup * try to get tests to run without mysql * switch from union type
1 parent fac9506 commit 5bfa9fa

File tree

8 files changed

+214
-92
lines changed

8 files changed

+214
-92
lines changed

empire/server/common/config.py

+61-23
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,68 @@
11
import sys
2-
from typing import Dict
2+
from typing import Dict, Union
33

44
import yaml
5+
from pydantic import BaseModel, Field
56

67
from empire.server.common import helpers
78

89

9-
class EmpireConfig(object):
10-
def __init__(self):
11-
self.yaml: Dict = {}
12-
if "--config" in sys.argv:
13-
location = sys.argv[sys.argv.index("--config") + 1]
14-
print(f"Loading config from {location}")
15-
self.set_yaml(location)
16-
if len(self.yaml.items()) == 0:
17-
print(helpers.color("[*] Loading default config"))
18-
self.set_yaml("./empire/server/config.yaml")
19-
20-
def set_yaml(self, location: str):
21-
try:
22-
with open(location, "r") as stream:
23-
self.yaml = yaml.safe_load(stream)
24-
except yaml.YAMLError as exc:
25-
print(exc)
26-
except FileNotFoundError as exc:
27-
print(exc)
28-
29-
30-
empire_config = EmpireConfig()
10+
class DatabaseConfig(BaseModel):
11+
type: str
12+
defaults: Dict[str, Union[bool, int, str]]
13+
14+
# sqlite
15+
location: str = "empire/server/data/empire.db"
16+
17+
# mysql
18+
url: str = "localhost:3306"
19+
username: str = ""
20+
password: str = ""
21+
22+
23+
class ModulesConfig(BaseModel):
24+
# todo vr In 5.0 we should pick a single naming convention for config.
25+
retain_last_value: bool = Field(alias="retain-last-value")
26+
27+
28+
class DirectoriesConfig(BaseModel):
29+
downloads: str
30+
module_source: str
31+
obfuscated_module_source: str
32+
33+
34+
class EmpireConfig(BaseModel):
35+
supress_self_cert_warning: bool = Field(
36+
alias="supress-self-cert-warning", default=True
37+
)
38+
database: DatabaseConfig
39+
modules: ModulesConfig
40+
plugins: Dict[str, Dict[str, str]] = {}
41+
directories: DirectoriesConfig
42+
43+
# For backwards compatibility
44+
@property
45+
def yaml(self):
46+
return self.dict()
47+
48+
49+
def set_yaml(location: str):
50+
try:
51+
with open(location, "r") as stream:
52+
return yaml.safe_load(stream)
53+
except yaml.YAMLError as exc:
54+
print(exc)
55+
except FileNotFoundError as exc:
56+
print(exc)
57+
58+
59+
config_dict = {}
60+
if "--config" in sys.argv:
61+
location = sys.argv[sys.argv.index("--config") + 1]
62+
print(f"Loading config from {location}")
63+
config_dict = set_yaml(location)
64+
if len(config_dict.items()) == 0:
65+
print(helpers.color("[*] Loading default config"))
66+
config_dict = set_yaml("./empire/server/config.yaml")
67+
68+
empire_config = EmpireConfig(**config_dict)

empire/server/common/modules.py

+8-12
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def execute_module(
147147
msg = f"tasked agent {session_id} to run module {module.name}"
148148
self.main_menu.agents.save_agent_log(session_id, msg)
149149

150-
if empire_config.yaml.get("modules", {}).get("retain-last-value", True):
150+
if empire_config.modules.retain_last_value:
151151
self._set_default_values(module, cleaned_options)
152152

153153
return {"success": True, "taskID": task_id, "msg": msg}, None
@@ -160,9 +160,9 @@ def get_module_source(
160160
"""
161161
try:
162162
if obfuscate:
163-
obfuscated_module_source = empire_config.yaml.get("directories", {})[
164-
"obfuscated_module_source"
165-
]
163+
obfuscated_module_source = (
164+
empire_config.directories.obfuscated_module_source
165+
)
166166
module_path = os.path.join(obfuscated_module_source, module_name)
167167
# If pre-obfuscated module exists then return code
168168
if os.path.exists(module_path):
@@ -172,9 +172,7 @@ def get_module_source(
172172

173173
# If pre-obfuscated module does not exist then generate obfuscated code and return it
174174
else:
175-
module_source = empire_config.yaml.get("directories", {})[
176-
"module_source"
177-
]
175+
module_source = empire_config.directories.module_source
178176
module_path = os.path.join(module_source, module_name)
179177
with open(module_path, "r") as f:
180178
module_code = f.read()
@@ -187,9 +185,7 @@ def get_module_source(
187185

188186
# Use regular/unobfuscated code
189187
else:
190-
module_source = empire_config.yaml.get("directories", {})[
191-
"module_source"
192-
]
188+
module_source = empire_config.directories.module_source
193189
module_path = os.path.join(module_source, module_name)
194190
with open(module_path, "r") as f:
195191
module_code = f.read()
@@ -334,7 +330,7 @@ def _generate_script_python(
334330
) -> Tuple[Optional[str], Optional[str]]:
335331
if module.script_path:
336332
script_path = os.path.join(
337-
empire_config.yaml.get("directories", {})["module_source"],
333+
empire_config.directories.module_source,
338334
module.script_path,
339335
)
340336
with open(script_path, "r") as stream:
@@ -530,7 +526,7 @@ def _load_module(self, yaml_module, root_path, file_path: str):
530526
elif my_model.script_path:
531527
if not path.exists(
532528
os.path.join(
533-
empire_config.yaml.get("directories", {})["module_source"],
529+
empire_config.directories.module_source,
534530
my_model.script_path,
535531
)
536532
):

empire/server/database/base.py

+32-28
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,26 @@
1313
)
1414
from empire.server.database.models import Base
1515

16-
database_config = empire_config.yaml.get('database', {})
17-
18-
if database_config.get('type') == 'mysql':
19-
url = database_config.get('url')
20-
username = database_config.get('username') or ''
21-
password = database_config.get('password') or ''
22-
engine = create_engine(f'mysql+pymysql://{username}:{password}@{url}/empire', echo=False)
16+
database_config = empire_config.database
17+
18+
if database_config.type == "mysql":
19+
url = database_config.url
20+
username = database_config.username
21+
password = database_config.password
22+
engine = create_engine(
23+
f"mysql+pymysql://{username}:{password}@{url}/empire", echo=False
24+
)
2325
else:
24-
location = database_config.get('location', 'data/empire.db')
25-
engine = create_engine(f'sqlite:///{location}?check_same_thread=false', echo=False)
26+
location = database_config.location
27+
engine = create_engine(f"sqlite:///{location}?check_same_thread=false", echo=False)
2628

2729
Session = scoped_session(sessionmaker(bind=engine))
2830

2931
args = arguments.args
3032
if args.reset:
31-
choice = input("\x1b[1;33m[>] Would you like to reset your Empire instance? [y/N]: \x1b[0m")
33+
choice = input(
34+
"\x1b[1;33m[>] Would you like to reset your Empire instance? [y/N]: \x1b[0m"
35+
)
3236
if choice.lower() == "y":
3337
# The reset script will delete the default db file. This will drop tables if connected to MySQL or
3438
# a different SQLite .db file.
@@ -48,53 +52,53 @@ def color(string, color=None):
4852
"""
4953
attr = []
5054
# bold
51-
attr.append('1')
55+
attr.append("1")
5256

5357
if color:
5458
if color.lower() == "red":
55-
attr.append('31')
59+
attr.append("31")
5660
elif color.lower() == "green":
57-
attr.append('32')
61+
attr.append("32")
5862
elif color.lower() == "yellow":
59-
attr.append('33')
63+
attr.append("33")
6064
elif color.lower() == "blue":
61-
attr.append('34')
62-
return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), string)
65+
attr.append("34")
66+
return "\x1b[%sm%s\x1b[0m" % (";".join(attr), string)
6367

6468
else:
6569
if string.strip().startswith("[!]"):
66-
attr.append('31')
67-
return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), string)
70+
attr.append("31")
71+
return "\x1b[%sm%s\x1b[0m" % (";".join(attr), string)
6872
elif string.strip().startswith("[+]"):
69-
attr.append('32')
70-
return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), string)
73+
attr.append("32")
74+
return "\x1b[%sm%s\x1b[0m" % (";".join(attr), string)
7175
elif string.strip().startswith("[*]"):
72-
attr.append('34')
73-
return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), string)
76+
attr.append("34")
77+
return "\x1b[%sm%s\x1b[0m" % (";".join(attr), string)
7478
elif string.strip().startswith("[>]"):
75-
attr.append('33')
76-
return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), string)
79+
attr.append("33")
80+
return "\x1b[%sm%s\x1b[0m" % (";".join(attr), string)
7781
else:
7882
return string
7983

8084

8185
# When Empire starts up for the first time, it will create the database and create
8286
# these default records.
8387
if len(Session().query(models.User).all()) == 0:
84-
print(color('[*] Setting up database.'))
85-
print(color('[*] Adding default user.'))
88+
print(color("[*] Setting up database."))
89+
print(color("[*] Adding default user."))
8690
Session().add(get_default_user())
8791
Session().commit()
8892
Session.remove()
8993

9094
if len(Session().query(models.Config).all()) == 0:
91-
print(color('[*] Adding database config.'))
95+
print(color("[*] Adding database config."))
9296
Session().add(get_default_config())
9397
Session().commit()
9498
Session.remove()
9599

96100
if len(Session().query(models.Function).all()) == 0:
97-
print(color('[*] Adding default keyword obfuscation functions.'))
101+
print(color("[*] Adding default keyword obfuscation functions."))
98102
functions = get_default_functions()
99103

100104
for function in functions:

empire/server/database/defaults.py

+44-26
Original file line numberDiff line numberDiff line change
@@ -8,55 +8,73 @@
88
from empire.server.common.config import empire_config
99
from empire.server.database import models
1010

11-
database_config = empire_config.yaml.get('database', {}).get('defaults', {})
11+
database_config = empire_config.database.defaults
1212

1313

1414
def get_default_hashed_password():
15-
password = database_config.get('password', 'password123')
16-
password = bytes(password, 'UTF-8')
15+
password = database_config.get("password", "password123")
16+
password = bytes(password, "UTF-8")
1717
return bcrypt.hashpw(password, bcrypt.gensalt())
1818

1919

2020
def get_default_user():
21-
return models.User(username=database_config.get('username', 'empireadmin'),
22-
password=get_default_hashed_password(),
23-
enabled=True,
24-
admin=True)
21+
return models.User(
22+
username=database_config.get("username", "empireadmin"),
23+
password=get_default_hashed_password(),
24+
enabled=True,
25+
admin=True,
26+
)
2527

2628

2729
def get_default_config():
2830
# Calculate the install path. We know the project directory will always be two levels up of the current directory.
2931
# Any modifications of the folder structure will need to be applied here.
3032
install_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
31-
return models.Config(staging_key=get_staging_key(),
32-
install_path=install_path,
33-
ip_whitelist=database_config.get('ip-whitelist', ''),
34-
ip_blacklist=database_config.get('ip-blacklist', ''),
35-
autorun_command="",
36-
autorun_data="",
37-
rootuser=True,
38-
obfuscate=database_config.get('obfuscate', False),
39-
obfuscate_command=database_config.get('obfuscate-command', r'Token\All\1'))
33+
return models.Config(
34+
staging_key=get_staging_key(),
35+
install_path=install_path,
36+
ip_whitelist=database_config.get("ip-whitelist", ""),
37+
ip_blacklist=database_config.get("ip-blacklist", ""),
38+
autorun_command="",
39+
autorun_data="",
40+
rootuser=True,
41+
obfuscate=database_config.get("obfuscate", False),
42+
obfuscate_command=database_config.get("obfuscate-command", r"Token\All\1"),
43+
)
4044

4145

4246
def get_default_functions():
4347
return [
44-
models.Function(keyword='Invoke_Empire',
45-
replacement=''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(5))),
46-
models.Function(keyword='Invoke_Mimikatz',
47-
replacement=''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(5)))
48+
models.Function(
49+
keyword="Invoke_Empire",
50+
replacement="".join(
51+
random.choice(string.ascii_uppercase + string.digits) for _ in range(5)
52+
),
53+
),
54+
models.Function(
55+
keyword="Invoke_Mimikatz",
56+
replacement="".join(
57+
random.choice(string.ascii_uppercase + string.digits) for _ in range(5)
58+
),
59+
),
4860
]
4961

5062

5163
def get_staging_key():
5264
# Staging Key is set up via environmental variable or config.yaml. By setting RANDOM a randomly selected password
5365
# will automatically be selected.
54-
staging_key = os.getenv('STAGING_KEY') or database_config.get('staging-key', 'BLANK')
55-
punctuation = '!#%&()*+,-./:;<=>?@[]^_{|}~'
66+
staging_key = os.getenv("STAGING_KEY") or database_config.get(
67+
"staging-key", "BLANK"
68+
)
69+
punctuation = "!#%&()*+,-./:;<=>?@[]^_{|}~"
5670
if staging_key == "BLANK":
57-
choice = input("\n [>] Enter server negotiation password, enter for random generation: ")
71+
choice = input(
72+
"\n [>] Enter server negotiation password, enter for random generation: "
73+
)
5874
if choice != "" and choice != "RANDOM":
59-
return hashlib.md5(choice.encode('utf-8')).hexdigest()
75+
return hashlib.md5(choice.encode("utf-8")).hexdigest()
6076

61-
print('\x1b[1;34m[*] Generating random staging key\x1b[0m')
62-
return ''.join(random.sample(string.ascii_letters + string.digits + punctuation, 32))
77+
print("\x1b[1;34m[*] Generating random staging key\x1b[0m")
78+
return "".join(
79+
random.sample(string.ascii_letters + string.digits + punctuation, 32)
80+
)

empire/server/server.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
cli.show_server_banner = lambda *x: None
5353

5454
# Disable http warnings
55-
if empire_config.yaml.get("suppress-self-cert-warning", True):
55+
if empire_config.supress_self_cert_warning:
5656
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
5757

5858
# Set proxy IDs
@@ -3269,7 +3269,7 @@ def autostart_plugins():
32693269
"""
32703270
Autorun plugin commands at server startup.
32713271
"""
3272-
plugins = empire_config.yaml.get("plugins")
3272+
plugins = empire_config.plugins
32733273
if plugins:
32743274
for plugin in plugins:
32753275
use_plugin = main.loadedPlugins[plugin]

0 commit comments

Comments
 (0)