Skip to content

Commit a689724

Browse files
authored
Drupal 8.5.0 honeypot
Low-interaction Drupal honeypot (v8.5.0), vulnerable to Drupalgeddon 2 (CVE-2018-7600).
1 parent 2445fdb commit a689724

19 files changed

+3058
-0
lines changed

services/drupal/LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2018 Cymmetria
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

services/drupal/README.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Drupal 8.50 Honeypot
2+
3+
Cymmetria Research, 2018.
4+
5+
https://www.cymmetria.com/
6+
7+
8+
9+
Drupal 8.50 is a low interaction honeypot meant to detect exploitation of CVE-2018-7600.
10+
The html/ directory contains files in order to be fingerprinted correctly by Droopescan (https://github.com/droope/droopescan) and Fingerprinter (https://github.com/erwanlr/Fingerprinter/).
11+
It is released under the MIT license for the use of the community.

services/drupal/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# -*- coding: utf-8 -*-
2+
"""Honeycomb Drupal service."""
3+
from __future__ import unicode_literals

services/drupal/config.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"event_types": [
3+
{
4+
"name": "drupal_rce",
5+
"label": "Remote code execution attempt (CVE-2018-7600)",
6+
"fields": ["originating_ip", "originating_port", "request"],
7+
"policy": "Alert"
8+
}
9+
],
10+
"service": {
11+
"allow_many": false,
12+
"supported_os_families": "All",
13+
"ports": [
14+
{
15+
"protocol": "TCP",
16+
"port": 80
17+
}
18+
],
19+
"name": "drupal",
20+
"label": "Drupal Honeypot",
21+
"description": "Drupal 8.50 honeypot, detecting CVE-2018-7600 exploit attempt",
22+
"conflicts_with": ["http"]
23+
},
24+
"parameters": [
25+
]
26+
}

services/drupal/drupal_server.py

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# -*- coding: utf-8 -*-
2+
"""A Drupal CMS server based on Python's HTTPServer."""
3+
from __future__ import unicode_literals
4+
5+
import os
6+
7+
from six.moves.socketserver import ThreadingMixIn
8+
from six.moves.BaseHTTPServer import HTTPServer
9+
from six.moves.SimpleHTTPServer import SimpleHTTPRequestHandler
10+
from six.moves.urllib_parse import unquote, urlparse
11+
12+
13+
WEB_PORT = 80
14+
WWW_FOLDER_NAME = "html"
15+
WEB_ALERT_TYPE_NAME = "drupal_rce"
16+
DEFAULT_SERVER_VERSION = "Apache 2"
17+
ALERTS = [WEB_ALERT_TYPE_NAME]
18+
19+
20+
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
21+
"""Extend both classes to have threading capabilities."""
22+
23+
24+
class HoneyHTTPRequestHandler(SimpleHTTPRequestHandler, object):
25+
"""Filter requests to catch Drupalgeddon 2 exploit attempts."""
26+
27+
def version_string(self):
28+
"""Return the web server name that we run on."""
29+
return DEFAULT_SERVER_VERSION
30+
31+
def verify(self, query):
32+
"""Filter HTTP request to make sure it's not an exploit attempt."""
33+
self.logger.debug("Query: %s", query)
34+
if query and query.find("&") != -1:
35+
query_components = {}
36+
for param in query.split("&"):
37+
if param.find("=") == -1:
38+
continue
39+
else:
40+
key, value = param.split("=")
41+
query_components[key] = value
42+
43+
for component in query_components:
44+
if component.find("[#") != -1 and len(component) > 1:
45+
self.alert(event_name=WEB_ALERT_TYPE_NAME,
46+
request=query,
47+
orig_ip=self.client_address[0],
48+
orig_port=self.client_address[1])
49+
break
50+
51+
def do_GET(self):
52+
"""Handle an HTTP GET request."""
53+
query = unquote(urlparse(self.path).query)
54+
self.verify(query)
55+
super(HoneyHTTPRequestHandler, self).do_GET()
56+
57+
def do_POST(self):
58+
"""Handle an HTTP POST request."""
59+
content_length = int(self.headers['Content-Length'])
60+
post_data = unquote(self.rfile.read(content_length).decode())
61+
self.verify(post_data)
62+
super(HoneyHTTPRequestHandler, self).do_GET()
63+
64+
def log_error(self, message, *args):
65+
"""Log an error."""
66+
self.log_message("error", message, *args)
67+
68+
def log_request(self, code="-", size="-"):
69+
"""Log an incoming request."""
70+
# Due to hilarity involving HTTPServer using "%s"-style formatting to format strings,
71+
# and the URLs sometimes having extra %'s in them, we have to escape them by making
72+
# them into %%'s.
73+
self.log_message("debug", '"{!s}" {!s} {!s}'.format(self.requestline.replace("%", "%%"), code, size))
74+
75+
def log_message(self, level, message, *args):
76+
"""Send message to logger with standard apache format."""
77+
try:
78+
getattr(self.logger, level)
79+
except AttributeError:
80+
self.logger.error("Invalid level of debug requested ({}), logging as debug".format(level))
81+
level = "debug"
82+
83+
self.logger.debug(message)
84+
self.logger.debug(str(args))
85+
getattr(self.logger, level)("{!s} - - [{!s}] {!s}".format(self.client_address[0],
86+
self.log_date_time_string(),
87+
message % args))
88+
89+
90+
class DrupalServer(object):
91+
"""Drupal CMS honeypot."""
92+
93+
def __init__(self, logger, alert):
94+
self.logger = logger
95+
alerting_client_handler = HoneyHTTPRequestHandler
96+
alerting_client_handler.logger = logger
97+
alerting_client_handler.alert = alert
98+
self.httpd = ThreadingHTTPServer(("", WEB_PORT), alerting_client_handler)
99+
100+
def start(self):
101+
"""Start serving requests by starting the underlying HTTP server."""
102+
os.chdir(os.path.join(os.path.dirname(__file__), WWW_FOLDER_NAME))
103+
self.logger.info("Starting Drupal server on port {port}".format(port=WEB_PORT))
104+
self.httpd.serve_forever()
105+
return True
106+
107+
def stop(self):
108+
"""Stop serving requests."""
109+
self.logger.info("Shutting down Drupal server...")
110+
if self.httpd:
111+
self.httpd.shutdown()
112+
self.httpd = None

