Invoking the Target Part 1
In this and the next section, we must finish our
reverse engineering and create proof-of-concept bypass code.
We have already found that the native
Microsoft.Workflow.Compiler application can be used to
invoke the Compile method
with arguments supplied on the command line.
The first command-line argument is a file path
which is parsed by the ReadCompilerInput method.
We must inspect the ReadCompilerInput method
to determine what the file format and content of the
first command line argument should be.
We previously found that ReadCompilerInput
creates a XmlReader stream
after which the ReadObject method is used to deserialize
the data of the stream & return it as the type CompilerInput.
To build upon this knowledge we must understand both the content
of the serialized XML file and the deserialization process.
ReadObject makes use of the DataContractSerializer method
to aid in the serialization.
This is in line with the MSDN documentation which reveals
that a similar serialization process would use
DataContractSerializer along with WriteObject.
At this point, to understand how to successfully serialize
our input we could either reverse engineer all the
required flags, or we could attempt to locate code related
to serialization inside the assembly.
We choose to do the latter
and press ctrl + shift + r to analyse DataContractSerializer
which tells us that it is only used in two methods.
Besides the ReadCompilerInput method we are currently
investigating, DataContractSerializer is only
used in SerializeInputToWrapper, which has a very promising name.
Selecting the method name reveals its body:
The code shows a serialization process similar to the one
encountered into ReadCompilerInput.
This code is perfect for our purposes since it serializes
a data object of type WorkflowCompilerParameters
into an XML file on the filesystem.
Since we have found a method that directly serializes into
our desired format, we can simply create a PowerShell
script that calls it.
Because the method is private,
we must use reflection to locate it with GetMethod.
With the method resolved,
we must determine which arguments it accepts.
The first are the WorkflowCompilerParameters.
Fortunately, the type is public, meaning we can
simply instantiate an object of this type.
The second argument is an array of strings containing file paths.
Once we have set up the argument values, we can call the method
through reflection with the Invoke method:
After executing the code,
we can dump the contents of the generated file
to view the serialized content:
We notice two things from the contents of the file.
First, the file path we supplied has been embedded into it and
will be used with the call to Compile
once the file is deserialized.
Second, quite a few compiler flags have been added,
but at this time we do not know if the values they contain will
lead us down the correct code path in order to process
and execute an arbitrary malicious assembly file.
With an understanding of how we can generate an input file
that will be deserialized correctly by
Microsoft.Workflow.Compiler, we must return to dnSpy.
Our goal is to determine what file format
and content the file name embedded in the XML file
should have, and which compiler flags are required to reach the
compilation, loading, and subsequent execution sections.
After returning to the Main method of
Microsoft.Workflow.Compiler in dnSpy, we find that the next
call is to Compile, where the deserialized parameters
and file names are supplied as arguments.
We follow the call and first notice null checks
on the input values
after which various actions are performed
depending on the given parameters:
The OutputAssembly parameter is only checked and modified
if the GenerateInMemory flag is set.
From our generated XML file,
we find that it is set to false by default.
The OutputAssembly parameter is likely the file name and path
of the generated assembly file,
and if it does not exist, the compilation will likely fail.
Because of this, we must update our PowerShell script to set
GenerateInMemory to true.
After parsing the parameter, we follow the call
into the Compile method of the
WorkflowCompilerInternal namespace,
where we find the following foreach loop:
Very interestingly, we find a comparison on the file name
against the xoml extension,
which is used by the relatively undocumented
Extensible Object Markup Language file format.
This is essentially an XML document that can contain
embedded code.
File names with xoml extensions
will be added to the array variable,
while file names with
other extensions will be added to array2.
Since xoml files can contain embedded code,
we can assume that this is a required file,
but we must continue our analysis to prove this.
From our investigation in the previous sections, we located
the code path to trigger the compilation
and loading of the assembly, and discovered that we must
trace into the call to GenerateLocalAssembly,
which was supplied the arguments.
Once inside the call, we can inspect the function prototype
of GenerateLocalAssembly to gain a better and somewhat
contradictory understanding of the arguments.
From the argument names, we find that the files with an xoml
extension are called files, while those with any other
extension are called codeFiles,
which leads us to believe we should avoid xoml files.
This seems contradictory to the previous analysis.
To understand this, we’ll turn our attention to the first call
inside the method, which is to GenerateCodeFromFileBatch:
this method is given the files variable,
which contained the xoml files, as its first argument.
If we were to reverse engineer the method, we would discover
rather extensive code
designed to parse the files and detect and extract embedded code
Still inside GenerateLocalAssembly,
GenerateCodeFromFileBatch returns the embedded code from
the xoml files into the value variable.
Near the end of the method, we find the following code block
The files containing code that were extracted from an xoml file
are added into the arrayList2 variable,
and those without this extension
are added into the arrayList3 variable.
In the second-to-last line of code, the extracted code is
converted to files and added to arrayList3.
In effect, this means that we do not have to worry about the
partially-undocumented xoml format and can instead simply
provide a file containing C# code with an arbitrary extension
To summarize what we have discovered so far,
Microsoft.Workflow.Compiler accepts two arguments.
The first must be the path to an XML file containing compiler
flags and the path to a file containing C# code.
The C# file will be compiled
and loaded into memory without restrictions.