CVE-2017-1000487 (Java, published 2018-01-03T20:29Z)
Description: value: Plexus-utils before 3.0.16 is vulnerable to command injection because it does not correctly process the contents of double quoted strings.
Severity: HIGH (CVSS v3 base score: 9.8)
Weakness: CWE-78 \u2014 Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.
Repository: https://github.com/codehaus-plexus/plexus-utils
Files changed:
src/main/java/org/codehaus/plexus/util/cli/Commandline.java
src/main/java/org/codehaus/plexus/util/cli/shell/BourneShell.java
src/main/java/org/codehaus/plexus/util/cli/shell/Shell.java
src/test/java/org/codehaus/plexus/util/cli/CommandlineTest.java
src/test/java/org/codehaus/plexus/util/cli/shell/BourneShellTest.java
Commit message: [PLXUTILS-161] Commandline shell injection problems

Patch by Charles Duffy, applied unmodified


Vulnerable code:
     * Create a new command line object.
     * Shell is autodetected from operating system
     * Shell is autodetected from operating system
                realPos = ( getExecutable() == null ? 0 : 1 );
        String executable = getExecutable();
                process = Runtime.getRuntime().exec( getShellCommandline(), environment );
                process = Runtime.getRuntime().exec( getShellCommandline(), environment, workingDir );
        if ( shell.getExecutable() == null )
import org.codehaus.plexus.util.StringUtils;
    private static final char[] BASH_QUOTING_TRIGGER_CHARS = {
        ' ',
        '$',
        ';',
        '&',
        '|',
        '<',
        '>',
        '*',
        '?',
        '(',
        ')',
        '[',
        ']',
        '{',
        '}',
        '`' };
        this( false );
        setExecutableQuoteDelimiter( '\"' );
        return unifyQuotes( super.getExecutable());
        sb.append( unifyQuotes( dir ) );
    protected char[] getQuotingTriggerChars()
    {
        return BASH_QUOTING_TRIGGER_CHARS;
    }

     * BourneShell.unifyQuotes(null)                       = null
     * BourneShell.unifyQuotes("")                         = (empty)
     * BourneShell.unifyQuotes("/test/quotedpath'abc")     = /test/quotedpath\'abc
     * BourneShell.unifyQuotes("/test/quoted path'abc")    = "/test/quoted path'abc"
     * BourneShell.unifyQuotes("/test/quotedpath\"abc")    = "/test/quotedpath\"abc"
     * BourneShell.unifyQuotes("/test/quoted path\"abc")   = "/test/quoted path\"abc"
     * BourneShell.unifyQuotes("/test/quotedpath\"'abc")   = "/test/quotedpath\"'abc"
     * BourneShell.unifyQuotes("/test/quoted path\"'abc")  = "/test/quoted path\"'abc"
    protected static String unifyQuotes( String path )
        if ( path.indexOf( " " ) == -1 && path.indexOf( "'" ) != -1 && path.indexOf( "\"" ) == -1 )
        {
            return StringUtils.escape( path );
        }
        return StringUtils.quoteAndEscape( path, '\"', BASH_QUOTING_TRIGGER_CHARS );
                char[] escapeChars = getEscapeChars( isSingleQuotedExecutableEscaped(), isDoubleQuotedExecutableEscaped() );

                sb.append( StringUtils.quoteAndEscape( getExecutable(), getExecutableQuoteDelimiter(), escapeChars, getQuotingTriggerChars(), '\\', false ) );
                char[] escapeChars = getEscapeChars( isSingleQuotedArgumentEscaped(), isDoubleQuotedArgumentEscaped() );

                sb.append( StringUtils.quoteAndEscape( arguments[i], getArgumentQuoteDelimiter(), escapeChars, getQuotingTriggerChars(), getArgumentEscapePattern(), false ) );
        commandLine.addAll( getCommandLine( getExecutable(), arguments ) );
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;

import junit.framework.TestCase;
        String expectedShellCmd = "/bin/echo \'hello world\'";
        String expectedShellCmd = "cd \"" + root.getAbsolutePath()
                                  + "path with spaces\" && /bin/echo \'hello world\'";
            expectedShellCmd = "cd \"" + root.getAbsolutePath()
                               + "path with spaces\" && \\bin\\echo \'hello world\'";
        String expectedShellCmd = "/bin/echo \'hello world\'";
            assertEquals( "/usr/bin a b", shellCommandline[2] );
