undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
This commit is contained in:
commit
0a580b313d
70422 changed files with 17213626 additions and 0 deletions
520
test/jdk/javax/management/security/HashedPasswordFileTest.java
Normal file
520
test/jdk/javax/management/security/HashedPasswordFileTest.java
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
/*
|
||||
* Copyright (c) 2017, 2025, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation.
|
||||
*
|
||||
* This code 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
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
/* @test
|
||||
* @bug 5016517 8204661
|
||||
* @summary Test Hashed passwords
|
||||
* @library /test/lib
|
||||
* @modules java.management
|
||||
* jdk.management.agent/jdk.internal.agent
|
||||
* @build HashedPasswordFileTest
|
||||
* @run testng/othervm HashedPasswordFileTest
|
||||
*
|
||||
*/
|
||||
|
||||
import jdk.internal.agent.ConnectorAddressLink;
|
||||
import jdk.test.lib.Utils;
|
||||
import jdk.test.lib.process.ProcessTools;
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.AfterClass;
|
||||
import org.testng.annotations.Test;
|
||||
|
||||
import javax.management.MBeanServer;
|
||||
import javax.management.remote.*;
|
||||
import java.io.*;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
@Test
|
||||
public class HashedPasswordFileTest {
|
||||
|
||||
private final String[] randomWords = {"accost", "savoie", "bogart", "merest",
|
||||
"azuela", "hoodie", "bursal", "lingua", "wincey", "trilby", "egesta",
|
||||
"wester", "gilgai", "weinek", "ochone", "sanest", "gainst", "defang",
|
||||
"ranket", "mayhem", "tagger", "timber", "eggcup", "mhren", "colloq",
|
||||
"dreamy", "hattie", "rootle", "bloody", "helyne", "beater", "cosine",
|
||||
"enmity", "outbox", "issuer", "lumina", "dekker", "vetoed", "dennis",
|
||||
"strove", "gurnet", "talkie", "bennie", "behove", "coates", "shiloh",
|
||||
"yemeni", "boleyn", "coaxal", "irne"};
|
||||
|
||||
private final String[] hashAlgs = {
|
||||
"MD2",
|
||||
"MD5",
|
||||
"SHA-1",
|
||||
"SHA-224",
|
||||
"SHA-256",
|
||||
"SHA-384",
|
||||
"SHA-512/224",
|
||||
"SHA-512/256",
|
||||
"SHA3-224",
|
||||
"SHA3-256",
|
||||
"SHA3-384",
|
||||
"SHA3-512"
|
||||
};
|
||||
|
||||
private final Random random = Utils.getRandomInstance();
|
||||
|
||||
private JMXConnectorServer cs;
|
||||
|
||||
private String randomWord() {
|
||||
int idx = random.nextInt(randomWords.length);
|
||||
return randomWords[idx];
|
||||
}
|
||||
|
||||
private String[] getHash(String algorithm, String password) {
|
||||
try {
|
||||
byte[] salt = new byte[64];
|
||||
random.nextBytes(salt);
|
||||
|
||||
MessageDigest digest = MessageDigest.getInstance(algorithm);
|
||||
digest.reset();
|
||||
digest.update(salt);
|
||||
byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
String saltStr = Base64.getEncoder().encodeToString(salt);
|
||||
String hashStr = Base64.getEncoder().encodeToString(hash);
|
||||
|
||||
return new String[]{saltStr, hashStr};
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private String getPasswordFilePath() {
|
||||
return "jmxremote.password";
|
||||
}
|
||||
|
||||
private File createNewPasswordFile() throws IOException {
|
||||
File file = new File(getPasswordFilePath());
|
||||
if (file.exists() && !file.delete()) {
|
||||
// if the password file exists but is not deleted, try to make it
|
||||
// writable then try again to remove it
|
||||
if (!file.canWrite() && file.setWritable(true, true)) {
|
||||
if (!file.delete())
|
||||
System.err.println("WARNING: Password file not deleted");
|
||||
}
|
||||
}
|
||||
file.createNewFile();
|
||||
return file;
|
||||
}
|
||||
|
||||
private Map<String, String> generateClearTextPasswordFile() throws IOException {
|
||||
File file = createNewPasswordFile();
|
||||
Map<String, String> props = new HashMap<>();
|
||||
BufferedWriter br;
|
||||
try (FileWriter fw = new FileWriter(file)) {
|
||||
br = new BufferedWriter(fw);
|
||||
int numentries = random.nextInt(5) + 3;
|
||||
for (int i = 0; i < numentries; i++) {
|
||||
String username;
|
||||
do {
|
||||
username = randomWord();
|
||||
} while (props.get(username) != null);
|
||||
String password = randomWord();
|
||||
props.put(username, password);
|
||||
br.write(username + " " + password + "\n");
|
||||
}
|
||||
br.flush();
|
||||
}
|
||||
br.close();
|
||||
return props;
|
||||
}
|
||||
|
||||
private boolean isPasswordFileHashed() throws IOException {
|
||||
BufferedReader br;
|
||||
boolean result;
|
||||
try (FileReader fr = new FileReader(getPasswordFilePath())) {
|
||||
br = new BufferedReader(fr);
|
||||
result = br.lines().anyMatch(line -> {
|
||||
if (line.startsWith("#")) {
|
||||
return false;
|
||||
}
|
||||
String[] tokens = line.split("\\s+");
|
||||
return tokens.length == 3 || tokens.length == 4;
|
||||
});
|
||||
}
|
||||
br.close();
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, String> generateHashedPasswordFile() throws IOException {
|
||||
File file = createNewPasswordFile();
|
||||
Map<String, String> props = new HashMap<>();
|
||||
BufferedWriter br;
|
||||
try (FileWriter fw = new FileWriter(file)) {
|
||||
br = new BufferedWriter(fw);
|
||||
int numentries = random.nextInt(5) + 3;
|
||||
for (int i = 0; i < numentries; i++) {
|
||||
String username;
|
||||
do {
|
||||
username = randomWord();
|
||||
} while (props.get(username) != null);
|
||||
String password = randomWord();
|
||||
String alg = hashAlgs[random.nextInt(hashAlgs.length)];
|
||||
String[] b64str = getHash(alg, password);
|
||||
br.write(username + " " + b64str[0] + " " + b64str[1] + " " + alg + "\n");
|
||||
props.put(username, password);
|
||||
}
|
||||
br.flush();
|
||||
}
|
||||
br.close();
|
||||
return props;
|
||||
}
|
||||
|
||||
private JMXServiceURL createServerSide(boolean useHash)
|
||||
throws IOException {
|
||||
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
|
||||
JMXServiceURL url = new JMXServiceURL("rmi", null, 0);
|
||||
|
||||
HashMap<String, Object> env = new HashMap<>();
|
||||
env.put("jmx.remote.x.password.file", getPasswordFilePath());
|
||||
env.put("jmx.remote.x.password.toHashes", useHash ? "true" : "false");
|
||||
cs = JMXConnectorServerFactory.newJMXConnectorServer(url, env, mbs);
|
||||
cs.start();
|
||||
return cs.getAddress();
|
||||
}
|
||||
|
||||
private void stopServerSide() throws IOException {
|
||||
if (cs != null) {
|
||||
cs.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClearTextPasswordFile() throws IOException {
|
||||
Boolean[] bvals = new Boolean[]{true, false};
|
||||
for (boolean bval : bvals) {
|
||||
try {
|
||||
Map<String, String> credentials = generateClearTextPasswordFile();
|
||||
JMXServiceURL serverUrl = createServerSide(bval);
|
||||
for (Map.Entry<String, String> entry : credentials.entrySet()) {
|
||||
HashMap<String, Object> env = new HashMap<>();
|
||||
env.put("jmx.remote.credentials",
|
||||
new String[]{entry.getKey(), entry.getValue()});
|
||||
try (JMXConnector cc = JMXConnectorFactory.connect(serverUrl, env)) {
|
||||
cc.getMBeanServerConnection();
|
||||
}
|
||||
}
|
||||
Assert.assertEquals(isPasswordFileHashed(), bval);
|
||||
} finally {
|
||||
stopServerSide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadOnlyPasswordFile() throws IOException {
|
||||
Boolean[] bvals = new Boolean[]{true, false};
|
||||
for (boolean bval : bvals) {
|
||||
try {
|
||||
Map<String, String> credentials = generateClearTextPasswordFile();
|
||||
File file = new File(getPasswordFilePath());
|
||||
file.setReadOnly();
|
||||
JMXServiceURL serverUrl = createServerSide(bval);
|
||||
for (Map.Entry<String, String> entry : credentials.entrySet()) {
|
||||
HashMap<String, Object> env = new HashMap<>();
|
||||
env.put("jmx.remote.credentials",
|
||||
new String[]{entry.getKey(), entry.getValue()});
|
||||
try (JMXConnector cc = JMXConnectorFactory.connect(serverUrl, env)) {
|
||||
cc.getMBeanServerConnection();
|
||||
}
|
||||
}
|
||||
Assert.assertEquals(isPasswordFileHashed(), false);
|
||||
} finally {
|
||||
stopServerSide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHashedPasswordFile() throws IOException {
|
||||
Boolean[] bvals = new Boolean[]{true, false};
|
||||
for (boolean bval : bvals) {
|
||||
try {
|
||||
Map<String, String> credentials = generateHashedPasswordFile();
|
||||
JMXServiceURL serverUrl = createServerSide(bval);
|
||||
Assert.assertEquals(isPasswordFileHashed(), true);
|
||||
for (Map.Entry<String, String> entry : credentials.entrySet()) {
|
||||
HashMap<String, Object> env = new HashMap<>();
|
||||
env.put("jmx.remote.credentials",
|
||||
new String[]{entry.getKey(), entry.getValue()});
|
||||
try (JMXConnector cc = JMXConnectorFactory.connect(serverUrl, env)) {
|
||||
cc.getMBeanServerConnection();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
stopServerSide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class SimpleJMXClient implements Callable {
|
||||
private final JMXServiceURL url;
|
||||
private final Map<String, String> credentials;
|
||||
|
||||
public SimpleJMXClient(JMXServiceURL url, Map<String, String> credentials) {
|
||||
this.url = url;
|
||||
this.credentials = credentials;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object call() throws Exception {
|
||||
for (Map.Entry<String, String> entry : credentials.entrySet()) {
|
||||
HashMap<String, Object> env = new HashMap<>();
|
||||
env.put("jmx.remote.credentials",
|
||||
new String[]{entry.getKey(), entry.getValue()});
|
||||
try (JMXConnector cc = JMXConnectorFactory.connect(url, env)) {
|
||||
cc.getMBeanServerConnection();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleClients() throws Throwable {
|
||||
Map<String, String> credentials = generateClearTextPasswordFile();
|
||||
JMXServiceURL serverUrl = createServerSide(true);
|
||||
Assert.assertEquals(isPasswordFileHashed(), false);
|
||||
// create random number of clients
|
||||
int numClients = random.nextInt(20) + 10;
|
||||
List<Future> futures = new ArrayList<>();
|
||||
ExecutorService executor = Executors.newFixedThreadPool(numClients);
|
||||
for (int i = 0; i < numClients; i++) {
|
||||
Future future = executor.submit(new SimpleJMXClient(serverUrl, credentials));
|
||||
futures.add(future);
|
||||
}
|
||||
try {
|
||||
for (Future future : futures) {
|
||||
future.get();
|
||||
}
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (ExecutionException ex) {
|
||||
throw ex.getCause();
|
||||
} finally {
|
||||
executor.shutdown();
|
||||
}
|
||||
|
||||
Assert.assertEquals(isPasswordFileHashed(), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPasswordChange() throws IOException {
|
||||
try {
|
||||
Map<String, String> credentials = generateClearTextPasswordFile();
|
||||
JMXServiceURL serverUrl = createServerSide(true);
|
||||
Assert.assertEquals(isPasswordFileHashed(), false);
|
||||
|
||||
for (Map.Entry<String, String> entry : credentials.entrySet()) {
|
||||
HashMap<String, Object> env = new HashMap<>();
|
||||
env.put("jmx.remote.credentials",
|
||||
new String[]{entry.getKey(), entry.getValue()});
|
||||
try (JMXConnector cc = JMXConnectorFactory.connect(serverUrl, env)) {
|
||||
cc.getMBeanServerConnection();
|
||||
}
|
||||
}
|
||||
Assert.assertEquals(isPasswordFileHashed(), true);
|
||||
|
||||
// Read the file back. Add new entries. Change passwords for few
|
||||
BufferedReader br = new BufferedReader(new FileReader(getPasswordFilePath()));
|
||||
String line;
|
||||
StringBuilder sbuild = new StringBuilder();
|
||||
while ((line = br.readLine()) != null) {
|
||||
if (line.trim().startsWith("#")) {
|
||||
sbuild.append(line).append("\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Change password for random entries
|
||||
if (random.nextBoolean()) {
|
||||
String[] tokens = line.split("\\s+");
|
||||
if ((tokens.length == 4 || tokens.length == 3)) {
|
||||
String password = randomWord();
|
||||
credentials.put(tokens[0], password);
|
||||
sbuild.append(tokens[0]).append(" ").append(password).append("\n");
|
||||
}
|
||||
} else {
|
||||
sbuild.append(line).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
// Add new entries in clear
|
||||
int newentries = random.nextInt(2) + 3;
|
||||
for (int i = 0; i < newentries; i++) {
|
||||
String username;
|
||||
do {
|
||||
username = randomWord();
|
||||
} while (credentials.get(username) != null);
|
||||
String password = randomWord();
|
||||
credentials.put(username, password);
|
||||
sbuild.append(username).append(" ").append(password).append("\n");
|
||||
}
|
||||
|
||||
// Add new entries as a hash
|
||||
int numentries = random.nextInt(2) + 3;
|
||||
for (int i = 0; i < numentries; i++) {
|
||||
String username;
|
||||
do {
|
||||
username = randomWord();
|
||||
} while (credentials.get(username) != null);
|
||||
String password = randomWord();
|
||||
String alg = hashAlgs[random.nextInt(hashAlgs.length)];
|
||||
String[] b64str = getHash(alg, password);
|
||||
credentials.put(username, password);
|
||||
sbuild.append(username).append(" ").append(b64str[0])
|
||||
.append(" ").append(b64str[1]).append(" ")
|
||||
.append(alg).append("\n");
|
||||
}
|
||||
|
||||
try (BufferedWriter bw = new BufferedWriter(new FileWriter(getPasswordFilePath()))) {
|
||||
bw.write(sbuild.toString());
|
||||
}
|
||||
|
||||
for (Map.Entry<String, String> entry : credentials.entrySet()) {
|
||||
HashMap<String, Object> env = new HashMap<>();
|
||||
env.put("jmx.remote.credentials",
|
||||
new String[]{entry.getKey(), entry.getValue()});
|
||||
try (JMXConnector cc = JMXConnectorFactory.connect(serverUrl, env)) {
|
||||
cc.getMBeanServerConnection();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
stopServerSide();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultAgent() throws Exception {
|
||||
List<String> pbArgs = new ArrayList<>();
|
||||
generateClearTextPasswordFile();
|
||||
|
||||
// This will run only on a POSIX compliant system
|
||||
if (!FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure only owner is able to read/write the file or else
|
||||
// default agent will fail to start
|
||||
File file = new File(getPasswordFilePath());
|
||||
Set<PosixFilePermission> perms = new HashSet<>();
|
||||
perms.add(PosixFilePermission.OWNER_READ);
|
||||
perms.add(PosixFilePermission.OWNER_WRITE);
|
||||
Files.setPosixFilePermissions(file.toPath(), perms);
|
||||
|
||||
pbArgs.add("-cp");
|
||||
pbArgs.add(System.getProperty("test.class.path"));
|
||||
|
||||
pbArgs.add("-Dcom.sun.management.jmxremote.port=0");
|
||||
pbArgs.add("-Dcom.sun.management.jmxremote.authenticate=true");
|
||||
pbArgs.add("-Dcom.sun.management.jmxremote.password.file=" + file.getAbsolutePath());
|
||||
pbArgs.add("-Dcom.sun.management.jmxremote.ssl=false");
|
||||
pbArgs.add("--add-exports");
|
||||
pbArgs.add("jdk.management.agent/jdk.internal.agent=ALL-UNNAMED");
|
||||
pbArgs.add(TestApp.class.getSimpleName());
|
||||
|
||||
ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder(
|
||||
pbArgs.toArray(new String[0]));
|
||||
Process process = ProcessTools.startProcess(
|
||||
TestApp.class.getSimpleName(),
|
||||
pb);
|
||||
|
||||
if (process.waitFor() != 0) {
|
||||
throw new RuntimeException("Test Failed : Error starting default agent");
|
||||
}
|
||||
Assert.assertEquals(isPasswordFileHashed(), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultAgentNoHash() throws Exception {
|
||||
List<String> pbArgs = new ArrayList<>();
|
||||
generateClearTextPasswordFile();
|
||||
|
||||
// This will run only on a POSIX compliant system
|
||||
if (!FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure only owner is able to read/write the file or else
|
||||
// default agent will fail to start
|
||||
File file = new File(getPasswordFilePath());
|
||||
Set<PosixFilePermission> perms = new HashSet<>();
|
||||
perms.add(PosixFilePermission.OWNER_READ);
|
||||
perms.add(PosixFilePermission.OWNER_WRITE);
|
||||
Files.setPosixFilePermissions(file.toPath(), perms);
|
||||
pbArgs.add("-Dcom.sun.management.jmxremote.port=0");
|
||||
pbArgs.add("-Dcom.sun.management.jmxremote.authenticate=true");
|
||||
pbArgs.add("-Dcom.sun.management.jmxremote.password.file=" + file.getAbsolutePath());
|
||||
pbArgs.add("-Dcom.sun.management.jmxremote.password.toHashes=false");
|
||||
pbArgs.add("-Dcom.sun.management.jmxremote.ssl=false");
|
||||
pbArgs.add("--add-exports");
|
||||
pbArgs.add("jdk.management.agent/jdk.internal.agent=ALL-UNNAMED");
|
||||
pbArgs.add(TestApp.class.getSimpleName());
|
||||
|
||||
ProcessBuilder pb = ProcessTools.createTestJavaProcessBuilder(
|
||||
pbArgs.toArray(new String[0]));
|
||||
Process process = ProcessTools.startProcess(
|
||||
TestApp.class.getSimpleName(),
|
||||
pb);
|
||||
|
||||
if (process.waitFor() != 0) {
|
||||
throw new RuntimeException("Test Failed : Error starting default agent");
|
||||
}
|
||||
Assert.assertEquals(isPasswordFileHashed(), false);
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public void cleanUp() {
|
||||
File file = new File(getPasswordFilePath());
|
||||
if (file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TestApp {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
try {
|
||||
Map<String, String> propsMap = ConnectorAddressLink.importRemoteFrom(0);
|
||||
String jmxServiceUrl = propsMap.get("sun.management.JMXConnectorServer.0.remoteAddress");
|
||||
Map<String, Object> env = new HashMap<>(1);
|
||||
// any dummy credentials will do. We just have to trigger password hashing
|
||||
env.put("jmx.remote.credentials", new String[]{"a", "a"});
|
||||
try (JMXConnector cc = JMXConnectorFactory.connect(new JMXServiceURL(jmxServiceUrl), env)) {
|
||||
cc.getMBeanServerConnection();
|
||||
}
|
||||
} catch (SecurityException ex) {
|
||||
// Catch authentication failure here
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue