สรุปสั้น

GitHub Actions คือ ระบบรันงานอัตโนมัติ (CI/CD) ของ GitHub ที่มักเก็บ secret สำคัญ เช่น คีย์คลาวด์และรหัสผ่านฐานข้อมูลโปรดักชันไว้ให้ workflow เรียกใช้ ซึ่งใครก็ตามที่มีสิทธิ์เขียน repo สามารถอ่าน secret ทั้งหมดได้ บริษัท Wiz ผู้ให้บริการความปลอดภัยคลาวด์ เปิดเผยแคมเปญอัตโนมัติชื่อ Gheist ที่ติดตามมาตั้งแต่ 15 กันยายน 2569 ผู้โจมตีใช้ GitHub token ที่ขโมยมาสร้าง branch ใหม่ใน repo ทุกแห่งที่ token เข้าถึงได้ แล้ววาง workflow ที่สั่งพิมพ์ secret ทุกตัวลงในล็อกของ GitHub Actions เมื่อรันเสร็จ เครื่องมือจะดาวน์โหลดล็อกแล้วลบรันทิ้งภายในไม่กี่วินาที Wiz กู้ร่องรอยได้กว่า 13,500 branch จากบัญชีที่ถูกยึดกว่า 850 บัญชี ตั้งแต่โปรเจกต์ส่วนตัวไปจนถึง repo ส่วนตัวขององค์กรใหญ่

ประเด็นสำคัญ คือ ระบบกักรันน่าสงสัยที่ GitHub เปิดใช้ตั้งแต่ 28 กรกฎาคม 2569 หยุดรันของ Gheist ใน repo สาธารณะได้ทุกรันที่ Wiz ตรวจ แต่ระบบนี้ไม่ครอบคลุม repo ส่วนตัวและ GitHub Enterprise Server ทำให้การดูด secret ในสองกลุ่มนี้สำเร็จจริง และในบางองค์กรยังพบการใช้คีย์คลาวด์ขององค์กรเดียวกันไล่สำรวจ AWS Secrets Manager ตามมาภายในไม่กี่ชั่วโมง ณ 24 ตุลาคม 2569 แคมเปญยังดำเนินอยู่และยังเปลี่ยนรูปแบบต่อเนื่อง ผู้ดูแล GitHub ควรค้นหา branch ชื่อขึ้นต้นด้วย ci-sync- ทันที หากพบต้องเปลี่ยน secret ทุกตัวที่ repo นั้นเข้าถึงได้ เพราะการลบ branch ไม่ได้ทำให้ secret ที่รั่วไปแล้วปลอดภัยขึ้น

รายละเอียดข่าว

เว็บไซต์ The Hacker News รายงานเมื่อวันที่ 26 ตุลาคม 2569 โดยอ้างการวิเคราะห์ของ Wiz ว่า Gheist เป็นชื่อที่ Wiz ตั้งตาม user agent “gheist” ที่เครื่องมือของผู้โจมตีส่งไปพร้อมคำขอ GitHub API ทุกครั้ง

ไทม์ไลน์ที่ Wiz และแหล่งอื่นบันทึกไว้เรียงได้ดังนี้ วันที่ 28 กรกฎาคม 2569 GitHub เริ่มกักรัน workflow ที่ระบบมองว่าอาจเป็นอันตรายไว้จนกว่าผู้ร่วมพัฒนาที่มีสิทธิ์เขียนจะอนุมัติ วันที่ 15 กันยายน Wiz เริ่มติดตาม Gheist วันที่ 6 ตุลาคม มีบัญชี 391 บัญชี ซึ่งหลายบัญชีเป็นชุดเดียวกับที่ Gheist ใช้ เปลี่ยนชื่อ repo ราว 1,461 แห่งให้ดูเหมือนซอฟต์แวร์แคร็กเพื่อแพร่มัลแวร์ การเปลี่ยนชื่อนี้ลบ branch ของ Gheist ไป 610 อัน วันที่ 9 ตุลาคม GitHub เริ่มบล็อก GhostAction สายพันธุ์ใหม่ที่ GitGuardian รายงาน วันที่ 19 ตุลาคม เครื่องมือ Gheist เวอร์ชันหนึ่งเริ่มเพิ่มงานที่เล็ง self-hosted runner และวันที่ 24 ตุลาคม แคมเปญยังทำงานอยู่ แต่บัญชีที่เกี่ยวข้องราวสองในสามหายไปจาก GitHub แล้ว ซึ่ง Wiz เชื่อว่าเป็นผลจากการตอบสนองของ GitHub หลังได้รับรายงาน

