Skip to content

Commit

Permalink
added an experimental generic scanner - oobtkube (RedHatProductSecuri…
Browse files Browse the repository at this point in the history
…ty#166)

* added an experimental generic scanner - oobtkube, with kubectl added in the container image

* oobtkube: show vulnerable parameter in url

* revert .pre-commit-config according to feedback

* disable pylint warnings for general exception in the oobtkube file

* changed var name to reduce the lenght of line
  • Loading branch information
jeremychoi authored Feb 12, 2024
1 parent b280f37 commit fac00c1
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 0 deletions.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ repos:
# R0801: Similar lines in 2 files. Disabled because it flags any file even those which are unrelated
# R1705: Unnecessary "elif" after "return", remove the leading "el" from "elif" (no-else-return)
# R1710: Either all return statements in a function should return an expression, or none of them should. (inconsistent-return-statements)

- repo: https://github.com/PyCQA/pylint
#rev: v3.0.3
rev: v2.17.4
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,20 @@ scanners:
inline: "echo 'any scan'"
```

(an experimental feature) The following example is to scan a Kubernetes Operator's controller code for a command injection attack:

```yaml
scanners:
generic:
results: "*stdout"
# this config is used when container.type is not 'podman'
# toolDir: scanners/generic/tools
inline: "python3 oobtkube.py -d 300 -p <port> -i <ipaddr> -f <cr_example>.yaml"
```



The following is another example to run a [Trivy](https://github.com/aquasecurity/trivy) scan using the container image:

```yaml
Expand Down
7 changes: 7 additions & 0 deletions containerize/Containerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,18 @@ RUN mkdir -p /opt/firefox /tmp/firefox && \
curl -sfL 'https://download.mozilla.org/?product=firefox-esr-latest-ssl&os=linux64&lang=en-US' | tar xjvf - -C /tmp/firefox && \
mv -T /tmp/firefox/firefox /opt/firefox

## kubectl

RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \
install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl

# Copy artifacts from deps to build RapiDAST
FROM registry.access.redhat.com/ubi9-minimal

COPY --from=deps /opt/zap /opt/zap
COPY --from=deps /opt/firefox /opt/firefox
COPY --from=deps /usr/local/bin/kubectl /usr/local/bin/kubectl

ENV PATH $PATH:/opt/zap/:/opt/rapidast/:/opt/firefox/

## RapiDAST
Expand Down
192 changes: 192 additions & 0 deletions scanners/generic/tools/oobtkube.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
#!/usr/bin/env python3
#######################################
#
# [POC v0.0.1] OOBT(Out of Band testing) for Kubernetes.
# This is to detect vulnerabilites that can be detected with OOBT such as blind command injection.
#
# Current internal workflow:
# 1. run a callback server
# 2. apply k8s config (via '-f'), modifying the value of each of the 'spec' parameters, with a 'curl' command for POC
# 3. check if a connection was requested to the server
# 4. print a result message
#
# A usage example (see options in the code):
# $ python3 oobtkube.py -d <timeout> -p <port> -i <ipaddr> -f <your_cr_config_example>.yaml
#
# Roadmap:
# - improve logging
# - more payload
# - improve modulization and extensibility
#
# Author: [email protected]
#
######################################
import argparse
import os
import socket
import sys
import threading
import time

import yaml

SERVER_HOST = "0.0.0.0"
MESSAGE_DETECTED = "OOB REQUEST DETECTED!!"
MESSAGE_NOT_DETECTED = "No OOB request detected"


def get_spec_from_yaml(yaml_file):
with open(yaml_file, "r", encoding="utf-8") as file:
data = yaml.safe_load(file)

spec = data.get(
"spec", {}
) # If 'spec' key is not present, return an empty dictionary
return spec


def scan_with_k8s_config(cfg_file_path, ipaddr, port):
# Apply Kubernetes config (e.g. CR for Operator, or Pod/resource for webhook)
tmp_file = "/tmp/oobtkube-test.yaml"

spec = get_spec_from_yaml(cfg_file_path)
if not spec:
# pylint: disable=W0719
raise Exception("no spec found")

# test each spec
for sitem in spec.keys():
cmd = f"sed 's/{sitem}:.*/{sitem}: \"curl {ipaddr}:{port}\\/{sitem}\"/g' {cfg_file_path} > {tmp_file}"
print(f"Command run: {cmd}")
os.system(cmd)

kube_cmd = f"kubectl apply -f {tmp_file}"

print(f"Command run: {kube_cmd}")
os.system(kube_cmd)


def start_socket_listener(port, data_received, stop_event, duration):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((SERVER_HOST, port))
server_socket.settimeout(duration)
server_socket.listen(1)

print(f"Listening on port {port}")

try:
client_socket, client_address = server_socket.accept()
print(f"Accepted connection from {client_address}")

while not stop_event.is_set():
try:
data = client_socket.recv(1024)
if not data:
break

print("Received data:", data.decode("utf-8"))

# Send a custom response back to the client
response = "HTTP/1.1 200 OK\r\n\r\nfrom oob_listener!\n"
client_socket.send(response.encode("utf-8"))

data_received.set()

# Stop the listener after the first request
stop_event.set()
break

except socket.timeout:
pass

except Exception as e:
raise RuntimeError("An error occurred. See logs for details.") from e

finally:
client_socket.close()
server_socket.close()


def main():
# Parse command-line arguments
parser = argparse.ArgumentParser(
description="Simulate a socket listener and respond to requests."
)
parser.add_argument(
"-i",
"--ip-addr",
type=str,
required=True,
help="Public IP address for the test target to access",
)
parser.add_argument(
"-p",
"--port",
type=int,
default=12345,
help="Port number for the socket listener (default: 12345)",
)
parser.add_argument(
"-d",
"--duration",
type=int,
default=300,
help="Duration for the listener thread to run in seconds (default: 300 seconds)",
)
parser.add_argument(
"-f", "--filename", type=str, required=True, help="Kubernetes config file path"
)

args = parser.parse_args()

if not args.filename:
print("Error: Please provide a filename using the --filename option.")
sys.exit(1)

# Check if the file exists before creating a thread
if not os.path.exists(args.filename):
raise FileNotFoundError(f"The file '{args.filename}' does not exist.")

# Create a few threading events
data_received = threading.Event()
stop_event = threading.Event()

# Start socket listener in a separate thread
socket_listener_thread = threading.Thread(
target=start_socket_listener,
args=(args.port, data_received, stop_event, args.duration),
)
socket_listener_thread.start()

# Wait for a while to ensure the socket listener is up
# You may need to adjust this delay based on your system
time.sleep(5)

print("Listener thread started")

# Record the start time for the main function
start_time_main = time.time()

# Run kubectl apply command
scan_with_k8s_config(args.filename, args.ip_addr, args.port)

# Check the overall duration periodically
while not stop_event.is_set():
time.sleep(1) # Adjust the sleep duration as needed
elapsed_time_main = time.time() - start_time_main
if elapsed_time_main >= args.duration:
print(f"Program running for {elapsed_time_main} seconds. Exiting...")
stop_event.set()

# Wait for the socket listener thread to finish or timeout
socket_listener_thread.join()

if data_received.is_set():
print(f"RESULT: {MESSAGE_DETECTED}")
else:
print(f"RESULT: {MESSAGE_NOT_DETECTED}")
sys.exit(0)


if __name__ == "__main__":
main()

0 comments on commit fac00c1

Please sign in to comment.