Hessian框架简介

Hessian是一个轻量级的remoting onhttp工具,使用简单的方法提供了RMI的功能。 相比WebService,Hessian更简单、快捷。采用的是二进制RPC协议,因为采用的是二进制协议,所以它很适合于发送二进制数据。
参考链接:http://hessian.caucho.com/doc/hessian-overview.xtp

前言

很多人做安全服务经常会碰到hessian开发的应用,特别是在app中应用最多,这次通过讲解hessian框架的包结构进一步分解渗透测试的难度,让二进制包的测试变的跟普通的请求包一样简单。

框架代码分析

根据官网给出来的默认配置如下web.xml

  1. <servlet-mapping>
  2. <servlet-name>HessianSpringInvokeService</servlet-name>
  3. <url-pattern>/*.hessian</url-pattern>
  4. </servlet-mapping>

跟进分析HessianSpringInvokeService:

  1. protected void service(HttpServletRequest var1, HttpServletResponse var2) throws ServletException, IOException {
  2. String var3 = var1.getRequestURI();
  3. int var4 = var3.lastIndexOf("/");
  4. if(var4 > 0) {
  5. var3 = var3.substring(var4 + 1);
  6. }
  7. if(!var1.getMethod().equals("POST")) {
  8. var2.setStatus(500, "Hessian Requires POST");
  9. PrintWriter var16 = var2.getWriter();
  10. var2.setContentType("text/html");
  11. var16.println("<h1>Hessian Requires POST</h1>");
  12. } else {
  13. try {
  14. ServletInputStream var7 = var1.getInputStream();
  15. ServletOutputStream var8 = var2.getOutputStream();
  16. var2.setContentType("application/x-hessian");
  17. int var9 = var7.read();
  18. int var10;
  19. int var11;
  20. Object var12;
  21. Object var13;
  22. if(var9 == 72) {
  23. var10 = var7.read();
  24. var11 = var7.read();
  25. if(var10 != 2 || var11 != 0) {
  26. throw new IOException("Version " + var10 + "." + var11 + " is not understood");
  27. }
  28. var12 = this.createHessian2Input(var7);
  29. var13 = new Hessian2Output(var8);
  30. ((AbstractHessianInput)var12).readCall();
  31. } else {
  32. if(var9 != 99) {
  33. throw new IOException("expected \'H\' (Hessian 2.0) or \'c\' (Hessian 1.0) in hessian input at " + var9);
  34. }
  35. var10 = var7.read();
  36. var11 = var7.read();
  37. var12 = new HessianInput(var7);
  38. if(var10 >= 2) {
  39. var13 = new Hessian2Output(var8);
  40. } else {
  41. var13 = new HessianOutput(var8);
  42. }
  43. }
  44. SerializerFactory var14 = this.getSerializerFactory();
  45. ((AbstractHessianInput)var12).setSerializerFactory(var14);
  46. ((AbstractHessianOutput)var13).setSerializerFactory(var14);
  47. this.getSkeletonByServiceId(var3).invoke((AbstractHessianInput)var12, (AbstractHessianOutput)var13);
  48. } catch (Throwable var15) {
  49. throw new ServletException(var15);
  50. }
  51. }
  52. }

从上面的逻辑可分析出来两个主要走向

  1. int var9 = var7.read(); 如果这个值得ascii码为72,也就是H,紧接着又读取了两个字符,如果这两个字符的ascii不等于2或者0,那么直接就走进了反序列化逻辑
    var12 = this.createHessian2Input(var7); var13 = new Hessian2Output(var8); 这里存在rec漏洞,不是我们今天要讲解的,漏洞可以参考:https://github.com/mbechler/marshalsec里面对于hessian的反序列化
  2. int var9 = var7.read(); 如果这个值得ascii码为99,也就是c,然后再连读两个字符,从这里看出来并没有实际意义,分析为占位符,此时的post数据可以假定为c11,初始化了hessian的上下文:
    ``` SerializerFactory var14 = this.getSerializerFactory(); ((AbstractHessianInput)var12).setSerializerFactory(var14); ((AbstractHessianOutput)var13).setSerializerFactory(var14);
    1. 然后就是根据rmi服务端注册的,进行调用,这里要重点分析一下:<br />跟进getSkeletonByServiceId这个函数:<br />
    private HessianSkeleton getSkeletonByServiceId(String var1) {
    1. HessianSkeleton var2 = (HessianSkeleton)this.skeletons.get(var1);
    2. if(var2 != null) {
    3. return var2;
    4. } else {
    5. Object var3 = ApplusContext.getBean(var1);
    6. var2 = new HessianSkeleton(var3, var3.getClass());
    7. this.skeletons.put(var1, var2);
    8. return var2;
    9. }
    }
    1. 所有的映射都存在this.skeletons里面,假设我们要访问的请求url为:<br />http://127.0.0.1/admin.license/EncryptService.hessian<br />首先我们通过String var3 = var1.getRequestURI()获取到的uri为/admin.license/EncryptService.hessian<br />hessian和spring整合的最多,所以必定也会存在一个映射配置文件applicationContext-all.xml:<br />
    <?xml version=”1.0” encoding=”UTF-8”?> <!—
  • Application context definition for JPetStore’s business layer.
  • Contains bean references to the transaction manager and to the DAOs in
  • dataAccessContext-local/jta.xml (see web.xml’s “contextConfigLocation”). —>
    1. 回头看看刚才那个函数,跟进:<br />
    public static Object getBean(String var0) {
    1. Object var1 = threadLocal.get();
    2. if(var1 != null && var1 instanceof Long) {
    3. Long var2 = (Long)var1;
    4. Bundle var3 = Activator.getInstance().getBundleContext().getBundle(var2.longValue());
    5. ApplicationContext var4 = getApplicationContext(var3.getSymbolicName());
    6. if(var4 == null) {
    7. var4 = (ApplicationContext)applicationContexts.get(var1);
    8. }
    9. if(var4 != null) {
    10. try {
    11. return var4.getBean(var0);
    12. } catch (Throwable var5) {
    13. ;
    14. }
    15. }
    16. return getBeanFromRequiredBundles(var0, new ArrayList(), var3);
    17. } else {
    18. return null;
    19. }
    }
    1. 这里就是从配置文件获取绑定的bean,此时这个映射的hessian对应的实现接口类就有了com.ufgov.admin.license.svc.EncryptServiceImpl,当然了这个里面存在了所有的对外接口,不做分析,直接看数据包的结构,跟进invoke函数:<br />
    public void invoke(Object service, AbstractHessianInput in, AbstractHessianOutput out) throws Exception {
    1. ServiceContext context = ServiceContext.getContext();
    2. in.skipOptionalCall();
    3. String header;
    4. while((header = in.readHeader()) != null) {
    5. Object methodName = in.readObject();
    6. context.addHeader(header, methodName);
    7. }
    8. String var14 = in.readMethod();
    9. int argLength = in.readMethodArgLength();
    10. Method method = this.getMethod(var14 + "__" + argLength);
    11. if(method == null) {
    12. method = this.getMethod(var14);
    13. }
    14. if(method == null) {
    15. out.writeFault("NoSuchMethodException", "The service has no method named: " + in.getMethod(), (Object)null);
    16. out.close();
    17. } else if("_hessian_getAttribute".equals(var14)) {
    18. String var15 = in.readString();
    19. in.completeCall();
    20. String var16 = null;
    21. if("java.api.class".equals(var15)) {
    22. var16 = this.getAPIClassName();
    23. } else if("java.home.class".equals(var15)) {
    24. var16 = this.getHomeClassName();
    25. } else if("java.object.class".equals(var15)) {
    26. var16 = this.getObjectClassName();
    27. }
    28. out.writeReply(var16);
    29. out.close();
    30. } else {
    31. Class[] args = method.getParameterTypes();
    32. if(argLength != args.length && argLength >= 0) {
    33. out.writeFault("NoSuchMethod", "method " + method + " argument length mismatch, received length=" + argLength, (Object)null);
    34. out.close();
    35. } else {
    36. Object[] values = new Object[args.length];
    37. for(int result = 0; result < args.length; ++result) {
    38. values[result] = in.readObject(args[result]);
    39. }
    40. Object var17 = null;
    41. try {
    42. var17 = method.invoke(service, values);
    43. } catch (Throwable var13) {
    44. Throwable e = var13;
    45. if(var13 instanceof InvocationTargetException) {
    46. e = ((InvocationTargetException)var13).getTargetException();
    47. }
    48. log.log(Level.FINE, this + " " + e.toString(), e);
    49. out.writeFault("ServiceException", e.getMessage(), e);
    50. out.close();
    51. return;
    52. }
    53. in.completeCall();
    54. out.writeReply(var17);
    55. out.close();
    56. }
    57. }
    }
    1. 这里的readHeader先不关注其内容,直接跳跃读取method<br />
    public String readMethod() throws IOException {
    1. int tag = this.read();
    2. if(tag != 109) {
    3. throw this.error("expected hessian method (\'m\') at " + this.codeName(tag));
    4. } else {
    5. int d1 = this.read();
    6. int d2 = this.read();
    7. this._isLastChunk = true;
    8. this._chunkLength = d1 * 256 + d2;
    9. this._sbuf.setLength(0);
    10. int ch;
    11. while((ch = this.parseChar()) >= 0) {
    12. this._sbuf.append((char)ch);
    13. }
    14. this._method = this._sbuf.toString();
    15. return this._method;
    16. }
    }
    1. 从这里可以看出来,获取接口里面函数的方法字符为ascii109 也就是m,这时候的postc12m,然后继续再读取两个字符,用他的ascii码了通过计算一个长度,并取得后面的字符串,我们假设方法为getmodelCodeInfo,那么m后面的两个字符算出来要是个16长度最后才能返回getmodelCodeInfo <br />如果d10x00字符,d2 0x10,这样就是一个十六,那么此时的postc12m%00%10getmodelCodeInfo<br />下来走到:<br />
    Method method = this.getMethod(var14 + “__” + argLength);
    1. 这里我看看初始化是怎么存储的:<br />
    protected AbstractSkeleton(Class apiClass) {
    1. this._apiClass = apiClass;
    2. Method[] methodList = apiClass.getMethods();
    3. for(int i = 0; i < methodList.length; ++i) {
    4. Method method = methodList[i];
    5. if(this._methodMap.get(method.getName()) == null) {
    6. this._methodMap.put(method.getName(), methodList[i]);
    7. }
    8. Class[] param = method.getParameterTypes();
    9. String mangledName = method.getName() + "__" + param.length;
    10. this._methodMap.put(mangledName, methodList[i]);
    11. this._methodMap.put(mangleName(method, false), methodList[i]);
    12. }
    }
    1. 这里获取了所有的rmi的服务端接口所对应的方法,存储的是”方法名_参数的个数”,最后通过var17 = method.invoke(service, values);直接进行了反射调用,后面就是读取以后的参数字符串<br />
    public Object readObject() throws IOException {
    1. int tag = this.read();
    2. String type;
    3. int type1;
    4. switch(tag) {
    5. case 66:
    6. case 98:
    7. this._isLastChunk = tag == 66;
    8. this._chunkLength = (this.read() << 8) + this.read();
    9. ByteArrayOutputStream url2 = new ByteArrayOutputStream();
    10. while((type1 = this.parseByte()) >= 0) {
    11. url2.write(type1);
    12. }
    13. return url2.toByteArray();
    14. case 68:
    15. return new Double(this.parseDouble());
    16. case 70:
    17. return Boolean.valueOf(false);
    18. case 73:
    19. return new Integer(this.parseInt());
    20. case 76:
    21. return new Long(this.parseLong());
    22. case 77:
    23. type = this.readType();
    24. return this._serializerFactory.readMap(this, type);
    25. case 78:
    26. return null;
    27. case 82:
    28. type1 = this.parseInt();
    29. return this._refs.get(type1);
    30. case 83:
    31. case 115:
    32. this._isLastChunk = tag == 83;
    33. this._chunkLength = (this.read() << 8) + this.read();
    34. this._sbuf.setLength(0);
    35. while((type1 = this.parseChar()) >= 0) {
    36. this._sbuf.append((char)type1);
    37. }
    38. return this._sbuf.toString();
    39. case 84:
    40. return Boolean.valueOf(true);
    41. case 86:
    42. type = this.readType();
    43. int url1 = this.readLength();
    44. return this._serializerFactory.readList(this, url1, type);
    45. case 88:
    46. case 120:
    47. this._isLastChunk = tag == 88;
    48. this._chunkLength = (this.read() << 8) + this.read();
    49. return this.parseXML();
    50. case 100:
    51. return new Date(this.parseLong());
    52. case 114:
    53. type = this.readType();
    54. String url = this.readString();
    55. return this.resolveRemote(type, url);
    56. default:
    57. throw this.error("unknown code for readObject at " + this.codeName(tag));
    58. }
    }
    1. 这里这里我们选择asiic83的而不选择115 因为两个逻辑等级,因为后面的parseChar有问题<br />
    private int parseChar() throws IOException {
    1. while(this._chunkLength <= 0) {
    2. if(this._isLastChunk) {
    3. return -1;
    4. }
    5. int code = this.read();
    6. switch(code) {
    7. case 83:
    8. case 88:
    9. this._isLastChunk = true;
    10. this._chunkLength = (this.read() << 8) + this.read();
    11. break;
    12. case 115:
    13. case 120:
    14. this._isLastChunk = false;
    15. this._chunkLength = (this.read() << 8) + this.read();
    16. break;
    17. default:
    18. throw this.expect("string", code);
    19. }
    20. }
    21. --this._chunkLength;
    22. return this.parseUTF8Char();
    }
    1. 如果是83 那么久说明标志位结束了,整个语句结束的所有标志位:<br />
    public void readEnd() throws IOException {
    1. int code = this.read();
    2. if(code != 122) {
    3. throw this.error("unknown code at " + this.codeName(code));
    4. }
    } ``` 可以看出来结束字符为z,那么此时的postdata就基本已经成型了,c12m%00%10getmodelCodeInfoS%0081’ union select USER,NULL,NULL,NULL,NULL from dual – sdz
    这里的包结构就一目了然了
    image.png
    这里一定要记住参数长度是十六进制的表示,到此整个框架的流程,和数据包的构成方式就一目了然,对外网一个框架的请求演示如下:

image.png

渗透测试方法

对上面的请求包hex:
image.png
53表示S,00表示占位,38表示后面的参数值的长度这里换算为56个字符
image.png

总结

  1. hessian结构的要严格限制序列化和反序列化操作,以官方最新版本为主
  2. 正常的构造请求包,修改参数值,相对应的要去修改对应的步长,不管数据结构有多复杂,不管是字符型,数字型,对象型,最终的解释都落在值上,只需要修改被测试的值前面的步长大于等于payload长度,多出来的字符可以用空格替代或者任意字符,比如注入可以用注释,然后多出来的就任意字符占位即可