ด้านร่องรอยในองค์กรที่ถูกโจมตี Wiz พบว่าใน repo ส่วนตัวและบน GitHub Enterprise Server audit log ของ GitHub แสดงรันบน branch ที่ถูกวางรันจนเสร็จแล้วถูกลบโดย user agent gheist ส่วน AWS CloudTrail แสดงว่า workflow ขององค์กรเองก็รันบน branch เหล่านั้นและ assume AWS role ด้วย ซึ่งเป็นสัญญาณว่า GitHub Actions รันได้โดยไม่ต้องรออนุมัติ ในองค์กรขนาดใหญ่ เครื่องมือสลับใช้ token ของผู้ใช้และบัญชีอัตโนมัติหลายบัญชีภายในไม่กี่นาที และ clone repo ส่วนตัวไปก่อนวาง workflow หลายชั่วโมงต่อมาจึงมีการใช้คีย์คลาวด์ขององค์กรเดียวกันไล่สำรวจ AWS Secrets Manager

แหล่งที่มาของ token ที่ Wiz สืบย้อนได้ ได้แก่ ล็อกของมัลแวร์ขโมยข้อมูล (infostealer) และ token ที่รั่วจากการโจมตีซัพพลายเชนก่อนหน้า เช่น เหตุเจาะเครื่องมือสแกนช่องโหว่ Trivy เมื่อเดือนมีนาคม บัญชีชุดเดียวกันยังถูกกลุ่มอื่นใช้ซ้ำ มีเหยื่อบางรายที่ดูเหมือนติดมัลแวร์ PolinRider ซึ่งนักวิจัยโยงไปยังเกาหลีเหนือ โดยเมื่อวันที่ 19-20 ตุลาคม เหยื่อ 3 รายเคยถูก push commit ของ Gheist ซ้ำพร้อมโค้ดโหลดในไฟล์ vite.config.js และ 57 บัญชีในชุดนี้เคยถูกแคมเปญ Megalodon ที่ฝัง workflow ขโมย secret ลง repo นับพันแห่งเมื่อเดือนพฤษภาคมโจมตีมาก่อน Wiz ระบุชัดว่า Gheist เป็นคนละแคมเปญกับ GhostAction เพราะใช้ไฟล์ workflow ต่างกัน

ข้อจำกัดของหลักฐานมี 3 ข้อ ข้อแรก Wiz ไม่ได้ระบุว่ามีกี่องค์กรที่ secret ถูกดูดออกไปจริง ข้อสอง Wiz ไม่ได้ระบุว่าคีย์ AWS ที่ใช้สำรวจ Secrets Manager มาจากไหนและใครเป็นผู้ใช้ ข้อสาม งานที่เล็ง self-hosted runner ยังไม่มีรายงานว่ารันสำเร็จ Wiz อธิบายว่าโค้ดส่วนนี้ยังมีบั๊ก และยังไม่มีการระบุตัวผู้อยู่เบื้องหลัง Gheist

วิธีการโจมตี

ใช้ token ที่ขโมยมาเป็นกุญแจ — ผู้โจมตีไม่ต้องเจาะช่องโหว่ใด เพราะตามเอกสารของ GitHub ใครที่มีสิทธิ์เขียน repo ก็อ่าน secret ทุกตัวที่ตั้งไว้ใน repo นั้นได้อยู่แล้ว token ที่มีสิทธิ์เขียนเพียงตัวเดียวจึงเพียงพอ

วาง workflow บน branch ใหม่ — สำหรับทุก repo ที่ token เข้าถึงได้ เครื่องมือจะสร้าง branch ชื่อ ci-sync- ตามด้วยอักขระสุ่ม 6 ตัว แล้ว commit ไฟล์เดียว คือ .github/workflows/sync-check.yml ด้วยข้อความ ci: sync config commit นี้ไม่ได้ลงนามและแสดงชื่อเจ้าของ token การ push branch นี้เข้าไปก็เพียงพอให้ workflow เริ่มรัน

