Skip to content

Link commands are now unencoded by default #2698

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/objects/c_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,8 @@ async def report(self, file_svc, data_svc, output=False):
for step in self.chain:
step_report = dict(link_id=step.id,
ability_id=step.ability.ability_id,
command=step.command,
plaintext_command=step.plaintext_command,
command=self.decode_bytes(step.command),
plaintext_command=self.decode_bytes(step.plaintext_command),
delegated=step.decide.strftime(self.TIME_FORMAT),
run=step.finish,
status=step.status,
Expand Down Expand Up @@ -362,8 +362,8 @@ async def _load_objective(self, data_svc):
self.objective = deepcopy(obj[0])

async def _convert_link_to_event_log(self, link, file_svc, data_svc, output=False):
event_dict = dict(command=link.command,
plaintext_command=link.plaintext_command,
event_dict = dict(command=self.decode_bytes(link.command),
plaintext_command=self.decode_bytes(link.plaintext_command),
delegated_timestamp=link.decide.strftime(self.TIME_FORMAT),
collected_timestamp=link.collect.strftime(self.TIME_FORMAT) if link.collect else None,
finished_timestamp=link.finish,
Expand Down
7 changes: 7 additions & 0 deletions app/objects/secondclass/c_link.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ def states(self):
def status(self):
return self._status

@property
def display(self):
dump = LinkSchema(exclude=['jitter']).dump(self)
dump['command'] = self.decode_bytes(dump['command'])
dump['plaintext_command'] = self.decode_bytes(dump['plaintext_command'])
return dump

@status.setter
def status(self, value):
previous_status = getattr(self, '_status', NO_STATUS_SET)
Expand Down
12 changes: 6 additions & 6 deletions templates/operations.html
Original file line number Diff line number Diff line change
Expand Up @@ -1696,9 +1696,9 @@ <h2>Operations</h2>
if (res.facts) {
this.selectedLinkFacts = Array.from(new Set(res.facts)).sort((a, b) => b.score - a.score);
}
this.selectedLinkCommand = b64DecodeUnicode(res.command);
this.selectedLinkPlaintextCommand = b64DecodeUnicode(res.plaintext_command);
this.editableCommand = b64DecodeUnicode(res.command);
this.selectedLinkCommand = res.command;
this.selectedLinkPlaintextCommand = res.plaintext_command;
this.editableCommand = res.command;
})
.catch(() => {
this.selectedLinkResults = null;
Expand All @@ -1722,7 +1722,7 @@ <h2>Operations</h2>
updateLink(status, command = null) {
const updateLink = {
...this.selectedLink,
command: b64DecodeUnicode(this.selectedLink.command)
command: this.selectedLink.command
};
if (command) {
updateLink.command = command;
Expand Down Expand Up @@ -1793,8 +1793,8 @@ <h2>Operations</h2>
rowItems.push(link.paw);
rowItems.push(link.host);
rowItems.push(link.pid);
rowItems.push(b64DecodeUnicode(link.command));
rowItems.push(b64DecodeUnicode(link.plaintext_command));
rowItems.push(link.command);
rowItems.push(link.plaintext_command);
csv.push(rowItems.join(','));
});
this.createDownloadReport(new Blob([csv.join('\n')], { type: 'text/csv' }), this.selectedOperation.name, true);
Expand Down
56 changes: 27 additions & 29 deletions tests/api/v2/handlers/test_operations_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ async def test_get_operation_event_logs_no_payload(self, api_v2_client, api_cook
test_operation, finished_link, test_agent):
resp = await api_v2_client.post('/api/v2/operations/123/event-logs', cookies=api_cookies)
event_logs = await resp.json()
assert event_logs[1]['command'] == finished_link['command']
assert event_logs[1]['command'] == str(b64decode(finished_link['command']), 'utf-8')
assert event_logs[1]['agent_metadata']['paw'] == test_agent.schema.dump(test_agent)['paw']
assert event_logs[1]['operation_metadata']['operation_name'] == test_operation['name']
assert not event_logs[1].get('output')
Expand All @@ -108,27 +108,25 @@ async def test_get_operation_event_logs_agent_output_disabled(self, api_v2_clien
payload = {'enable_agent_output': False}
resp = await api_v2_client.post('/api/v2/operations/123/event-logs', cookies=api_cookies, json=payload)
event_logs = await resp.json()
assert event_logs[1]['command'] == finished_link['command']
assert event_logs[1]['command'] == str(b64decode(finished_link['command']), 'utf-8')
assert event_logs[1]['agent_metadata']['paw'] == test_agent.schema.dump(test_agent)['paw']
assert event_logs[1]['operation_metadata']['operation_name'] == test_operation['name']
assert not event_logs[1].get('output')

async def test_get_operation_event_logs_agent_output_enabled(self, api_v2_client, api_cookies, mocker, async_return,
test_operation, finished_link, test_agent,
expected_link_output):
with mocker.patch('app.objects.c_operation.Operation.decode_bytes') as mock_decode:
expected_link_output_dict = dict(stdout=expected_link_output, stderr="")
mock_decode.return_value = json.dumps(expected_link_output_dict)
with mocker.patch('app.service.file_svc.FileSvc.read_result_file') as mock_readfile:
mock_readfile.return_value = ''
payload = {'enable_agent_output': True}
resp = await api_v2_client.post('/api/v2/operations/123/event-logs', cookies=api_cookies, json=payload)
event_logs = await resp.json()
assert event_logs[1]['command'] == finished_link['command']
assert event_logs[1]['agent_metadata']['paw'] == test_agent.schema.dump(test_agent)['paw']
assert event_logs[1]['operation_metadata']['operation_name'] == test_operation['name']
assert event_logs[1]['output'] == expected_link_output_dict
assert not event_logs[0].get('output')
expected_link_output_dict = dict(stdout=expected_link_output, stderr="")
with mocker.patch('app.service.file_svc.FileSvc.read_result_file') as mock_readfile:
mock_readfile.return_value = b64encode(json.dumps(expected_link_output_dict).encode())
payload = {'enable_agent_output': True}
resp = await api_v2_client.post('/api/v2/operations/123/event-logs', cookies=api_cookies, json=payload)
event_logs = await resp.json()
assert event_logs[1]['command'] == str(b64decode(finished_link['command']), 'utf-8')
assert event_logs[1]['agent_metadata']['paw'] == test_agent.schema.dump(test_agent)['paw']
assert event_logs[1]['operation_metadata']['operation_name'] == test_operation['name']
assert event_logs[1]['output'] == expected_link_output_dict
assert not event_logs[0].get('output')

async def test_unauthorized_get_operation_event_logs(self, api_v2_client):
resp = await api_v2_client.post('/api/v2/operations/123/event-logs')
Expand Down Expand Up @@ -234,7 +232,7 @@ async def test_get_links(self, api_v2_client, api_cookies, active_link):
assert len(links) == 2
assert links[0]['id'] == active_link['id']
assert links[0]['paw'] == active_link['paw']
assert links[0]['command'] == active_link['command']
assert links[0]['command'] == str(b64decode(active_link['command']), 'utf-8')

async def test_unauthorized_get_links(self, api_v2_client):
resp = await api_v2_client.get('/api/v2/operations/123/links')
Expand All @@ -246,7 +244,7 @@ async def test_get_operation_link(self, api_v2_client, api_cookies, active_link)
link = await resp.json()
assert link['id'] == active_link['id']
assert link['paw'] == active_link['paw']
assert link['command'] == active_link['command']
assert link['command'] == str(b64decode(active_link['command']), 'utf-8')

async def test_unauthorized_get_operation_link(self, api_v2_client):
resp = await api_v2_client.get('/api/v2/operations/123/links/456')
Expand All @@ -261,7 +259,7 @@ async def test_nonexistent_link_get_operation_link(self, api_v2_client, api_cook
assert resp.status == HTTPStatus.NOT_FOUND

async def test_update_operation_link(self, api_v2_client, api_cookies, active_link):
original_command = active_link['command']
original_command = str(b64decode(active_link['command']), 'utf-8')
payload = dict(command='whoami')
resp = await api_v2_client.patch('/api/v2/operations/123/links/456', cookies=api_cookies, json=payload)
assert resp.status == HTTPStatus.OK
Expand All @@ -272,37 +270,37 @@ async def test_update_operation_link(self, api_v2_client, api_cookies, active_li
assert op.chain[0].paw == active_link['paw']

async def test_unauthorized_update_operation_link(self, api_v2_client):
payload = dict(command='bHM=')
payload = dict(command='ls')
resp = await api_v2_client.patch('/api/v2/operations/123/links/456', json=payload)
assert resp.status == HTTPStatus.UNAUTHORIZED

async def test_nonexistent_operation_update_operation_link(self, api_v2_client, api_cookies):
payload = dict(command='bHM=')
payload = dict(command='ls')
resp = await api_v2_client.patch('/api/v2/operations/999/links/123', json=payload, cookies=api_cookies)
assert resp.status == HTTPStatus.NOT_FOUND

async def test_nonexistent_link_update_operation_link(self, api_v2_client, api_cookies):
payload = dict(command='bHM=')
payload = dict(command='ls')
resp = await api_v2_client.patch('/api/v2/operations/123/links/999', json=payload, cookies=api_cookies)
assert resp.status == HTTPStatus.NOT_FOUND

async def test_update_finished_operation_link(self, api_v2_client, api_cookies):
payload = dict(command='bHM=', status=-1)
payload = dict(command='ls', status=-1)
resp = await api_v2_client.patch('/api/v2/operations/123/links/789', json=payload, cookies=api_cookies)
assert resp.status == HTTPStatus.FORBIDDEN

async def test_get_potential_links(self, api_v2_client, api_cookies, mocker, async_return):
BaseService.get_service('rest_svc').build_potential_abilities = mocker.Mock()
BaseService.get_service('rest_svc').build_potential_abilities.return_value = async_return([])
expected_link = Link(command='whoami', paw='123456', id='789')
expected_link = Link(command='d2hvYW1p', paw='123456', id='789')
BaseService.get_service('rest_svc').build_potential_links = mocker.Mock()
BaseService.get_service('rest_svc').build_potential_links.return_value = async_return([expected_link])
resp = await api_v2_client.get('/api/v2/operations/123/potential-links', cookies=api_cookies)
result = await resp.json()
assert len(result) == 1
assert result[0]['id'] == expected_link.id
assert result[0]['paw'] == expected_link.paw
assert result[0]['command'] == expected_link.command
assert result[0]['command'] == str(b64decode(expected_link.command), 'utf-8')

async def test_unauthorized_get_potential_links(self, api_v2_client):
resp = await api_v2_client.get('/api/v2/operations/123/potential-links')
Expand All @@ -315,15 +313,15 @@ async def test_nonexistent_operation_get_potential_links(self, api_v2_client, ap
async def test_get_potential_links_by_paw(self, api_v2_client, api_cookies, mocker, async_return, ability, executor):
BaseService.get_service('rest_svc').build_potential_abilities = mocker.Mock()
BaseService.get_service('rest_svc').build_potential_abilities.return_value = async_return([])
expected_link = Link(command='whoami', paw='123', id='789')
expected_link = Link(command='d2hvYW1p', paw='123', id='789')
BaseService.get_service('rest_svc').build_potential_links = mocker.Mock()
BaseService.get_service('rest_svc').build_potential_links.return_value = async_return([expected_link])
resp = await api_v2_client.get('/api/v2/operations/123/potential-links/123', cookies=api_cookies)
result = await resp.json()
assert len(result) == 1
assert result[0]['id'] == expected_link.id
assert result[0]['paw'] == expected_link.paw
assert result[0]['command'] == expected_link.command
assert result[0]['command'] == str(b64decode(expected_link.command), 'utf-8')

async def test_unauthorized_get_potential_links_by_paw(self, api_v2_client):
resp = await api_v2_client.get('/api/v2/operations/123/potential-links/123')
Expand Down Expand Up @@ -354,7 +352,7 @@ async def test_create_potential_link_with_globals(self, api_v2_client, api_cooki
assert result['paw'] == payload['paw']
assert result['id']
assert result['ability']['name'] == 'Manual Command'
assert b64decode(result['command']).decode('ascii') == "://None:None 123"
assert result['command'] == "://None:None 123"

async def test_create_potential_link(self, api_v2_client, api_cookies, mocker, async_return):
with mocker.patch('app.objects.c_operation.Operation.apply') as mock_apply:
Expand Down Expand Up @@ -410,7 +408,7 @@ async def test_get_operation_link_result(self, api_v2_client, api_cookies, finis
output = await resp.json()
assert output['link']['id'] == finished_link['id']
assert output['link']['paw'] == finished_link['paw']
assert output['link']['command'] == finished_link['command']
assert output['link']['command'] == str(b64decode(finished_link['command']), 'utf-8')
assert output['result'] == encoded_result

async def test_unauthorized_get_operation_link_result(self, api_v2_client, finished_link):
Expand All @@ -424,7 +422,7 @@ async def test_get_operation_link_no_result(self, api_v2_client, api_cookies, ac
assert output['result'] == ''
assert output['link']['paw'] == active_link['paw']
assert output['link']['id'] == active_link['id']
assert output['link']['command'] == active_link['command']
assert output['link']['command'] == str(b64decode(active_link['command']), 'utf-8')

async def test_nonexistent_get_operation_link_result(self, api_v2_client, api_cookies):
resp = await api_v2_client.get('/api/v2/operations/123/links/999/result', cookies=api_cookies)
Expand Down
16 changes: 8 additions & 8 deletions tests/objects/test_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,8 @@ def test_event_logs(self, event_loop, op_for_event_logs, operation_agent, file_s
)
want = [
dict(
command='d2hvYW1p',
plaintext_command='d2hvYW1p',
command='whoami',
plaintext_command='whoami',
delegated_timestamp=LINK1_DECIDE_TIME,
collected_timestamp=LINK1_COLLECT_TIME,
finished_timestamp=LINK1_FINISH_TIME,
Expand All @@ -241,8 +241,8 @@ def test_event_logs(self, event_loop, op_for_event_logs, operation_agent, file_s
attack_metadata=want_attack_metadata,
),
dict(
command='aG9zdG5hbWU=',
plaintext_command='aG9zdG5hbWU=',
command='hostname',
plaintext_command='hostname',
delegated_timestamp=LINK2_DECIDE_TIME,
collected_timestamp=LINK2_COLLECT_TIME,
finished_timestamp=LINK2_FINISH_TIME,
Expand Down Expand Up @@ -293,8 +293,8 @@ def test_writing_event_logs_to_disk(self, event_loop, op_for_event_logs, operati
)
want = [
dict(
command='d2hvYW1p',
plaintext_command='d2hvYW1p',
command='whoami',
plaintext_command='whoami',
delegated_timestamp=LINK1_DECIDE_TIME,
collected_timestamp=LINK1_COLLECT_TIME,
finished_timestamp=LINK1_FINISH_TIME,
Expand All @@ -312,8 +312,8 @@ def test_writing_event_logs_to_disk(self, event_loop, op_for_event_logs, operati
attack_metadata=want_attack_metadata,
),
dict(
command='aG9zdG5hbWU=',
plaintext_command='aG9zdG5hbWU=',
command='hostname',
plaintext_command='hostname',
delegated_timestamp=LINK2_DECIDE_TIME,
collected_timestamp=LINK2_COLLECT_TIME,
finished_timestamp=LINK2_FINISH_TIME,
Expand Down
7 changes: 3 additions & 4 deletions tests/services/test_planning_svc.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,9 +331,8 @@ async def test_link_fact_coverage(self, setup_planning_test, planning_svc):
f3 = Fact(trait='a.b.e', value='3')

gen = await planning_svc.add_test_variants([link], agent, facts=[f0, f1, f2, f3])

assert len(gen) == 2
assert BaseWorld.decode_bytes(gen[1].display['command']) == target_string
assert gen[1].display['command'] == target_string

async def test_trim_links(self, setup_planning_test, planning_svc):
"""
Expand Down Expand Up @@ -364,7 +363,7 @@ async def test_trim_links(self, setup_planning_test, planning_svc):
trimmed_links = await planning_svc.trim_links(operation, [link], agent)

assert len(trimmed_links) == 1
assert BaseWorld.decode_bytes(trimmed_links[0].display['command']) == target_string
assert trimmed_links[0].display['command'] == target_string

async def test_filter_bs(self, setup_planning_test, planning_svc):
_, agent, operation, ability = setup_planning_test
Expand All @@ -382,7 +381,7 @@ async def test_filter_bs(self, setup_planning_test, planning_svc):
gen = await planning_svc.add_test_variants([link], agent, facts=[f0, f1, f2, f3, f4, f5, f6])

assert len(gen) == 4
assert BaseWorld.decode_bytes(gen[1].display['command']) == target_string
assert gen[1].display['command'] == target_string

async def test_duplicate_lateral_filter(self, setup_planning_test, planning_svc, link, fact):
ability, agent, operation, sability = setup_planning_test
Expand Down