From f3c326bb3c47653d7f7ffa76bc02e94eec8c4b4b Mon Sep 17 00:00:00 2001 From: DanHam Date: Thu, 21 Sep 2017 20:18:47 +0100 Subject: [PATCH 1/5] Escape chars special to PowerShell in user supplied data --- provisioner/powershell/provisioner.go | 85 ++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/provisioner/powershell/provisioner.go b/provisioner/powershell/provisioner.go index 3d2f16f85..cc11ebc0c 100644 --- a/provisioner/powershell/provisioner.go +++ b/provisioner/powershell/provisioner.go @@ -8,12 +8,14 @@ import ( "encoding/xml" "errors" "fmt" + "io" "io/ioutil" "log" "os" "sort" "strings" "time" + "unicode/utf8" "github.com/hashicorp/packer/common" "github.com/hashicorp/packer/common/uuid" @@ -345,6 +347,8 @@ func (p *Provisioner) prepareEnvVars(elevated bool) (envVarPath string, err erro } func (p *Provisioner) createFlattenedEnvVars(elevated bool) (flattened string) { + var buf bytes.Buffer + escapedEnvVarValue := "" flattened = "" envVars := make(map[string]string) @@ -359,7 +363,16 @@ func (p *Provisioner) createFlattenedEnvVars(elevated bool) (flattened string) { // Split vars into key/value components for _, envVar := range p.config.Vars { keyValue := strings.SplitN(envVar, "=", 2) - envVars[keyValue[0]] = keyValue[1] + // Escape chars special to PS in each env var value + err := escapeSpecialPS(&buf, []byte(keyValue[1])) + if err != nil { + fmt.Printf("An error occured escaping chars special to PowerShell in env var value %s", keyValue[1]) + } + escapedEnvVarValue = buf.String() + buf.Reset() + + log.Printf("Env var %s converted to %s after escaping chars special to PS", keyValue[1], escapedEnvVarValue) + envVars[keyValue[0]] = escapedEnvVarValue } // Create a list of env var keys in sorted order @@ -483,10 +496,27 @@ func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath strin buffer.Reset() + // Escape characters special to PowerShell in the ElevatedUser string + err = escapeSpecialPS(&buffer, []byte(p.config.ElevatedUser)) + if err != nil { + fmt.Printf("Error escaping chars special to Powershell in ElevatedUser %s", p.config.ElevatedUser) + } + escapedElevatedUser := buffer.String() + log.Printf("Elevated user %s converted to %s after escaping chars special to PowerShell", p.config.ElevatedUser, escapedElevatedUser) + buffer.Reset() + // Escape characters special to PowerShell in the ElevatedPassword string + err = escapeSpecialPS(&buffer, []byte(p.config.ElevatedPassword)) + if err != nil { + fmt.Printf("Error escaping chars special to Powershell in ElevatedPassword %s", p.config.ElevatedPassword) + } + escapedElevatedPassword := buffer.String() + log.Printf("Elevated password %s converted to %s after escaping chars special to PowerShell", p.config.ElevatedPassword, escapedElevatedPassword) + buffer.Reset() + // Generate command err = elevatedTemplate.Execute(&buffer, elevatedOptions{ - User: p.config.ElevatedUser, - Password: p.config.ElevatedPassword, + User: escapedElevatedUser, + Password: escapedElevatedPassword, TaskName: taskName, TaskDescription: "Packer elevated task", LogFile: logFile, @@ -509,3 +539,52 @@ func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath strin path = fmt.Sprintf("%s-%s.ps1", "%TEMP%\\packer-elevated-shell", uuid) return path, err } + +func escapeSpecialPS(w io.Writer, s []byte) error { + var esc []byte + + last := 0 + for i := 0; i < len(s); { + r, width := utf8.DecodeRune(s[i:]) + i += width + switch r { + case '"': + esc = []byte("`\"") // Double quotes + case '\'': + esc = []byte("`'") // Single quotes + case '$': + esc = []byte("`$") // Dollar sign + case '`': + esc = []byte("``") // Backticks + default: + if !isInCharacterRange(r) || (r == 0xFFFD && width == 1) { + esc = []byte("\uFFFD") // Unicode replacement character + break + } + continue + } + if _, err := w.Write(s[last : i-width]); err != nil { + return err + } + if _, err := w.Write(esc); err != nil { + return err + } + last = i + } + if _, err := w.Write(s[last:]); err != nil { + return err + } + return nil +} + +// Decide whether the given rune is in the XML Character Range, per +// the Char production of http://www.xml.com/axml/testaxml.htm, +// Section 2.2 Characters. +func isInCharacterRange(r rune) (inrange bool) { + return r == 0x09 || + r == 0x0A || + r == 0x0D || + r >= 0x20 && r <= 0xDF77 || + r >= 0xE000 && r <= 0xFFFD || + r >= 0x10000 && r <= 0x10FFFF +} From b67c64fd66874d64dc24bd7bda26c895e877135b Mon Sep 17 00:00:00 2001 From: DanHam Date: Thu, 21 Sep 2017 23:26:14 +0100 Subject: [PATCH 2/5] Tests for escape of chars special to PowerShell in user supplied data --- provisioner/powershell/provisioner_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/provisioner/powershell/provisioner_test.go b/provisioner/powershell/provisioner_test.go index 749564d4d..2791601fa 100644 --- a/provisioner/powershell/provisioner_test.go +++ b/provisioner/powershell/provisioner_test.go @@ -518,6 +518,12 @@ func TestProvisioner_createFlattenedElevatedEnvVars_windows(t *testing.T) { {"FOO=bar", "BAZ=qux"}, // Multiple user env vars {"FOO=bar=baz"}, // User env var with value containing equals {"FOO==bar"}, // User env var with value starting with equals + // Test escaping of characters special to PowerShell + {"FOO=bar$baz"}, // User env var with value containing dollar + {"FOO=bar\"baz"}, // User env var with value containing a double quote + {"FOO=bar'baz"}, // User env var with value containing a single quote + {"FOO=bar`baz"}, // User env var with value containing a backtick + } expected := []string{ `$env:PACKER_BUILDER_TYPE="iso"; $env:PACKER_BUILD_NAME="vmware"; `, @@ -525,6 +531,10 @@ func TestProvisioner_createFlattenedElevatedEnvVars_windows(t *testing.T) { `$env:BAZ="qux"; $env:FOO="bar"; $env:PACKER_BUILDER_TYPE="iso"; $env:PACKER_BUILD_NAME="vmware"; `, `$env:FOO="bar=baz"; $env:PACKER_BUILDER_TYPE="iso"; $env:PACKER_BUILD_NAME="vmware"; `, `$env:FOO="=bar"; $env:PACKER_BUILDER_TYPE="iso"; $env:PACKER_BUILD_NAME="vmware"; `, + "$env:FOO=\"bar`$baz\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; ", + "$env:FOO=\"bar`\"baz\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; ", + "$env:FOO=\"bar`'baz\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; ", + "$env:FOO=\"bar``baz\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; ", } p := new(Provisioner) @@ -553,6 +563,11 @@ func TestProvisioner_createFlattenedEnvVars_windows(t *testing.T) { {"FOO=bar", "BAZ=qux"}, // Multiple user env vars {"FOO=bar=baz"}, // User env var with value containing equals {"FOO==bar"}, // User env var with value starting with equals + // Test escaping of characters special to PowerShell + {"FOO=bar$baz"}, // User env var with value containing dollar + {"FOO=bar\"baz"}, // User env var with value containing a double quote + {"FOO=bar'baz"}, // User env var with value containing a single quote + {"FOO=bar`baz"}, // User env var with value containing a backtick } expected := []string{ `$env:PACKER_BUILDER_TYPE="iso"; $env:PACKER_BUILD_NAME="vmware"; `, @@ -560,6 +575,10 @@ func TestProvisioner_createFlattenedEnvVars_windows(t *testing.T) { `$env:BAZ="qux"; $env:FOO="bar"; $env:PACKER_BUILDER_TYPE="iso"; $env:PACKER_BUILD_NAME="vmware"; `, `$env:FOO="bar=baz"; $env:PACKER_BUILDER_TYPE="iso"; $env:PACKER_BUILD_NAME="vmware"; `, `$env:FOO="=bar"; $env:PACKER_BUILDER_TYPE="iso"; $env:PACKER_BUILD_NAME="vmware"; `, + "$env:FOO=\"bar`$baz\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; ", + "$env:FOO=\"bar`\"baz\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; ", + "$env:FOO=\"bar`'baz\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; ", + "$env:FOO=\"bar``baz\"; $env:PACKER_BUILDER_TYPE=\"iso\"; $env:PACKER_BUILD_NAME=\"vmware\"; ", } p := new(Provisioner) From 68e13c90b1c7ef50a7c262a4e1341ce2dafce5bb Mon Sep 17 00:00:00 2001 From: DanHam Date: Sun, 15 Oct 2017 14:30:17 +0100 Subject: [PATCH 3/5] Revert "Escape chars special to PowerShell in user supplied data" This reverts commit 53aefe744bcd74ee6ac866f764eafe9e7f507d80. --- provisioner/powershell/provisioner.go | 85 +-------------------------- 1 file changed, 3 insertions(+), 82 deletions(-) diff --git a/provisioner/powershell/provisioner.go b/provisioner/powershell/provisioner.go index cc11ebc0c..3d2f16f85 100644 --- a/provisioner/powershell/provisioner.go +++ b/provisioner/powershell/provisioner.go @@ -8,14 +8,12 @@ import ( "encoding/xml" "errors" "fmt" - "io" "io/ioutil" "log" "os" "sort" "strings" "time" - "unicode/utf8" "github.com/hashicorp/packer/common" "github.com/hashicorp/packer/common/uuid" @@ -347,8 +345,6 @@ func (p *Provisioner) prepareEnvVars(elevated bool) (envVarPath string, err erro } func (p *Provisioner) createFlattenedEnvVars(elevated bool) (flattened string) { - var buf bytes.Buffer - escapedEnvVarValue := "" flattened = "" envVars := make(map[string]string) @@ -363,16 +359,7 @@ func (p *Provisioner) createFlattenedEnvVars(elevated bool) (flattened string) { // Split vars into key/value components for _, envVar := range p.config.Vars { keyValue := strings.SplitN(envVar, "=", 2) - // Escape chars special to PS in each env var value - err := escapeSpecialPS(&buf, []byte(keyValue[1])) - if err != nil { - fmt.Printf("An error occured escaping chars special to PowerShell in env var value %s", keyValue[1]) - } - escapedEnvVarValue = buf.String() - buf.Reset() - - log.Printf("Env var %s converted to %s after escaping chars special to PS", keyValue[1], escapedEnvVarValue) - envVars[keyValue[0]] = escapedEnvVarValue + envVars[keyValue[0]] = keyValue[1] } // Create a list of env var keys in sorted order @@ -496,27 +483,10 @@ func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath strin buffer.Reset() - // Escape characters special to PowerShell in the ElevatedUser string - err = escapeSpecialPS(&buffer, []byte(p.config.ElevatedUser)) - if err != nil { - fmt.Printf("Error escaping chars special to Powershell in ElevatedUser %s", p.config.ElevatedUser) - } - escapedElevatedUser := buffer.String() - log.Printf("Elevated user %s converted to %s after escaping chars special to PowerShell", p.config.ElevatedUser, escapedElevatedUser) - buffer.Reset() - // Escape characters special to PowerShell in the ElevatedPassword string - err = escapeSpecialPS(&buffer, []byte(p.config.ElevatedPassword)) - if err != nil { - fmt.Printf("Error escaping chars special to Powershell in ElevatedPassword %s", p.config.ElevatedPassword) - } - escapedElevatedPassword := buffer.String() - log.Printf("Elevated password %s converted to %s after escaping chars special to PowerShell", p.config.ElevatedPassword, escapedElevatedPassword) - buffer.Reset() - // Generate command err = elevatedTemplate.Execute(&buffer, elevatedOptions{ - User: escapedElevatedUser, - Password: escapedElevatedPassword, + User: p.config.ElevatedUser, + Password: p.config.ElevatedPassword, TaskName: taskName, TaskDescription: "Packer elevated task", LogFile: logFile, @@ -539,52 +509,3 @@ func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath strin path = fmt.Sprintf("%s-%s.ps1", "%TEMP%\\packer-elevated-shell", uuid) return path, err } - -func escapeSpecialPS(w io.Writer, s []byte) error { - var esc []byte - - last := 0 - for i := 0; i < len(s); { - r, width := utf8.DecodeRune(s[i:]) - i += width - switch r { - case '"': - esc = []byte("`\"") // Double quotes - case '\'': - esc = []byte("`'") // Single quotes - case '$': - esc = []byte("`$") // Dollar sign - case '`': - esc = []byte("``") // Backticks - default: - if !isInCharacterRange(r) || (r == 0xFFFD && width == 1) { - esc = []byte("\uFFFD") // Unicode replacement character - break - } - continue - } - if _, err := w.Write(s[last : i-width]); err != nil { - return err - } - if _, err := w.Write(esc); err != nil { - return err - } - last = i - } - if _, err := w.Write(s[last:]); err != nil { - return err - } - return nil -} - -// Decide whether the given rune is in the XML Character Range, per -// the Char production of http://www.xml.com/axml/testaxml.htm, -// Section 2.2 Characters. -func isInCharacterRange(r rune) (inrange bool) { - return r == 0x09 || - r == 0x0A || - r == 0x0D || - r >= 0x20 && r <= 0xDF77 || - r >= 0xE000 && r <= 0xFFFD || - r >= 0x10000 && r <= 0x10FFFF -} From 7443adf2fd3560b309e3628420dc231bc078c204 Mon Sep 17 00:00:00 2001 From: DanHam Date: Sun, 15 Oct 2017 13:28:59 +0100 Subject: [PATCH 4/5] Simpler escape of chars special to PowerShell in user supplied data --- provisioner/powershell/provisioner.go | 34 +++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/provisioner/powershell/provisioner.go b/provisioner/powershell/provisioner.go index 3d2f16f85..41b38ed6d 100644 --- a/provisioner/powershell/provisioner.go +++ b/provisioner/powershell/provisioner.go @@ -24,6 +24,13 @@ import ( var retryableSleep = 2 * time.Second +var psEscape = strings.NewReplacer( + "$", "`$", + "\"", "`\"", + "`", "``", + "'", "`'", +) + type Config struct { common.PackerConfig `mapstructure:",squash"` @@ -359,7 +366,13 @@ func (p *Provisioner) createFlattenedEnvVars(elevated bool) (flattened string) { // Split vars into key/value components for _, envVar := range p.config.Vars { keyValue := strings.SplitN(envVar, "=", 2) - envVars[keyValue[0]] = keyValue[1] + // Escape chars special to PS in each env var value + escapedEnvVarValue := psEscape.Replace(keyValue[1]) + if escapedEnvVarValue != keyValue[1] { + log.Printf("Env var %s converted to %s after escaping chars special to PS", keyValue[1], + escapedEnvVarValue) + } + envVars[keyValue[0]] = escapedEnvVarValue } // Create a list of env var keys in sorted order @@ -480,13 +493,26 @@ func (p *Provisioner) generateElevatedRunner(command string) (uploadedPath strin } escapedCommand := buffer.String() log.Printf("Command [%s] converted to [%s] for use in XML string", command, escapedCommand) - buffer.Reset() + // Escape chars special to PowerShell in the ElevatedUser string + escapedElevatedUser := psEscape.Replace(p.config.ElevatedUser) + if escapedElevatedUser != p.config.ElevatedUser { + log.Printf("Elevated user %s converted to %s after escaping chars special to PowerShell", + p.config.ElevatedUser, escapedElevatedUser) + } + + // Escape chars special to PowerShell in the ElevatedPassword string + escapedElevatedPassword := psEscape.Replace(p.config.ElevatedPassword) + if escapedElevatedPassword != p.config.ElevatedPassword { + log.Printf("Elevated password %s converted to %s after escaping chars special to PowerShell", + p.config.ElevatedPassword, escapedElevatedPassword) + } + // Generate command err = elevatedTemplate.Execute(&buffer, elevatedOptions{ - User: p.config.ElevatedUser, - Password: p.config.ElevatedPassword, + User: escapedElevatedUser, + Password: escapedElevatedPassword, TaskName: taskName, TaskDescription: "Packer elevated task", LogFile: logFile, From 6ad4917960c251e16ac0f6d6f4e0a9dcf27e2f01 Mon Sep 17 00:00:00 2001 From: DanHam Date: Mon, 30 Oct 2017 00:14:06 +0000 Subject: [PATCH 5/5] First attempt at fixer for powershell escapes --- fix/fixer.go | 2 + fix/fixer_powershell_escapes.go | 73 +++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 fix/fixer_powershell_escapes.go diff --git a/fix/fixer.go b/fix/fixer.go index 5ca0b3a18..d5622b299 100644 --- a/fix/fixer.go +++ b/fix/fixer.go @@ -34,6 +34,7 @@ func init() { "amazon-shutdown_behavior": new(FixerAmazonShutdownBehavior), "amazon-enhanced-networking": new(FixerAmazonEnhancedNetworking), "docker-email": new(FixerDockerEmail), + "powershell-escapes": new(FixerPowerShellEscapes), } FixerOrder = []string{ @@ -51,5 +52,6 @@ func init() { "amazon-shutdown_behavior", "amazon-enhanced-networking", "docker-email", + "powershell-escapes", } } diff --git a/fix/fixer_powershell_escapes.go b/fix/fixer_powershell_escapes.go new file mode 100644 index 000000000..9da9ae91f --- /dev/null +++ b/fix/fixer_powershell_escapes.go @@ -0,0 +1,73 @@ +package fix + +import ( + "github.com/mitchellh/mapstructure" + "strings" +) + +// FixerPowerShellEscapes removes the PowerShell escape character from user +// environment variables and elevated username and password strings +type FixerPowerShellEscapes struct{} + +func (FixerPowerShellEscapes) Fix(input map[string]interface{}) (map[string]interface{}, error) { + type template struct { + Provisioners []interface{} + } + + var psUnescape = strings.NewReplacer( + "`$", "$", + "`\"", "\"", + "``", "`", + "`'", "'", + ) + + // Decode the input into our structure, if we can + var tpl template + if err := mapstructure.WeakDecode(input, &tpl); err != nil { + return nil, err + } + + for i, raw := range tpl.Provisioners { + var provisioners map[string]interface{} + if err := mapstructure.Decode(raw, &provisioners); err != nil { + // Ignore errors, could be a non-map + continue + } + + if ok := provisioners["type"] == "powershell"; !ok { + continue + } + + if _, ok := provisioners["elevated_user"]; ok { + provisioners["elevated_user"] = psUnescape.Replace(provisioners["elevated_user"].(string)) + } + if _, ok := provisioners["elevated_password"]; ok { + provisioners["elevated_password"] = psUnescape.Replace(provisioners["elevated_password"].(string)) + } + if raw, ok := provisioners["environment_vars"]; ok { + var env_vars []string + if err := mapstructure.Decode(raw, &env_vars); err != nil { + continue + } + env_vars_unescaped := make([]interface{}, len(env_vars)) + for j, env_var := range env_vars { + env_vars_unescaped[j] = psUnescape.Replace(env_var) + } + // Replace with unescaped environment variables + provisioners["environment_vars"] = env_vars_unescaped + } + + // Write all changes back to template + tpl.Provisioners[i] = provisioners + } + + if len(tpl.Provisioners) > 0 { + input["provisioners"] = tpl.Provisioners + } + + return input, nil +} + +func (FixerPowerShellEscapes) Synopsis() string { + return `Removes PowerShell escapes from user env vars and elevated username and password strings` +}