พิมพ์ secret ออกมาให้หลุดการปิดบัง — workflow สั่งพิมพ์ environment ของงานและ secret ทุกตัวที่งานอ่านได้ โดยเข้ารหัสเป็น base32 ก่อน ปกติ GitHub จะปิดบังค่า secret ในล็อก แต่เอกสารของ GitHub ระบุว่าไม่รับประกันเมื่อค่าถูกเข้ารหัสหรือแปลงรูป ข้อความ base32 จึงไม่ตรงกับค่าเดิมและถูกพิมพ์ออกมาครบ workflow ยังตั้ง permissions ว่างไว้ ซึ่งจำกัดเฉพาะ GITHUB_TOKEN ที่ผู้โจมตีไม่ได้ต้องการ แต่ไม่ได้หยุดการอ่าน secret เลย

ขยายไปถึง secret ระดับ environment — เครื่องมือเวอร์ชันหนึ่งสร้างงานแยกสำหรับทุก deployment environment ใน repo ทำให้ secret ที่จำกัดไว้เฉพาะ environment ถูกพิมพ์ออกมาด้วย Wiz ระบุว่า secret กลุ่มนี้มักเป็นข้อมูลรับรองของระบบโปรดักชัน

ดึงข้อมูลออกผ่าน GitHub เองแล้วลบร่องรอย — เครื่องมือดาวน์โหลดล็อกผ่าน GitHub API แล้วลบรันทิ้งภายในไม่กี่วินาที เพราะ secret ออกไปผ่านโครงสร้างของ GitHub เอง จึงไม่มีเซิร์ฟเวอร์ผู้โจมตีหรือการเชื่อมต่อขาออกให้บล็อก

เล็ง self-hosted runner (ตั้งแต่ราว 19 ต.ค.) — เวอร์ชันใหม่ push งานที่สองสำหรับ repo ที่มี self-hosted runner ลงทะเบียนไว้ งานนี้ค้นหาข้อมูลรับรอง 31 ประเภทบนเครื่อง runner ได้แก่ private key, environment ของโปรเซส, ข้อมูลรับรอง AWS instance role, ไฟล์ข้อมูลรับรองของ runner และ Kubernetes service account token แต่ยังไม่มีรายงานว่ารันสำเร็จ

ทำไมระบบกักของ GitHub หยุดได้แค่บางส่วน — GitHub ระบุว่าการกักรันน่าสงสัย “ใช้กับ repo สาธารณะบน github.com เท่านั้น” และ GitHub Enterprise Server ยังไม่มีฟีเจอร์นี้ รันของ Gheist ใน repo สาธารณะทุกรันที่ Wiz ตรวจจึงค้างรออนุมัติและไม่เคยทำงาน แต่ใน repo ส่วนตัวกลับรันได้ตามปกติ และระบบนี้จับได้เฉพาะรันที่ GitHub ตรวจพบเท่านั้น

ผลกระทบ

กลุ่มที่เสี่ยงที่สุด คือ องค์กรที่ใช้ repo ส่วนตัวบน github.com หรือ GitHub Enterprise Server แล้วเก็บ secret ของระบบโปรดักชันไว้ใน GitHub Actions โดยเฉพาะองค์กรที่ token ของนักพัฒนาหรือบัญชีอัตโนมัติเคยรั่วจาก infostealer หรือจากเหตุซัพพลายเชนก่อนหน้า ขนาดที่เห็นแล้ว คือ บัญชีที่ถูกยึดกว่า 850 บัญชีและ branch ที่ถูกวางกว่า 13,500 อัน แม้ยังไม่ทราบจำนวนองค์กรที่ secret รั่วจริง

ผลที่ตามมาจริง คือ คีย์คลาวด์ รหัสผ่านฐานข้อมูล และ token ของบริการภายนอกที่หลุดไปแล้วจะใช้ต่อได้จนกว่าจะถูกเปลี่ยน ตัวอย่างการไล่สำรวจ AWS Secrets Manager หลังถูกดูด secret แสดงว่าผู้โจมตีพร้อมขยายจากระบบ CI ไปสู่ระบบคลาวด์ขององค์กร อีกจุดที่ต้องระวัง คือ บัญชีชุดเดียวกันถูกหลายกลุ่มใช้ร่วมกัน ทั้งกลุ่มแพร่มัลแวร์ผ่าน repo ปลอมและมัลแวร์ที่โยงกับเกาหลีเหนือ หาก token ขององค์กรอยู่ในมือผู้โจมตีแล้ว จึงอาจไม่ได้ถูกใช้โดยกลุ่มเดียว และการที่ GitHub ลบบัญชีที่เกี่ยวข้องไปไม่ได้แปลว่า secret ที่รั่วไปแล้วปลอดภัย

