Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,31 @@ module UnsafeDeserialization {
typeName = "system.resources.resxresourcereader"
}

/**
* An argument to Import-Clixml or ConvertFrom-CliXml, which deserializes CLIXML
* and can trigger gadget chains leading to code execution.
*/
class CliXmlDeserializationSink extends Sink {
string cmdletName;

CliXmlDeserializationSink() {
exists(DataFlow::CallNode call |
call.matchesName(cmdletName) and
(
this = call.getAnArgument()
or
this.asExpr().(CfgNodes::ExprNodes::PipelineArgumentCfgNode).getCall() =
call.getCallNode()
)
|
cmdletName = "Import-Clixml" or
cmdletName = "ConvertFrom-CliXml"
)
}

override string getSinkType() { result = "call to " + cmdletName }
}

/**
* A BinaryFormatter deserialization method call, including Deserialize, UnsafeDeserialize,
* and UnsafeDeserializeMethodResponse.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
When deriving cryptographic keys from passwords using <code>Rfc2898DeriveBytes</code> (PBKDF2),
both the iteration count and hash algorithm must be configured securely.
An insufficient iteration count or a weak hash algorithm makes the derived key
vulnerable to brute-force attacks.
</p>
</overview>

<recommendation>
<p>
Always specify at least 100,000 iterations and use SHA-256 or a stronger hash algorithm
(SHA-384, SHA-512) when creating an <code>Rfc2898DeriveBytes</code> instance or calling the
static <code>Pbkdf2</code> method.
</p>
</recommendation>

<example>
<p>The following example shows insecure usage with default settings:</p>
<sample src="examples/WeakKDFConfigurationBad.ps1" />
<p>The following example shows secure usage with adequate iterations and a strong hash:</p>
<sample src="examples/WeakKDFConfigurationGood.ps1" />
</example>

<references>
<li>
OWASP: <a href="https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html">Password Storage Cheat Sheet</a>
</li>
<li>
Microsoft: <a href="https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.rfc2898derivebytes">Rfc2898DeriveBytes Class</a>
</li>
<li>
CWE-327: <a href="https://cwe.mitre.org/data/definitions/327.html">Use of a Broken or Risky Cryptographic Algorithm</a>
</li>
</references>
</qhelp>
168 changes: 168 additions & 0 deletions powershell/ql/src/queries/security/cwe-327/WeakKDFConfiguration.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* @name Weak key derivation function configuration
* @description Rfc2898DeriveBytes (PBKDF2) should use at least 100,000 iterations
* and a hash algorithm of SHA-256 or stronger to resist brute-force attacks.
* @kind problem
* @problem.severity error
* @security-severity 7.5
* @precision high
* @id powershell/weak-kdf-configuration
* @tags security
* external/cwe/cwe-327
* external/cwe/cwe-328
* cryptography
*/

import powershell
import semmle.code.powershell.ApiGraphs
import semmle.code.powershell.dataflow.DataFlow

private int minIterationCount() { result = 100000 }

/**
* An instantiation of Rfc2898DeriveBytes via New-Object or [Type]::new().
*/
class Rfc2898DeriveBytesCreation extends DataFlow::CallNode {
Rfc2898DeriveBytesCreation() {
this =
API::getTopLevelMember("system")
.getMember("security")
.getMember("cryptography")
.getMember("rfc2898derivebytes")
.getMember("new")
.asCall()
or
// New-Object pattern
exists(DataFlow::ObjectCreationNode oc |
oc = this and
oc.getExprNode().getExpr().(CallExpr).getAnArgument().getValue().asString().toLowerCase() =
[
"system.security.cryptography.rfc2898derivebytes",
"rfc2898derivebytes"
]
)
}

/** Gets the iteration count argument (position 2, 0-indexed), if any. */
DataFlow::Node getIterationCountArg() { result = this.getPositionalArgument(2) }

/** Gets the hash algorithm argument (position 3, 0-indexed), if any. */
DataFlow::Node getHashAlgorithmArg() { result = this.getPositionalArgument(3) }

/** Holds if the constructor has an explicit iteration count argument. */
predicate hasIterationCountArg() { exists(this.getIterationCountArg()) }

/** Holds if the constructor has an explicit hash algorithm argument. */
predicate hasHashAlgorithmArg() { exists(this.getHashAlgorithmArg()) }
}

