Repository: jtesta/ssh-audit

Issue:
RuntimeError when auditing SSH-Server
```
❯ python3.8 ssh-audit.py xx.xxx.xxx.xxx -p 22022 --json
Traceback (most recent call last):
  File "ssh-audit.py", line 3103, in <module>
    main()
  File "ssh-audit.py", line 3099, in main
    audit(conf)
  File "ssh-audit.py", line 3088, in audit
    print(json.dumps(build_struct(banner, kex=kex, client_host=s.client_host), sort_keys=True))
  File "ssh-audit.py", line 2996, in build_struct
    for host_key_type in host_keys.keys():
RuntimeError: dictionary keys changed during iteration
```

This is reproducible for one of my old, inherited SSH Servers, but not for the new one, which I configured after your guide.

I can provide you the address in private if you need it to debug.

Anyway, the problem is pretty obvious, and the exception says it clearly. It is no good idea to change an Iterable while iterating over it.

The fix is trivial - just wrap `host_keys.keys()` from above in a `list()`.

The above line is not covered by a test, though, as all the JSON generation. So, this is good chance to raise test coverage.

I'll try to find some time and create a PR for this issue.

Comments:

Patch:
diff --git a/ssh-audit.py b/ssh-audit.py
index a5702ab..c51241f 100755
--- a/ssh-audit.py
+++ b/ssh-audit.py
@@ -3366,7 +3366,7 @@ def build_struct(banner: Optional['SSH.Banner'], kex: Optional['SSH2.Kex'] = Non
         host_keys = kex.host_keys()
 
         # Normalize all RSA key types to 'ssh-rsa'.  Otherwise, due to Python's order-indifference dictionary types, we would iterate key types in unpredictable orders, which interferes with the docker testing framework (i.e.: tests would fail because elements are reported out of order, even though the output is semantically the same).
-        for host_key_type in host_keys.keys():
+        for host_key_type in list(host_keys.keys())[:]:
             if host_key_type in SSH2.HostKeyTest.RSA_FAMILY:
                 val = host_keys[host_key_type]
                 del host_keys[host_key_type]


Test patch:
diff --git a/test/test_build_struct.py b/test/test_build_struct.py
new file mode 100644
index 0000000..37c4c0f
--- /dev/null
+++ b/test/test_build_struct.py
@@ -0,0 +1,41 @@
+import os
+
+import pytest
+
+
+@pytest.fixture
+def kex(ssh_audit):
+    kex_algs, key_algs = [], []
+    enc, mac, compression, languages = [], [], ['none'], []
+    cli = ssh_audit.SSH2.KexParty(enc, mac, compression, languages)
+    enc, mac, compression, languages = [], [], ['none'], []
+    srv = ssh_audit.SSH2.KexParty(enc, mac, compression, languages)
+    cookie = os.urandom(16)
+    kex = ssh_audit.SSH2.Kex(cookie, kex_algs, key_algs, cli, srv, 0)
+    return kex
+
+
+def test_prevent_runtime_error_regression(ssh_audit, kex):
+    """Prevent a regression of https://github.com/jtesta/ssh-audit/issues/41
+
+    The following test setup does not contain any sensible data.
+    It was made up to reproduce a situation when there are several host
+    keys, and an error occurred when iterating and modifying them at the
+    same time.
+    """
+    kex.set_host_key("ssh-rsa", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
+    kex.set_host_key("ssh-rsa1", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
+    kex.set_host_key("ssh-rsa2", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
+    kex.set_host_key("ssh-rsa3", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
+    kex.set_host_key("ssh-rsa4", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
+    kex.set_host_key("ssh-rsa5", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
+    kex.set_host_key("ssh-rsa6", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
+    kex.set_host_key("ssh-rsa7", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
+    kex.set_host_key("ssh-rsa8", b"\x00\x00\x00\x07ssh-rsa\x00\x00\x00")
+
+    rv = ssh_audit.build_struct(banner=None, kex=kex)
+
+    assert len(rv["fingerprints"]) == 9
+
+    for key in ['banner', 'compression', 'enc', 'fingerprints', 'kex', 'key', 'mac']:
+        assert key in rv