Repository navigation
Add Android counterpart to MASTG-TEST-0211 for broken hashing algorithms #3952
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 5 commits
0d1df84
7949fa3
3e8f7d9
21df7dc
5fa412b
b4e2ab7
b3fd991
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| --- | ||
| platform: android | ||
| title: Uses of Broken Hashing Algorithms in MessageDigest with semgrep | ||
| id: MASTG-DEMO-0x01 | ||
| code: [kotlin] | ||
| test: MASTG-TEST-0x01 | ||
| --- | ||
|
|
||
| ## Sample | ||
|
|
||
| The code snippet below shows sample code that hashes data with MD5, SHA-1 (including the `SHA1` alias), and SHA-256. | ||
|
|
||
| {{ MastgTest.kt # MastgTest_reversed.java }} | ||
|
|
||
| ## Steps | ||
|
|
||
| Let's run our @MASTG-TOOL-0110 rule against the sample code. | ||
|
|
||
| {{ ../../../../rules/mastg-android-broken-hashing-algorithms.yaml }} | ||
|
|
||
| {{ run.sh }} | ||
|
|
||
| ## Observation | ||
|
|
||
| The rule has identified several instances in the code file where broken hashing algorithms are used. The specified line numbers can be located in the reverse-engineered code for further investigation and remediation. | ||
|
|
||
| {{ output.txt }} | ||
|
|
||
| ## Evaluation | ||
|
|
||
| The test case fails because the app hashes security-relevant data using broken algorithms, specifically MD5 and SHA-1. | ||
|
|
||
| Review each of the reported instances: | ||
|
|
||
| - Line 43 (FAIL): Hashes a password with MD5. In addition to being broken for collision resistance, raw MD5 is vulnerable to rapid offline dictionary and rainbow table attacks. | ||
| - Line 50 (FAIL): Hashes authentication token material with SHA-1. | ||
| - Line 57 (FAIL): Hashes authentication token material with the `SHA1` JCA alias of SHA-1. This is the same algorithm as line 50; the alias is included because Android code often uses `SHA1` instead of `SHA-1`. | ||
| - Line 64 (Requires Contextual Validation): Hashes `readme.txt` using MD5 for a checksum. While flagged by static analysis, using MD5 for non-security checksums (where collision resistance is not a security requirement) is generally acceptable; contextual validation is required rather than treating it as an unconditional failure. | ||
|
|
||
| Note that line 71 did not trigger the rule because the hash is generated using SHA-256 on ordinary file data (`readme.txt`), which passes this algorithm check. Note that while raw SHA-256 is appropriate for data integrity and hashing non-sensitive inputs, it is [unsuitable for password storage](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) (which requires salted, memory-hard key derivation functions such as Argon2id, scrypt, or PBKDF2). | ||
|
|
||
| See @MASTG-TEST-0x01 for more information. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package org.owasp.mastestapp | ||
|
|
||
| import android.content.Context | ||
| import android.util.Log | ||
| import java.security.MessageDigest | ||
|
|
||
| class MastgTest(private val context: Context) { | ||
|
|
||
| // SUMMARY: This sample demonstrates various common ways of hashing data with broken algorithms (MD5, SHA-1) via MessageDigest, plus a SHA-256 pass case. | ||
|
|
||
| private fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) } | ||
|
|
||
| fun mastgTest(): String { | ||
| val password = "superSecretPassword!" | ||
| val tokenMaterial = "session-token-user-42" | ||
| val fileContents = "readme.txt" | ||
|
|
||
| // FAIL: [MASTG-TEST-0x01] The app hashes a password with MD5. | ||
| val md5 = MessageDigest.getInstance("MD5") | ||
| val md5Password = md5.digest(password.toByteArray(Charsets.UTF_8)).toHex() | ||
| Log.d("MASTG-TEST", "MD5 password hash: $md5Password") | ||
|
|
||
| // FAIL: [MASTG-TEST-0x01] The app hashes authentication token material with SHA-1. | ||
| val sha1 = MessageDigest.getInstance("SHA-1") | ||
| val sha1Token = sha1.digest(tokenMaterial.toByteArray(Charsets.UTF_8)).toHex() | ||
| Log.d("MASTG-TEST", "SHA-1 token hash: $sha1Token") | ||
|
|
||
| // FAIL: [MASTG-TEST-0x01] The app hashes authentication token material with the SHA1 JCA alias of SHA-1. | ||
| val sha1Alias = MessageDigest.getInstance("SHA1") | ||
| val sha1AliasToken = sha1Alias.digest(tokenMaterial.toByteArray(Charsets.UTF_8)).toHex() | ||
| Log.d("MASTG-TEST", "SHA1 alias token hash: $sha1AliasToken") | ||
|
|
||
| // CONTEXTUAL: [MASTG-TEST-0x01] The app uses MD5 for a checksum. Flagged by static analysis, but requires contextual validation to determine whether it is used in a security-relevant context. | ||
| val md5ChecksumDigest = MessageDigest.getInstance("MD5") | ||
| val md5Checksum = md5ChecksumDigest.digest(fileContents.toByteArray(Charsets.UTF_8)).toHex() | ||
| Log.d("MASTG-TEST", "MD5 checksum: $md5Checksum") | ||
|
|
||
| // PASS: [MASTG-TEST-0x01] The app hashes non-sensitive file/document data with SHA-256. | ||
| val sha256 = MessageDigest.getInstance("SHA-256") | ||
| val sha256Document = sha256.digest(fileContents.toByteArray(Charsets.UTF_8)).toHex() | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code hashes the string |
||
| Log.d("MASTG-TEST", "SHA-256 document hash: $sha256Document") | ||
|
|
||
| return buildString { | ||
| appendLine("MD5 password hash: $md5Password") | ||
| appendLine("SHA-1 token hash: $sha1Token") | ||
| appendLine("SHA1 alias token hash: $sha1AliasToken") | ||
| appendLine("MD5 checksum: $md5Checksum") | ||
| appendLine("SHA-256 document hash: $sha256Document") | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| package org.owasp.mastestapp; | ||
|
|
||
| import android.content.Context; | ||
| import android.util.Log; | ||
| import java.security.MessageDigest; | ||
| import java.security.NoSuchAlgorithmException; | ||
| import java.util.Arrays; | ||
| import kotlin.Metadata; | ||
| import kotlin.collections.ArraysKt; | ||
| import kotlin.jvm.functions.Function1; | ||
| import kotlin.jvm.internal.Intrinsics; | ||
| import kotlin.text.Charsets; | ||
|
|
||
| /* JADX INFO: compiled from: MastgTest.kt */ | ||
| /* JADX INFO: loaded from: classes3.dex */ | ||
| @Metadata(d1 = {"\u0000\u001e\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0002\u0010\u0012\n\u0002\b\u0002\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\f\u0010\u0006\u001a\u00020\u0007*\u00020\bH\u0002J\u0006\u0010\t\u001a\u00020\u0007R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\n"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "<init>", "(Landroid/content/Context;)V", "toHex", "", "", "mastgTest", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) | ||
| public final class MastgTest { | ||
| public static final int $stable = 8; | ||
| private final Context context; | ||
|
|
||
| public MastgTest(Context context) { | ||
| Intrinsics.checkNotNullParameter(context, "context"); | ||
| this.context = context; | ||
| } | ||
|
|
||
| private final String toHex(byte[] $this$toHex) { | ||
| return ArraysKt.joinToString$default($this$toHex, (CharSequence) "", (CharSequence) null, (CharSequence) null, 0, (CharSequence) null, new Function1() { // from class: org.owasp.mastestapp.MastgTest$$ExternalSyntheticLambda0 | ||
| @Override // kotlin.jvm.functions.Function1 | ||
| public final Object invoke(Object obj) { | ||
| return MastgTest.toHex$lambda$0(((Byte) obj).byteValue()); | ||
| } | ||
| }, 30, (Object) null); | ||
| } | ||
|
|
||
| /* JADX INFO: Access modifiers changed from: private */ | ||
| public static final CharSequence toHex$lambda$0(byte it) { | ||
| String str = String.format("%02x", Arrays.copyOf(new Object[]{Byte.valueOf(it)}, 1)); | ||
| Intrinsics.checkNotNullExpressionValue(str, "format(...)"); | ||
| return str; | ||
| } | ||
|
|
||
| public final String mastgTest() throws NoSuchAlgorithmException { | ||
| MessageDigest md5 = MessageDigest.getInstance("MD5"); | ||
| byte[] bytes = "superSecretPassword!".getBytes(Charsets.UTF_8); | ||
| Intrinsics.checkNotNullExpressionValue(bytes, "getBytes(...)"); | ||
| byte[] bArrDigest = md5.digest(bytes); | ||
| Intrinsics.checkNotNullExpressionValue(bArrDigest, "digest(...)"); | ||
| String md5Password = toHex(bArrDigest); | ||
| Log.d("MASTG-TEST", "MD5 password hash: " + md5Password); | ||
| MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); | ||
| byte[] bytes2 = "session-token-user-42".getBytes(Charsets.UTF_8); | ||
| Intrinsics.checkNotNullExpressionValue(bytes2, "getBytes(...)"); | ||
| byte[] bArrDigest2 = sha1.digest(bytes2); | ||
| Intrinsics.checkNotNullExpressionValue(bArrDigest2, "digest(...)"); | ||
| String sha1Token = toHex(bArrDigest2); | ||
| Log.d("MASTG-TEST", "SHA-1 token hash: " + sha1Token); | ||
| MessageDigest sha1Alias = MessageDigest.getInstance("SHA1"); | ||
| byte[] bytes3 = "session-token-user-42".getBytes(Charsets.UTF_8); | ||
| Intrinsics.checkNotNullExpressionValue(bytes3, "getBytes(...)"); | ||
| byte[] bArrDigest3 = sha1Alias.digest(bytes3); | ||
| Intrinsics.checkNotNullExpressionValue(bArrDigest3, "digest(...)"); | ||
| String sha1AliasToken = toHex(bArrDigest3); | ||
| Log.d("MASTG-TEST", "SHA1 alias token hash: " + sha1AliasToken); | ||
| MessageDigest md5ChecksumDigest = MessageDigest.getInstance("MD5"); | ||
| byte[] bytes4 = "readme.txt".getBytes(Charsets.UTF_8); | ||
| Intrinsics.checkNotNullExpressionValue(bytes4, "getBytes(...)"); | ||
| byte[] bArrDigest4 = md5ChecksumDigest.digest(bytes4); | ||
| Intrinsics.checkNotNullExpressionValue(bArrDigest4, "digest(...)"); | ||
| String md5Checksum = toHex(bArrDigest4); | ||
| Log.d("MASTG-TEST", "MD5 checksum: " + md5Checksum); | ||
| MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); | ||
| byte[] bytes5 = "readme.txt".getBytes(Charsets.UTF_8); | ||
| Intrinsics.checkNotNullExpressionValue(bytes5, "getBytes(...)"); | ||
| byte[] bArrDigest5 = sha256.digest(bytes5); | ||
| Intrinsics.checkNotNullExpressionValue(bArrDigest5, "digest(...)"); | ||
| String sha256Document = toHex(bArrDigest5); | ||
| Log.d("MASTG-TEST", "SHA-256 document hash: " + sha256Document); | ||
| StringBuilder $this$mastgTest_u24lambda_u241 = new StringBuilder(); | ||
| $this$mastgTest_u24lambda_u241.append("MD5 password hash: " + md5Password).append('\n'); | ||
| $this$mastgTest_u24lambda_u241.append("SHA-1 token hash: " + sha1Token).append('\n'); | ||
| $this$mastgTest_u24lambda_u241.append("SHA1 alias token hash: " + sha1AliasToken).append('\n'); | ||
| $this$mastgTest_u24lambda_u241.append("MD5 checksum: " + md5Checksum).append('\n'); | ||
| $this$mastgTest_u24lambda_u241.append("SHA-256 document hash: " + sha256Document).append('\n'); | ||
| return $this$mastgTest_u24lambda_u241.toString(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
|
|
||
|
|
||
| ┌─────────────────┐ | ||
| │ 4 Code Findings │ | ||
| └─────────────────┘ | ||
|
|
||
| MastgTest_reversed.java | ||
| ❯❱ rules.mastg-android-broken-hashing-algorithms | ||
| ❰❰ Blocking ❱❱ | ||
| [MASVS-CRYPTO-1] Broken hashing algorithms found in use. | ||
|
|
||
| 43┆ MessageDigest md5 = | ||
| MessageDigest.getInstance("MD5"); | ||
| ⋮┆---------------------------------------- | ||
| 50┆ MessageDigest sha1 = | ||
| MessageDigest.getInstance("SHA-1"); | ||
| ⋮┆---------------------------------------- | ||
| 57┆ MessageDigest sha1Alias = | ||
| MessageDigest.getInstance("SHA1"); | ||
| ⋮┆---------------------------------------- | ||
| 64┆ MessageDigest md5ChecksumDigest = | ||
| MessageDigest.getInstance("MD5"); | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| NO_COLOR=true semgrep -c ../../../../rules/mastg-android-broken-hashing-algorithms.yaml ./MastgTest_reversed.java > output.txt |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| rules: | ||
| - id: mastg-android-broken-hashing-algorithms | ||
| languages: | ||
| - java | ||
| severity: WARNING | ||
| metadata: | ||
| summary: This rule looks for broken hashing algorithms in MessageDigest.getInstance calls and common third-party wrappers. | ||
| message: "[MASVS-CRYPTO-1] Broken hashing algorithms found in use." | ||
| pattern-either: | ||
| - pattern: MessageDigest.getInstance("MD2") | ||
| - pattern: MessageDigest.getInstance("MD2", ...) | ||
| - pattern: MessageDigest.getInstance("MD4") | ||
| - pattern: MessageDigest.getInstance("MD4", ...) | ||
| - pattern: MessageDigest.getInstance("MD5") | ||
| - pattern: MessageDigest.getInstance("MD5", ...) | ||
| - pattern: MessageDigest.getInstance("SHA-1") | ||
| - pattern: MessageDigest.getInstance("SHA-1", ...) | ||
|
jankesec marked this conversation as resolved.
Outdated
|
||
| - pattern: MessageDigest.getInstance("SHA1") | ||
| - pattern: MessageDigest.getInstance("SHA1", ...) | ||
| - pattern: MessageDigest.getInstance("SHA") | ||
| - pattern: MessageDigest.getInstance("SHA", ...) | ||
| - pattern: Hashing.md5() | ||
| - pattern: Hashing.md5(...) | ||
| - pattern: Hashing.sha1() | ||
| - pattern: Hashing.sha1(...) | ||
| - pattern: DigestUtils.md5(...) | ||
| - pattern: DigestUtils.md5Hex(...) | ||
| - pattern: DigestUtils.sha1(...) | ||
| - pattern: DigestUtils.sha1Hex(...) | ||
| - pattern: DigestUtils.md5Digest(...) | ||
| - pattern: DigestUtils.sha1Digest(...) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| --- | ||
| title: Broken Hashing Algorithms | ||
| platform: android | ||
| id: MASTG-TEST-0x01 | ||
| type: [static, code, manual] | ||
| weakness: MASWE-0008 | ||
| profiles: [L1, L2] | ||
|
jankesec marked this conversation as resolved.
|
||
| --- | ||
|
|
||
| ## Overview | ||
|
|
||
| If an Android app uses broken hashing algorithms such as MD5 or SHA-1 to protect sensitive data (e.g., credentials, tokens, or integrity checks), attackers can exploit practical collision attacks to forge data or leverage high-speed brute-force and dictionary attacks against unsalted or weakly hashed secrets. To test for this, we need to focus on APIs from cryptographic frameworks and libraries that are used to perform hashing operations. | ||
|
|
||
| - **Java Cryptography Architecture (JCA)**: [`MessageDigest.getInstance`](https://developer.android.com/reference/java/security/MessageDigest#getInstance(java.lang.String)) initializes a `MessageDigest` object for hashing. The `algorithm` parameter can be one of the [supported algorithms](https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#MessageDigest). Common JCA names and aliases include: | ||
| - `MD2` | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it make sense to have MD2 and MD4, which aren't supported on Android? (MD4 is included in the regexes) |
||
| - `MD5` | ||
| - `SHA-1`, `SHA1`, `SHA` (aliases of SHA-1) | ||
| - `SHA-224` | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also only available on specific versions of Android |
||
| - `SHA-256` | ||
| - `SHA-384` | ||
| - `SHA-512` | ||
|
|
||
| Some broken hashing algorithms include: | ||
|
|
||
| - **MD5**: Collision attacks are practical (producing collisions in seconds). While preimage attacks remain theoretical (costing ~2^123.4 operations per [RFC 6151 §2.2](https://www.rfc-editor.org/rfc/rfc6151.html#section-2.2)), practical collision attacks and precomputed dictionary/rainbow table lookups make MD5 completely inadequate for cryptographic protection, digital signatures, or credential storage. It is not approved by NIST for cryptographic use. | ||
| - **SHA-1**: Collision attacks are practical: identical-prefix collisions were demonstrated by [SHAttered](https://shattered.io/), and practical chosen-prefix collisions were demonstrated by [SHA-1 is a Shambles](https://sha-mbles.github.io/). NIST has [deprecated and retired SHA-1](https://csrc.nist.gov/news/2022/nist-retires-sha-1-and-discusses-sha-3) for all cryptographic applications. | ||
|
|
||
| Android also provides additional guidance on [broken cryptographic algorithms](https://developer.android.com/privacy-and-security/risks/broken-cryptographic-algorithm). | ||
|
|
||
| Third-party wrappers such as Guava `Hashing.md5()` / `Hashing.sha1()` and Apache Commons Codec `DigestUtils` may call the same algorithms. Native libraries (for example OpenSSL `MD5_*` / `SHA1_*`) can implement hashing without going through `MessageDigest`. | ||
|
|
||
| **Out of Scope**: `Mac.getInstance` (HMAC) and password-based key derivation functions such as `PBKDF2WithHmacSHA1` are out of scope for this test. | ||
|
|
||
| ## Steps | ||
|
|
||
| 1. Use @MASTG-TECH-0013 to reverse engineer the app. | ||
| 2. Use @MASTG-TECH-0014 to look for the relevant APIs. | ||
|
|
||
| ## Observation | ||
|
|
||
| The output should contain a list of locations where broken hashing algorithms are used. | ||
|
|
||
| ## Evaluation | ||
|
|
||
| The test case fails if you can find the use of broken hashing algorithms. For example: | ||
|
|
||
| - MD5 | ||
| - SHA-1 | ||
|
|
||
| **Further Validation Required:** | ||
|
|
||
| Inspect each reported code location using @MASTG-TECH-0023 to determine whether the algorithm is used in a security-relevant context to protect sensitive data: | ||
|
|
||
| - Determine whether the hashing algorithm is used for cryptographic security purposes rather than for non-security tasks such as checksums. For example, using MD5 for hashing passwords is disallowed by NIST, but using MD5 for checksums where security is not a concern is generally acceptable. | ||
|
|
||
| **Stay up-to-date**: This is a non-exhaustive list of broken algorithms. Make sure to check the latest standards and recommendations from organizations such as the National Institute of Standards and Technology (NIST), the German Federal Office for Information Security (BSI) or any other relevant authority in your region. This is important when building an app that uses data that will be stored for a long time. Make sure you follow the NIST recommendations from [NIST IR 8547 "Transition to Post-Quantum Cryptography Standards", 2024](https://csrc.nist.gov/pubs/ir/8547/ipd). | ||
|
|
||
| **Expected False Negatives:** | ||
|
|
||
| This test may produce false negatives if the algorithm name is not a string literal (for example, it is stored in a variable or constructed at runtime), if hashing is implemented in native code, or if a third-party wrapper does not call `MessageDigest.getInstance` with a detectable algorithm name. If you suspect native hashing, use @MASTG-TECH-0018 on bundled libraries and look for symbols such as OpenSSL `MD5_*` or `SHA1_*`. Additional manual reverse engineering of native libraries or third-party code may be required. | ||
Uh oh!
There was an error while loading. Please reload this page.