services/drupal/drupal_service.py

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# -*- coding: utf-8 -*-
2+
"""Drupal CMS honeypot service, for catching CVE-2018-7600 (Drupalgeddon 2)."""
3+
from __future__ import unicode_literals
4+
5+
from base_service import ServerCustomService
6+
7+
from drupal_server import DrupalServer, ALERTS, WEB_PORT
8+
9+
EVENT_TYPE_FIELD_NAME = "event_type"
10+
ORIGINATING_IP_FIELD_NAME = "originating_ip"
11+
ORIGINATING_PORT_FIELD_NAME = "originating_port"
12+
REQUEST_FIELD_NAME = "request"
13+
EXPLOIT_USING_POST = "user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax"
14+
EXPLOIT_USING_GET = "user/password?name[%23post_render][]=passthru&name[%23markup]=id&name[%23type]=markup"
15+
EXPLOIT_FORMAT = "{target}{exploit}"
16+
17+
18+
class DrupalService(ServerCustomService):
19+
"""Plugin class for Honeycomb/Mazerunner that runs a Drupal CMS service."""
20+
21+
def __init__(self, *args, **kwargs):
22+
super(DrupalService, self).__init__(*args, **kwargs)
23+
self.honeypot = None
24+
25+
def alert(self, event_name, orig_ip, orig_port, request):
26+
"""Report an alert to the framework."""
27+
params = {
28+
EVENT_TYPE_FIELD_NAME: event_name,
29+
ORIGINATING_IP_FIELD_NAME: orig_ip,
30+
ORIGINATING_PORT_FIELD_NAME: orig_port,
31+
REQUEST_FIELD_NAME: request
32+
}
33+
34+
self.add_alert_to_queue(params)
35+
36+
def on_server_start(self):
37+
"""Set up a drupal honeypot server."""
38+
self.logger.info("{name!s} received start".format(name=self))
39+
self.honeypot = DrupalServer(self.logger, self.alert)
40+
self.signal_ready()
41+
if not self.honeypot.start():
42+
self.logger.debug("Failed to start simple HTTP Drupal server")
43+
44+
def on_server_shutdown(self):
45+
"""Stop the honeypot server."""
46+
self.logger.debug("{name!s} received stop".format(name=self))
47+
if self.honeypot:
48+
self.honeypot.stop()
49+
50+
def test(self):
51+
"""Trigger service alerts and return a list of triggered event types."""
52+
import requests
53+
54+
event_types = list()
55+
56+
self.logger.debug("Executing service test...")
57+
58+
# Drupal CVE-2018-7600 test - once with POST, once with GET
59+
target = "http://127.0.0.1:{port}/".format(port=WEB_PORT)
60+
payload = {"form_id": "user_register_form",
61+
"_drupal_ajax": "1",
62+
"timezone[a][#lazy_builder][]": "exec",
63+
"timezone[a][#lazy_builder][][]": "touch+/tmp/1"}
64+
requests.post(EXPLOIT_FORMAT.format(target=target, exploit=EXPLOIT_USING_POST), data=payload)
65+
event_types += ALERTS
66+
67+
# Now it's GET's turn..
68+
requests.get(EXPLOIT_FORMAT.format(target=target, exploit=EXPLOIT_USING_GET))
69+
event_types += ALERTS
70+
71+
return event_types
72+
73+
def __str__(self):
74+
return "Drupal Honeypot"
75+
76+
77+
service_class = DrupalService

0 commit comments

Comments
 (0)