Vài ghi chú về CVE-2024-34351: Server-Side Request Forgery in NextJS Server Actions
Giới thiệu
Tuần vừa rồi, các researcher của Assetnote.io có công bố bài viết liên quan đến lỗi SSRF ở Server Actions của NextJS phiên bản < 14.1.1 ở link: https://www.assetnote.io/resources/research/digging-for-ssrf-in-nextjs-apps. Bài viết đã ghi rất chi tiết về lỗ hổng, tuy nhiên với bản thân mình, còn nhiều điểm mình muốn tìm hiểu thêm nên đã setup môi trường và tái hiện lại. Mình sẽ tổng hợp một vài ghi chú của mình trong bài viết sau.
Cài đặt
Cài đặt NextJS rất dễ dàng, chúng ta có thể sử dụng công cụ create-next-app. Với môi trường nodejs đã cài đặt, chạy lệnh sau:
npx create-next-app cve-2024-34351
Xem trên Github NextJS thì phiên bản gần nhất tồn tại lỗ hổng này là 14.1.0
được release vào 19/01/2024:
Mặc định, create-next-app
sẽ cài cho ta phiên bản mới nhất, nên để thay đổi thành phiên bản lỗi, chúng ta cần làm thêm bước sau:
cd cve-2024-34351
npm i -S next@14.1.0
Cuối cùng, chạy lệnh sau là ta có thể truy cập vào server dev của NextJS ở http://localhost:3000
npx next dev
Phân tích thêm
Đây là các điều kiện cần để có thể khai thác được lỗi này:
Prerequisites
- Next.js (<14.1.1) is running in a self-hosted* manner.
- The Next.js application makes use of Server Actions.
- The Server Action performs a redirect to a relative path which starts with a /.
Điều kiện 1
Điều kiện đầu tiên là NextJS phải được self-host. Theo document của NextJS ở https://nextjs.org/docs/pages/building-your-application/deploying , khi deploy chúng ta có thể sử dụng Node.js Server để self-host.
Với code example, chạy 2 lệnh sau để build và deploy:
npx next build
npx next start
Kiểm tra với Wappalyzer (nếu host trên AWS thì sẽ được ghi ở PaaS
)
Còn trong trường hợp không phải self-host (VD: sử dụng Vercel.js) như ở https://tailwindcss.com/ hoặc https://bun.sh/, phần PaaS sẽ ghi cụ thể là Vercel
Để kiểm tra phiên bản NextJS đang được sử dụng, chúng ta có thể lấy thông tin từ cmd console: next.version
hoặc tìm trong file js, thường sẽ nằm trong file main-xxxxxxxxxxxxxxxxx.js
.
VD: https://alternativeto.net/_next/static/chunks/main-fbef0a7867924acf.js
Điều kiện 2*
Trước khi đi vào điều kiện này thì cùng xem thử lỗi liên quan đến _next/image
component trước.
- Ảnh không tồn tại
Với các settings mặc định, khi ta đưa ảnh SVG vào sẽ có lỗi sau:
- Ảnh hợp lệ,
dangerouslyAllowSVG
isfalse
Giờ thêm config vào:
- Ảnh hợp lệ,
dangerouslyAllowSVG
istrue
Mặc định sẽ không cho phép lấy các ảnh bên ngoài localhost, khi truyền vào param có dạng url sẽ có lỗi sau:
Chúng ta cần thêm config vào next.config.js
như trong bài blog:
và kết quả là vẫn không XSS được vì có CSP header sau:
Kiểm tra lại trong code thì NextJS đã có cấu hình CSP mặc định này: https://github.com/vercel/next.js/blob/fedb675520e0de8f360f8d2e262acdf76b4f0491/packages/next/src/shared/lib/image-config.ts#L114
Cấu hình mặc định này bắt đầu được thêm vào từ phiên bản 12.1.0
Với những thông tin trên, ta có thể probe được phiên bản NextJS bị lỗi cũng như kiểm tra được có khai thác được lỗi với component này không.
Điều kiện 2 và 3
Quay lại với điều kiện 2 và 3, thì ở đây mình muốn trả lời các câu hỏi:
- Chúng ta cần lấy
Next-Action
ở đâu? - Việc ứng dụng redirect về relative URL như vậy liệu có phổ biến hay không?
- Liệu chúng ta có thể phát hiện việc redirect này chỉ từ code JS của client?
Setup routes giống như trong blog khá đơn giản:
- Đầu tiên là tạo một thư mục mới
search
bên trong thư mụcapp
và tạo filepage.js
trong thư mục này với nội dung sau:
'use client';
import { useFormState } from 'react-dom';
import { handleSearch } from '@/app/lib/actions';
export default function Search() {
const initialState = { keyword: "hogehoge" };
const [state, dispatch] = useFormState(handleSearch, initialState);
return (
<form action={dispatch}>
<div>
<h1>Search</h1>
<input
type="text"
id="keyword"
placeholder={state.keyword}
/>
<button type="submit">Search</button>
</div>
</form>
);
}
- Tạo tiếp thư mục
lib
và thêm fileactions.js
:
"use server";
import { redirect } from "next/navigation";
export const handleSearch = async (data) => {
redirect("/login");
};
Cấu trúc thư mục như sau:
Truy cập đến http://localhost:3000/search chúng ta sẽ thấy giao diện và khi thực hiện search sẽ có request:
về giá trị của header Next-Action
thì đã được hardcode trong HTML khi truy cập vào UI:
Nếu giữ nguyên request và chỉ sửa Host header, chúng ta sẽ gặp lỗi:
Cụ thể hơn là:
Và nếu sửa cả 2 thì cũng vẫn lỗi:
và cách giải quyết ở đây là hãy xóa Origin
header đi:
Và bắt buộc chúng ta cần lấy được giá trị đúng của Next-Action
:
và rất tiếc theo như mình thấy là không có cách nào để phát hiện được quá trình redirect này từ phía client, bắt buộc phải tìm ra chức năng tương ứng. Tuy nhiên, việc code redirect
đến một URL bắt đầu bằng /
có vẻ khá phổ biến, ngay cả trong Tutorial của NextJS cũng có sử dụng code pattern này:
Github cũng có khoảng 6K code matching:
Tham khảo
All rights reserved