Skip to content

Latest commit

 

History

History
361 lines (285 loc) · 13.7 KB

File metadata and controls

361 lines (285 loc) · 13.7 KB

[TOC]

overview

WebService的实现定义了Cobalt strike的具体uri所做的事情,相当于MVC里的Controller。

WebService的子类如上图,说明了Cobaltstrike的http server实现的功能

WebServer.WebListener是一个接收已处理请求的接口

WebCalls实现了ServerHook, WebServer.WebListener,其中WebListener的实现为添加一个weblog,然后广播给所有用户

beacon.BeaconHTTP有两个内部类,beacon.BeaconHTTP.PostHandlerbeacon.BeaconHTTP.GetHandler继承于MalleableHook,并且满足:

  • post负责接收结果,不返回任何数据
  • get负责发送任务,并使用对称加密task数据

分析

HTTP

NanoHTTPD

cloudstrike.NanoHTTPD是修改了部分代码的NanoHTTPD

主要逻辑很简单,新连接新建HTTPSession:

cloudstrike.NanoHTTPD#listen

                    //...
					while (NanoHTTPD.this.alive) {
                        try {
                            final Socket temp = NanoHTTPD.this.ss.accept();
                            if (temp == null) {
                                continue;
                            }
                            new HTTPSession(temp);
                            //...

HTTPSession实现了Runnable,主要逻辑在run内:

cloudstrike.NanoHTTPD.HTTPSession#run

// 空白字符分割第一段http报文
final String request = this.readLine(in);
final StringTokenizer st = new StringTokenizer((request == null) ? "" : request);
// ...
final String method = st.nextToken();
// ...
String uri = st.nextToken();
final Properties header = new Properties();
final Properties parms = new Properties();
final int qmi = uri.indexOf(63);  // 63 == '?'
if (qmi >= 0) {
    header.put("QUERY_STRING", uri.substring(qmi + 1));
    this.decodeParms(uri.substring(qmi + 1), parms);
    uri = this.decodePercent(uri.substring(0, qmi));
}
else {
    uri = this.decodePercent(uri);
}
if (st.hasMoreTokens()) {
	// 这一段就是parse headers的过程,根据':'分割接下来的每行,放入header中去
}
if (method.equalsIgnoreCase("POST")) {  // 如果是post额外进行下面的步骤
	long size = 0L;
	final String contentLength = header.getProperty("Content-Length");
    // 根据content-length获取body的大小
	if (contentLength != null) {
	    try {
	        size = Integer.parseInt(contentLength);
	    }
	    catch (NumberFormatException ex) {}
	}
	if (size > 10485760L) {
	    this.sendError("413 Entity Too Large", "BAD REQUEST: Request Entity Too Large");
	}
	if (size > 0L) {
        // 根据上面获取的大小读取http报文剩下的内容
	    final byte[] all = new byte[(int)size];
	    in.readFully(all, 0, (int)size);
        // 当Content-Type为application/octet-stream或者是事先注册的只接收原始字节流的uri
	    if ("application/octet-stream".equals(header.getProperty("Content-Type")) || NanoHTTPD.this.alwaysRaw(uri)) {
            // 二进制数据两个键值:length和input
	        final ByteArrayInputStream bis = new ByteArrayInputStream(all);
	        parms.put("length", new Long(all.length));
	        parms.put("input", bis);
	    }
	    else {
            // 非二进制数据则假设是application/x-www-form-urlencoded,跟get query string一样解码放入parms
	        this.decodeParms(new String(all), parms);
	    }
	}
}

header.put("REMOTE_ADDRESS", this.mySocket.getInetAddress().toString());
final Response r = NanoHTTPD.this.serve(uri, method, header, parms);  // 最后调用外部类的serve方法

// ...

上面一些方法的用途:

  • cloudstrike.NanoHTTPD.HTTPSession#decodePercent:就是urldecode
  • cloudstrike.NanoHTTPD.HTTPSession#decodeParms: 解析get query string,存入parms里,parms是Properties对象实际上就是一个HashTable

run方法剩下的部分还有一点:

// 如果定义了过滤器,会根据调用过滤器
if (NanoHTTPD.this.filter != null) {
    NanoHTTPD.this.filter.filterResponse(r);
}

WebServer

cloudstrike.WebServer继承了NanoHTTPD

字段的说明:

    // Map<String, WebService>  相当于路由中用来存放uri对应controller的映射
    // @key: uri
    // @val: 实现了特定功能WebService,比如ServeFile、StaticContent
    protected Map hooks;
    protected Map hooksSecondary;
    // Map<String, bool> 这里面存放着不需要parse body里的post参数的url,即raw url
    // @key: uri
    // @val: 始终是true
    protected Map always;
    // Map<String, String>
    // @key: uri
    // @val: host
    protected Map hosts;
    // 每受到一个请求都会向该链表里的WebListener发该请求的结果,由于只有WebCalls实现了,因此可以理解为每个WebListener都会
    // 对该次请求产生weblog
    protected List weblisteners;

上面分析到run方法最终会调用外部类的serve方法:

    public Response serve(final String uri, final String method, final Properties header, final Properties param) {
        return this.handleRanges(method, header, this._serve(uri, method, header, param));
    }

serve方法调用_serve方法

这个方法实际上是一个路由,根据不同的uri获取WebService,然后调用真正的WebService

cloudstrike.WebServer#_serve做了下列的判断:

  • 符合这个判断直接返回404:useragent.startsWith("lynx") || useragent.startsWith("curl") || useragent.startsWith("wget")
  • OPTIONS请求返回OPTIONS,GET,HEAD,POST

然后进入路由判断:

  • this.hooks中有该uri,取出WebService,调用
  • this.hooksSecondary中有该uri,取出WebService,调用
  • this.hooks中有该uri + '/',取出WebService,调用
  • this.hooksSecondary中有该uri + '/',取出WebService,调用
  • uri开始是http://,如果this.hooks中有proxy,则调用,否则返回404

如果上面的都不满足,则进入判断默认的stager uri的流程。

先看它是如何判断是stager的uri的:

cloudstrike.WebServer

	public static long checksum8(String text) {
        if (text.length() < 4) {
            return 0L;
        }
        text = text.replace("/", "");
        long sum = 0L;
        for (int x = 0; x < text.length(); ++x) {
            sum += text.charAt(x);
        }
        return sum % 256L;
    }
    
    public static boolean isStager(final String uri) {
        return checksum8(uri) == 92L;
    }
    
    public static boolean isStagerX64(final String uri) {
        return checksum8(uri) == 93L && uri.matches("/[A-Za-z0-9]{4}");
    }
    
    public static boolean isStagerStrict(final String uri) {
        return isStager(uri) && uri.length() == 5;
    }
    
    public static boolean isStagerX64Strict(final String uri) {
        return isStagerX64(uri) && uri.length() == 5;
    }

也就是我们常常看到默认设置下请求stage请求的uri是像这种/AbCd,就会进入到这里了。

然后就会根据上面这些方法来使用对应的WebService,如果不存在则返回404。

中间还会遍历this.hooksSecondary:

cloudstrike.WebServer#_serve

// ...
for (final Map.Entry e : this.hooksSecondary.entrySet()) {
    final WebService svc = e.getValue();
    final String hook = e.getKey() + "";
    // 如果uri开始部分是hooksSecondary的键,并且该WebService是模糊匹配
    if (uri.startsWith(hook) && svc.isFuzzy()) {
        return this.processResponse(uri, method, header, param, false, svc, svc.serve(uri.substring(hook.length()), method, header, param));
    }
}
// ...

当产生响应对象后会进入:

protected Response processResponse(final String uri, final String method, final Properties header, final Properties param, final boolean primary, final WebService service, final Response r) {
    String desc;
    if (service == null) {
        desc = null;
    }
    else {
        desc = service.getType() + " " + service.toString();
    }
    final String resp = r.status;
    final long size = r.size;
    if (service == null || !service.suppressEvent(uri)) {
        // 如果service不为null并且会产生事件
        this.fireWebListener(uri, method, header, param, desc, primary, resp, size);
    }
    return r;
}

cloudstrike.WebServer#fireWebListener

        final Iterator i = this.weblisteners.iterator();
        while (i.hasNext()) {
            i.next().receivedClient(uri, method, header, param, desc, primary, response, size);
        }

遍历所有WebListener调用receivedClient方法

实践上WebListener的实现只有WebCallsWebCallsreceivedClient就是记录weblog的过程。

processResponse结束后response对象继续进入handleRanges方法,当条件符合header.containsKey("Range") && "GET".equals(method) && original.size > 0L && original.data != null && "200 OK".equals(original.status),original就是Response对象,也就是会根据http协议规范里的定义,对返回结果进行处理,只返回相应范围的数据。

Malleable Hook

c2profile.MalleableHook是实现了cloudstrike.WebService的类,负责实现可自定义的c2 http的通信。

serve方法是主要逻辑:

    public Response serve(final String uri, final String method, final Properties headers, final Properties param) {
        try {
            final Response response = new Response("200 OK", null, (InputStream)null);
            this.profile.apply(this.key + ".server", response, this.hook.serve(uri, method, headers, param));  // 调用MyHook接口的serve方法,然后使用c2profile将response转换为c2profile里定义的格式
            return response;
        }
        catch (Exception ex) {
            ex.printStackTrace();
            return new Response("500 Internal Server Error", "text/plain", "Oops... something went wrong");
        }
    }

BeaconHTTP

beacon.BeaconHTTP实现了MyHook接口,提供了Get和Post Handler。

beacon.BeaconHTTP.PostHandler

private class PostHandler implements MalleableHook.MyHook
{
    @Override
    public byte[] serve(final String uri, final String method, final Properties headers, final Properties param) {
        try {
        	// 如果定义了trust_x_forwarded_for并且存在X-Forwarded-For,则从X-Forwarded-For中取得远程地址
            final String remoteAddress = ServerUtils.getRemoteAddress(BeaconHTTP.this.c2profile, headers);
            // 获取param中input键的流,实际上就是未parse的body,然后转换成string
            final String postedData = BeaconHTTP.this.getPostedData(param);
            // 获取beacon id
            final String s = new String(BeaconHTTP.this.c2profile.recover(".http-post.client.id", headers, param, postedData, uri));
            if (s.length() == 0) {
                CommonUtils.print_error("HTTP " + method + " to " + uri + " from " + remoteAddress + " has no session ID! This could be an error (or mid-engagement change) in your c2 profile");
                MudgeSanity.debugRequest(".http-post.client.id", headers, param, postedData, uri, remoteAddress);
            }
            else {
            // 根据c2profile的定义转换接受的数据
                final byte[] bytes = CommonUtils.toBytes(BeaconHTTP.this.c2profile.recover(".http-post.client.output", headers, param, postedData, uri));
                // 进入真正处理beacon请求的方法,即beacon.BeaconC2#process_beacon_data
                if (bytes.length == 0 || !BeaconHTTP.this.controller.process_beacon_data(s, bytes)) {
                    MudgeSanity.debugRequest(".http-post.client.output", headers, param, postedData, uri, remoteAddress);
                }
            }
        }
        catch (Exception ex) {
            MudgeSanity.logException("beacon post handler", ex, false);
        }
        return new byte[0];  // 只接受不返回任何数据
    }
}

beacon.BeaconHTTP.GetHandler

public byte[] serve(final String s, final String s2, final Properties properties, final Properties properties2) {
    // 如果定义了trust_x_forwarded_for并且存在X-Forwarded-For,则从X-Forwarded-For中取得远程地址
    final String remoteAddress = ServerUtils.getRemoteAddress(BeaconHTTP.this.c2profile, properties);
    // 根据C2Profile转换metadata
    final String recover = BeaconHTTP.this.c2profile.recover(".http-get.client.metadata", properties, properties2, BeaconHTTP.this.getPostedData(properties2), s);
    if (recover.length() == 0 || recover.length() != 128) {
        CommonUtils.print_error("Invalid session id");
        MudgeSanity.debugRequest(".http-get.client.metadata", properties, properties2, "", s, remoteAddress);
        return new byte[0];
    }
    
    // parse metadata,然后获取对应的BeaconEntry
    final BeaconEntry process_beacon_metadata = BeaconHTTP.this.controller.process_beacon_metadata(remoteAddress, CommonUtils.toBytes(recover), null, 0);
    if (process_beacon_metadata == null) {
        MudgeSanity.debugRequest(".http-get.client.metadata", properties, properties2, "", s, remoteAddress);
        return new byte[0];
    }
    
    // 获取该beacon队列中的task
    final byte[] dump = BeaconHTTP.this.controller.dump(process_beacon_metadata.getId(), 921600, 1048576);
    if (dump.length > 0) {
        // 对称加密task返回
        return BeaconHTTP.this.controller.getSymmetricCrypto().encrypt(process_beacon_metadata.getId(), dump);
    }
    return new byte[0];
}

详细协议分析参考C2Protocol.md