/**
* A call to the static Rfc2898DeriveBytes.Pbkdf2 method (.NET 6+).
*/
class Pbkdf2StaticCall extends DataFlow::CallNode {
Pbkdf2StaticCall() {
this =
API::getTopLevelMember("system")
.getMember("security")
.getMember("cryptography")
.getMember("rfc2898derivebytes")
.getMember("pbkdf2")
.asCall()
}

/** Gets the iteration count argument (position 2, 0-indexed). */
DataFlow::Node getIterationCountArg() { result = this.getPositionalArgument(2) }

/** Gets the hash algorithm argument (position 3, 0-indexed). */
DataFlow::Node getHashAlgorithmArg() { result = this.getPositionalArgument(3) }
}

/**
* Holds if `node` is an integer literal less than the minimum iteration count.
*/
private predicate isLowIterationValue(DataFlow::Node node, int value) {
value = node.asExpr().getExpr().getValue().asInt() and
value < minIterationCount()
}

/**
* Holds if `node` references a weak hash algorithm (MD5 or SHA1).
*/
private predicate isWeakHashAlgorithm(DataFlow::Node node, string name) {
// [HashAlgorithmName]::MD5 or [HashAlgorithmName]::SHA1
node =
API::getTopLevelMember("system")
.getMember("security")
.getMember("cryptography")
.getMember("hashalgorithmname")
.getMember(name)
.asSource() and
name = ["md5", "sha1"]
or
// String literal "MD5" or "SHA1"
exists(string s |
s = node.asExpr().getExpr().getValue().asString().toLowerCase() and
s = ["md5", "sha1"] and
name = s
)
}

/**
* Gets a human-readable name for the weak hash algorithm.
*/
private string prettyHashName(string name) {
name = "md5" and result = "MD5"
or
name = "sha1" and result = "SHA1"
}

from DataFlow::CallNode call, string message
where
// Case 1: Rfc2898DeriveBytes created without specifying iteration count
call instanceof Rfc2898DeriveBytesCreation and
not call.(Rfc2898DeriveBytesCreation).hasIterationCountArg() and
message =
"Rfc2898DeriveBytes uses default iteration count of 1000. Specify at least " +
minIterationCount().toString() + " iterations."
or
// Case 2: Rfc2898DeriveBytes created with low iteration count
exists(int value |
call instanceof Rfc2898DeriveBytesCreation and
isLowIterationValue(call.(Rfc2898DeriveBytesCreation).getIterationCountArg(), value) and
message =
"Rfc2898DeriveBytes uses iteration count of " + value.toString() +
", which is below the minimum of " + minIterationCount().toString() + "."
)
or
// Case 3: Rfc2898DeriveBytes created without specifying hash algorithm (defaults to SHA1)
call instanceof Rfc2898DeriveBytesCreation and
not call.(Rfc2898DeriveBytesCreation).hasHashAlgorithmArg() and
message = "Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger."
or
// Case 4: Rfc2898DeriveBytes created with weak hash algorithm
exists(string name |
call instanceof Rfc2898DeriveBytesCreation and
isWeakHashAlgorithm(call.(Rfc2898DeriveBytesCreation).getHashAlgorithmArg(), name) and
message =
"Rfc2898DeriveBytes uses weak hash algorithm " + prettyHashName(name) +
". Use SHA-256 or stronger."
)
or
// Case 5: Pbkdf2 static method with low iteration count
exists(int value |
call instanceof Pbkdf2StaticCall and
isLowIterationValue(call.(Pbkdf2StaticCall).getIterationCountArg(), value) and
message =
"Rfc2898DeriveBytes.Pbkdf2 uses iteration count of " + value.toString() +
", which is below the minimum of " + minIterationCount().toString() + "."
)
or
// Case 6: Pbkdf2 static method with weak hash algorithm
exists(string name |
call instanceof Pbkdf2StaticCall and
isWeakHashAlgorithm(call.(Pbkdf2StaticCall).getHashAlgorithmArg(), name) and
message =
"Rfc2898DeriveBytes.Pbkdf2 uses weak hash algorithm " + prettyHashName(name) +
". Use SHA-256 or stronger."
)
select call, message
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# BAD: Default iteration count (1000) and default hash algorithm (SHA1)
$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt)