import junit.framework.TestCase;

        assertEquals( "/bin/sh -c cd /usr/local/bin && chmod", executable );
        assertEquals( "/bin/sh -c cd \"/usr/local/\'something else\'\" && chmod", executable );
        assertEquals( "/bin/sh -c cd \"\\usr\\local\\\'something else\'\" && chmod", executable );
        assertTrue( cli.endsWith( args[0] ) );
        assertEquals("cd /usr/bin && chmod 'arg'\\''withquote'", shellCommandLine.get(shellCommandLine.size() - 1));
        assertEquals( "chmod --password ';password'", lines[2] );
        assertEquals( "chmod --password ';password'", lines[2] );
        assertEquals( "chmod ' ' '|' '&&' '||' ';' ';;' '&' '()' '<' '<<' '>' '>>' '*' '?' '[' ']' '{' '}' '`'",

Fixed code:
     * Shell usage is only desirable when generating code for remote execution.
     *
     *
     * Shell usage is only desirable when generating code for remote execution.
     * Create a new command line object, given a command following POSIX sh quoting rules
                realPos = ( getLiteralExecutable() == null ? 0 : 1 );
    /**
     * @return Executable to be run, as a literal string (no shell quoting/munging)
     */
    public String getLiteralExecutable()
    {
        return executable;
    }

    /**
     * Return an executable name, quoted for shell use.
     *
     * Shell usage is only desirable when generating code for remote execution.
     *
     * @return Executable to be run, quoted for shell interpretation
     */
        String executable = getLiteralExecutable();
     *
     * Shell usage is only desirable when generating code for remote execution.
                process = Runtime.getRuntime().exec( getCommandline(), environment, workingDir );
                process = Runtime.getRuntime().exec( getCommandline(), environment, workingDir );
        if ( shell.getOriginalExecutable() == null )
     * Shell usage is only desirable when generating code for remote execution.
     *
     * Shell usage is only desirable when generating code for remote execution.
        this(false);
        setUnconditionalQuoting( true );
        setExecutableQuoteDelimiter( '\'' );
        return quoteOneItem( super.getOriginalExecutable(), true );
        sb.append( quoteOneItem( dir, false ) );
     * BourneShell.quoteOneItem(null)                       = null
     * BourneShell.quoteOneItem("")                         = ''
     * BourneShell.quoteOneItem("/test/quotedpath'abc")     = '/test/quotedpath'"'"'abc'
     * BourneShell.quoteOneItem("/test/quoted path'abc")    = '/test/quoted pat'"'"'habc'
     * BourneShell.quoteOneItem("/test/quotedpath\"abc")    = '/test/quotedpath"abc'
     * BourneShell.quoteOneItem("/test/quoted path\"abc")   = '/test/quoted path"abc'
     * BourneShell.quoteOneItem("/test/quotedpath\"'abc")   = '/test/quotedpath"'"'"'abc'
     * BourneShell.quoteOneItem("/test/quoted path\"'abc")  = '/test/quoted path"'"'"'abc'
    protected String quoteOneItem( String path, boolean isExecutable )
        StringBuilder sb = new StringBuilder();
        sb.append( "'" );
        sb.append( path.replace( "'", "'\"'\"'" ) );
        sb.append( "'" );
        return sb.toString();
    private boolean unconditionallyQuote = false;

    /**
     * Toggle unconditional quoting
     *
     * @param unconditionallyQuote
     */
    public void setUnconditionalQuoting(boolean unconditionallyQuote)
    {
        this.unconditionallyQuote = unconditionallyQuote;
    }

    protected String quoteOneItem(String inputString, boolean isExecutable)
    {
        char[] escapeChars = getEscapeChars( isSingleQuotedExecutableEscaped(), isDoubleQuotedExecutableEscaped() );
        return StringUtils.quoteAndEscape(
            inputString,
            isExecutable ? getExecutableQuoteDelimiter() : getArgumentQuoteDelimiter(),
            escapeChars,
            getQuotingTriggerChars(),
            '\\',
            unconditionallyQuote
        );
    }

                sb.append( quoteOneItem( getOriginalExecutable(), true ) );
                sb.append( quoteOneItem( arguments[i], false ) );
        commandLine.addAll( getCommandLine( getOriginalExecutable(), arguments ) );
import junit.framework.TestCase;
import java.io.*;
        String expectedShellCmd = "'/bin/echo' 'hello world'";
        String expectedShellCmd = "cd '" + root.getAbsolutePath()
                                  + "path with spaces' && '/bin/echo' 'hello world'";
            expectedShellCmd = "cd '" + root.getAbsolutePath()
                               + "path with spaces' && '\\bin\\echo' 'hello world'";
        String expectedShellCmd = "'/bin/echo' ''\"'\"'hello world'\"'\"''";
            assertEquals( "'/usr/bin' 'a' 'b'", shellCommandline[2] );
    /**
     * Test an executable with shell-expandable content in its path.
     *
     * @throws Exception
     */
    public void testPathWithShellExpansionStrings()
        throws Exception
    {
        File dir = new File( System.getProperty( "basedir" ), "target/test/dollar$test" );
        createAndCallScript( dir, "echo Quoted" );
    }

import junit.framework.TestCase;
        assertEquals( "/bin/sh -c cd '/usr/local/bin' && 'chmod'", executable );
        assertEquals( "/bin/sh -c cd '/usr/local/'\"'\"'something else'\"'\"'' && 'chmod'", executable );
        assertEquals( "/bin/sh -c cd '\\usr\\local\\\'\"'\"'something else'\"'\"'' && 'chmod'", executable );
        assertTrue( cli.endsWith("''\"'\"'some arg with spaces'\"'\"''"));
        assertEquals("cd '/usr/bin' && 'chmod' 'arg'\"'\"'withquote'", shellCommandLine.get(shellCommandLine.size() - 1));
        assertEquals( "'chmod' '--password' ';password'", lines[2] );
        assertEquals( "'chmod' '--password' ';password'", lines[2] );
        assertEquals( "'chmod' ' ' '|' '&&' '||' ';' ';;' '&' '()' '<' '<<' '>' '>>' '*' '?' '[' ']' '{' '}' '`'",