CodeDom漏洞模式与SharePoint RCE

0x01 什么是CodeDom机制

.NET Framework 提供一种叫做 代码文档对象模型 (CodeDom)  的机制。我们可以使用 CodeDom 元素组合成 CodeDom 图来表示一段源代码的逻辑。

CodeDom 有两个主要的功能:

  1. 根据 CodeDom 图生成源代码;

  2. 将源代码即时编译为程序集。

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

0x02 关于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. 通过反射加载前面生成的程序集,并调用其中的方法。

0x03 CodeDom的危险细节

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

下面举其中一个例子:

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

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

CodeTypeOfExpression("System.String");

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

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

假如传入的 “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 中的所有标识符元素都可以被注入,比如变量名、类型名、命名空间名

0x04 CVE-2020-0646

4.1 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 吗?

0x05 CVE-2023-24955

5.1 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 外部数据源的时候,很明显可以看到 “代理命名空间” 这个可控字段,实际证明这里就是注入点。

0x06 总 结

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

CodeDom 机制常被用于即时代码编译(wcf客户端),或者给用户提供一个有限的可编程功能(workflow 编程)。

在审计类似功能时可检查是否使用了 CodeDom,以及是否使用了 CodeGenerator.ValidateIdentifiers() 或 CodeDomProvider.IsValidIdentifier()  进行安全防护。