From 1dc92dbecae871a4629a948306d764dfcf54b930 Mon Sep 17 00:00:00 2001 From: Doge Date: Fri, 29 Nov 2024 14:53:54 +0000 Subject: [PATCH] ## [1.0.6] - 2024-11-29 ### Added - Load default secrets and salts - Encrypted Ruby Rails tokens ### Changed - Ruby on rails brute force logic Signed-off-by: d4d --- CHANGELOG.md | 11 + README.md | 4 +- build.gradle | 2 +- .../one/d4d/signsaboteur/forms/EditorTab.form | 12 +- .../one/d4d/signsaboteur/forms/EditorTab.java | 10 + .../forms/dialog/EncryptionDialog.form | 95 +++++++++ .../forms/dialog/EncryptionDialog.java | 90 ++++++++ .../signsaboteur/itsdangerous/BruteForce.java | 22 +- .../signsaboteur/itsdangerous/Derivation.java | 3 +- .../crypto/RubyEncryptionTokenSigner.java | 194 ++++++++++++++++++ .../itsdangerous/crypto/Signers.java | 2 +- .../itsdangerous/crypto/TokenSigner.java | 40 +++- .../model/RubyEncryptedToken.java | 89 ++++++++ .../itsdangerous/model/RubySignedToken.java | 8 + .../model/SignedTokenObjectFinder.java | 44 +++- .../presenter/EditorPresenter.java | 83 ++++++-- .../one/d4d/signsaboteur/utils/Utils.java | 2 +- src/main/resources/strings.properties | 3 + src/test/java/BruteForceTest.java | 32 +++ src/test/java/SignUnsignTest.java | 1 + 20 files changed, 704 insertions(+), 43 deletions(-) create mode 100644 src/main/java/one/d4d/signsaboteur/forms/dialog/EncryptionDialog.form create mode 100644 src/main/java/one/d4d/signsaboteur/forms/dialog/EncryptionDialog.java create mode 100644 src/main/java/one/d4d/signsaboteur/itsdangerous/crypto/RubyEncryptionTokenSigner.java create mode 100644 src/main/java/one/d4d/signsaboteur/itsdangerous/model/RubyEncryptedToken.java diff --git a/CHANGELOG.md b/CHANGELOG.md index a21138e..4640c46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [1.0.6] - 2024-11-29 + +### Added + +- Load default secrets and salts +- Encrypted Ruby Rails tokens + +### Changed + +- Ruby on rails brute force logic + ## [1.0.5] - 2024-05-22 ### Changed diff --git a/README.md b/README.md index 11b684b..77041fd 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # SignSaboteur SignSaboteur is a Burp Suite extension for editing, signing, verifying, and attacking signed tokens. -It supports different types of tokens: [Django TimestampSigner](https://docs.djangoproject.com/en/5.0/topics/signing/#verifying-timestamped-values), [ItsDangerous Signer](https://itsdangerous.palletsprojects.com/en/2.1.x/signer/), [Express cookie-session middleware](https://expressjs.com/en/resources/middleware/cookie-session.html), [OAuth2 Proxy](https://github.com/oauth2-proxy/oauth2-proxy), [Tornado’s signed cookies](https://www.tornadoweb.org/en/stable/guide/security.html), [Ruby Rails Signed cookies](https://api.rubyonrails.org/classes/ActiveSupport/MessageVerifier.html), [Nimbus JOSE + JWT](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/) +It supports different types of tokens: [Django TimestampSigner](https://docs.djangoproject.com/en/5.0/topics/signing/#verifying-timestamped-values), [ItsDangerous Signer](https://itsdangerous.palletsprojects.com/en/2.1.x/signer/), [Express cookie-session middleware](https://expressjs.com/en/resources/middleware/cookie-session.html), [OAuth2 Proxy](https://github.com/oauth2-proxy/oauth2-proxy), [Tornado’s signed cookies](https://www.tornadoweb.org/en/stable/guide/security.html), [Ruby Rails Signed cookies](https://api.rubyonrails.org/classes/ActiveSupport/MessageVerifier.html), [Ruby Rails Encrypted cookies](https://api.rubyonrails.org/v5.2.3/classes/ActiveSupport/MessageEncryptor.html), [Nimbus JOSE + JWT](https://bitbucket.org/connect2id/nimbus-jose-jwt/src/master/) and Unknown signed string. You can find more information about the extension on the Portswigger Research blog post page - [Introducing SignSaboteur: forge signed web tokens with ease](https://portswigger.net/research/introducing-signsaboteur-forge-signed-web-tokens-with-ease). @@ -17,7 +17,7 @@ found [here](https://github.com/blackberry/jwt-editor) and [here](https://github * Ensure that Java JDK 17 or newer is installed * From root of project, run the command `./gradlew jar` -* This should place the JAR file `sign-saboteur-1.0.5.jar` within the `build/libs` directory +* This should place the JAR file `sign-saboteur-1.0.6.jar` within the `build/libs` directory * This can be loaded into Burp by navigating to the `Extensions` tab, `Installed` sub-tab, clicking `Add` and loading the JAR file * This BApp is using the newer Montoya API, so it's best to use the latest version of Burp (try the earlier adopter diff --git a/build.gradle b/build.gradle index 4a32f5e..6bb0a03 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'one.d4d' -version = '1.0.5' +version = '1.0.6' description = 'sign-saboteur' repositories { diff --git a/src/main/java/one/d4d/signsaboteur/forms/EditorTab.form b/src/main/java/one/d4d/signsaboteur/forms/EditorTab.form index 8a66090..0a2f458 100644 --- a/src/main/java/one/d4d/signsaboteur/forms/EditorTab.form +++ b/src/main/java/one/d4d/signsaboteur/forms/EditorTab.form @@ -563,7 +563,7 @@ - + @@ -581,9 +581,17 @@ - + + + + + + + + + diff --git a/src/main/java/one/d4d/signsaboteur/forms/EditorTab.java b/src/main/java/one/d4d/signsaboteur/forms/EditorTab.java index 593d97f..7cf601a 100644 --- a/src/main/java/one/d4d/signsaboteur/forms/EditorTab.java +++ b/src/main/java/one/d4d/signsaboteur/forms/EditorTab.java @@ -83,6 +83,7 @@ public abstract class EditorTab implements ExtensionProvidedEditor { private RSyntaxTextArea textAreaJSONWebSignaturePayload; private JCheckBox checkBoxUnknownURLEncoded; private JCheckBox checkBoxRubyURLEncoded; + private JButton decryptButton; private CodeArea codeAreaDangerousSignature; private CodeArea codeAreaDangerousSeparator; private CodeArea codeAreaOAuthSignature; @@ -108,6 +109,7 @@ public abstract class EditorTab implements ExtensionProvidedEditor { this.signerConfig = signerConfig; this.presenter = new EditorPresenter( this, + rstaFactory, collaboratorPayloadGenerator, actionListenerFactory, presenters, @@ -168,6 +170,7 @@ public void changedUpdate(DocumentEvent e) { buttonSign.addActionListener(e -> presenter.onSignClicked()); buttonAttack.addActionListener(e -> presenter.onAttackClicked()); buttonCopyExpressSignature.addActionListener(e -> presenter.copyExpressSignature()); + decryptButton.addActionListener(e -> presenter.decryptRubyMessage()); } public Window window() { @@ -344,6 +347,9 @@ public void setRubyMessage(String text) { public boolean getRubyIsURLEncoded() { return checkBoxRubyURLEncoded.isSelected(); } + public boolean getRubyIsEncrypted() { + return decryptButton.isEnabled(); + } public void setRubyIsURLEncoded(boolean enabled) { checkBoxRubyURLEncoded.setSelected(enabled); @@ -365,6 +371,10 @@ public void setRubySeparator(byte[] separator) { codeAreaRubySeparator.setData(new ByteArrayEditableData(separator)); } + public void setRubyDecryptButton(boolean enabled) { + decryptButton.setEnabled(enabled); + } + public String getJWTHeader() { return textAreaJSONWebSignatureHeader.getText(); } diff --git a/src/main/java/one/d4d/signsaboteur/forms/dialog/EncryptionDialog.form b/src/main/java/one/d4d/signsaboteur/forms/dialog/EncryptionDialog.form new file mode 100644 index 0000000..72908f6 --- /dev/null +++ b/src/main/java/one/d4d/signsaboteur/forms/dialog/EncryptionDialog.form @@ -0,0 +1,95 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/java/one/d4d/signsaboteur/forms/dialog/EncryptionDialog.java b/src/main/java/one/d4d/signsaboteur/forms/dialog/EncryptionDialog.java new file mode 100644 index 0000000..67ab19d --- /dev/null +++ b/src/main/java/one/d4d/signsaboteur/forms/dialog/EncryptionDialog.java @@ -0,0 +1,90 @@ +package one.d4d.signsaboteur.forms.dialog; + +import one.d4d.signsaboteur.itsdangerous.crypto.*; +import one.d4d.signsaboteur.itsdangerous.model.*; +import one.d4d.signsaboteur.keys.SecretKey; +import one.d4d.signsaboteur.rsta.RstaFactory; +import one.d4d.signsaboteur.utils.ErrorLoggingActionListenerFactory; +import one.d4d.signsaboteur.utils.Utils; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.border.LineBorder; +import java.awt.*; +import java.awt.event.*; +import java.util.List; + +import static java.awt.Color.RED; +import static javax.swing.JOptionPane.WARNING_MESSAGE; + +public class EncryptionDialog extends AbstractDialog { + private JPanel contentPane; + private JButton buttonOK; + private JButton buttonCancel; + private JComboBox comboBoxEncryptionKeys; + private RSyntaxTextArea cypherText; + private SignedToken tokenObject; + private RstaFactory rstaFactory; + + public EncryptionDialog(Window parent, + RstaFactory rstaFactory, + ErrorLoggingActionListenerFactory actionListenerFactory, + List signingKeys, + SignedToken tokenObject) { + super(parent, "encryption_dialog_title"); + this.rstaFactory = rstaFactory; + this.tokenObject = tokenObject; + + setContentPane(contentPane); + getRootPane().setDefaultButton(buttonOK); + + buttonOK.addActionListener(actionListenerFactory.from(e -> onOK())); + buttonCancel.addActionListener(e -> onCancel()); + + contentPane.registerKeyboardAction( + e -> onCancel(), + KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), + JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT + ); + + SecretKey[] signingKeysArray = new SecretKey[signingKeys.size()]; + signingKeys.toArray(signingKeysArray); + + comboBoxEncryptionKeys.setModel(new DefaultComboBoxModel<>(signingKeysArray)); + comboBoxEncryptionKeys.setSelectedIndex(0); + } + + private void onOK() { + SecretKey selectedKey = (SecretKey) comboBoxEncryptionKeys.getSelectedItem(); + + try { + assert selectedKey != null; + TokenSigner s; + if (tokenObject instanceof RubyEncryptedToken) { + s = new RubyEncryptionTokenSigner(selectedKey); + } else { + throw new Exception("Unknown"); + } + tokenObject.setSigner(s); + String text = ((RubyEncryptedToken) tokenObject).getCypherText(); + cypherText.setText(text); + + Border serializedTextAreaBorder = text.equals("Error") ? new LineBorder(RED, 1) : null; + cypherText.setBorder(serializedTextAreaBorder); + } catch (Exception e) { + tokenObject = null; + JOptionPane.showMessageDialog( + this, + e.getMessage(), + Utils.getResourceString("error_title_unable_to_sign"), + WARNING_MESSAGE + ); + } + } + + private void createUIComponents() { + // TODO: place custom component creation code here + cypherText = rstaFactory.buildDefaultTextArea(); + } +} diff --git a/src/main/java/one/d4d/signsaboteur/itsdangerous/BruteForce.java b/src/main/java/one/d4d/signsaboteur/itsdangerous/BruteForce.java index 9eb6bae..b3e1e8f 100644 --- a/src/main/java/one/d4d/signsaboteur/itsdangerous/BruteForce.java +++ b/src/main/java/one/d4d/signsaboteur/itsdangerous/BruteForce.java @@ -2,6 +2,7 @@ import com.google.common.collect.Lists; import one.d4d.signsaboteur.itsdangerous.crypto.TokenSigner; +import one.d4d.signsaboteur.itsdangerous.model.RubyEncryptedToken; import one.d4d.signsaboteur.itsdangerous.model.SignedToken; import one.d4d.signsaboteur.itsdangerous.model.UnknownSignedToken; import one.d4d.signsaboteur.keys.SecretKey; @@ -9,10 +10,7 @@ import one.d4d.signsaboteur.presenter.PresenterStore; import one.d4d.signsaboteur.utils.Utils; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.concurrent.*; public class BruteForce extends Presenter { @@ -52,10 +50,24 @@ public BruteForce (Set secrets, presenters.register(this); } + public List prepareEncryption() { + List attacks = new ArrayList<>(); + TokenSigner is = token.getSigner(); + this.signingKeys.forEach(key -> { + TokenSigner ks = new TokenSigner(key); + attacks.add(ks); + }); + secrets.forEach(secret -> + is.getKnownDerivations().forEach(d -> attacks.addAll(is.cloneWithSaltDerivation(secret, salts, d))) + ); + return attacks; + } + public List prepareAdvanced() { List attacks = new ArrayList<>(); List derivations = new ArrayList<>(List.of(Derivation.values())); + derivations.remove(Derivation.RUBY_ENCRYPTION); Set messages = new HashSet<>(List.of(MessageDerivation.NONE)); @@ -112,7 +124,7 @@ public SecretKey search(List attacks) { public SecretKey parallel() { int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors(); - List attacks = prepareAdvanced(); + List attacks = token instanceof RubyEncryptedToken ? prepareEncryption() : prepareAdvanced(); if (NUMBER_OF_CORES < 2) { return search(attacks); } diff --git a/src/main/java/one/d4d/signsaboteur/itsdangerous/Derivation.java b/src/main/java/one/d4d/signsaboteur/itsdangerous/Derivation.java index 7abf0dd..3379ee8 100644 --- a/src/main/java/one/d4d/signsaboteur/itsdangerous/Derivation.java +++ b/src/main/java/one/d4d/signsaboteur/itsdangerous/Derivation.java @@ -13,7 +13,8 @@ public enum Derivation { @Expose @SerializedName("7") RUBY("RUBY"), @Expose @SerializedName("8") RUBY5("RUBY5"), @Expose @SerializedName("9") RUBY5_TRUNCATED("RUBY5_TRUNCATED"), - @Expose @SerializedName("10") RUBY_KEY_GENERATOR("RUBY_KEY_GENERATOR"); + @Expose @SerializedName("10") RUBY_KEY_GENERATOR("RUBY_KEY_GENERATOR"), + @Expose @SerializedName("11") RUBY_ENCRYPTION("RUBY_ENCRYPTION"); public final String name; Derivation(String name) { diff --git a/src/main/java/one/d4d/signsaboteur/itsdangerous/crypto/RubyEncryptionTokenSigner.java b/src/main/java/one/d4d/signsaboteur/itsdangerous/crypto/RubyEncryptionTokenSigner.java new file mode 100644 index 0000000..d499ea2 --- /dev/null +++ b/src/main/java/one/d4d/signsaboteur/itsdangerous/crypto/RubyEncryptionTokenSigner.java @@ -0,0 +1,194 @@ +package one.d4d.signsaboteur.itsdangerous.crypto; + +import com.google.common.primitives.Bytes; +import one.d4d.signsaboteur.itsdangerous.*; +import one.d4d.signsaboteur.keys.SecretKey; +import one.d4d.signsaboteur.utils.Utils; + +import javax.crypto.*; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class RubyEncryptionTokenSigner extends TokenSigner { + public RubyEncryptionTokenSigner(SecretKey key) { + super(key); + this.knownDerivations = EnumSet.of(Derivation.RUBY_ENCRYPTION); + } + + public RubyEncryptionTokenSigner(byte[] sep) { + this(new byte[]{}, sep); + } + + public RubyEncryptionTokenSigner(byte[] secret_key, byte[] sep) { + this(Algorithms.SHA256, Derivation.RUBY_ENCRYPTION, MessageDerivation.NONE, MessageDigestAlgorithm.NONE, secret_key, new byte[]{}, sep); + } + + public RubyEncryptionTokenSigner( + Algorithms digestMethod, + Derivation keyDerivation, + MessageDerivation messageDerivation, + MessageDigestAlgorithm digest, + byte[] secret_key, + byte[] salt, + byte[] sep) { + super(digestMethod, keyDerivation, messageDerivation, digest, secret_key, salt, sep); + this.knownDerivations = EnumSet.of(Derivation.RUBY_ENCRYPTION); + } + + private byte[] decrypt(byte[] keyBytes, byte[] ciphertextBytes, byte[] ivBytes, byte[] tagBytes) throws Exception { + try { + byte[] cb = Base64.getDecoder().decode(ciphertextBytes); + byte[] iv = Base64.getDecoder().decode(ivBytes); + byte[] tag = Base64.getDecoder().decode(tagBytes); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + javax.crypto.SecretKey secretKey = new SecretKeySpec(keyBytes, "AES"); + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(tag.length * Byte.SIZE, iv); + cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec); + + cipher.update(cb); + return cipher.doFinal(tag); + + } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | + InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException ignored){ + throw new Exception("Invalid"); + } + } + private String encrypt(byte[] keyBytes, byte[] ciphertextBytes, byte[] ivBytes) throws Exception { + try { + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + javax.crypto.SecretKey secretKey = new SecretKeySpec(keyBytes, "AES"); + GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(16 * Byte.SIZE, ivBytes); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec); + + byte[] encryptedData = cipher.doFinal(ciphertextBytes); + byte[] ciphertext = Arrays.copyOf(encryptedData, encryptedData.length - 16); + byte[] authTag = Arrays.copyOfRange(encryptedData, encryptedData.length - 16, encryptedData.length); + + return Stream.of(ciphertext, ivBytes, authTag) + .map(arr -> new String(Base64.getEncoder().encode(arr), Charset.defaultCharset())) + .collect(Collectors.joining("--")); + + + } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | + InvalidAlgorithmParameterException | BadPaddingException | IllegalBlockSizeException ignored){ + throw new Exception("failed"); + } + } + + public byte[] get_signature(byte[] value) { + try { + byte[] key = derive_key(); + byte[] ivBytes = new byte[12]; + // tag 16 + + Random random = new Random(); + random.nextBytes(ivBytes); + + String encryptedData = encrypt(key, value, ivBytes); + return encryptedData.substring(encryptedData.lastIndexOf("--") + 2).getBytes(StandardCharsets.UTF_8); + } catch (Exception e) { + return new byte[]{}; + } + } + + public byte[] get_signature_unsafe(byte[] value) throws Exception { + byte[] key = derive_key(); + byte[] ivBytes = new byte[12]; + // tag 16 + + Random random = new Random(); + random.nextBytes(ivBytes); + + String encryptedData = encrypt(key, value, ivBytes); + return encryptedData.substring(encryptedData.lastIndexOf("--") + 2).getBytes(StandardCharsets.UTF_8); + } + + public byte[] get_signature_bytes(byte[] value) { + try { + byte[] message = derive_message(value); + byte[] key = derive_key(); + byte[] ivBytes = new byte[12]; + + Random random = new Random(); + random.nextBytes(ivBytes); + + String encryptedData = encrypt(key, message, ivBytes); + return encryptedData.substring(encryptedData.lastIndexOf("--") + 2).getBytes(StandardCharsets.UTF_8); + } catch (Exception e) { + return new byte[]{}; + } + } + + public byte[] sign(byte[] value) { + try { + byte[] message = derive_message(value); + byte[] key = derive_key(); + byte[] ivBytes = new byte[12]; + + Random random = new Random(); + random.nextBytes(ivBytes); + + return encrypt(key, message, ivBytes).getBytes(StandardCharsets.UTF_8); + }catch (Exception ignored) {} + return value; + } + + public boolean verify_signature(byte[] value, byte[] sign) { + try { + byte[] key = derive_key(); + int i = Collections.lastIndexOfSubList(Bytes.asList(value), Bytes.asList(sep)); + byte[] cipher = Arrays.copyOfRange(value, 0, i); + byte[] iv = Arrays.copyOfRange(value, i + 1, value.length); + + decrypt(key, cipher, iv, sign); + return true; + } catch (Exception e) { + return false; + } + } + + public boolean verify_signature_bytes(byte[] value, byte[] sign) { + try { + byte[] key = derive_key(); + int i = Collections.lastIndexOfSubList(Bytes.asList(value), Bytes.asList(sep)); + byte[] cipher = Arrays.copyOfRange(value, 0, i); + byte[] iv = Arrays.copyOfRange(value, i + 1, value.length); + + decrypt(key, cipher, iv, sign); + return true; + } catch (Exception e) { + return false; + } + } + + public byte[] unsign(byte[] value) throws BadSignatureException { + int i = Collections.lastIndexOfSubList(Bytes.asList(value), Bytes.asList(sep)); + byte[] message = Arrays.copyOfRange(value, 0, i); + byte[] signature = Arrays.copyOfRange(value, i + 1, value.length); + return fast_unsign(message, signature); + } + + public byte[] fast_unsign(byte[] message, byte[] signature) throws BadSignatureException { + try { + byte[] key = derive_key(); + int i = Collections.lastIndexOfSubList(Bytes.asList(message), Bytes.asList(sep)); + byte[] cipher = Arrays.copyOfRange(message, 0, i); + byte[] iv = Arrays.copyOfRange(message, i + 2, message.length); + + return decrypt(key, cipher, iv, signature); + } catch (Exception e) { + throw new BadSignatureException("Signature didn't match"); + } + } +} diff --git a/src/main/java/one/d4d/signsaboteur/itsdangerous/crypto/Signers.java b/src/main/java/one/d4d/signsaboteur/itsdangerous/crypto/Signers.java index b0b081e..ab3ccc5 100644 --- a/src/main/java/one/d4d/signsaboteur/itsdangerous/crypto/Signers.java +++ b/src/main/java/one/d4d/signsaboteur/itsdangerous/crypto/Signers.java @@ -1,5 +1,5 @@ package one.d4d.signsaboteur.itsdangerous.crypto; public enum Signers { - DANGEROUS, EXPRESS, OAUTH, TORNADO, RUBY, JWT, NIMBUSDS, UNKNOWN + DANGEROUS, EXPRESS, OAUTH, TORNADO, RUBY, JWT, NIMBUSDS, UNKNOWN, ENCRYPTION } diff --git a/src/main/java/one/d4d/signsaboteur/itsdangerous/crypto/TokenSigner.java b/src/main/java/one/d4d/signsaboteur/itsdangerous/crypto/TokenSigner.java index 71b8319..e02b2a2 100644 --- a/src/main/java/one/d4d/signsaboteur/itsdangerous/crypto/TokenSigner.java +++ b/src/main/java/one/d4d/signsaboteur/itsdangerous/crypto/TokenSigner.java @@ -229,6 +229,16 @@ public byte[] derive_key() throws DerivationException { SecretKeyFactory f = SecretKeyFactory.getInstance(String.format("PBKDF2With%s", PRF)); return f.generateSecret(spec).getEncoded(); } + case RUBY_ENCRYPTION -> { + KeySpec spec = new PBEKeySpec( + (new String(secret_key)).toCharArray(), + salt, + 1000, + 32 * 8 + ); + SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + return f.generateSecret(spec).getEncoded(); + } default -> throw new DerivationException("Unknown key derivation method"); } } catch (NoSuchAlgorithmException | InvalidKeySpecException e) { @@ -325,7 +335,8 @@ public SecretKey getKey(String hash) { new String(sep), digestMethod, keyDerivation, - messageDerivation, messageDigestAlgorithm); + messageDerivation, + messageDigestAlgorithm); } public SecretKey getKey() { return new SecretKey( @@ -335,7 +346,8 @@ public SecretKey getKey() { new String(sep), digestMethod, keyDerivation, - messageDerivation, messageDigestAlgorithm); + messageDerivation, + messageDigestAlgorithm); } @Override @@ -355,6 +367,30 @@ public TokenSigner clone() { } } + public List cloneWithSaltDerivation(byte[] secret, List salts) { + List copies = new ArrayList<>(); + if (keyDerivation == Derivation.NONE || keyDerivation == Derivation.HASH) { + TokenSigner s = this.clone(); + s.setSecretKey(secret); + copies.add(s); + } else { + salts.forEach(salt -> { + TokenSigner s = this.clone(); + s.setSecretKey(secret); + s.setSalt(salt); + copies.add(s); + }); + } + return copies; + } + public List cloneWithSaltDerivation( + byte[] secret, + List salts, + Derivation keyDerivation) { + this.keyDerivation = keyDerivation; + return this.cloneWithSaltDerivation(secret, salts); + } + public List cloneWithSaltDerivation(String secret, Set salts) { List copies = new ArrayList<>(); if (keyDerivation == Derivation.NONE || keyDerivation == Derivation.HASH) { diff --git a/src/main/java/one/d4d/signsaboteur/itsdangerous/model/RubyEncryptedToken.java b/src/main/java/one/d4d/signsaboteur/itsdangerous/model/RubyEncryptedToken.java new file mode 100644 index 0000000..3104f5e --- /dev/null +++ b/src/main/java/one/d4d/signsaboteur/itsdangerous/model/RubyEncryptedToken.java @@ -0,0 +1,89 @@ +package one.d4d.signsaboteur.itsdangerous.model; + +import com.nimbusds.jwt.JWTClaimsSet; +import one.d4d.signsaboteur.itsdangerous.*; +import one.d4d.signsaboteur.itsdangerous.crypto.RubyEncryptionTokenSigner; +import one.d4d.signsaboteur.itsdangerous.crypto.RubyTokenSigner; +import one.d4d.signsaboteur.itsdangerous.crypto.Signers; +import one.d4d.signsaboteur.itsdangerous.crypto.TokenSigner; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HexFormat; + +public class RubyEncryptedToken extends SignedToken { + public byte[] separator; + public boolean isURLEncoded; + + public RubyEncryptedToken(String message, String signature) { + this(message, signature, "--".getBytes(), false); + } + public RubyEncryptedToken(String message, String signature, boolean isURLEncoded) { + this(message, signature, "--".getBytes(), isURLEncoded); + } + + public RubyEncryptedToken(String message, String signature, byte[] separator, boolean isURLEncoded) { + super(message); + this.signature = signature; + this.separator = separator; + this.isURLEncoded = isURLEncoded; + this.signer = new RubyEncryptionTokenSigner( + Algorithms.SHA256, + Derivation.RUBY_ENCRYPTION, + MessageDerivation.NONE, + MessageDigestAlgorithm.NONE, + new byte[]{}, + new byte[]{}, + separator); + } + + @Override + public String serialize() { + String raw = String.format("%s%s%s", message, new String(separator), signature); + return isURLEncoded ? URLEncoder.encode(raw, StandardCharsets.UTF_8) : raw; + } + + @Override + public void resign() throws Exception { + try { + byte[] decrypted = this.signer.fast_unsign(message.getBytes(StandardCharsets.UTF_8), signature.getBytes(StandardCharsets.UTF_8)); + String encrypted = new String(this.signer.sign(decrypted)); + this.message = encrypted.substring(0, encrypted.lastIndexOf("--")); + this.signature = encrypted.substring(encrypted.lastIndexOf("--") + 2); + }catch (BadSignatureException ignored) { + } + } + + @Override + public void setClaims(JWTClaimsSet claims) { + + } + + public String getCypherText() { + try { + return new String(this.signer.fast_unsign(message.getBytes(StandardCharsets.UTF_8), signature.getBytes(StandardCharsets.UTF_8))); + } catch (BadSignatureException e) { + return "Error"; + } + } + + public void setCypherText(String text) { + String encrypted = new String(this.signer.sign(text.getBytes(StandardCharsets.UTF_8))); + this.message = encrypted.substring(0, encrypted.lastIndexOf("--")); + this.signature = encrypted.substring(encrypted.lastIndexOf("--") + 2); + } + + public boolean isURLEncoded() { return isURLEncoded;} + + public String getSignersName() { + return Signers.ENCRYPTION.name(); + } + + public byte[] getSeparator() { + return separator; + } + + public void setSeparator(byte[] separator) { + this.separator = separator; + } +} diff --git a/src/main/java/one/d4d/signsaboteur/itsdangerous/model/RubySignedToken.java b/src/main/java/one/d4d/signsaboteur/itsdangerous/model/RubySignedToken.java index c794276..017d441 100644 --- a/src/main/java/one/d4d/signsaboteur/itsdangerous/model/RubySignedToken.java +++ b/src/main/java/one/d4d/signsaboteur/itsdangerous/model/RubySignedToken.java @@ -3,6 +3,8 @@ import one.d4d.signsaboteur.itsdangerous.crypto.RubyTokenSigner; import one.d4d.signsaboteur.itsdangerous.crypto.Signers; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.HexFormat; public class RubySignedToken extends UnknownSignedToken { @@ -24,6 +26,12 @@ public void resign() throws Exception { HexFormat hexFormat = HexFormat.of(); this.signature = hexFormat.formatHex(signer.get_signature_unsafe(message.getBytes())); } + @Override + public String serialize() { + String raw = isURLEncoded ? URLEncoder.encode(message, StandardCharsets.UTF_8) : message; + return String.format("%s%s%s", raw, new String(separator), signature); + } + @Override public String getEncodedSignature() { diff --git a/src/main/java/one/d4d/signsaboteur/itsdangerous/model/SignedTokenObjectFinder.java b/src/main/java/one/d4d/signsaboteur/itsdangerous/model/SignedTokenObjectFinder.java index 5b6ae90..77d822c 100644 --- a/src/main/java/one/d4d/signsaboteur/itsdangerous/model/SignedTokenObjectFinder.java +++ b/src/main/java/one/d4d/signsaboteur/itsdangerous/model/SignedTokenObjectFinder.java @@ -85,8 +85,12 @@ public static List extractSignedTokenObjects(SignerConfig si .ifPresent(value -> { if(signedTokensObjects.stream().noneMatch(test -> test.getOriginal().equalsIgnoreCase(candidate.toString()))) signedTokensObjects.add(new MutableSignedToken(candidate.toString(), value)); - } - ); + }); + parseRubyEncryptedToken("", candidate.toString()) + .ifPresent(value -> { + if(signedTokensObjects.stream().noneMatch(test -> test.getOriginal().equalsIgnoreCase(candidate.toString()))) + signedTokensObjects.add(new MutableSignedToken(candidate.toString(), value)); + }); } } @@ -529,10 +533,10 @@ public static Optional parseRubySignedToken(String key, String valu String signature = parts[1]; boolean isURLEncoded = payload.indexOf('%') > -1; try { - String tmp = payload; - if (isURLEncoded) tmp = URLDecoder.decode(payload, StandardCharsets.UTF_8); - Base64.getUrlDecoder().decode(tmp); - byte[] sign = Utils.normalization((URLDecoder.decode(signature, StandardCharsets.UTF_8)).getBytes()); + if (isURLEncoded) { + payload = URLDecoder.decode(payload, StandardCharsets.UTF_8); + } + byte[] sign = Utils.normalization(signature.getBytes(StandardCharsets.UTF_8)); if (sign == null) return Optional.empty(); if (Arrays.stream(SIGNATURES_LENGTH).noneMatch(x -> x == sign.length)) return Optional.empty(); } catch (Exception e) { @@ -544,4 +548,32 @@ public static Optional parseRubySignedToken(String key, String valu return Optional.empty(); } + + public static Optional parseRubyEncryptedToken(String key, String value) { + String[] parts = value.split("--"); + if (parts.length == 3) { + String payload = parts[0]; + String iv = parts[1]; + String signature = parts[2]; + boolean isURLEncoded = payload.indexOf('%') > -1; + try { + if (isURLEncoded) { + payload = URLDecoder.decode(payload, StandardCharsets.UTF_8); + iv = URLDecoder.decode(iv, StandardCharsets.UTF_8); + signature = URLDecoder.decode(signature, StandardCharsets.UTF_8); + } + Base64.getDecoder().decode(payload); + Base64.getDecoder().decode(iv); + Base64.getDecoder().decode(signature); + + } catch (Exception e) { + return Optional.empty(); + } + if (iv.length() != 16 ) return Optional.empty(); + RubyEncryptedToken t = new RubyEncryptedToken(payload + "--" + iv, signature, isURLEncoded); + return Optional.of(t); + } + + return Optional.empty(); + } } diff --git a/src/main/java/one/d4d/signsaboteur/presenter/EditorPresenter.java b/src/main/java/one/d4d/signsaboteur/presenter/EditorPresenter.java index f295f8f..54dae48 100644 --- a/src/main/java/one/d4d/signsaboteur/presenter/EditorPresenter.java +++ b/src/main/java/one/d4d/signsaboteur/presenter/EditorPresenter.java @@ -12,6 +12,7 @@ import one.d4d.signsaboteur.itsdangerous.model.*; import one.d4d.signsaboteur.keys.Key; import one.d4d.signsaboteur.keys.SecretKey; +import one.d4d.signsaboteur.rsta.RstaFactory; import one.d4d.signsaboteur.utils.ErrorLoggingActionListenerFactory; import one.d4d.signsaboteur.utils.Utils; @@ -33,11 +34,13 @@ public class EditorPresenter extends Presenter { private final CollaboratorPayloadGenerator collaboratorPayloadGenerator; private final ErrorLoggingActionListenerFactory actionListenerFactory; private final MessageDialogFactory messageDialogFactory; + private final RstaFactory rstaFactory; private boolean selectionChanging; private URL targetURL; public EditorPresenter( EditorTab view, + RstaFactory rstaFactory, CollaboratorPayloadGenerator collaboratorPayloadGenerator, ErrorLoggingActionListenerFactory actionListenerFactory, PresenterStore presenters, @@ -45,6 +48,7 @@ public EditorPresenter( this.view = view; this.model = new EditorModel(signerConfig); this.collaboratorPayloadGenerator = collaboratorPayloadGenerator; + this.rstaFactory = rstaFactory; this.actionListenerFactory = actionListenerFactory; this.presenters = presenters; messageDialogFactory = new MessageDialogFactory(view.uiComponent()); @@ -167,31 +171,43 @@ private void setTornado(TornadoSignedToken token) { view.setTornadoSignature(token.getSignature()); } - private RubySignedToken getRuby() { + private SignedToken getRuby() { // String message = URLEncoder.encode(Base64.getUrlEncoder().encodeToString(view.getRubyMessage().getBytes()), StandardCharsets.UTF_8); boolean isURLEncoded = view.getRubyIsURLEncoded(); - String message = Base64.getUrlEncoder().encodeToString(Utils.compactJSON(view.getRubyMessage()).getBytes()); - if(isURLEncoded) { - message = URLEncoder.encode(message, StandardCharsets.UTF_8); + boolean isEncrypted = view.getRubyIsEncrypted(); + if (isEncrypted) { + String message = view.getRubyMessage(); + String signature = view.getRubySignature(); + byte[] separator = view.getRubySeparator().length == 0 ? new byte[]{'-','-'} : view.getRubySeparator(); + return new RubyEncryptedToken(message, signature, separator, isURLEncoded); + } else { + String message = Base64.getUrlEncoder().encodeToString(Utils.compactJSON(view.getRubyMessage()).getBytes()); + String signature = view.getRubySignature(); + byte[] separator = view.getRubySeparator().length == 0 ? new byte[]{'-','-'} : view.getRubySeparator(); + return new RubySignedToken(message, signature, separator, isURLEncoded); } - String signature = view.getRubySignature(); - byte[] separator = view.getRubySeparator().length == 0 ? new byte[]{46} : view.getRubySeparator(); - return new RubySignedToken(message, signature, separator, isURLEncoded); } - private void setRuby(RubySignedToken token) { -// view.setRubyMessage(new String(Base64.getUrlDecoder().decode(URLDecoder.decode(token.getEncodedMessage(), StandardCharsets.UTF_8)))); - boolean isURLEncoded = token.isURLEncoded(); - String message = token.getEncodedMessage(); - if (isURLEncoded) { - message = URLDecoder.decode(token.getEncodedMessage(), StandardCharsets.UTF_8); + private void setRuby(SignedToken token) { + if (token instanceof RubySignedToken signedToken) { + // view.setRubyMessage(new String(Base64.getUrlDecoder().decode(URLDecoder.decode(token.getEncodedMessage(), StandardCharsets.UTF_8)))); + boolean isURLEncoded = signedToken.isURLEncoded(); + String message = signedToken.getEncodedMessage(); + message = new String(Base64.getUrlDecoder().decode(message)); + message = Utils.prettyPrintJSON(message); + view.setRubyDecryptButton(false); + view.setRubyIsURLEncoded(isURLEncoded); + view.setRubyMessage(message); + view.setRubySignature(signedToken.getEncodedSignature()); + view.setRubySeparator(signedToken.getSeparator()); + } + if (token instanceof RubyEncryptedToken encryptedToken) { + view.setRubyDecryptButton(true); + view.setRubyIsURLEncoded(encryptedToken.isURLEncoded()); + view.setRubyMessage(encryptedToken.getEncodedMessage()); + view.setRubySignature(encryptedToken.getEncodedSignature()); + view.setRubySeparator(encryptedToken.getSeparator()); } - message = new String(Base64.getUrlDecoder().decode(message)); - message = Utils.prettyPrintJSON(message); - view.setRubyMessage(message); - view.setRubySignature(token.getEncodedSignature()); - view.setRubySeparator(token.getSeparator()); - view.setUnknownIsURLEncoded(isURLEncoded); } private JSONWebSignature getJSONWebSignature() { @@ -261,9 +277,9 @@ public void onSelectionChanged() { } else if (tokenObject instanceof TornadoSignedToken) { view.setTornadoMode(); setTornado((TornadoSignedToken) tokenObject); - } else if (tokenObject instanceof RubySignedToken) { + } else if (tokenObject instanceof RubySignedToken || tokenObject instanceof RubyEncryptedToken) { view.setRubyMode(); - setRuby((RubySignedToken) tokenObject); + setRuby(tokenObject); } else if (tokenObject instanceof JSONWebSignature) { view.setJWTMode(); setJSONWebSignature((JSONWebSignature) tokenObject); @@ -286,6 +302,8 @@ public void onAttackClicked() { attackDialog(); } + public void decryptRubyMessage() { decryptDialog(); } + private void attackDialog() { KeyPresenter keysPresenter = (KeyPresenter) presenters.get(KeyPresenter.class); @@ -340,7 +358,7 @@ private void signingDialog() { MutableSignedToken mutableJoseObject = model.getSignedTokenObject(view.getSelectedSignedTokenObjectIndex()); SignedToken tokenObject = mutableJoseObject.getModified(); - if (keysPresenter.getSigningKeys().size() == 0) { + if (keysPresenter.getSigningKeys().isEmpty()) { messageDialogFactory.showWarningDialog("error_title_no_signing_keys", "error_no_signing_keys"); return; } @@ -380,6 +398,27 @@ private void signingDialog() { } } + private void decryptDialog() { + KeyPresenter keysPresenter = (KeyPresenter) presenters.get(KeyPresenter.class); + + MutableSignedToken mutableJoseObject = model.getSignedTokenObject(view.getSelectedSignedTokenObjectIndex()); + SignedToken tokenObject = mutableJoseObject.getModified(); + + if (keysPresenter.getSigningKeys().isEmpty()) { + messageDialogFactory.showWarningDialog("error_title_no_signing_keys", "error_no_signing_keys"); + return; + } + + EncryptionDialog signDialog = new EncryptionDialog( + view.window(), + rstaFactory, + actionListenerFactory, + keysPresenter.getSigningKeys(), + tokenObject + ); + signDialog.display(); + } + public void onAttackClicked(Attack mode) { KeyPresenter keysPresenter = (KeyPresenter) presenters.get(KeyPresenter.class); diff --git a/src/main/java/one/d4d/signsaboteur/utils/Utils.java b/src/main/java/one/d4d/signsaboteur/utils/Utils.java index 8cd7740..d4131a0 100644 --- a/src/main/java/one/d4d/signsaboteur/utils/Utils.java +++ b/src/main/java/one/d4d/signsaboteur/utils/Utils.java @@ -70,7 +70,7 @@ public static List searchByteArrayBase64(ByteArray data) { } public static List searchByteArrayRuby(ByteArray data) { - return searchByteArray(data, Sets.union(BASE64_URL_SET, Set.of(37, 61)) , 28); + return searchByteArray(data, Sets.union(BASE64_URL_SET, Set.of(37)) , 28); } public static String getSignedTokenIDWithHash(String token) { diff --git a/src/main/resources/strings.properties b/src/main/resources/strings.properties index f135108..7b6f78b 100644 --- a/src/main/resources/strings.properties +++ b/src/main/resources/strings.properties @@ -44,6 +44,7 @@ keys_confirm_overwrite_title=Overwrite key error_title_no_signing_keys=No signing keys found error_no_signing_keys=Try to brute force first or add keys manually sign_dialog_title=Sign Token +encryption_dialog_title=Decrypt Token error_title_unable_to_sign=Unable to sign the token oauth_tab_label=OAuth oauth_payload_label=Payload @@ -116,3 +117,5 @@ button_load_defaults=Load defaults tooltip_NIMBUSDS=Use Nimbusds library to parse Json Web tokens urlencoded_checkbox=URL Encode proxy_settings_enable_passwive_scan=Enable Passive scan +button_ruby_decrypt=Decrypt +syntaxis=text/json diff --git a/src/test/java/BruteForceTest.java b/src/test/java/BruteForceTest.java index d002135..8589a77 100644 --- a/src/test/java/BruteForceTest.java +++ b/src/test/java/BruteForceTest.java @@ -1,6 +1,7 @@ import one.d4d.signsaboteur.itsdangerous.Attack; import one.d4d.signsaboteur.itsdangerous.BruteForce; import one.d4d.signsaboteur.itsdangerous.crypto.DangerousTokenSigner; +import one.d4d.signsaboteur.itsdangerous.crypto.RubyEncryptionTokenSigner; import one.d4d.signsaboteur.itsdangerous.model.SignedToken; import one.d4d.signsaboteur.itsdangerous.model.SignedTokenObjectFinder; import one.d4d.signsaboteur.keys.SecretKey; @@ -55,4 +56,35 @@ void BruteForceMultiThreatAttack() { }); } + @Test + void encryptionTest() { + String secret = "aeb977de013ade650b97e0aa5246813591104017871a7753fe186e9634c9129b367306606878985c759ca4fddd17d955207011bb855ef01ed414398b4ac8317b"; + String salt = "authenticated encrypted cookie"; + String app_session = "isteTiyNSFdbUoabLodAVDd4jQuj%2F5t%2FRTE6BqyklssH0ye%2F2RnMJ3fIBkFfr9tei5yh5agfgX%2F9Mi8gQIA4zAOXwGyCuJBhauvszTYDCW7Q%2FVwDXIc4lAtiO%2FmBf5txRBoAulkc4ZTAaT1FMM%2F6ky7p8oul0hbi4xZf1%2ByURhPci4f%2FEGNYsJ2eLx9BALX7sVOB3dYpN6eQb%2B7LTXRxy2bnObmiHQaNaTx6jhdWwRcdEgGph7le6dN49gi%2FiLp%2B0yecWNyEzQbZ%2FRHKniIf%2FmCFTVw%3D--e7EBPhAdylQsT6It--wIte3m%2F2WUhtfKQewysoSQ%3D%3D"; + + Assertions.assertDoesNotThrow(() -> { + Optional optionalSignedToken = SignedTokenObjectFinder.parseRubyEncryptedToken("",app_session); + if (optionalSignedToken.isPresent()) { + SignedToken token = optionalSignedToken.get(); + byte[] sep = new byte[]{'-','-'}; + RubyEncryptionTokenSigner s = new RubyEncryptionTokenSigner(sep); + token.setSigner(s); + final Set secrets = new HashSet<>(List.of(secret)); + final Set salts = new HashSet<>(List.of(salt)); + final List knownKeys = new ArrayList<>(); + + BruteForce bf = new BruteForce(secrets, salts, knownKeys, Attack.FAST, token); + SecretKey sk = bf.parallel(); + Assertions.assertNotNull(sk); + RubyEncryptionTokenSigner ns = new RubyEncryptionTokenSigner(sk); + System.out.println(sk.toJSONString()); + token.setSigner(ns); + token.resign(); + System.out.println(token); + } else { + Assertions.fail("Token not found."); + } + }); + } + } diff --git a/src/test/java/SignUnsignTest.java b/src/test/java/SignUnsignTest.java index 35874d9..1e9ddbd 100644 --- a/src/test/java/SignUnsignTest.java +++ b/src/test/java/SignUnsignTest.java @@ -19,6 +19,7 @@ void KeyDerivationTest() { long start = System.currentTimeMillis(); for (Algorithms a : Algorithms.values()) { for (Derivation d : Derivation.values()) { + if (d == Derivation.RUBY_ENCRYPTION) continue; for (MessageDerivation md : MessageDerivation.values()) { for (MessageDigestAlgorithm mda : MessageDigestAlgorithm.values()) { byte[] secret = "secret".getBytes();