คำแนะนำ

ตรวจทันทีว่าโดนแล้วหรือยัง

  • ค้นหา branch ที่ตรงรูปแบบ ^ci-sync-[0-9a-z]{6}$ ซึ่งมีเพียงการเพิ่มไฟล์ .github/workflows/sync-check.yml ด้วย commit message ci: sync config
  • ค้นใน GitHub audit log หารันบน branch ci-sync-* ที่ถูกลบภายในหนึ่งนาทีหลังรันเสร็จ และกิจกรรมทุกอย่างจาก user agent gheist
  • ถ้า workflow ล็อกอินคลาวด์ด้วย OIDC ให้ค้นใน audit log ของคลาวด์ (เช่น AWS CloudTrail) หาการ assume role หรือแลก token ที่ subject มีชื่อ branch ci-sync- วิธีนี้ใช้ได้แม้ไม่ได้เก็บล็อกของ GitHub
  • บน self-hosted runner ค้นหาไฟล์ .gdmp_pat, .gdmp_patg, .gdmp_sweep และสตริง gdmp-pivot ในล็อกของงาน

ถ้าพบ branch ci-sync

  • เปลี่ยน secret ทุกตัวระดับ repo, organization และ environment ที่ repo นั้นใช้ได้ การลบ branch หรือรันไม่ได้ย้อนการรั่วไหล
  • เพิกถอน token ที่ใช้ push commit แล้วตรวจเครื่องของเจ้าของ token หา infostealer
  • หาก branch ci-sync เคยรันบน self-hosted runner ให้ถือว่าเครื่องนั้นและ cloud instance role ของมันถูกยึดแล้ว
  • ห้ามอนุมัติรัน sync-check ที่ค้างรออนุมัติ และห้ามรันโค้ดจาก branch ci-sync

ลดคุณค่าของ token ที่ถูกขโมยในระยะยาว

  • ตั้ง required reviewers ให้ environment ที่เก็บ secret โปรดักชัน เพราะงานจะอ่าน secret ของ environment ไม่ได้จนกว่าผู้ตรวจจะอนุมัติ
  • จำกัด OIDC trust policy ให้รับเฉพาะ protected branch หรือ environment ที่กำหนด
  • เปลี่ยนจาก personal access token มาใช้ GitHub Apps หรือ fine-grained token อายุสั้น
  • รัน self-hosted runner เป็นเครื่องใช้แล้วทิ้ง ไม่เปิดสิทธิ์ Docker socket และไม่ให้เข้าถึง cloud metadata ได้อิสระ

Indicators of Compromise (IoCs)

ประเภทIndicatorคำอธิบาย
User agentgheistuser agent ที่เครื่องมือส่งไปกับคำขอ GitHub API
Branch^ci-sync-[0-9a-z]{6}$รูปแบบชื่อ branch ที่ถูกวาง
Commit messageci: sync configcommit ที่เพิ่มเฉพาะไฟล์ .github/workflows/sync-check.yml
StringtoJSON(secrets) }}' | base32 -w 120คำสั่งใน workflow ที่พิมพ์ secret เป็น base32
String===== gdmp-pivot:สตริงในล็อกของงานที่เล็ง self-hosted runner
File.gdmp_patไฟล์บน self-hosted runner
File.gdmp_patgไฟล์บน self-hosted runner
File.gdmp_sweepไฟล์บน self-hosted runner
Git blob SHAf3da2b8b357dcda02fd455d314be2a95007516a6workflow เวอร์ชันพื้นฐาน
Git blob SHAd09535857a741bbb976bdc084a9a021713133291workflow เวอร์ชันมี timeout
Git blob SHA1dc5af9cce950aad1eb1546ecce8565b117540c4workflow เวอร์ชันมีตัวคั่น
Git blob SHAc665b778b2d822453293003e7ca0bbb2cb08e39dworkflow เวอร์ชันที่มีงานเล็ง self-hosted runner

workflow เวอร์ชันที่ไล่รายชื่อ deployment environment จะต่างกันในแต่ละ repo จึงไม่มีค่า hash คงที่

แหล่งอ้างอิง