一、介绍 CodeDom 机制

.NET Framework 提供一种叫做 代码文档对象模型 (CodeDom) 的机制。

我们可以使用 CodeDom 元素组合成 CodeDom 图来表示一段源代码的逻辑。

CodeDom 有两个主要的功能:

  1. 根据 CodeDom 图生成源代码。
  2. 将源代码即时编译为程序集。

当然,也可以忽略中间过程,直接将 CodeDom 图编译为程序集。

二、关于 CodeDom 的例子

为了介绍 CodeDom 的一般用法,下面是提供一个关于 CodeDom 例子。该例子展示了如何使用一段 CodeDom 程序描述一段源码的逻辑。

目标源码类似下面这样:

namespace MyNamespace {
    using System;

    public class MyClass {
        public static void MyMethod() {
         Console.WriteLine("Hello, World!");
        }
    }
}

对应的 CodeDom 例子:

using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
using System.Reflection;

namespace CodeDomExample
{
    class Program
    {
        static void Main(string[] args)
        {
            /************************************************************
            // 第一部分:创建 CodeCompileUnit,构建 CodeDom 图以表示一段代码逻辑
            ************************************************************/
            // 创建一个 CodeCompileUnit 对象,表示要编译的代码单元
            CodeCompileUnit compileUnit = new CodeCompileUnit();

            // 创建一个 CodeNamespace 对象,表示代码的命名空间
            CodeNamespace codeNamespace = new CodeNamespace("MyNamespace");

            // 添加需要引用的命名空间
            codeNamespace.Imports.Add(new CodeNamespaceImport("System"));

            // 创建一个 CodeTypeDeclaration 对象,表示要编译的类型
            CodeTypeDeclaration codeType = new CodeTypeDeclaration("MyClass");
            codeType.IsClass = true;

            // 在类型中添加一个方法
            CodeMemberMethod codeMethod = new CodeMemberMethod();
            codeMethod.Name = "MyMethod";
            codeMethod.Attributes = MemberAttributes.Public | MemberAttributes.Static;
            codeMethod.ReturnType = new CodeTypeReference(typeof(void));

            // 方法体中的代码
            CodeSnippetStatement codeSnippet = new CodeSnippetStatement("Console.WriteLine(\"Hello, World!\");");
            codeMethod.Statements.Add(codeSnippet);

            // 将方法添加到类型中
            codeType.Members.Add(codeMethod);

            // 将类型添加到命名空间中
            codeNamespace.Types.Add(codeType);

            // 将命名空间添加到代码单元中
            compileUnit.Namespaces.Add(codeNamespace);

            /*************************************************************
            // 第二部分:将 CodeDom 图编译为程序集
            **************************************************************/
            // 创建一个 CSharpCodeProvider 对象,用于编译代码
            CodeDomProvider provider = new CSharpCodeProvider();

            // 创建一个编译参数对象
            CompilerParameters parameters = new CompilerParameters();
            parameters.GenerateExecutable = true;
            parameters.OutputAssembly = "MyCode.exe";

            // 编译代码
            CompilerResults results = provider.CompileAssemblyFromDom(parameters, compileUnit);

            // 检查编译是否成功
            if (results.Errors.HasErrors)
            {
                foreach (CompilerError error in results.Errors)
                {
                    Console.WriteLine("Error: {0}", error.ErrorText);
                }
            }
            else
            {
                Console.WriteLine("Code compiled successfully!");
            }

            /*****************************************************
            // 第三部分:通过反射加载生成的程序集,并调用相应的方法
            ******************************************************/
            // 加载并执行编译生成的程序集
            Assembly assembly = Assembly.LoadFrom("MyCode.exe");
            Type type = assembly.GetType("MyNamespace.MyClass");
            MethodInfo method = type.GetMethod("MyMethod");
            method.Invoke(null, null);
        }
    }
}

像这样一个典型的 CodeDom 程序往往包括三部分:

  1. 依据某些信息,使用 CodeDom 元素 描述 一段源代码的逻辑

  2. 将 CodeDom 图转换为源码,然后进行编译,或者 直接将 CodeDom 图编译为程序集

    // 根据 CompileUnit (CodeDom图) 生成源码
    CodeDomProvider.GenerateCodeFromCompileUnit();
    // 从不同的源码表示形式编译程序集
    CodeDomProvider.CompileAssemblyFromDom();
    CodeDomProvider.CompileAssemblyFromFile();
    CodeDomProvider.CompileAssemblyFromSource();
    
  3. 通过反射加载前面生成的程序集,并调用其中的方法。

三、CodeDom 的危险细节

CodeDom 中有大量的元素用于描述源代码中的结构,比如 算术运算、赋值语句、函数调用、循环语句、条件语句等。

下面举其中一个例子:

public class CodeTypeOfExpression : System.CodeDom.CodeExpression
{
    public CodeTypeOfExpression(string type);
    ...
}

CodeTypeOfExpression 用于描述一个 typeof() 语句,其参数是指定的类型名称标识符。示例如下:

CodeTypeOfExpression("System.String");

// 对应的源码如下:
typeof(System.String)

