Đôi dòng về file system trong Linux
Table of contents
Trong một Linux system tồn tại rất nhiều kiểu file khác nhau, thường thấy là các file thực thi, file ASCII text, vv. Chúng đều được chia ra và lưu trong các directory khác nhau, nhưng nhìn chung một hệ thống Linux sẽ có các directory sau:
Tuy nhiên trong blog này mình sẽ chỉ nói về directory /proc, dựa theo một case study khá hay gần đây mà mình học được.
Directory /proc là gì?
Đây là directory chứa đựng các thông tin về các process đang chạy, và các tham số của kernel. Dữ liệu từ /proc được rất nhiều công cụ sử dụng để lấy runtime system information (cái này mình chả biết dịch sao cho hay :v).
Ví dụ, nếu ta muốn kiểm tra thông tin về CPU trong Linux, chỉ cần tham chiếu đến file /proc/cpuinfo, hoặc kiểm tra /proc/meminfo nếu muốn biết thông tin về bộ nhớ hệ thống.
Bản chất của /proc
Trong hệ thống Linux, người dùng thường được làm quen với hai loại file chính, đó là file text và binary. Tuy nhiên, ngoài hai loại file đó ta còn có thêm loại file khá đặc biệt: virtual file. Vậy thì virtual file là cái gì? Thực chất, dữ liệu của system được lưu trên hard drive như SSD, HDD, USB, và nếu ta muốn truy cập đến chúng thì cần phải thông qua một abstract layer, được quy định bởi hệ điều hành. Abstract layer cung cấp interface để các process thực hiện việc lấy dữ liệu, từ đó dễ dàng quản lí bộ nhớ hơn. Các virtual file đơn giản là các interface để cho các application truy cập đến filesystem. Virtual file cũng là system file, hành vi của nó cũng giống system file, chỉ khác ở chỗ nó không được lưu ở trên disk.
Trong /proc chứa rất nhiều virtual file (vì thế còn được gọi là virtual file system). Nó đặc biệt ở chỗ, nếu như ta kiểm tra thông tin của bất kì 1 file nào thì đều thấy nó có kích thước là 0 byte (trừ một số ngoại lệ như /proc/kcore). Ví dụ như khi sử dụng câu lệnh stat /proc/cpuinfo
hoặc là lệnh file /proc/cpuinfo
:
Kết quả trên là mâu thuẫn so với khi ta thực hiện đọc dữ liệu từ cpuinfo bằng lệnh cat
, vì rõ ràng file này chứa thông tin về CPU của system:
Case study
Gần đây mình có chơi giải SekaiCTF, trong giải này có 1 challenge web khá hay.
Khi truy cập vào site thì liền bị báo 500 internal server error:
Do site không có gì cả nên ta đọc mã nguồn. Mã nguồn cũng đơn giản, chỉ gồm 1 file Docker và app.py:
# app.py
from starlette.applications import Starlette
from starlette.routing import Route
from starlette.responses import FileResponse
async def download(request):
return FileResponse(request.query_params.get("file"))
app = Starlette(routes=[Route("/", endpoint=download)])
# Dockerfile
FROM python:3.9-slim
RUN pip install --no-cache-dir starlette uvicorn
WORKDIR /app
COPY app.py .
ENV FLAG="SEKAI{test_flag}"
CMD ["uvicorn", "app:app", "--host", "0", "--port", "1337"]
EXPOSE 1337
Ở file Docker, ta thấy rằng flag được lưu ở trong biến môi trường FLAG. Ở file app.py, trang web có sử dụng Starlette (một ASGI framework, dùng để build các website bất đồng bộ), framework này chỉ serve một endpoint duy nhất là / với query parameter là file. Đọc sơ qua code, có thể thấy rằng webapp này nhận filepath từ user input và trả về content của file đó.
Thử đọc file /etc/passwd:
Ta cũng biết rằng trong Linux có một file là /proc/self/environ chứa thông tin về toàn bộ các biến môi trường của process hiện tại. Vì thế, ta chỉ cần đọc file này là có thể lấy được nội dung của biến FLAG. Nhưng đời không như là mơ, ta chỉ nhận về 1 trang web trắng tinh:
Quan sát request bằng BurpSuite, ta có thể thấy một điều lạ là content-length của server trả về là 0:
Nếu quan sát log trong docker, ta có thể nhận ra có một lỗi đã xảy ra (message hơi dài nên mình copy qua đây luôn):
2024-08-27 20:29:49 INFO: 172.17.0.1:33922 - "GET /?file=/proc/self/environ HTTP/1.1" 200 OK
2024-08-27 20:29:49 ERROR: Exception in ASGI application
2024-08-27 20:29:49 Traceback (most recent call last):
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py", line 406, in run_asgi
2024-08-27 20:29:49 result = await app( # type: ignore[func-returns-value]
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 70, in __call__
2024-08-27 20:29:49 return await self.app(scope, receive, send)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/applications.py", line 123, in __call__
2024-08-27 20:29:49 await self.middleware_stack(scope, receive, send)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 186, in __call__
2024-08-27 20:29:49 raise exc
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 164, in __call__
2024-08-27 20:29:49 await self.app(scope, receive, _send)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/middleware/exceptions.py", line 65, in __call__
2024-08-27 20:29:49 await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
2024-08-27 20:29:49 raise exc
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
2024-08-27 20:29:49 await app(scope, receive, sender)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 754, in __call__
2024-08-27 20:29:49 await self.middleware_stack(scope, receive, send)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 774, in app
2024-08-27 20:29:49 await route.handle(scope, receive, send)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 295, in handle
2024-08-27 20:29:49 await self.app(scope, receive, send)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 77, in app
2024-08-27 20:29:49 await wrap_app_handling_exceptions(app, request)(scope, receive, send)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
2024-08-27 20:29:49 raise exc
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
2024-08-27 20:29:49 await app(scope, receive, sender)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/routing.py", line 75, in app
2024-08-27 20:29:49 await response(scope, receive, send)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/responses.py", line 348, in __call__
2024-08-27 20:29:49 await send(
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/_exception_handler.py", line 50, in sender
2024-08-27 20:29:49 await send(message)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/_exception_handler.py", line 50, in sender
2024-08-27 20:29:49 await send(message)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 161, in _send
2024-08-27 20:29:49 await send(message)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py", line 503, in send
2024-08-27 20:29:49 output = self.conn.send(event=h11.Data(data=data))
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/h11/_connection.py", line 512, in send
2024-08-27 20:29:49 data_list = self.send_with_data_passthrough(event)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/h11/_connection.py", line 545, in send_with_data_passthrough
2024-08-27 20:29:49 writer(event, data_list.append)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/h11/_writers.py", line 65, in __call__
2024-08-27 20:29:49 self.send_data(event.data, write)
2024-08-27 20:29:49 File "/usr/local/lib/python3.9/site-packages/h11/_writers.py", line 91, in send_data
2024-08-27 20:29:49 raise LocalProtocolError("Too much data for declared Content-Length")
2024-08-27 20:29:49 h11._util.LocalProtocolError: Too much data for declared Content-Length
Message Too much data for declared Content-Length chứng tỏ rằng nội dung của /proc/self/environ đã được dump ra để trả về cho web user, tuy nhiên bằng cách nào đó nó lại có kích thước bằng 0.
Nếu kiểm tra sâu hơn về cách mà Starlette trả file về cho user, ta thấy đoạn code sau khi trace vào class FileResponse:
Ở đây, __call__ là một magic method trong Python, cho phép khi một instance của class được gọi lên như hàm thông qua cặp ngoặc () thì method này sẽ được thực thi. Có thể thấy, method này có phần kiểm tra filepath có hợp lệ không thông qua gọi hàm os.stat
(hàm này gọi lệnh stat của hệ điều hành lên).
Như đã giải thích ở đầu blog, nếu dùng lệnh stat để check một virtual file thì nó sẽ trả về size = 0. Tuy nhiên nội dung mà file /proc/self/environ tham chiếu đến thì lại lớn hơn 0, vì thế nên Starlette chỉ đơn giản là báo lỗi Too much data for declared Content-Length
.
Vậy thì có bất khả thi để lấy flag không? Nếu để ý, os.stat thực hiện việc truy xuất đến dữ liệu 1 file và trả thông tin tổng quát về nó, vì thế nó có thể bị tấn công race condition. Ý tưởng là, nếu ta tạo một symlink đến một file thực (chẳng hạn /etc/passwd) rồi cho os.stat kiểm tra symlink, sau đó nhanh chóng đổi symlink sang /proc/self/environ thì có thể "lừa" được Starlette rằng nội dung của virtual file là lớn hơn 0. Sơ đồ exploit chain như sau (mượn từ https://blog.neilhommes.xyz/docs/Writeups/2024/sekaictf.html):
Vì challenge cho phép SSH nên ta có thể thực hiện việc tạo symlink dễ dàng:
Thực hiện gửi request đến symlink và thấy rằng toàn bộ biến môi trường đã được dump ra:
Reference:
https://opensource.com/article/19/3/virtual-filesystems-linux
https://blog.neilhommes.xyz/docs/Writeups/2024/sekaictf.html
https://unix.stackexchange.com/questions/507106/do-all-virtual-files-have-a-size-of-0-in-linux
Subscribe to my newsletter
Read articles from Le Quoc Cuong directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by