# BAD: Low iteration count
$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 1000)

# BAD: Weak hash algorithm
$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 100000, [System.Security.Cryptography.HashAlgorithmName]::SHA1)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# GOOD: 100,000+ iterations with SHA-256
$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 600000, [System.Security.Cryptography.HashAlgorithmName]::SHA256)

# GOOD: Static Pbkdf2 with strong configuration
$key = [System.Security.Cryptography.Rfc2898DeriveBytes]::Pbkdf2($password, $salt, 600000, "SHA256", 32)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
| WeakKDFConfiguration.ps1:7:8:7:79 | Call to new | Rfc2898DeriveBytes uses default iteration count of 1000. Specify at least 100000 iterations. |
| WeakKDFConfiguration.ps1:7:8:7:79 | Call to new | Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger. |
| WeakKDFConfiguration.ps1:11:8:11:85 | Call to new | Rfc2898DeriveBytes uses iteration count of 1000, which is below the minimum of 100000. |
| WeakKDFConfiguration.ps1:11:8:11:85 | Call to new | Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger. |
| WeakKDFConfiguration.ps1:14:8:14:86 | Call to new | Rfc2898DeriveBytes uses iteration count of 10000, which is below the minimum of 100000. |
| WeakKDFConfiguration.ps1:14:8:14:86 | Call to new | Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger. |
| WeakKDFConfiguration.ps1:17:8:17:86 | Call to new | Rfc2898DeriveBytes uses iteration count of 50000, which is below the minimum of 100000. |
| WeakKDFConfiguration.ps1:17:8:17:86 | Call to new | Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger. |
| WeakKDFConfiguration.ps1:21:8:21:87 | Call to new | Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger. |
| WeakKDFConfiguration.ps1:25:8:25:143 | Call to new | Rfc2898DeriveBytes uses weak hash algorithm SHA1. Use SHA-256 or stronger. |
| WeakKDFConfiguration.ps1:28:8:28:142 | Call to new | Rfc2898DeriveBytes uses weak hash algorithm MD5. Use SHA-256 or stronger. |
| WeakKDFConfiguration.ps1:32:8:32:89 | Call to new-object | Rfc2898DeriveBytes uses default iteration count of 1000. Specify at least 100000 iterations. |
| WeakKDFConfiguration.ps1:32:8:32:89 | Call to new-object | Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger. |
| WeakKDFConfiguration.ps1:36:8:36:83 | Call to new-object | Rfc2898DeriveBytes uses default iteration count of 1000. Specify at least 100000 iterations. |
| WeakKDFConfiguration.ps1:36:8:36:83 | Call to new-object | Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger. |
| WeakKDFConfiguration.ps1:40:8:40:102 | Call to pbkdf2 | Rfc2898DeriveBytes.Pbkdf2 uses iteration count of 1000, which is below the minimum of 100000. |
| WeakKDFConfiguration.ps1:44:8:44:102 | Call to pbkdf2 | Rfc2898DeriveBytes.Pbkdf2 uses weak hash algorithm SHA1. Use SHA-256 or stronger. |
| WeakKDFConfiguration.ps1:47:8:47:84 | Call to new | Rfc2898DeriveBytes uses iteration count of 500, which is below the minimum of 100000. |
| WeakKDFConfiguration.ps1:47:8:47:84 | Call to new | Rfc2898DeriveBytes uses the default hash algorithm SHA1. Specify SHA-256 or stronger. |
| WeakKDFConfiguration.ps1:50:8:50:140 | Call to new | Rfc2898DeriveBytes uses iteration count of 500, which is below the minimum of 100000. |
| WeakKDFConfiguration.ps1:50:8:50:140 | Call to new | Rfc2898DeriveBytes uses weak hash algorithm SHA1. Use SHA-256 or stronger. |
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# ===================================================================
# ========== TRUE POSITIVES (should trigger alert) ==================
# ===================================================================