可以看到,在 ComDOM 元素里,我们传入的是一个 字符串。但在生成的源码里,它变成了一个类型名称标识符。

假如传入的 “System.String” 字符串用户可控呢?是否存在注入?

CodeTypeOfExpression typeof1 =  new CodeTypeOfExpression(@"System.String); Object/**/test2 = System.Diagnostics.Process.Start(""cmd.exe"", "" /c calc"");//");

CodeVariableDeclarationStatement myVariable = new CodeVariableDeclarationStatement( typeof(Type), "test", typeof1);

myMethod.Statements.Add(myVariable);

生产的C#源码会像下面这样:

System.Type test = typeof(System.String); 
Object/**/test2 = System.Diagnostics.Process.Start("cmd.exe", " / c calc");//);

当然,即便跳过生成源码的中间过程,直接将 CodeDom 编译为程序集,也是会被注入的。

CodeDom 中的所有标识符元素都可以被注入,比如变量名、类型名、命名空间名

四、 CVE-2020-0646

SharePoint WorkFlow RCE 漏洞

SharePoint 的 WorkFlow 提供一些受限的 Activity 以供用户编辑自定义工作流,比如事件到达某个节点后给某个人发一封邮件。其中发一封邮件就是一个 Activity。

用户使用 XOML 格式的文件定义一段工作流,SharePoint 根据 XOML 中使用的 Activity 生成 CodeDom 图,然后生成相应源码,再编译成为相应的程序集。最后当工作流启动时,就会加载该程序集并调用其中的方法。

以上过程发生在:System.Workflow.ComponentModel.dll 的 WorkflowCompiler.Compile() 函数中。

但是由于 CodeDom 的注入问题,而 Workflow 在编译时又没有检查,导致了通过工作流进行代码注入。

payload 如下:

<SequentialWorkflowActivity x:Class="MyWorkflow" x:Name="foobar" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow">
  <CallExternalMethodActivity x:Name="codeActivity1" MethodName='test1' InterfaceType='System.String);}Object/**/test2=System.Diagnostics.Process.Start("cmd.exe","/c calc");private/**/void/**/foobar(){//' />
</SequentialWorkflowActivity>

InterfaceType 属性指定的接口类型,在构造 CodeDom 图时被用作 CodeTypeOfExpression 的参数,即被传递给 typeof() 语句。

通过对 InterfaceType 注入可以实现代码执行。

补丁:

在进行编译前,调用 CodeGenerator.ValidateIdentifiers(CodeObject) 方法来检查 CodeDom 图中的标识符是否符合语言要求。

CodeGenerator.ValidateIdentifiers() 是 CodeDom 提供的一个检查方法。

补丁是修补在 System.Workflow.ComponentModel.dll 中的,所以这个用于编译工作流的公共组件是已经被修复了。那还会有人/组件 这样使用 CodeDom 吗?

五、 CVE-2023-24955

2023 P2O SharePoint RCE 漏洞

下面是 SharePoint 的补丁比较内容,红色为新加代码:

经过前面的学习,可以看到一些关键字:CodeDom、IsValidIdentifier,NamespaceName。

Namespace 也是标识符,所以在 CodeDom 中也是存在注入风险的,这里加上了 IsValidIdentifier 似乎表明这里可以被注入,proxyNamespaceName 可以被用户控制吗?要如何触发该位置?

寻找触发路径:

补丁在 Microsoft.SharePoint.BusinessData.SystemSpecific.Wcf 命名空间下的 WcfProxyGenerator.GenerateProxyAssembly() 方法附近。该命名空间属于 Microsoft Business Connectivity Services (BCS) 组件。

通过BCS,用户可以在SharePoint中创建外部内容类型(External Content Type),这些内容类型定义了与外部数据源的连接和访问方式。使用这些外部内容类型,用户可以创建外部列表(External List)、外部数据列(External Data Column)和外部内容网站(External Content Site),从而实现对外部数据的展示和操作。

GenerateProxyAssembly、外部内容类型、连接外部数据源、Wcf Service,结合这些关键词,已经可以理清漏洞触发的原因:

  1. 定义一个 恶意的 Wcf Service
  2. 在 SharePoint 里创建外部数据源,并指向我们的 恶意 Wcf Service
  3. 在 SharePoint 里定义一个外部列表,并与我们的外部数据源绑定
  4. 请求外部列表,SharePoint 就会请求我们的 Wcf Service,也意味着 SharePoint 会通过 GenerateProxyAssembly 来编译出一个客户端代理程序集。

在创建 wcf 外部数据源的时候,很明显可以看到 “代理命名空间” 这个可控字段,实际证明这里就是注入点。

六、 总结

CodeDom 在生成源码和编译时,不会自己检查标识符里是否含有违法字符,而是提供了 Validate 方法由开发者调用以进行检查。不了解 CodeDom 安全问题的开发者很可能会忽略这个检查。

CodeDom 机制常被用于即时代码编译(wcf客户端),或者给用户提供一个有限的可编程功能(workflow 编程)。在审计类似功能时可检查是否使用了 CodeDom,以及是否使用了 CodeGenerator.ValidateIdentifiers() 或 CodeDomProvider.IsValidIdentifier() 进行安全防护。