@@ -11,11 +11,47 @@ package.preload.http = function() return {download_file = function() return nil
1111package.preload .html = function () return {} end
1212package.preload .json = function () return {} end
1313local oldExecute , oldPopen , oldOpen , oldClose , oldRemove = os.execute , io.popen , io.open , io.close , os.remove
14+
15+ -- Decode the command independently so the test inspects what PowerShell will
16+ -- execute, rather than only checking a command string prefix.
17+ local alphabet = ' ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
18+ local function decodeBase64 (value )
19+ local result , accumulator , bits = {}, 0 , 0
20+ for char in value :gmatch (' .' ) do
21+ if char == ' =' then break end
22+ local index = assert (alphabet :find (char , 1 , true ), ' invalid base64' ) - 1
23+ accumulator = accumulator * 64 + index
24+ bits = bits + 6
25+ if bits >= 8 then
26+ bits = bits - 8
27+ local scale = 2 ^ bits
28+ result [# result + 1 ] = string.char (math.floor (accumulator / scale ))
29+ accumulator = accumulator % scale
30+ end
31+ end
32+ return table.concat (result )
33+ end
34+ local function decodeCommand (value )
35+ local payload = assert (value :match (' ^powershell %-NoProfile %-NonInteractive %-EncodedCommand ([A-Za-z0-9+/=]+)$' ),
36+ ' PowerShell command must contain only an unquoted base64 payload: ' .. value )
37+ local utf16 = decodeBase64 (payload )
38+ assert (# utf16 % 2 == 0 , ' incomplete UTF-16LE code unit' )
39+ local bytes = {}
40+ for i = 1 , # utf16 , 2 do
41+ assert (utf16 :byte (i ) < 128 and utf16 :byte (i + 1 ) == 0 , ' script must use ASCII UTF-16LE' )
42+ bytes [# bytes + 1 ] = utf16 :sub (i , i )
43+ end
44+ local script = table.concat (bytes )
45+ assert (script :find (" $ErrorActionPreference = 'Stop'; " , 1 , true ) == 1 , ' error preference must execute' )
46+ return script
47+ end
48+
1449os .execute = function (command )
1550 state .commands [# state .commands + 1 ] = command
1651 -- Native paths must never be expanded as cmd variables or tokenized on spaces.
1752 assert (command :find (' powershell ' , 1 , true ), ' unquoted Windows command: ' .. command )
1853 assert (not command :find (' %%' ) and not command :find (' !' ), ' cmd expansion in command: ' .. command )
54+ decodeCommand (command )
1955 return # state .commands == state .failOn and 1 or 0
2056end
2157io .popen = function (command )
@@ -74,6 +110,19 @@ for _, failure in ipairs({
74110 assert (# state .commands == 2 , failure .name .. ' : no MSI or pip may run before enumeration succeeds' )
75111end
76112local windows = require (' windows_command' )
113+ for _ , value in ipairs ({' ' , ' f' , ' fo' , ' foo' , ' foob' , ' fooba' , ' foobar' , " C:\\ 用户\\ Mechael's & %USERNAME% !\\ " , ' quotes"stay data' }) do
114+ local script = decodeCommand (windows .native (' program.exe' , {value }))
115+ local arguments = {}
116+ for encoded in script :gmatch (" FromBase64String%('([A-Za-z0-9+/=]*)'%)" ) do
117+ arguments [# arguments + 1 ] = decodeBase64 (encoded )
118+ end
119+ assert (# arguments == 2 and arguments [1 ] == ' program.exe' and arguments [2 ] == value ,
120+ ' native argument changed after payload decoding' )
121+ assert (script :find (' ; exit $LASTEXITCODE' , 1 , true ), ' native exit status must propagate' )
122+ end
123+ local msi = decodeCommand (windows .msi (' C:\\ MSI files\\ core.msi' , path ))
124+ assert (msi :find (' Start-Process -FilePath msiexec.exe -Wait -PassThru' , 1 , true ))
125+ assert (msi :find (' ; exit $process.ExitCode' , 1 , true ))
77126assert (not pcall (windows .msi , ' C:\\ bad"path\\ python.msi' , path ), ' invalid Windows quote must fail' )
78127assert (not pcall (windows .native , ' bad\n program' , {}), ' control characters must fail' )
79128os.execute , io.popen , io.open , io.close , os .remove = oldExecute , oldPopen , oldOpen , oldClose , oldRemove
0 commit comments