# --- Case 1: Default iteration count AND default algorithm ---
# BAD: Only password and salt, defaults to 1000 iterations and SHA1
$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt) # $ Alert $ Alert

# --- Case 2: Low iteration count AND default algorithm ---
# BAD: 1000 iterations is too low, and no algorithm specified
$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 1000) # $ Alert $ Alert

# BAD: 10000 iterations is still below 100k, and no algorithm specified
$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 10000) # $ Alert $ Alert

# BAD: 50000 iterations is still below 100k, and no algorithm specified
$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 50000) # $ Alert $ Alert

# --- Case 3: Default hash algorithm only (iterations are adequate) ---
# BAD: Has iterations but no algorithm, defaults to SHA1
$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 100000) # $ Alert

# --- Case 4: Weak hash algorithm explicitly specified ---
# BAD: SHA1 explicitly used
$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 100000, [System.Security.Cryptography.HashAlgorithmName]::SHA1) # $ Alert

# BAD: MD5 explicitly used
$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 100000, [System.Security.Cryptography.HashAlgorithmName]::MD5) # $ Alert

# --- Case 5: New-Object pattern (defaults to both issues) ---
# BAD: New-Object with low iteration count (detected as default iterations + default algorithm)
$kdf = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($password, $salt, 5000) # $ Alert $ Alert

# --- Case 6: New-Object pattern with no extra args ---
# BAD: New-Object with default iterations and algorithm
$kdf = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($password, $salt) # $ Alert $ Alert

# --- Case 7: Pbkdf2 static method with low iterations ---
# BAD: Static Pbkdf2 method with 1000 iterations
$key = [System.Security.Cryptography.Rfc2898DeriveBytes]::Pbkdf2($password, $salt, 1000, "SHA256", 32) # $ Alert

# --- Case 8: Pbkdf2 static method with weak hash ---
# BAD: Static Pbkdf2 method with SHA1
$key = [System.Security.Cryptography.Rfc2898DeriveBytes]::Pbkdf2($password, $salt, 100000, "SHA1", 32) # $ Alert

# --- Case 9: Combined issues - low iterations AND no algorithm ---
$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 500) # $ Alert $ Alert

# --- Case 10: Combined issues - low iterations AND weak algorithm ---
$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 500, [System.Security.Cryptography.HashAlgorithmName]::SHA1) # $ Alert $ Alert

# ===================================================================
# ========== TRUE NEGATIVES (should NOT trigger alert) ==============
# ===================================================================

# --- Safe: Proper iterations and strong hash ---
# GOOD: 100000 iterations with SHA256
$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 100000, [System.Security.Cryptography.HashAlgorithmName]::SHA256)

# GOOD: 200000 iterations with SHA512
$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 200000, [System.Security.Cryptography.HashAlgorithmName]::SHA512)

# GOOD: 600000 iterations with SHA256
$kdf = [System.Security.Cryptography.Rfc2898DeriveBytes]::new($password, $salt, 600000, [System.Security.Cryptography.HashAlgorithmName]::SHA256)

# --- Safe: Pbkdf2 with proper config ---
# GOOD: Static Pbkdf2 with adequate iterations and strong hash
$key = [System.Security.Cryptography.Rfc2898DeriveBytes]::Pbkdf2($password, $salt, 100000, "SHA256", 32)

# GOOD: Static Pbkdf2 with high iterations
$key = [System.Security.Cryptography.Rfc2898DeriveBytes]::Pbkdf2($password, $salt, 600000, "SHA512", 32)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
query: queries/security/cwe-327/WeakKDFConfiguration.ql
postprocess: utils/test/InlineExpectationsTestQuery.ql
Loading
Loading